././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667233486.721467 galpy-1.8.1/0000755000175100001710000000000014327773317012252 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/AUTHORS.txt0000644000175100001710000000406414327773303014137 0ustar00runnerdockerAuthor: ======= Jo Bovy Major contributions: ===================== Aladdin Seaifan (KuzminDiskPotential,SCFPotential) Direct contributions to the code base: ======================================= Denis Erkal (_mass for TwoPowerSphericalPotential; also simulations to test streamgapdf) Mark Fardal (streamdf improvements) Rok Roskar (first implementation of SnapshotRZPotential and InterpSnapshotRZPotential) Jason Sanders (velocity kicks induced by interactions with Plummer, Hernquist, and general potentials, currently in streamgapdf) Wilma Trick (KuzminKutuzovPotential and various other contributions) Anna Juranova (FerrersPotential) Semyeong Oh (FerrersPotential) Jack Hong (SpiralArmsPotential) Ted Mackereth (addition to estimateDeltaStaeckel function, and fast orbit characterisation docs) Jeremy Webb (rtide and ttensor Potentials methods/functions) Mathew Bub (from_name class method of Orbit) Samuel Wong (sampleV_interpolate method of quasiisothermaldf) James Lane (General mass distributions in MovingObjectPotential, many contributions to the spherical distribution functions) Henry Leung (addition of Python and C implementation of Dormand-Prince 8(5,3) integrator) Nathaniel Starkman (custom coordinates in bovy_coords, DehnenSphericalPotential, time dependences in potential, amuse-astropy Potential compatibility) Morgan Bennett (scf_compute_.._nbody functions for computing SCF expansions for N-body sims) Marius Cautun (Cautun20 potential and help with implementing the AdiabaticContractionWrapperPotential) Anya Dovgal (Gaia EDR3 parameters for MW satellites and globular clusters) General help: ============== Greg Stinson (advice on SnapshotRZPotential tests) Matt Craig (added conda build) Brigitta Sipocz (help with conda build) Bug reports: ============ (very incomplete list, apologies if I forgot anybody!) Alexander de la Vega Wai-Hin Ngan Wilma Trick T. E. Pickering Željko Ivezić Resmi Lekshmi Rok Roskar Dylan Gregersen Eduardo Balbinot Semyeong Oh Jorrit Hagen Ruth Angus Anthony Brown Larry Widrow Jean-François Robitaille ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/HISTORY.txt0000644000175100001710000010306214327773303014151 0ustar00runnerdockerv1.8.1 (2022-10-31) =================== - Changed estimateDeltaStaeckel to by default return delta0=1e-6 when delta=3.8 wheels for arm64 Macs (M1s) - Switched to using cibuildwheel to build Linux, Mac (incl. M1/arm64), and Windows wheels. - Switched to using pyodide-build to build the pyodide wheel. v1.8.0 (2022-07-04) =================== - Added the particle-spray model galpy.df.streamspraydf that was previously part of the jobovy/streamtools package (#479). - Added necessary derivatives to allow spherical DFs to be constructed using PowerSphericalPotentialwCutoff and PlummerPotential. - Updated existing and added new phase-space positions for MW satellite galaxies from Pace et al. (2022; 2205.05699). Also add uncertainties (#484 by Anya Dovgal). - Updated existing and added new phase-space positions for MW globular clusters from Baumgardt et al. (2019; 1811.01507), Vasiliev & Baumgardt (2021; 2102.09568), and Baumgardt & Vasiliev (2021; 2105.09526). Also add uncertainties (#485 by Anya Dovgal). - Added SCFPotential.from_density to directly initialize an SCFPotential based on a density function. Allows for full correct and consistent handling of Quantity inputs and outputs. - Renamed phiforce --> phitorque everywhere (including potential.evaluatephiforces and potential.evaluateplanarphiforces), such that the method's name actually reflect what it returns (a torque, not a force). phiforce will be fully removed in version 1.9 and may later be re-used for the actual phi component of the force, so switch to the new name now. - Added code to check whether a newer version of galpy is available and, if so encourage users to upgrade (to try to have people use more recent versions). - Added a 'Try galpy' interactive session on the documentation's home page powered by the new pyodide/emscripten/wasm wheels for galpy. - Add support to build pyodide/emscripten/wasm wheels for using galpy in the browser at C-ish speeds. - Fully drop Python 2 support (removed all Python 2 compatibility code). v1.7.2 (2022-04-15) =================== - Added NonInertialFrameForce, which provides the fictitious forces when integrating orbits in non-inertial frames. Arbitrary rotations and origin accelerations are supported (#471). - Added a progress bar when integrating multiple objects in a single orbit instance (requires tqdm) (see #476). - Turn on NFWPotential physical output when initializing using the virial mass mvir (see #465) - Added TimeDependentAmplitudeWrapperPotential, a wrapper allowing for arbitrary time dependence of the amplitude of any potential. - Added NullPotential, a potential with a constant value. - Added Potential methods/functions rE and LcE to compute the radius and angular momentum of an orbit with energy E. Also added these as Orbit methods for efficient calculation for collections of orbits. - Added the offset= keyword to RotateAndTiltWrapperPotential, which allows a Potential/Force instance to also be offset from (0,0,0) in addition to being rotated or tilted. - Allow actions to be computed for Orbit instances with actionAngle methods that don't compute frequencies. - Correctly parse potential method/function R/z/x inputs if they are given as keyword arguments. v1.7.1 (2021-11-17) =================== - Main reason for this minor version is to add a Python 3.10 wheel v1.7 (2021-07-04) ================= - Added a general framework for spherical distribution function as well as a general implementation of (a) the Eddington inversion to obtain the ergodic distribution function, (b) the equivalent inversion to get the Osipkov-Merritt-style anisotropy, and (c) the Cuddeford inversion to obtain a distribution function with constant anisotropy, all three for any spherical density in any spherical potential (where it exists). Also added the distribution function of a few well-known distribution functions, including (a) Hernquist distribution functions that are isotropic, have constant anisotropy, or have Osipkov-Merritt type anisotropy; (b) an isotropic Plummer profile; (c) the isotropic NFW profile (either using the approx. from Widrow 2000 or using an improved approximation) and the Osipkov-Merritt NFW profile (new approximate form); (d) the King model (also added as a potential as KingPotential). These distribution functions can be evaluated and sampled, and their velocity moments can be computed. Work started in #432 and continued on from there. Distribution functions with constant anisotropy require the JAX. - Implemented the calculation of SCF density/potential expansion coefficients based on an N-body representation of the density (#444). - Added NFWPotential initialization method using rmax and vmax, the radius and value at which the rotation curve peaks; also added rmax and vmax methods to NFWPotential to compute these quantities for any NFWPotential. - Added general class SphericalPotential to make defining spherical potentials easier (#424). - Added interpSphericalPotential class to build interpolated versions of spherical potentials (#424). - Added AdiabaticContractionWrapperPotential, a wrapper potential to adiabatically contract a spherical dark-matter halo in response to the adiabatic growth of a baryonic component. - Added potential functions to compute the zero-velocity curve: zvc and zvc_range; the latter computes the range in R over which the zero-velocity curve is defined, the former gives the positive z position on the zero-velocity curve for a given radius in this range. - Added potential function and method rhalf to compute the half-mass radius. - Added potential function and member tdyn to compute the dynamical time from the average density. - Added TriaxialGaussianPotential, the potential of a Gaussian that is stratified on triaxial ellipsoids (Emsellem et al. 1994). Useful for implementing multi-Gaussian expansions of galactic potentials. - Added PowerTriaxialPotential, the potential of a triaxial power-law density (like PowerSphericalPotential, but triaxial). - Added AnyAxisymmetricRazorThinDiskPotential, the potential of an arbitrary razor-thin, axisymmetric disk. - Added AnySphericalPotential, the potential of an arbitrary spherical density distribution. - Added RotateAndTiltWrapperPotential, a wrapper potential to re-orient a potential arbitrarily in three dimensions using three angles. - Updated the definition of a Potential's mass to always return the mass within a spherical shell if only one argument is specified. Also implemented faster implementations of the masses of SCF and diskSCF potentials. - Added mixed azimuthal,vertical second derivative for all non-axisymmetric potentials: potential function evaluatephizderivs and Potential method phizderiv. - Added inverse action-angle transformations for the isochrone potential (actionAngleIsochroneInverse) and for a one-dimensional harmonic oscillator (actionAngleHarmonicInverse; also added the [x,v] -> [J,O,a] transformation for the latter, actionAngleHarmonic). - Added Potential.plotSurfaceDensity (potential.plotSurfaceDensities) method (function) to plot the surface density of a Potential instance (of Potential instance of a list of Potential instances). - Re-implemented the DoubleExponentialDiskPotential in a simpler and more accurate manner, using the double-exponential formula (no relation) for integrals of Bessel functions from Ogata 2005. The DoubleExponentialDiskPotential is now calculated to ~machine precision almost everywhere and there is no longer any switch to a Keplerian behavior at large R. - Made potentials as much as possible numerically stable at r=0 and r=numpy.inf (i.e., they don't give NaN). Still a few missing, but the majority of potentials are well behaved now. - Fixed angle calculation for actionAngleIsochrone and actionAngleSpherical for non-inclined orbits (which are tricky). - Replace Quantity parsing through the package as much as possible with a set of centralized parsers (#430). - Renamed galpy.util.bovy_coords to galpy.util.coords, galpy.util.bovy_conversion to galpy.util.conversion, and galpy.util.bovy_plot to galpy.util.plot (but old 'from galpy.util import bovy_X' will keep working for now). Also renamed some other internal utility modules in the same way (bovy_symplecticode, bovy_quadpack, and bovy_ars; these are not kept backwards-compatible). - Added Python 3.9 support. - Switched Linux CI from Travis to GitHub Actions. Also added Windows CI to GitHub Actions covering almost all tests. Appveyor CI for Windows also still running. v1.6 (2020-04-24) ================= - Added HomogeneousSpherePotential, the potential of a constant density sphere out to some radius R. - Added DehnenSphericalPotential and DehnenCoreSphericalPotential, the general potential from Dehnen (1993) and its cored special case (#403). - Standardized the way packages are imported: numpy as 'import numpy', avoid use of math, don't use scipy's numpy interface, internal imports for all galpy imports. - Implemented ChandrasekharDynamicalFrictionForce in C, introducing a general scheme for easily implementing velocity-dependent forces in C without requiring all forces to take velocity arguments (#420). - Fixed AMUSE compatibility with Potentials instantiated with physical inputs (#405). - Fix how DiskSCFPotential instances with a hole in the surface density get passed to C (was wrong when type == 'exp', but 'Rhole' was in the list of parameters, hole was not passed; meant that McMillan17 was wrong in C in v1.5). - Add time dependence to all relevant Potential functions and method (#404). - Raise warning when r < minr (where dynamical friction gets turned off) in an orbit integration that includes dynamical friction (to avoid silent turning off of the dynamical friction force; #356). - Improve performance of orbit animations (#414). - Fixed compilation issue for Intel compilers: no need to include 'lgomp' in the linking step (#416). - Compile all main galpy C extensions into a single shared-object library libgalpy. - Automatically build Linux, Mac, and Windows wheels with GitHub Actions and upload them to PyPI upon release creation (#421). v1.5 (2019-09-12) ================= - Added support for holding multiple objects in an Orbit instance, with efficient handling of multiple objects and parallelized integration and action-angle-related functions. Orbit instances can now have arbitrary shapes. Full re-write of Orbit class (PR #384). - Added support for 1D orbit integration in C (PR #354). - Added a (JSON) list with the phase-space coordinates of known objects (mainly globular clusters and dwarf galaxies) for easy Orbit.from_name initialization. For ease of use, Orbit.from_name also supports tab completion for known objects in this list in IPython/Jupyter (PR #392). - Added the galpy.potentials.mwpotentials module with various Milky-Way-like potentials. Currently included are MWPotential2014, McMillan17 for the potential from McMillan (2017), models 1 through 4 from Dehnen & Binney (1998), and the three models from Irrgang et al. (2013). See PR #395. - Added potential.to_amuse to create an AMUSE (http://www.amusecode.org) representation of any galpy potential, allowing galpy potentials to be used as external gravitational fields in AMUSE N-body simulations (#398). - Added potential.toVerticalPotential to convert any 3D potential to a 1D potential at a given (R,phi) [generalizes RZToverticalPotential to non-axi potentials; RZToverticalPotential retained for backwards compatibility]. - Re-wrote potential.MovingObjectPotential to allow general mass distributions for the moving object, implemented now as standard galpy potentials rather than with separate ForceSoftening class, which has now been removed (#355). Initialization keywords for potential.MovingObjectPotential have changed because of this in a non-backwards-compatible manner. - Added support to combine Potential instances or lists thereof through the addition operator. E.g., pot= pot1+pot2+pot3 to create the combined potential of the three component potentials (pot1,pot2,pot3). Each of these components can be a combined potential itself. As before, combined potentials are simply lists of potentials, so this is simply an alternative (and perhaps more intuitive) way to create these lists (#369). - Added support to adjust the amplitude of a Potential instance through multiplication of the instance by a number or through division by a number. E.g., pot= 2.*pot1 returns a Potential instance that is the same as pot1, except that the amplitude is twice larger. Similarly, pot= pot1/2. decreases the amplitude by a factor of two. This is useful, for example, to quickly change the mass of a potential. Only works for Potential instances, not for lists of Potential instances (#369). - Added support to plot arbitrary combinations of the basic Orbit attributes by giving them as an expression (e.g., orb.plot(d2='vR*R/r+vz*z/r')); requires the numexpr package. - Switched default Sun's vertical height zo parameter for Orbit initialization to be the value of 20.8 pc from Bennett & Bovy (2019). - Added IsothermalDiskPotential, the one-dimensional potential of an isothermal self-gravitating disk (sech^2 profile). - Added NumericalPotentialDerivativesMixin, a Mixin class to add numerically-computed forces and second derivatives to any Potential class, allowing new potentials to be implemented quickly by only implementing the potential itself and obtaining all forces and second derivatives numerically. - Added nemo_accname and nemo_accpars for the HernquistPotential, allowing it to be converted to NEMO. - DehnenSmoothWrapperPotential can now decay rather than grow a potential by setting ``decay=True``. - DehnenBarPotential and SpiralArmsPotential method now work for array inputs (PR #388, by @webbjj). - Allow transformations of custom coordinate systems (positions and proper motions) to ICRS (ra,dec) and (pmra,pmdec). - Allow orbit integrations to be KeyboardInterrupted on Windows as well (#362 by Henry Leung) - Add Python and C implementation of Dormand-Prince 8(5,3) integrator (#363 by Henry Leung) - Added function galpy.util.bovy_conversion.get_physical to obtain the ro and vo parameters describing the conversion between physical and internal units as a dictionary. This works for any galpy object or lists of such objects. - Improved efficiency of requesting an integrated orbit at the exact times at which it was integrated (~10x faster now). v1.4 (2018-09-09) ================= - Added ChandrasekharDynamicalFrictionForce, an implementation of dynamical friction based on the classical Chandrasekhar formula (with recent tweaks from the literature to better represent the results from N-body simulations). - Added galpy.df.jeans with tools for Jeans modeling. Currently only contains the functions sigmar and sigmalos to calculate the velocity dispersion in the radial or line-of-sight direction using the spherical Jeans equation in a given potential, density profile, and anisotropy profile (anisotropy can be radially varying). - Added CorotatingRotationWrapperPotential to galpy.potential: a wrapper to make a pattern wind up over time such that it is always corotating (for use in simulating corotating spiral structure like that of Grand, Kawata, Baba, et al.) - Added GaussianAmplitudeWrapperPotential to galpy.potential: a wrapper to modulate the amplitude of any Potential instance with a Gaussian (growing from zero to the full amplitude and dying off again). - Added a general class EllipsoidalPotential that is a superclass for implementing potentials with densities that are constant on ellipsoids (functions of m^2 = x^2 + y^2/b^2 + z^2/c^2). Also implemented in C. Implementing new types of ellipsoidal potentials now only requires three simple functions to be defined: the density as a function of m, its derivative with respect to m, and its integral with respect to m^2 (#348). - Added PerfectEllipsoidPotential, the potential of a perfect ellipsoid (de Zeeuw 1985): this is a fully integrable (Staeckel-type), triaxial potential. - Re-implemented TwoPowerTriaxialPotential and special cases TriaxialHernquistPotential, TriaxialJaffePotential, and TriaxialNFWPotential using the general EllipsoidalPotential class. - Allow nested lists of Potential instances everywhere where lists of Potential instances were previously allowed; allow easy adding of components (e.g., a bar) to previously defined potentials (which may be lists themselves): new_pot= [pot,bar_pot]. - Add from_name class method of Orbit, which allows Orbit instances to be initialized using the coordinates of a named object found in SIMBAD. - Add rguiding method of Orbit, to compute the guiding-center radius of an orbit. Also added Lz method to easily obtain the z-component of the angular momentum for different types of orbits. - Defined Orbit initialization without any arguments (or, more generally, without specifying the vxvv initial phase-space input) to return an Orbit instances representing the Sun. Can therefore setup an Orbit representing the Sun's as o= Orbit(). - Allow Orbit instances to be initialized using a SkyCoord with position and velocity data, with optional Galactocentric frame specification as part of the SkyCoord that is correctly propagated to the Orbit instance. Requires astropy>3 - Added rtide and ttensor methods/functions for Potentials: tidal radius and tidal tensor. - Added surfdens and evaluateSurfaceDensities method and function, respectively, for Potentials for evaluating the surface density up to a given z. - Added potentials SphericalShellPotential and RingPotential for the potential of a spherical shell and a circular ring, respectively. Useful for talking about Newton's theorems. - Switched default solarmotion parameter for Orbit initialization to be schoenrich (for the Schoenrich, Binney, & Dehnen 2010 solar motion wrt the LSR). - Added Potential method r2deriv and function evaluater2derivs to evaluate the second derivative wrt the spherical radius. Also added convenience functions evaluatephi2derivs and evaluateRphiderivs. - Added quasiisothermaldf method sampleV_interpolate that allows velocities at different positions to be sampled efficiently (quasiisothermaldf.sampleV only works for a single position). See PR #350. - Add warnings/verbose configuration parameter to set level of verbosity of non-crucial warnings (like: "Using C implementation to integrate orbits"). - If astropy version > 3, Orbit.SkyCoord returns a SkyCoord object that includes the velocity information and the Galactocentric frame used by the Orbit instance. - Tweaked coordinate-transformations to Galactocentric coordinates to be consistent with astropy's. - Introduced general Force class of which Potential and DissipativeForce inherit, the former for forces that derive from a potential, the latter for those that do not. - Introduced DissipativeForce, a superclass for all dissipative forces. ChandrasekharDynamicalFrictionForce is currently the only class that inherits from DissipativeForce. - Re-arranged the package structure to better comply with the standard layout. All subpackages (e.g., galpy.potential) are now contained in subdirectories of the same name (e.g., galpy/potential/ rather than the old galpy/potential_src/). - Made the code fully compilable on Windows with MSVC and test Windows builds automatically on appveyor (#333). v1.3 (2018-02-06) ================== - Added a fast and precise method for approximating an orbit's eccentricity, peri- and apocenter radii, and maximum height above the midplane using the Staeckel approximation (see Mackereth & Bovy 2018); available as an actionAngle method EccZmaxRperiRap and for Orbit instances through the e, rperi, rap, and zmax methods. - Added support for potential wrappers---classes that wrap existing potentials to modify their behavior (#307). See the documentation on potentials and the potential API for more information on these. - Added DehnenSmoothWrapperPotential, a potential wrapper to smoothly grow a gravitational potential (e.g., a bar) following Dehnen (2000). - Added SolidBodyRotationWrapperPotential, a potential wrapper to make a gravitational potential rotate around the z axis with a fixed, uniform pattern speed. - Added DiskSCFPotential, a class that implements general density/potential pairs for disk potentials by combining Kuijken & Dubinski (1995)'s trick for turning a separable disk density [rho(R,z) = \Sigma(R) x h(z)] into a ellipsoidal-ish density, with the SCF basis-function-expansion approach of Hernquist & Ostriker (1992) for solving for the ellipsoidal-ish density's potential. This is a fully general Poisson solver (i.e., any density can be represented in this way). - Added SoftenedNeedleBarPotential, a simple potential for a bar that consists of a needle softened by a Miyamoto-Nagai kernel (Long & Murali 1992). - Added FerrersPotential, a class that implements the potential for triaxial ellipsoids (Binney & Tremaine 2008, Ch 2.5) with an option to give a pattern speed to the ellipsoid. - Added SpiralArmsPotential, a class that implements the spiral arms potential from Cox and Gomez (2002). https://arxiv.org/abs/astro-ph/0207635v1 (#305) - Added the Henon & Heiles (1964) potential - Added an Orbit method to display an animation of an integrated orbit in jupyter notebooks (Orbit.animate). - Added galpy.df.schwarzschilddf, the simple Schwarzschild distribution function for a razor-thin disk. - Upgraded LogarithmicHaloPotential to allow it to be a triaxial potential. - Upgraded DehnenBarPotential to a 3D potential following Monari et al. (2016) (#304). - Generalized CosmphiDiskPotential to allow for a break radius within which the radial dependence of the potential changes from R^p to R^-p; also added C implementation of CosmphiDiskPotential. - Changed default method for computing actions, frequencies, and angles for Orbit instances to be the Staeckel approximation with an automatically-estimated delta parameter. - Added an option to the estimateDeltaStaeckel function to facilitate the return of an estimated delta parameter at every phase space point passed, rather than returning a median of the estimate at each point. - Generalized actionAngleStaeckel to allow for different focal lengths delta for different phase-space points. Also allowed the order of the Gauss-Legendre integration to be specified (default: 10, which is good enough when using actionAngleStaeckel to compute approximate actions etc. for an axisymmetric potential). - Allow transformations of (ra,dec) and (pmra,pmdec) to custom coordinate systems. - Allow plotting of the spherical radius in Orbit.plot - Allow plotting of arbitrary user-supplied functions of time in Orbit.plot, Orbit.plot3d, and Orbit.animate. - Added conversion to and from oblate spheroidal coordinates. Also added conversion of velocities to the momenta in oblate or prolate spheroidal coordinates (but not the other way around). - Made SCFPotential compatible with GSL v2. - Orbit methods now all return a scalar when called with a single time (see #247 and #294). - Added a warning whenever the orbit-integration method that is used is different from the requested one (for when C integration falls back onto Python; see #286). - Turn off changing the seaborn default plot configuration to that preferred by this code's maintainer; can be turned back on by specifying ``seaborn-bovy-defaults = True`` in the configuration file (see documentation). Old configuration files will be automatically updated to add this option (but remain otherwise the same). - Added button to code examples in the documentation that copies the examples to the user's clipboard for easy pasting into a Python interpreter. - Switched test suite from nose to pytest (#303). - quasiisothermaldf.tilt return unit switched from deg to rad. - streamdf.misalignment return unit switched from deg to rad. - evolveddiskdf.vertexdev return unit switched from deg to rad. - In quasiisothermaldf.py, the following prefactors were added to the following functions: [nsigma^2/2] --> pvT(), [nsigma/2] --> pvRvT(), [nsigma/2] --> pvTvz(), [vTmax/2] --> pvRvz(). This change was required to correctly account for the Gauss-Legendre integration limits. v1.2 (2016-09-06) ================== - Added support for providing inputs to all initializations, methods, and functions as Quantities with units and for providing outputs as astropy Quantities. See pull request #258 and the documentation for full details. - Added TwoPowerTriaxialPotential, a set of triaxial potentials with iso-density contours that are arbitrary, similar, coaxial ellipsoids whose 'radial' density is a (different) power-law at small and large radii: 1/m^alpha/(1+m)^beta-alpha (the triaxial generalization of TwoPowerSphericalPotential, with flattening in the density rather than in the potential, see Merritt & Fridman 1996, Binney & Tremaine 2008). These include triaxial Hernquist and NFW potentials. Includes fast C implementations that allow efficient orbit integration in these potentials (<~5 ms / orbit). - Added SCFPotential, a class that implements general density/potential pairs through the basis expansion approach to solving the Poisson equation of Hernquist & Ostriker (1992). Also implemented functions to compute the coefficients for a given density function. - Implemented galpy.actionAngle.actionAngleIsochroneApprox (Bovy 2014) for the general case of a time-independent potential, allowing action-angle coordinates to be computed for triaxial potentials. Previously, this module only supported axisymmetric potentials. Also allow the maximum number of terms in the expansion to be set object-wide and allow a fixed time-step to be used in the necessary orbit integration. - Added an (experimental) interface to Binney & McMillan's TorusMapper code for computing positions and velocities for given actions and angles. See the installation instructions for how to properly install this. - Re-defined the amplitude parameters of a few potentials to allow for easier setups with amplitudes provided as Quantities with units. This is the case for FlattenedPowerPotential, PowerSphericalPotential, and PowerSphericalPotentialwCutoff, CosmphiDiskPotential, LopsidedDiskPotential, and EllipticalDiskPotential. - Added a module for modeling the effect of a subhalo impact on a tidal stream (galpy.df.streamgapdf); see Sanders, Bovy, & Erkal (2016). Also includes the fast methods for computing the density along the stream and the stream track for a perturbed stream from Bovy, Erkal, & Sanders (2016). - Added functions to galpy.df.streamdf to compute the length of the stream, the density along the stream quickly (in various coordinates, including custom sky coordinates), to compute the probability p(freq_parallel,angle_parallel), and to estimate the number of encounters with DM subhalos. Also allow the stream track to be computing using the new actionAngleTorus (this last addition is still very experimental). - Added PseudoIsothermalPotential, a standard pseudo-isothermal-sphere potential. - Added KuzminDiskPotential, a potential represented by a razor thin disk - Allow transformations to custom sets of sky coordinates in galpy.util.bovy_coords. - Added the spherical radius as an Orbit method. - Added the spherical-radial force as a Potential method (rforce) and function (evaluaterforces). - Support for flipping the velocities of an orbit in-place (useful for backwards integration). - quasiisothermaldf input ro replaced by refr to avoid clash with ro that specifies units (see above). - Properly intercept CTRL-C (SIGINT) signals during orbit integration in C, allowing long orbit integrations to be interrupted without killing an entire Python session. - Internally use astropy.coordinates transformations to transform between (ra,dec) and (l,b). Can be tuned using the astropy-coords configuration parameter. Also allow general epochs for the (ra,dec) specification. v1.1 (2015-06-30) ================== - Added Python 3 support: in particular, Python 3.3 and 3.4 are now supported; earlier 3.* may also work, but haven't been tested. Python 2 and 3 are supported using a single common codebase. - Added SnapshotRZPotential and InterpSnapshotRZPotential potential classes: these can be used to get a frozen snapshot of the potential of an N-body simulation into galpy. SnapshotRZPotential directly calculates the potential and forces using direct summation; InterpSnapshotRZPotential builds an interpolation table (it's a subclass of interpRZPotential and can be used in the same way). This code was mainly written by Rok Roskar. - Added KuzminKutuzovStaeckelPotential, a Staeckel potential that can be used to approximate the potential of a disk galaxy (see Batsleer & Dejonghe 1994). - Added MN3ExponentialDiskPotential that gives the approximation to a radially-exponential disk potential as three Miyamoto-Nagai disks from Smith et al. (2015; arXiv:1502.00627v1) - Added PlummerPotential, a standard Plummer potential. - Add support for converting potential parameters to NEMO format and units: nemo_accname and nemo_accpars (both instance and general Potential method). - Added nemo/ directory for C++ implementations of additional NEMO potentials (not included in the NEMO release); add PowSphwCut.cc for PowerSphericalPotential2Cutoff and Makefile to compile and install it. This allows gyrfalcON simulations with MWPotential2014. - Allow user to directly specify the integration step size to use in orbit integration. - Better implementations of vcirc, eipfreq, and verticalfreq for lists of potentials that allows for negative amplitude components. - Improvements to streamdf: add progIsTrack keyword to specify that the progenitor= keyword actually specifies the desired starting phase-space point of the track; the phase-space point of the progenitor is calculated. Refactored streamdf setup to make this easy to implement. - Orbit fitting can now be performed in custom sky coordinates (requires one to specify customsky=True and to implement and pass the functions (a) lb_to_customsky= and (b) pmllpmbb_to_customsky=, similar to lb_to_radec and pmllpmbb_to_pmradec). For example, this allows one to do an orbit fit in the GD-1 frame of Koposov et al. (2010). - Orbit.time now returns the times at which an Orbit is integrated if called without arguments. - Slightly improved implementation of actionAngleStaeckel.py (more robust umin/umax finding). v1.0 (2014-12-10) ================== - Added MWPotential2014: a Milky-Way-like potential fit to a bunch of relevant data on the Milky Way; supersedes MWPotential, which is still included; changed many examples to use this new potential; - Changed default integrator to symplec4_c; - Changed default vo and ro to 220 km/s and 8 kpc, respectively; - Allow Orbit initialization to specify a distance and velocity scale that is then used to output distances, velocities, energies, actions, etc. in physical coordinates; - Added an orbit-fitting routine (Orbit.fit). - Added virial radius of NFW potential, allow initialization of NFW potential with concentration and mass; - Added potential.mass to return the mass enclosed for individual potential instances; both spherical and axisymmetric potentials are covered. - Sped up many of the functions in galpy.util.bovy_coords by removing scipy.frompyfunc dependency; - Added galpy.util.bovy_conversion.dens_in_gevcc, galpy.util.bovy_conversion.force_in_10m13kms2, galpy.util.bovy_conversion.dens_in_criticaldens, galpy.util.bovy_conversion.dens_in_meanmatterdens. - Much improved test coverage. - Added planarPotential.LinShuReductionFactor to calculate the reduction factor for the response of a kinematically warm population to a spiral perturbation. - Added non-axisymmetric DFs to the top-level and the documentation. - New streamdf track calculation (see arXiv_v2/published version of Bovy 2014); better handdling of coordinate-transformation parameters in streamdf. - Installation now prints informative message about whether or not the C extensions were installed. - More informative warning messages when C functions are not used. - Fix issues with non-square grids in the C implementation of interpRZPotential. - Added Orbit.flip to generate new Orbit instances with flipped velocities. - Improved actionAngle frequencies for circular orbits. - Removed actionAngleFlat and actionAnglePower, because they are superseded. - Added hasC_dxdv member to Potential instances to indicate whether or not they can be used with integrate_dxdv - Add C implementation of PowerSphericalPotentialwCutoff; - Moved MovingObjectPotential, BurkertPotential, and interpRZPotential to top level; better implementation of interpRZPotential; - Better handling of vo,ro, and solarmotion in Orbit instances (Orbit initialization value is now used to generate RA, Dec, etc. outputs); - Added --no-openmp installation option to allow installation without OpenMP support (workaround for clang not supporting OpenMP; not necessary anymore); v0.1 (2014-01-09) ================== - Initial release ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/LICENSE0000644000175100001710000000270614327773303013257 0ustar00runnerdockerCopyright (c) 2010, Jo Bovy All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/MANIFEST.in0000644000175100001710000000062014327773303014001 0ustar00runnerdockerinclude README.md README.dev README.nemo LICENSE HISTORY.txt AUTHORS.txt include gsl-config.bat include galpy/df/data/*.sav include galpy/orbit/named_objects.json include galpy/actionAngle/actionAngle_c_ext/*.h include galpy/actionAngle/actionAngleTorus_c_ext/*.h include galpy/orbit/orbit_c_ext/*.h include galpy/potential/potential_c_ext/*.h include galpy/util/*.h include galpy/util/interp_2d/*.h ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667233486.721467 galpy-1.8.1/PKG-INFO0000644000175100001710000001352414327773317013354 0ustar00runnerdockerMetadata-Version: 2.1 Name: galpy Version: 1.8.1 Summary: Galactic Dynamics in python Home-page: http://github.com/jobovy/galpy Author: Jo Bovy Author-email: bovy@astro.utoronto.ca License: New BSD Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: C Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Scientific/Engineering :: Astronomy Classifier: Topic :: Scientific/Engineering :: Physics Description-Content-Type: text/markdown License-File: LICENSE License-File: AUTHORS.txt

Galactic Dynamics in python

[galpy](http://www.galpy.org) is a Python package for galactic dynamics. It supports orbit integration in a variety of potentials, evaluating and sampling various distribution functions, and the calculation of action-angle coordinates for all static potentials. `galpy` is an [astropy](http://www.astropy.org/) [affiliated package](http://www.astropy.org/affiliated/) and provides full support for astropy’s [Quantity](http://docs.astropy.org/en/stable/api/astropy.units.Quantity.html) framework for variables with units. AUTHOR ====== Jo Bovy - bovy at astro dot utoronto dot ca See [AUTHORS.txt](https://github.com/jobovy/galpy/blob/main/AUTHORS.txt) for a full list of contributors. If you find this code useful in your research, please let me know. **If you use galpy in a publication, please cite** [Bovy (2015)](http://adsabs.harvard.edu/abs/2015ApJS..216...29B) **and link to http://github.com/jobovy/galpy**. See [the acknowledgement documentation section](http://docs.galpy.org/en/latest/index.html#acknowledging-galpy) for a more detailed guide to citing parts of the code. Thanks! LOOKING FOR HELP? ================= The latest documentation can be found [here](http://docs.galpy.org/en/latest/). You can also join the [galpy slack community](https://galpy.slack.com/) for any questions related to `galpy`; join [here](https://join.slack.com/t/galpy/shared_invite/zt-p6upr4si-mX7u8MRdtm~3bW7o8NA_Ww). If you find *any* bug in the code, please report these using the [Issue Tracker](http://github.com/jobovy/galpy/issues) or by joining the [galpy slack community](https://galpy.slack.com/). If you are having issues with the installation of `galpy`, please first consult the [Installation FAQ](http://docs.galpy.org/en/latest/installation.html#installation-faq). PYTHON VERSIONS AND DEPENDENCIES ================================ `galpy` supports Python 3. Specifically, galpy supports Python 3.7, 3.8, 3.9, 3.10, and 3.11. GitHub Actions CI builds regularly check support for Python 3.10 (and of 3.7, 3.8, and 3.9 using a more limited, core set of tests) on Linux and Windows (and 3.10 on Mac OS); Appveyor builds regularly check support for Python 3.10 on Windows. Python 2.7 is no longer supported. This package requires [Numpy](https://numpy.org/), [Scipy](http://www.scipy.org/), and [Matplotlib](http://matplotlib.sourceforge.net/). Certain advanced features require the GNU Scientific Library ([GSL](http://www.gnu.org/software/gsl/)), with action calculations requiring version 1.14 or higher. Other optional dependencies include: * Support for providing inputs and getting outputs as Quantities with units is provided through [`astropy`](http://www.astropy.org/). * Querying SIMBAD for the coordinates of an object in the `Orbit.from_name` initialization method requires [`astroquery`](https://astroquery.readthedocs.io/en/latest/). * Displaying a progress bar for certain operations (e.g., orbit integration of multiple objects at once) requires [`tqdm`](https://github.com/tqdm/tqdm). * Plotting arbitrary functions of Orbit attributes requires [`numexpr`](https://github.com/pydata/numexpr). * Speeding up the evaluation of certain functions in the C code requires [`numba`](https://numba.pydata.org/). * Constant-anisotropy DFs in `galpy.df.constantbetadf` require [`JAX`](https://github.com/google/jax). * Use of `SnapshotRZPotential` and `InterpSnapshotRZPotential` requires [`pynbody`](https://github.com/pynbody/pynbody). Other parts of the code may require additional packages and you will be alerted by the code if they are not installed. CONTRIBUTING TO GALPY ===================== If you are interested in contributing to galpy\'s development, take a look at [this brief guide](https://github.com/jobovy/galpy/wiki/Guide-for-new-contributors) on the wiki. This will hopefully help you get started! Some further development notes can be found on the [wiki](http://github.com/jobovy/galpy/wiki/). This includes a list of small and larger extensions of galpy that would be useful [here](http://github.com/jobovy/galpy/wiki/Possible-galpy-extensions) as well as a longer-term roadmap [here](http://github.com/jobovy/galpy/wiki/Roadmap). Please let the main developer know if you need any help contributing! DISK DF CORRECTIONS =================== The dehnendf and shudf disk distribution functions can be corrected to follow the desired surface-mass density and radial-velocity-dispersion profiles more closely (see [1999AJ\....118.1201D](http://adsabs.harvard.edu/abs/1999AJ....118.1201D)). Calculating these corrections is expensive, and a large set of precalculated corrections can be found [here](http://github.com/downloads/jobovy/galpy/galpy-dfcorrections.tar.gz) \[tar.gz archive\]. Install these by downloading them and unpacking them into the galpy/df/data directory before running the setup.py installation. E.g.: curl -O https://github.s3.amazonaws.com/downloads/jobovy/galpy/galpy-dfcorrections.tar.gz tar xvzf galpy-dfcorrections.tar.gz -C ./galpy/df/data/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/README.dev0000644000175100001710000000275614327773303013714 0ustar00runnerdockerDevelop README ============== Adding a potential to the C integrator -------------------------------------- 1) Implement the potential in a .c file under potential/potential_c_ext. Look at potential/potential_c_ext/LogarithmicHaloPotential.c for the right format 2) Add your new potential to potential/potential_c_ext/galpy_potentials.h 3) Edit the code under orbit/orbit_c_ext/integratePlanarOrbit.c to set up your new potential (in the 'parse_leapFuncArgs' function) 4) Edit the code in orbit/integratePlanarOrbit.py to set up your new potential 5) Edit the code under orbit/orbit_c_ext/integrateFullOrbit.c to set up your new potential (in the 'parse_leapFuncArgs_Full' function) 6) Edit the code in orbit/integrateFullOrbit.py to set up your new potential 7) Edit the code in actionAngle/actionAngle_c_ext/actionAngle.c to parse the new potential 8) Finally, add 'self.hasC= True' to the initialization of the potential in question (after the initialization of the super class) 9) It should work now! 10) If you implement the second derivatives of the potential necessary to integrate phase-space volumes, also set self.hasC_dxdv=True to the initialization of the potential in question. 11) If you add a potential that gets passed to C as a list, you need to edit orbit/integrateLinearOrbit.py and orbit/orbit_c_ext/integrateLinearPotential.c to parse it properly (for regular 3D potentials this works out of the box). 12) If you add a 1D potential, do the steps above, but for integrateLinearOrbit.* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/README.md0000644000175100001710000001564514327773303013537 0ustar00runnerdocker


Galactic Dynamics in python

[galpy](http://www.galpy.org) is a Python package for galactic dynamics. It supports orbit integration in a variety of potentials, evaluating and sampling various distribution functions, and the calculation of action-angle coordinates for all static potentials. `galpy` is an [astropy](http://www.astropy.org/) [affiliated package](http://www.astropy.org/affiliated/) and provides full support for astropy’s [Quantity](http://docs.astropy.org/en/stable/api/astropy.units.Quantity.html) framework for variables with units. [![image](https://github.com/jobovy/galpy/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/jobovy/galpy/actions/workflows/build.yml) [![image](https://github.com/jobovy/galpy/actions/workflows/build_windows.yml/badge.svg?branch=main)](https://github.com/jobovy/galpy/actions/workflows/build_windows.yml) [![image](https://ci.appveyor.com/api/projects/status/wmgs1sq3i7tbtap2/branch/main?svg=true)](https://ci.appveyor.com/project/jobovy/galpy) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/jobovy/galpy/main.svg)](https://results.pre-commit.ci/latest/github/jobovy/galpy/main) [![image](http://codecov.io/github/jobovy/galpy/coverage.svg?branch=main)](http://codecov.io/github/jobovy/galpy?branch=main) [![image](https://readthedocs.org/projects/galpy/badge/?version=latest)](http://docs.galpy.org/en/latest/) [![image](http://img.shields.io/pypi/v/galpy.svg)](https://pypi.python.org/pypi/galpy/) [![image](https://img.shields.io/pypi/pyversions/galpy?logo=python&logoColor=white)](https://pypi.python.org/pypi/galpy/) [![image](https://anaconda.org/conda-forge/galpy/badges/installer/conda.svg)](https://anaconda.org/conda-forge/galpy) [![image](https://img.shields.io/github/commits-since/jobovy/galpy/latest)](https://github.com/jobovy/galpy/commits/main) [![image](http://img.shields.io/badge/license-New%20BSD-brightgreen.svg)](https://github.com/jobovy/galpy/blob/main/LICENSE) [![image](http://img.shields.io/badge/DOI-10.1088/0067%2D%2D0049/216/2/29-blue.svg)](http://dx.doi.org/10.1088/0067-0049/216/2/29) [![image](http://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat)](http://www.astropy.org/) [![image](https://img.shields.io/badge/join-slack-E01563.svg?style=flat&logo=slack&logoWidth=10)](https://join.slack.com/t/galpy/shared_invite/zt-p6upr4si-mX7u8MRdtm~3bW7o8NA_Ww) AUTHOR ====== Jo Bovy - bovy at astro dot utoronto dot ca See [AUTHORS.txt](https://github.com/jobovy/galpy/blob/main/AUTHORS.txt) for a full list of contributors. If you find this code useful in your research, please let me know. **If you use galpy in a publication, please cite** [Bovy (2015)](http://adsabs.harvard.edu/abs/2015ApJS..216...29B) **and link to http://github.com/jobovy/galpy**. See [the acknowledgement documentation section](http://docs.galpy.org/en/latest/index.html#acknowledging-galpy) for a more detailed guide to citing parts of the code. Thanks! LOOKING FOR HELP? ================= The latest documentation can be found [here](http://docs.galpy.org/en/latest/). You can also join the [galpy slack community](https://galpy.slack.com/) for any questions related to `galpy`; join [here](https://join.slack.com/t/galpy/shared_invite/zt-p6upr4si-mX7u8MRdtm~3bW7o8NA_Ww). If you find *any* bug in the code, please report these using the [Issue Tracker](http://github.com/jobovy/galpy/issues) or by joining the [galpy slack community](https://galpy.slack.com/). If you are having issues with the installation of `galpy`, please first consult the [Installation FAQ](http://docs.galpy.org/en/latest/installation.html#installation-faq). PYTHON VERSIONS AND DEPENDENCIES ================================ `galpy` supports Python 3. Specifically, galpy supports Python 3.7, 3.8, 3.9, 3.10, and 3.11. GitHub Actions CI builds regularly check support for Python 3.10 (and of 3.7, 3.8, and 3.9 using a more limited, core set of tests) on Linux and Windows (and 3.10 on Mac OS); Appveyor builds regularly check support for Python 3.10 on Windows. Python 2.7 is no longer supported. This package requires [Numpy](https://numpy.org/), [Scipy](http://www.scipy.org/), and [Matplotlib](http://matplotlib.sourceforge.net/). Certain advanced features require the GNU Scientific Library ([GSL](http://www.gnu.org/software/gsl/)), with action calculations requiring version 1.14 or higher. Other optional dependencies include: * Support for providing inputs and getting outputs as Quantities with units is provided through [`astropy`](http://www.astropy.org/). * Querying SIMBAD for the coordinates of an object in the `Orbit.from_name` initialization method requires [`astroquery`](https://astroquery.readthedocs.io/en/latest/). * Displaying a progress bar for certain operations (e.g., orbit integration of multiple objects at once) requires [`tqdm`](https://github.com/tqdm/tqdm). * Plotting arbitrary functions of Orbit attributes requires [`numexpr`](https://github.com/pydata/numexpr). * Speeding up the evaluation of certain functions in the C code requires [`numba`](https://numba.pydata.org/). * Constant-anisotropy DFs in `galpy.df.constantbetadf` require [`JAX`](https://github.com/google/jax). * Use of `SnapshotRZPotential` and `InterpSnapshotRZPotential` requires [`pynbody`](https://github.com/pynbody/pynbody). Other parts of the code may require additional packages and you will be alerted by the code if they are not installed. CONTRIBUTING TO GALPY ===================== If you are interested in contributing to galpy\'s development, take a look at [this brief guide](https://github.com/jobovy/galpy/wiki/Guide-for-new-contributors) on the wiki. This will hopefully help you get started! Some further development notes can be found on the [wiki](http://github.com/jobovy/galpy/wiki/). This includes a list of small and larger extensions of galpy that would be useful [here](http://github.com/jobovy/galpy/wiki/Possible-galpy-extensions) as well as a longer-term roadmap [here](http://github.com/jobovy/galpy/wiki/Roadmap). Please let the main developer know if you need any help contributing! DISK DF CORRECTIONS =================== The dehnendf and shudf disk distribution functions can be corrected to follow the desired surface-mass density and radial-velocity-dispersion profiles more closely (see [1999AJ\....118.1201D](http://adsabs.harvard.edu/abs/1999AJ....118.1201D)). Calculating these corrections is expensive, and a large set of precalculated corrections can be found [here](http://github.com/downloads/jobovy/galpy/galpy-dfcorrections.tar.gz) \[tar.gz archive\]. Install these by downloading them and unpacking them into the galpy/df/data directory before running the setup.py installation. E.g.: curl -O https://github.s3.amazonaws.com/downloads/jobovy/galpy/galpy-dfcorrections.tar.gz tar xvzf galpy-dfcorrections.tar.gz -C ./galpy/df/data/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/README.nemo0000644000175100001710000000222514327773303014063 0ustar00runnerdockerAdding potentials in NEMO format ================================= NEMO (http://bima.astro.umd.edu/nemo/) uses potentials by compiling each potential into a shared-object library (*.so) that can then be dynamically loaded at runtime. Therefore, adding a potential just requires one to compile a new library for the potential in a way that NEMO can pass parameters to it. The directory nemo/ contains C++ implementations of additional potentials that are not included in the NEMO release. To add a new potential to this, do: 1) Use nemo/PowSphwCut.cc as a template and implement your new potential based on this and store it in the nemo/ directory; 2) Add the new potential to nemo/Makefile in a similar manner as the PowSphwCut.so target (if you need additional libraries, make sure to link to these). Also add this target to the 'all' target near the top. 3) With NEMO running (having sourced nemo_start), such that the NEMO environment variables are defined, you can then install the new potential by running make in the nemo directory. 4) If it works, consider contributing this potential to the galpy codebase by adding it to a fork and opening a pull request! ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667233486.697467 galpy-1.8.1/galpy/0000755000175100001710000000000014327773317013366 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/__init__.py0000644000175100001710000000664714327773303015507 0ustar00runnerdocker__version__ = "1.8.1" # Check whether a new version is available import datetime import http.client import platform import subprocess import sys from packaging.version import Version from packaging.version import parse as parse_version from .util.config import (__config__, __orig__config__, configfilename, write_config) def latest_pypi_version(name): # First check whether there's internet, code below this works w/o # but is very slow without internet, so this is a quick check def online(): try: conn= http.client.HTTPSConnection("8.8.8.8",timeout=5) conn.request("HEAD","/") return True except Exception: # pragma: no cover return False finally: conn.close() if not online(): # pragma: no cover return __version__ # Essentially from https://stackoverflow.com/a/58649262 try: # Wrap everything in try/except to avoid any issues with connections latest_version= str(subprocess.run([sys.executable, '-m', 'pip', 'install', f'{name}==random'], capture_output=True, text=True)) latest_version= latest_version[latest_version.find('(from versions:')+15:] latest_version= latest_version[:latest_version.find(')')] latest_version= latest_version.replace(' ','').split(',') # Remove any pre-releases latest_version= [v for v in latest_version if not Version(v).is_prerelease] # Latest is now the final one latest_version= latest_version[-1] except Exception: # pragma: no cover return __version__ else: # Offline can return 'none', then just return __version__ to avoid warning return __version__ if latest_version == 'none' else latest_version def check_pypi_version(name): latest_version= latest_pypi_version(name) if parse_version(latest_version) > parse_version(__version__): # pragma: no cover return True else: return False def print_version_warning(): # pragma: no cover print("\033[91mA new version of galpy ({}) is available, please upgrade using pip/conda/... to get the latest features and bug fixes!\033[0m".format(latest_pypi_version("galpy"))) _CHECK_VERSION_UPGRADE= __config__.getboolean('version-check','do-check') \ and not platform.system() == 'Emscripten' if _CHECK_VERSION_UPGRADE and hasattr(sys,'ps1'): # pragma: no cover # Interactive session, https://stackoverflow.com/a/64523765 if check_pypi_version("galpy"): print_version_warning() elif _CHECK_VERSION_UPGRADE \ and __config__.getboolean('version-check','check-non-interactive'): # Non-interactive session, only check once every # 'check-non-interactive-every' days today= datetime.date.today() last_check= datetime.date.fromisoformat(\ __config__.get('version-check','last-non-interactive-check')) if today-last_check >= \ datetime.timedelta(days=__config__.getint('version-check', 'check-non-interactive-every')): if check_pypi_version("galpy"): # pragma: no cover print_version_warning() # Also write the date of the last check to the configuration file __orig__config__.set('version-check','last-non-interactive-check',today.isoformat()) write_config(configfilename,__orig__config__) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667233486.697467 galpy-1.8.1/galpy/actionAngle/0000755000175100001710000000000014327773317015612 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/__init__.py0000644000175100001710000000316114327773303017717 0ustar00runnerdockerfrom . import (actionAngle, actionAngleAdiabatic, actionAngleAdiabaticGrid, actionAngleAxi, actionAngleHarmonic, actionAngleHarmonicInverse, actionAngleInverse, actionAngleIsochrone, actionAngleIsochroneApprox, actionAngleIsochroneInverse, actionAngleSpherical, actionAngleStaeckel, actionAngleStaeckelGrid, actionAngleTorus) # # Exceptions # UnboundError= actionAngle.UnboundError # # Functions # estimateDeltaStaeckel= actionAngleStaeckel.estimateDeltaStaeckel estimateBIsochrone= actionAngleIsochroneApprox.estimateBIsochrone dePeriod= actionAngleIsochroneApprox.dePeriod # # Classes # actionAngle= actionAngle.actionAngle actionAngleInverse= actionAngleInverse.actionAngleInverse actionAngleAxi= actionAngleAxi.actionAngleAxi actionAngleAdiabatic= actionAngleAdiabatic.actionAngleAdiabatic actionAngleAdiabaticGrid= actionAngleAdiabaticGrid.actionAngleAdiabaticGrid actionAngleStaeckelSingle= actionAngleStaeckel.actionAngleStaeckelSingle actionAngleStaeckel= actionAngleStaeckel.actionAngleStaeckel actionAngleStaeckelGrid= actionAngleStaeckelGrid.actionAngleStaeckelGrid actionAngleIsochrone= actionAngleIsochrone.actionAngleIsochrone actionAngleIsochroneApprox=\ actionAngleIsochroneApprox.actionAngleIsochroneApprox actionAngleSpherical= actionAngleSpherical.actionAngleSpherical actionAngleTorus= actionAngleTorus.actionAngleTorus actionAngleIsochroneInverse= actionAngleIsochroneInverse.actionAngleIsochroneInverse actionAngleHarmonic= actionAngleHarmonic.actionAngleHarmonic actionAngleHarmonicInverse= actionAngleHarmonicInverse.actionAngleHarmonicInverse ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngle.py0000644000175100001710000002537714327773303020421 0ustar00runnerdockerimport copy import types import numpy from ..util import config, conversion from ..util.conversion import (actionAngle_physical_input, physical_compatible, physical_conversion_actionAngle) # Metaclass for copying docstrings from subclass methods, first func # to copy func def copyfunc(func): return types.FunctionType(func.__code__,func.__globals__, name=func.__name__, argdefs=func.__defaults__, closure=func.__closure__) class MetaActionAngle(type): """Metaclass to assign subclass' docstrings for methods _evaluate, _actionsFreqs, _actionsFreqsAngles, and _EccZmaxRperiRap to their public cousins __call__, actionsFreqs, etc.""" def __new__(meta,name,bases,attrs): for key in copy.copy(attrs): # copy bc size changes if key[0] == '_': skey= copy.copy(key[1:]) if skey == 'evaluate': skey= '__call__' for base in bases: original= getattr(base,skey,None) if original is not None: funccopy= copyfunc(original) funccopy.__doc__= attrs[key].__doc__ attrs[skey]= funccopy break return type.__new__(meta,name,bases,attrs) # Python 2 & 3 compatible way to have a metaclass class actionAngle(metaclass=MetaActionAngle): """Top-level class for actionAngle classes""" def __init__(self,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize an actionAngle object INPUT: ro= (None) distance scale vo= (None) velocity scale OUTPUT: HISTORY: 2016-02-18 - Written - Bovy (UofT) """ # Parse ro and vo if ro is None: self._ro= config.__config__.getfloat('normalization','ro') self._roSet= False else: self._ro= conversion.parse_length_kpc(ro) self._roSet= True if vo is None: self._vo= config.__config__.getfloat('normalization','vo') self._voSet= False else: self._vo= conversion.parse_velocity_kms(vo) self._voSet= True return None def _check_consistent_units(self): """Internal function to check that the set of units for this object is consistent with that for the potential""" assert physical_compatible(self,self._pot), 'Physical conversion for the actionAngle object is not consistent with that of the Potential given to it' def _check_consistent_units_orbitInput(self,orb): """Internal function to check that the set of units for this object is consistent with that for an input orbit""" assert physical_compatible(self,orb), 'Physical conversion for the actionAngle object is not consistent with that of the Orbit given to it' def turn_physical_off(self): """ NAME: turn_physical_off PURPOSE: turn off automatic returning of outputs in physical units INPUT: (none) OUTPUT: (none) HISTORY: 2017-06-05 - Written - Bovy (UofT) """ self._roSet= False self._voSet= False return None def turn_physical_on(self,ro=None,vo=None): """ NAME: turn_physical_on PURPOSE: turn on automatic returning of outputs in physical units INPUT: ro= reference distance (kpc; can be Quantity) vo= reference velocity (km/s; can be Quantity) OUTPUT: (none) HISTORY: 2016-06-05 - Written - Bovy (UofT) 2020-04-22 - Don't turn on a parameter when it is False - Bovy (UofT) """ if not ro is False: self._roSet= True if not vo is False: self._voSet= True if not ro is None and ro: self._ro= conversion.parse_length_kpc(ro) if not vo is None and vo: self._vo= conversion.parse_velocity_kms(vo) return None def _parse_eval_args(self,*args,**kwargs): """ NAME: _parse_eval_args PURPOSE: Internal function to parse the arguments given for an action/frequency/angle evaluation INPUT: OUTPUT: HISTORY: 2010-07-11 - Written - Bovy (NYU) """ if len(args) == 3: #R, vR, vT R,vR,vT= args self._eval_R= R self._eval_vR= vR self._eval_vT= vT self._eval_z= numpy.zeros_like(R) self._eval_vz= numpy.zeros_like(R) elif len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args self._eval_R= R self._eval_vR= vR self._eval_vT= vT self._eval_z= z self._eval_vz= vz elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args self._eval_R= R self._eval_vR= vR self._eval_vT= vT self._eval_z= z self._eval_vz= vz self._eval_phi= phi else: # Orbit instance if not kwargs.get('_noOrbUnitsCheck',False): self._check_consistent_units_orbitInput(args[0]) if len(args) == 2: orb= args[0](args[1]) else: orb= args[0] if len(orb.shape) > 1: raise RuntimeError("Evaluating actionAngle methods with Orbit instances with multi-dimensional shapes is not supported") self._eval_R= orb.R(use_physical=False) self._eval_vR= orb.vR(use_physical=False) self._eval_vT= orb.vT(use_physical=False) if args[0].phasedim() > 4: self._eval_z= orb.z(use_physical=False) self._eval_vz= orb.vz(use_physical=False) if args[0].phasedim() > 5: self._eval_phi= orb.phi(use_physical=False) else: if args[0].phasedim() > 3: self._eval_phi= orb.phi(use_physical=False) self._eval_z= numpy.zeros_like(self._eval_R) self._eval_vz= numpy.zeros_like(self._eval_R) if hasattr(self,'_eval_z'): #calculate the polar angle self._eval_theta= numpy.arctan2(self._eval_R,self._eval_z) return None @actionAngle_physical_input @physical_conversion_actionAngle('__call__',pop=True) def __call__(self,*args,**kwargs): """ NAME: __call__ PURPOSE: evaluate the actions (jr,lz,jz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument OUTPUT: (jr,lz,jz) HISTORY: 2014-01-03 - Written for top level - Bovy (IAS) """ try: return self._evaluate(*args,**kwargs) except AttributeError: #pragma: no cover raise NotImplementedError("'__call__' method not implemented for this actionAngle module") @actionAngle_physical_input @physical_conversion_actionAngle('actionsFreqs',pop=True) def actionsFreqs(self,*args,**kwargs): """ NAME: actionsFreqs PURPOSE: evaluate the actions and frequencies (jr,lz,jz,Omegar,Omegaphi,Omegaz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument OUTPUT: (jr,lz,jz,Omegar,Omegaphi,Omegaz) HISTORY: 2014-01-03 - Written for top level - Bovy (IAS) """ try: return self._actionsFreqs(*args,**kwargs) except AttributeError: #pragma: no cover raise NotImplementedError("'actionsFreqs' method not implemented for this actionAngle module") @actionAngle_physical_input @physical_conversion_actionAngle('actionsFreqsAngles',pop=True) def actionsFreqsAngles(self,*args,**kwargs): """ NAME: actionsFreqsAngles PURPOSE: evaluate the actions, frequencies, and angles (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) INPUT: Either: a) R,vR,vT,z,vz,phi: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument OUTPUT: (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) HISTORY: 2014-01-03 - Written for top level - Bovy (IAS) """ try: return self._actionsFreqsAngles(*args,**kwargs) except AttributeError: #pragma: no cover raise NotImplementedError("'actionsFreqsAngles' method not implemented for this actionAngle module") @actionAngle_physical_input @physical_conversion_actionAngle('EccZmaxRperiRap',pop=True) def EccZmaxRperiRap(self,*args,**kwargs): """ NAME: EccZmaxRperiRap PURPOSE: evaluate the eccentricity, maximum height above the plane, peri- and apocenter INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument OUTPUT: (e,zmax,rperi,rap) HISTORY: 2017-12-12 - Written - Bovy (UofT) """ try: return self._EccZmaxRperiRap(*args,**kwargs) except AttributeError: #pragma: no cover raise NotImplementedError("'EccZmaxRperiRap' method not implemented for this actionAngle module") class UnboundError(Exception): #pragma: no cover def __init__(self, value): self.value = value def __str__(self): return repr(self.value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleAdiabatic.py0000644000175100001710000002722414327773303022174 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleAdiabatic # # wrapper around actionAngleAxi (adiabatic approximation) to do # this for any (x,v) # # methods: # __call__: returns (jr,lz,jz) # ############################################################################### import copy import warnings import numpy from ..potential import MWPotential, toPlanarPotential, toVerticalPotential from ..potential.Potential import _check_c from ..potential.Potential import flatten as flatten_potential from ..util import galpyWarning from . import actionAngleAdiabatic_c from .actionAngle import actionAngle from .actionAngleAdiabatic_c import _ext_loaded as ext_loaded from .actionAngleAxi import actionAngleAxi class actionAngleAdiabatic(actionAngle): """Action-angle formalism for axisymmetric potentials using the adiabatic approximation""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleAdiabatic object INPUT: pot= potential or list of potentials (planarPotentials) gamma= (default=1.) replace Lz by Lz+gamma Jz in effective potential ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) OUTPUT: instance HISTORY: 2012-07-26 - Written - Bovy (IAS@MPIA) """ actionAngle.__init__(self, ro=kwargs.get('ro',None),vo=kwargs.get('vo',None)) if not 'pot' in kwargs: #pragma: no cover raise OSError("Must specify pot= for actionAngleAxi") self._pot= flatten_potential(kwargs['pot']) if self._pot == MWPotential: warnings.warn("Use of MWPotential as a Milky-Way-like potential is deprecated; galpy.potential.MWPotential2014, a potential fit to a large variety of dynamical constraints (see Bovy 2015), is the preferred Milky-Way-like potential in galpy", galpyWarning) if ext_loaded and 'c' in kwargs and kwargs['c']: self._c= _check_c(self._pot) if 'c' in kwargs and kwargs['c'] and not self._c: warnings.warn("C module not used because potential does not have a C implementation",galpyWarning) #pragma: no cover else: self._c= False self._gamma= kwargs.get('gamma',1.) # Check the units self._check_consistent_units() return None def _evaluate(self,*args,**kwargs): """ NAME: __call__ (_evaluate) PURPOSE: evaluate the actions (jr,lz,jz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument c= (object-wide default, bool) True/False to override the object-wide setting for whether or not to use the C implementation scipy.integrate.quadrature keywords _justjr, _justjz= if True, only calculate the radial or vertical action (internal use) OUTPUT: (jr,lz,jz) HISTORY: 2012-07-26 - Written - Bovy (IAS@MPIA) """ if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) if ((self._c and not ('c' in kwargs and not kwargs['c']))\ or (ext_loaded and ('c' in kwargs and kwargs['c']))) \ and _check_c(self._pot): Lz= R*vT jr, jz, err= actionAngleAdiabatic_c.actionAngleAdiabatic_c(\ self._pot,self._gamma,R,vR,vT,z,vz) if err == 0: return (jr,Lz,jz) else: #pragma: no cover raise RuntimeError("C-code for calculation actions failed; try with c=False") else: if 'c' in kwargs and kwargs['c'] and not self._c: warnings.warn("C module not used because potential does not have a C implementation",galpyWarning) #pragma: no cover kwargs.pop('c',None) if len(R) > 1: ojr= numpy.zeros(len(R)) olz= numpy.zeros(len(R)) ojz= numpy.zeros(len(R)) for ii in range(len(R)): targs= (R[ii],vR[ii],vT[ii],z[ii],vz[ii]) tjr,tlz,tjz= self(*targs,**copy.copy(kwargs)) ojr[ii]= tjr ojz[ii]= tjz olz[ii]= tlz return (ojr,olz,ojz) else: #Set up the actionAngleAxi object thispot= toPlanarPotential(self._pot) thisverticalpot= toVerticalPotential(self._pot,R[0]) aAAxi= actionAngleAxi(R[0],vR[0],vT[0],z[0],vz[0], pot=thispot, verticalPot=thisverticalpot, gamma=self._gamma) if kwargs.get('_justjr',False): kwargs.pop('_justjr') return (aAAxi.JR(**kwargs),numpy.nan,numpy.nan) elif kwargs.get('_justjz',False): kwargs.pop('_justjz') return (numpy.atleast_1d(numpy.nan), numpy.atleast_1d(numpy.nan), numpy.atleast_1d(aAAxi.Jz(**kwargs))) else: return (numpy.atleast_1d(aAAxi.JR(**kwargs)), numpy.atleast_1d(aAAxi._R*aAAxi._vT), numpy.atleast_1d(aAAxi.Jz(**kwargs))) def _EccZmaxRperiRap(self,*args,**kwargs): """ NAME: EccZmaxRperiRap (_EccZmaxRperiRap) PURPOSE: evaluate the eccentricity, maximum height above the plane, peri- and apocenter in the adiabatic approximation INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument c= (object-wide default, bool) True/False to override the object-wide setting for whether or not to use the C implementation OUTPUT: (e,zmax,rperi,rap) HISTORY: 2017-12-21 - Written - Bovy (UofT) """ if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) if ((self._c and not ('c' in kwargs and not kwargs['c']))\ or (ext_loaded and ('c' in kwargs and kwargs['c']))) \ and _check_c(self._pot): rperi,Rap,zmax, err= actionAngleAdiabatic_c.actionAngleRperiRapZmaxAdiabatic_c(\ self._pot,self._gamma,R,vR,vT,z,vz) if err == 0: rap= numpy.sqrt(Rap**2.+zmax**2.) ecc= (rap-rperi)/(rap+rperi) return (ecc,zmax,rperi,rap) else: #pragma: no cover raise RuntimeError("C-code for calculation actions failed; try with c=False") else: if 'c' in kwargs and kwargs['c'] and not self._c: warnings.warn("C module not used because potential does not have a C implementation",galpyWarning) #pragma: no cover kwargs.pop('c',None) if len(R) > 1: oecc= numpy.zeros(len(R)) orperi= numpy.zeros(len(R)) orap= numpy.zeros(len(R)) ozmax= numpy.zeros(len(R)) for ii in range(len(R)): targs= (R[ii],vR[ii],vT[ii],z[ii],vz[ii]) tecc, tzmax, trperi,trap= self._EccZmaxRperiRap(\ *targs,**copy.copy(kwargs)) oecc[ii]= tecc ozmax[ii]= tzmax orperi[ii]= trperi orap[ii]= trap return (oecc,ozmax,orperi,orap) else: #Set up the actionAngleAxi object thispot= toPlanarPotential(self._pot) thisverticalpot= toVerticalPotential(self._pot,R[0]) aAAxi= actionAngleAxi(R[0],vR[0],vT[0],z[0],vz[0], pot=thispot, verticalPot=thisverticalpot, gamma=self._gamma) rperi,Rap= aAAxi.calcRapRperi(**kwargs) zmax= aAAxi.calczmax(**kwargs) rap= numpy.sqrt(Rap**2.+zmax**2.) return (numpy.atleast_1d((rap-rperi)/(rap+rperi)), numpy.atleast_1d(zmax),numpy.atleast_1d(rperi), numpy.atleast_1d(rap)) def calcRapRperi(self,*args,**kwargs): """ NAME: calcRapRperi PURPOSE: calculate the apocenter and pericenter radii INPUT: Either: a) R,vR,vT,z,vz b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well OUTPUT: (rperi,rap) HISTORY: 2013-11-27 - Written - Bovy (IAS) """ #Set up the actionAngleAxi object thispot= toPlanarPotential(self._pot) aAAxi= actionAngleAxi(*args,pot=thispot,amma=self._gamma) return aAAxi.calcRapRperi(**kwargs) def calczmax(self,*args,**kwargs): #pragma: no cover """ NAME: calczmax PURPOSE: calculate the maximum height INPUT: Either: a) R,vR,vT,z,vz b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well OUTPUT: zmax HISTORY: 2012-06-01 - Written - Bovy (IAS) """ warnings.warn("actionAngleAdiabatic.calczmax function will soon be deprecated; please contact galpy's maintainer if you require this function") #Set up the actionAngleAxi object self._parse_eval_args(*args) thispot= toPlanarPotential(self._pot) thisverticalpot= toVerticalPotential(self._pot,self._eval_R) aAAxi= actionAngleAxi(*args,pot=thispot, verticalPot=thisverticalpot, gamma=self._gamma) return aAAxi.calczmax(**kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleAdiabaticGrid.py0000644000175100001710000004202614327773303022777 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleAdiabaticGrid # # build grid in integrals of motion to quickly evaluate # actionAngleAdiabatic # # methods: # __call__: returns (jr,lz,jz) # ############################################################################### import numpy from scipy import interpolate from .. import potential from ..potential.Potential import _evaluatePotentials from ..potential.Potential import flatten as flatten_potential from ..util import multi from .actionAngle import UnboundError, actionAngle from .actionAngleAdiabatic import actionAngleAdiabatic _PRINTOUTSIDEGRID= False class actionAngleAdiabaticGrid(actionAngle): """Action-angle formalism for axisymmetric potentials using the adiabatic approximation, grid-based interpolation""" def __init__(self,pot=None,zmax=1.,gamma=1.,Rmax=5., nR=16,nEz=16,nEr=31,nLz=31,numcores=1, **kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleAdiabaticGrid object INPUT: pot= potential or list of potentials zmax= zmax for building Ez grid Rmax = Rmax for building grids gamma= (default=1.) replace Lz by Lz+gamma Jz in effective potential nEz=, nEr=, nLz, nR= grid size numcores= number of cpus to use to parallellize c= if True, use C to calculate actions ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) +scipy.integrate.quad keywords OUTPUT: instance HISTORY: 2012-07-27 - Written - Bovy (IAS@MPIA) """ actionAngle.__init__(self, ro=kwargs.get('ro',None),vo=kwargs.get('vo',None)) if pot is None: #pragma: no cover raise OSError("Must specify pot= for actionAngleAxi") self._c= kwargs.pop('c',False) self._gamma= gamma self._pot= flatten_potential(pot) self._zmax= zmax self._Rmax= Rmax self._Rmin= 0.01 #Set up the actionAngleAdiabatic object that we will use to interpolate self._aA= actionAngleAdiabatic(pot=self._pot,gamma=self._gamma, c=self._c) #Build grid for Ez, first calculate Ez(zmax;R) function self._Rs= numpy.linspace(self._Rmin,self._Rmax,nR) self._EzZmaxs= _evaluatePotentials(self._pot,self._Rs, self._zmax*numpy.ones(nR))\ -_evaluatePotentials(self._pot,self._Rs,numpy.zeros(nR)) self._EzZmaxsInterp= interpolate.InterpolatedUnivariateSpline(self._Rs,numpy.log(self._EzZmaxs),k=3) y= numpy.linspace(0.,1.,nEz) jz= numpy.zeros((nR,nEz)) jzEzzmax= numpy.zeros(nR) thisRs= (numpy.tile(self._Rs,(nEz,1)).T).flatten() thisEzZmaxs= (numpy.tile(self._EzZmaxs,(nEz,1)).T).flatten() this= (numpy.tile(y,(nR,1))).flatten() if self._c: jz= self._aA(thisRs, numpy.zeros(len(thisRs)), numpy.ones(len(thisRs)),#these two r dummies numpy.zeros(len(thisRs)), numpy.sqrt(2.*this*thisEzZmaxs), **kwargs)[2] jz= numpy.reshape(jz,(nR,nEz)) jzEzzmax[0:nR]= jz[:,nEz-1] else: if numcores > 1: jz= multi.parallel_map((lambda x: self._aA(thisRs[x],0.,1.,#these two r dummies 0.,numpy.sqrt(2.*this[x]*thisEzZmaxs[x]), _justjz=True, **kwargs)[2]), range(nR*nEz),numcores=numcores) jz= numpy.reshape(jz,(nR,nEz)) jzEzzmax[0:nR]= jz[:,nEz-1] else: for ii in range(nR): for jj in range(nEz): #Calculate Jz jz[ii,jj]= self._aA(self._Rs[ii],0.,1.,#these two r dummies 0.,numpy.sqrt(2.*y[jj]*self._EzZmaxs[ii]), _justjz=True,**kwargs)[2] if jj == nEz-1: jzEzzmax[ii]= jz[ii,jj] for ii in range(nR): jz[ii,:]/= jzEzzmax[ii] #First interpolate Ez=Ezmax self._jzEzmaxInterp= interpolate.InterpolatedUnivariateSpline(self._Rs,numpy.log(jzEzzmax+10.**-5.),k=3) self._jz= jz self._jzInterp= interpolate.RectBivariateSpline(self._Rs, y, jz, kx=3,ky=3,s=0.) #JR grid self._Lzmin= 0.01 self._Lzs= numpy.linspace(self._Lzmin, self._Rmax\ *potential.vcirc(self._pot,self._Rmax), nLz) self._Lzmax= self._Lzs[-1] #Calculate ER(vr=0,R=RL) self._RL= numpy.array([potential.rl(self._pot,l) for l in self._Lzs]) self._RLInterp= interpolate.InterpolatedUnivariateSpline(self._Lzs, self._RL,k=3) self._ERRL= _evaluatePotentials(self._pot,self._RL,numpy.zeros(nLz)) +self._Lzs**2./2./self._RL**2. self._ERRLmax= numpy.amax(self._ERRL)+1. self._ERRLInterp= interpolate.InterpolatedUnivariateSpline(self._Lzs, numpy.log(-(self._ERRL-self._ERRLmax)),k=3) self._Ramax= 99. self._ERRa= _evaluatePotentials(self._pot,self._Ramax,0.) +self._Lzs**2./2./self._Ramax**2. self._ERRamax= numpy.amax(self._ERRa)+1. self._ERRaInterp= interpolate.InterpolatedUnivariateSpline(self._Lzs, numpy.log(-(self._ERRa-self._ERRamax)),k=3) y= numpy.linspace(0.,1.,nEr) jr= numpy.zeros((nLz,nEr)) jrERRa= numpy.zeros(nLz) thisRL= (numpy.tile(self._RL,(nEr-1,1)).T).flatten() thisLzs= (numpy.tile(self._Lzs,(nEr-1,1)).T).flatten() thisERRL= (numpy.tile(self._ERRL,(nEr-1,1)).T).flatten() thisERRa= (numpy.tile(self._ERRa,(nEr-1,1)).T).flatten() this= (numpy.tile(y[0:-1],(nLz,1))).flatten() if self._c: mjr= self._aA(thisRL, numpy.sqrt(2.*(thisERRa+this*(thisERRL-thisERRa)-_evaluatePotentials(self._pot,thisRL,numpy.zeros((nEr-1)*nLz)))-thisLzs**2./thisRL**2.), thisLzs/thisRL, numpy.zeros(len(thisRL)), numpy.zeros(len(thisRL)), **kwargs)[0] jr[:,0:-1]= numpy.reshape(mjr,(nLz,nEr-1)) jrERRa[0:nLz]= jr[:,0] else: if numcores > 1: mjr= multi.parallel_map((lambda x: self._aA(thisRL[x], numpy.sqrt(2.*(thisERRa[x]+this[x]*(thisERRL[x]-thisERRa[x])-_evaluatePotentials(self._pot,thisRL[x],0.))-thisLzs[x]**2./thisRL[x]**2.), thisLzs[x]/thisRL[x], 0.,0., _justjr=True, **kwargs)[0]), range((nEr-1)*nLz), numcores=numcores) jr[:,0:-1]= numpy.reshape(mjr,(nLz,nEr-1)) jrERRa[0:nLz]= jr[:,0] else: for ii in range(nLz): for jj in range(nEr-1): #Last one is zero by construction try: jr[ii,jj]= self._aA(self._RL[ii], numpy.sqrt(2.*(self._ERRa[ii]+y[jj]*(self._ERRL[ii]-self._ERRa[ii])-_evaluatePotentials(self._pot,self._RL[ii],0.))-self._Lzs[ii]**2./self._RL[ii]**2.), self._Lzs[ii]/self._RL[ii], 0.,0., _justjr=True, **kwargs)[0] except UnboundError: #pragma: no cover raise if jj == 0: jrERRa[ii]= jr[ii,jj] for ii in range(nLz): jr[ii,:]/= jrERRa[ii] #First interpolate Ez=Ezmax self._jr= jr self._jrERRaInterp= interpolate.InterpolatedUnivariateSpline(self._Lzs, numpy.log(jrERRa+10.**-5.),k=3) self._jrInterp= interpolate.RectBivariateSpline(self._Lzs, y, jr, kx=3,ky=3,s=0.) # Check the units self._check_consistent_units() return None def _evaluate(self,*args,**kwargs): """ NAME: __call__ (_evaluate) PURPOSE: evaluate the actions (jr,lz,jz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument scipy.integrate.quadrature keywords (used when directly evaluating a point off the grid) OUTPUT: (jr,lz,jz) HISTORY: 2012-07-27 - Written - Bovy (IAS@MPIA) NOTE: For a Miyamoto-Nagai potential, this seems accurate to 0.1% and takes ~0.13 ms For a MWPotential, this takes ~ 0.17 ms """ if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz #First work on the vertical action Phi= _evaluatePotentials(self._pot,R,z) try: Phio= _evaluatePotentials(self._pot,R,numpy.zeros(len(R))) except TypeError: Phio= _evaluatePotentials(self._pot,R,0.) Ez= Phi-Phio+vz**2./2. #Bigger than Ezzmax? thisEzZmax= numpy.exp(self._EzZmaxsInterp(R)) if isinstance(R,numpy.ndarray): indx= (R > self._Rmax) indx+= (R < self._Rmin) indx+= (Ez != 0.)*(numpy.log(Ez) > thisEzZmax) indxc= True^indx jz= numpy.empty(R.shape) if numpy.sum(indxc) > 0: jz[indxc]= (self._jzInterp.ev(R[indxc],Ez[indxc]/thisEzZmax[indxc])\ *(numpy.exp(self._jzEzmaxInterp(R[indxc]))-10.**-5.)) if numpy.sum(indx) > 0: jz[indx]= self._aA(R[indx], numpy.zeros(numpy.sum(indx)), numpy.ones(numpy.sum(indx)),#these two r dummies numpy.zeros(numpy.sum(indx)), numpy.sqrt(2.*Ez[indx]), _justjz=True, **kwargs)[2] else: if R > self._Rmax or R < self._Rmin or (Ez != 0 and numpy.log(Ez) > thisEzZmax): #Outside of the grid if _PRINTOUTSIDEGRID: #pragma: no cover print("Outside of grid in Ez", R > self._Rmax , R < self._Rmin , (Ez != 0 and numpy.log(Ez) > thisEzZmax)) jz= self._aA(R,0.,1.,#these two r dummies 0.,numpy.sqrt(2.*Ez), _justjz=True, **kwargs)[2] else: jz= (self._jzInterp(R,Ez/thisEzZmax)\ *(numpy.exp(self._jzEzmaxInterp(R))-10.**-5.))[0][0] #Radial action ERLz= numpy.fabs(R*vT)+self._gamma*jz ER= Phio+vR**2./2.+ERLz**2./2./R**2. thisRL= self._RLInterp(ERLz) thisERRL= -numpy.exp(self._ERRLInterp(ERLz))+self._ERRLmax thisERRa= -numpy.exp(self._ERRaInterp(ERLz))+self._ERRamax if isinstance(R,numpy.ndarray): indx= ((ER-thisERRa)/(thisERRL-thisERRa) > 1.)\ *(((ER-thisERRa)/(thisERRL-thisERRa)-1.) < 10.**-2.) ER[indx]= thisERRL[indx] indx= ((ER-thisERRa)/(thisERRL-thisERRa) < 0.)\ *((ER-thisERRa)/(thisERRL-thisERRa) > -10.**-2.) ER[indx]= thisERRa[indx] indx= (ERLz < self._Lzmin) indx+= (ERLz > self._Lzmax) indx+= ((ER-thisERRa)/(thisERRL-thisERRa) > 1.) indx+= ((ER-thisERRa)/(thisERRL-thisERRa) < 0.) indxc= True^indx jr= numpy.empty(R.shape) if numpy.sum(indxc) > 0: jr[indxc]= (self._jrInterp.ev(ERLz[indxc], (ER[indxc]-thisERRa[indxc])/(thisERRL[indxc]-thisERRa[indxc]))\ *(numpy.exp(self._jrERRaInterp(ERLz[indxc]))-10.**-5.)) if numpy.sum(indx) > 0: jr[indx]= self._aA(thisRL[indx], numpy.sqrt(2.*(ER[indx]-_evaluatePotentials(self._pot,thisRL[indx],0.))-ERLz[indx]**2./thisRL[indx]**2.), ERLz[indx]/thisRL[indx], numpy.zeros(len(thisRL)), numpy.zeros(len(thisRL)), _justjr=True, **kwargs)[0] else: if (ER-thisERRa)/(thisERRL-thisERRa) > 1. \ and ((ER-thisERRa)/(thisERRL-thisERRa)-1.) < 10.**-2.: ER= thisERRL elif (ER-thisERRa)/(thisERRL-thisERRa) < 0. \ and (ER-thisERRa)/(thisERRL-thisERRa) > -10.**-2.: ER= thisERRa #Outside of grid? if ERLz < self._Lzmin or ERLz > self._Lzmax \ or (ER-thisERRa)/(thisERRL-thisERRa) > 1. \ or (ER-thisERRa)/(thisERRL-thisERRa) < 0.: if _PRINTOUTSIDEGRID: #pragma: no cover print("Outside of grid in ER/Lz", ERLz < self._Lzmin , ERLz > self._Lzmax \ , (ER-thisERRa)/(thisERRL-thisERRa) > 1. \ , (ER-thisERRa)/(thisERRL-thisERRa) < 0., ER, thisERRL, thisERRa, (ER-thisERRa)/(thisERRL-thisERRa)) jr= self._aA(thisRL[0], numpy.sqrt(2.*(ER-_evaluatePotentials(self._pot,thisRL,0.))-ERLz**2./thisRL**2.)[0], (ERLz/thisRL)[0], 0.,0., _justjr=True, **kwargs)[0] else: jr= (self._jrInterp(ERLz, (ER-thisERRa)/(thisERRL-thisERRa))\ *(numpy.exp(self._jrERRaInterp(ERLz))-10.**-5.))[0][0] return (jr,R*vT,jz) def Jz(self,*args,**kwargs): """ NAME: Jz PURPOSE: evaluate the action jz INPUT: Either: a) R,vR,vT,z,vz b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well scipy.integrate.quadrature keywords OUTPUT: jz HISTORY: 2012-07-30 - Written - Bovy (IAS@MPIA) """ self._parse_eval_args(*args) Phi= _evaluatePotentials(self._pot,self._eval_R,self._eval_z) Phio= _evaluatePotentials(self._pot,self._eval_R,0.) Ez= Phi-Phio+self._eval_vz**2./2. #Bigger than Ezzmax? thisEzZmax= numpy.exp(self._EzZmaxsInterp(self._eval_R)) if self._eval_R > self._Rmax or self._eval_R < self._Rmin or (Ez != 0. and numpy.log(Ez) > thisEzZmax): #Outside of the grid if _PRINTOUTSIDEGRID: #pragma: no cover print("Outside of grid in Ez") jz= self._aA(self._eval_R,0.,1.,#these two r dummies 0.,numpy.sqrt(2.*Ez), _justjz=True, **kwargs)[2] else: jz= (self._jzInterp(self._eval_R,Ez/thisEzZmax)\ *(numpy.exp(self._jzEzmaxInterp(self._eval_R))-10.**-5.))[0][0] return jz ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleAdiabatic_c.py0000644000175100001710000002026714327773303022476 0ustar00runnerdockerimport ctypes import ctypes.util import numpy from numpy.ctypeslib import ndpointer from ..util import _load_extension_libs _lib, _ext_loaded= _load_extension_libs.load_libgalpy() def actionAngleAdiabatic_c(pot,gamma,R,vR,vT,z,vz): r""" NAME: actionAngleAdiabatic_c PURPOSE: Use C to calculate actions using the adiabatic approximation INPUT: pot - Potential or list of such instances gamma - as in Lz -> Lz+\gamma * J_z R, vR, vT, z, vz - coordinates (arrays) OUTPUT: (jr,jz,err) jr,jz : array, shape (len(R)) err - non-zero if error occurred HISTORY: 2012-12-10 - Written - Bovy (IAS) """ #Parse the potential from ..orbit.integrateFullOrbit import _parse_pot from ..orbit.integratePlanarOrbit import _prep_tfuncs npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot,potforactions=True) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Set up result arrays jr= numpy.empty(len(R)) jz= numpy.empty(len(R)) err= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') actionAngleAdiabatic_actionsFunc= _lib.actionAngleAdiabatic_actions actionAngleAdiabatic_actionsFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_double, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements, first store old order f_cont= [R.flags['F_CONTIGUOUS'], vR.flags['F_CONTIGUOUS'], vT.flags['F_CONTIGUOUS'], z.flags['F_CONTIGUOUS'], vz.flags['F_CONTIGUOUS']] R= numpy.require(R,dtype=numpy.float64,requirements=['C','W']) vR= numpy.require(vR,dtype=numpy.float64,requirements=['C','W']) vT= numpy.require(vT,dtype=numpy.float64,requirements=['C','W']) z= numpy.require(z,dtype=numpy.float64,requirements=['C','W']) vz= numpy.require(vz,dtype=numpy.float64,requirements=['C','W']) jr= numpy.require(jr,dtype=numpy.float64,requirements=['C','W']) jz= numpy.require(jz,dtype=numpy.float64,requirements=['C','W']) #Run the C code actionAngleAdiabatic_actionsFunc(len(R), R, vR, vT, z, vz, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(gamma), jr, jz, ctypes.byref(err)) #Reset input arrays if f_cont[0]: R= numpy.asfortranarray(R) if f_cont[1]: vR= numpy.asfortranarray(vR) if f_cont[2]: vT= numpy.asfortranarray(vT) if f_cont[3]: z= numpy.asfortranarray(z) if f_cont[4]: vz= numpy.asfortranarray(vz) return (jr,jz,err.value) def actionAngleRperiRapZmaxAdiabatic_c(pot,gamma,R,vR,vT,z,vz): r""" NAME: actionAngleRperiRapZmaxAdiabatic_c PURPOSE: Use C to calculate planar (Rperi,Rap) and the maximum height Zmax using the adiabatic approximation (rap = sqrt(Rap^2+Zmax^2)) INPUT: pot - Potential or list of such instances gamma - as in Lz -> Lz+\gamma * J_z R, vR, vT, z, vz - coordinates (arrays) OUTPUT: (Rperi,Rap,Zmax,err) Rperi,Rap,Zmax : array, shape (len(R)) err - non-zero if error occurred HISTORY: 2017-12-21 - Written - Bovy (UofT) """ #Parse the potential from ..orbit.integrateFullOrbit import _parse_pot from ..orbit.integratePlanarOrbit import _prep_tfuncs npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot,potforactions=True) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Set up result arrays rperi= numpy.empty(len(R)) rap= numpy.empty(len(R)) zmax= numpy.empty(len(R)) err= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') actionAngleAdiabatic_actionsFunc= _lib.actionAngleAdiabatic_RperiRapZmax actionAngleAdiabatic_actionsFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_double, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements, first store old order f_cont= [R.flags['F_CONTIGUOUS'], vR.flags['F_CONTIGUOUS'], vT.flags['F_CONTIGUOUS'], z.flags['F_CONTIGUOUS'], vz.flags['F_CONTIGUOUS']] R= numpy.require(R,dtype=numpy.float64,requirements=['C','W']) vR= numpy.require(vR,dtype=numpy.float64,requirements=['C','W']) vT= numpy.require(vT,dtype=numpy.float64,requirements=['C','W']) z= numpy.require(z,dtype=numpy.float64,requirements=['C','W']) vz= numpy.require(vz,dtype=numpy.float64,requirements=['C','W']) rperi= numpy.require(rperi,dtype=numpy.float64,requirements=['C','W']) rap= numpy.require(rap,dtype=numpy.float64,requirements=['C','W']) zmax= numpy.require(zmax,dtype=numpy.float64,requirements=['C','W']) #Run the C code actionAngleAdiabatic_actionsFunc(len(R), R, vR, vT, z, vz, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(gamma), rperi, rap, zmax, ctypes.byref(err)) #Reset input arrays if f_cont[0]: R= numpy.asfortranarray(R) if f_cont[1]: vR= numpy.asfortranarray(vR) if f_cont[2]: vT= numpy.asfortranarray(vT) if f_cont[3]: z= numpy.asfortranarray(z) if f_cont[4]: vz= numpy.asfortranarray(vz) return (rperi,rap,zmax,err.value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleAxi.py0000644000175100001710000003461514327773303021056 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleAxi # # methods: # JR # Jphi # angleR # TR # Tphi # I # calcRapRperi # calcEL ############################################################################### import numpy from scipy import integrate, optimize from ..potential import vcirc from ..potential.planarPotential import _evaluateplanarPotentials from ..potential.Potential import epifreq from .actionAngle import UnboundError from .actionAngleVertical import actionAngleVertical _EPS= 10.**-15. class actionAngleAxi(actionAngleVertical): """Action-angle formalism for axisymmetric potentials""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleAxi object INPUT: Either: a) R,vR,vT b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well pot= potential or list of potentials (planarPotentials) verticalPot= the vertical Potential gamma= (default=1.) replace Lz by Lz+gamma Jz in effective potential (if there is no vertical potential, this is set to zero) OUTPUT: HISTORY: 2010-12-01 - Written - Bovy (NYU) """ self._parse_eval_args(*args,_noOrbUnitsCheck=True,**kwargs) self._R= self._eval_R self._vR= self._eval_vR self._vT= self._eval_vT if not 'pot' in kwargs: #pragma: no cover raise OSError("Must specify pot= for actionAngleAxi") self._pot= kwargs['pot'] if 'verticalPot' in kwargs: kwargs.pop('pot') actionAngleVertical.__init__(self,*args,pot=kwargs['verticalPot'], **kwargs) self._gamma= kwargs.get('gamma',1.) else: self._gamma= 0. return None def angleR(self,**kwargs): #pragma: no cover """ NAME: AngleR PURPOSE: Calculate the radial angle INPUT: scipy.integrate.quadrature keywords OUTPUT: w_R(R,vT,vT) in radians + estimate of the error (does not include TR error) HISTORY: 2010-12-01 - Written - Bovy (NYU) """ if hasattr(self,'_angleR'): return self._angleR (rperi,rap)= self.calcRapRperi(**kwargs) if rap == rperi: return 0. TR= self.TR(**kwargs)[0] EL= self.calcEL(**kwargs) E, L= EL Rmean= numpy.exp((numpy.log(rperi)+numpy.log(rap))/2.) if self._R < Rmean: if self._R > rperi: wR= (2.*numpy.pi/TR* numpy.array(integrate.quadrature(_TRAxiIntegrandSmall, 0.,numpy.sqrt(self._R-rperi), args=(E,L,self._pot,rperi), **kwargs)))\ +numpy.array([numpy.pi,0.]) else: wR= numpy.array([numpy.pi,0.]) else: if self._R < rap: wR= -(2.*numpy.pi/TR* numpy.array(integrate.quadrature(_TRAxiIntegrandLarge, 0.,numpy.sqrt(rap-self._R), args=(E,L,self._pot,rap), **kwargs))) else: wR= numpy.array([0.,0.]) if self._vR < 0.: wR[0]+= numpy.pi self._angleR= numpy.array([wR[0] % (2.*numpy.pi),wR[1]]) return self._angleR def TR(self,**kwargs): #pragma: no cover """ NAME: TR PURPOSE: Calculate the radial period for a power-law rotation curve INPUT: scipy.integrate.quadrature keywords OUTPUT: T_R(R,vT,vT)*vc/ro + estimate of the error HISTORY: 2010-12-01 - Written - Bovy (NYU) """ if hasattr(self,'_TR'): return self._TR (rperi,rap)= self.calcRapRperi(**kwargs) if numpy.fabs(rap-rperi)/rap < 10.**-4.: #Rough limit self._TR= 2.*numpy.pi/epifreq(self._pot,self._R,use_physical=False) return self._TR Rmean= numpy.exp((numpy.log(rperi)+numpy.log(rap))/2.) EL= self.calcEL(**kwargs) E, L= EL TR= 0. if Rmean > rperi: TR+= integrate.quadrature(_TRAxiIntegrandSmall, 0.,numpy.sqrt(Rmean-rperi), args=(E,L,self._pot,rperi), **kwargs)[0] if Rmean < rap: TR+= integrate.quadrature(_TRAxiIntegrandLarge, 0.,numpy.sqrt(rap-Rmean), args=(E,L,self._pot,rap), **kwargs)[0] self._TR= 2.*TR return self._TR def Tphi(self,**kwargs): #pragma: no cover """ NAME: Tphi PURPOSE: Calculate the azimuthal period INPUT: +scipy.integrate.quadrature keywords OUTPUT: T_phi(R,vT,vT)/ro/vc + estimate of the error HISTORY: 2010-12-01 - Written - Bovy (NYU) """ if hasattr(self,'_Tphi'): return self._Tphi (rperi,rap)= self.calcRapRperi(**kwargs) if rap == rperi:#Circular orbit return 2.*numpy.pi*self._R/self._vT TR= self.TR(**kwargs) I= self.I(**kwargs) Tphi= TR/I*numpy.pi self._Tphi= Tphi return self._Tphi def I(self,**kwargs): #pragma: no cover """ NAME: I PURPOSE: Calculate I, the 'ratio' between the radial and azimutha period INPUT: +scipy.integrate.quadrature keywords OUTPUT: I(R,vT,vT) + estimate of the error HISTORY: 2010-12-01 - Written - Bovy (NYU) """ if hasattr(self,'_I'): return self._I (rperi,rap)= self.calcRapRperi(**kwargs) Rmean= numpy.exp((numpy.log(rperi)+numpy.log(rap))/2.) if numpy.fabs(rap-rperi)/rap < 10.**-4.: #Rough limit TR= self.TR()[0] Tphi= self.Tphi()[0] self._I= TR/Tphi*numpy.pi return self._I EL= self.calcEL(**kwargs) E, L= EL I= 0. if Rmean > rperi: I+= integrate.quadrature(_IAxiIntegrandSmall, 0.,numpy.sqrt(Rmean-rperi), args=(E,L,self._pot,rperi), **kwargs)[0] if Rmean < rap: I+= integrate.quadrature(_IAxiIntegrandLarge, 0.,numpy.sqrt(rap-Rmean), args=(E,L,self._pot,rap), **kwargs)[0] self._I= I*self._R*self._vT return self._I def Jphi(self): #pragma: no cover """ NAME: Jphi PURPOSE: Calculate the azimuthal action INPUT: OUTPUT: J_R(R,vT,vT)/ro/vc HISTORY: 2010-12-01 - Written - Bovy (NYU) """ return self._R*self._vT def JR(self,**kwargs): """ NAME: JR PURPOSE: Calculate the radial action INPUT: +scipy.integrate.quad keywords OUTPUT: J_R(R,vT,vT)/ro/vc + estimate of the error HISTORY: 2010-12-01 - Written - Bovy (NYU) """ if hasattr(self,'_JR'): #pragma: no cover return self._JR (rperi,rap)= self.calcRapRperi(**kwargs) EL= self.calcEL(**kwargs) E, L= EL self._JR= 1./numpy.pi*integrate.quad(_JRAxiIntegrand,rperi,rap, args=(E,L,self._pot), **kwargs)[0] return self._JR def calcEL(self,**kwargs): """ NAME: calcEL PURPOSE: calculate the energy and angular momentum INPUT: scipy.integrate.quadrature keywords OUTPUT: (E,L) HISTORY: 2012-07-26 - Written - Bovy (IAS) """ E,L= calcELAxi(self._R,self._vR,self._vT,self._pot) if self._gamma != 0.: #Adjust E E-= self._vT**2./2. L= numpy.fabs(L)+self._gamma*self.Jz(**kwargs) E+= L**2./2./self._R**2. return (E,L) def calcRapRperi(self,**kwargs): """ NAME: calcRapRperi PURPOSE: calculate the apocenter and pericenter radii INPUT: OUTPUT: (rperi,rap) HISTORY: 2010-12-01 - Written - Bovy (NYU) """ if hasattr(self,'_rperirap'): #pragma: no cover return self._rperirap EL= self.calcEL(**kwargs) E, L= EL if self._vR == 0. and numpy.fabs(self._vT - vcirc(self._pot,self._R,use_physical=False)) < _EPS: #We are on a circular orbit rperi= self._R rap = self._R elif self._vR == 0. and self._vT > vcirc(self._pot,self._R,use_physical=False): #We are exactly at pericenter rperi= self._R if self._gamma != 0.: startsign= _rapRperiAxiEq(self._R+10.**-8.,E,L,self._pot) startsign/= numpy.fabs(startsign) else: startsign= 1. rend= _rapRperiAxiFindStart(self._R,E,L,self._pot,rap=True, startsign=startsign) rap= optimize.brentq(_rapRperiAxiEq,rperi+0.00001,rend, args=(E,L,self._pot)) # fprime=_rapRperiAxiDeriv) elif self._vR == 0. and self._vT < vcirc(self._pot,self._R,use_physical=False): #We are exactly at apocenter rap= self._R if self._gamma != 0.: startsign= _rapRperiAxiEq(self._R-10.**-8.,E,L,self._pot) startsign/= numpy.fabs(startsign) else: startsign= 1. rstart= _rapRperiAxiFindStart(self._R,E,L,self._pot, startsign=startsign) if rstart == 0.: rperi= 0. else: rperi= optimize.brentq(_rapRperiAxiEq,rstart,rap-0.000001, args=(E,L,self._pot)) # fprime=_rapRperiAxiDeriv) else: if self._gamma != 0.: startsign= _rapRperiAxiEq(self._R,E,L,self._pot) startsign/= numpy.fabs(startsign) else: startsign= 1. rstart= _rapRperiAxiFindStart(self._R,E,L,self._pot, startsign=startsign) if rstart == 0.: rperi= 0. else: try: rperi= optimize.brentq(_rapRperiAxiEq,rstart,self._R, (E,L,self._pot), maxiter=200) except RuntimeError: #pragma: no cover raise UnboundError("Orbit seems to be unbound") rend= _rapRperiAxiFindStart(self._R,E,L,self._pot,rap=True, startsign=startsign) rap= optimize.brentq(_rapRperiAxiEq,self._R,rend, (E,L,self._pot)) self._rperirap= (rperi,rap) return self._rperirap def calcELAxi(R,vR,vT,pot,vc=1.,ro=1.): """ NAME: calcELAxi PURPOSE: calculate the energy and angular momentum INPUT: R - Galactocentric radius (/ro) vR - radial part of the velocity (/vc) vT - azimuthal part of the velocity (/vc) vc - circular velocity ro - reference radius OUTPUT: (E,L) HISTORY: 2010-11-30 - Written - Bovy (NYU) """ return (potentialAxi(R,pot)+vR**2./2.+vT**2./2.,R*vT) def potentialAxi(R,pot,vc=1.,ro=1.): """ NAME: potentialAxi PURPOSE: return the potential INPUT: R - Galactocentric radius (/ro) pot - potential vc - circular velocity ro - reference radius OUTPUT: Phi(R) HISTORY: 2010-11-30 - Written - Bovy (NYU) """ return _evaluateplanarPotentials(pot,R) def _JRAxiIntegrand(r,E,L,pot): """The J_R integrand""" return numpy.sqrt(2.*(E-potentialAxi(r,pot))-L**2./r**2.) def _TRAxiIntegrandSmall(t,E,L,pot,rperi): #pragma: no cover r= rperi+t**2.#part of the transformation return 2.*t/_JRAxiIntegrand(r,E,L,pot) def _TRAxiIntegrandLarge(t,E,L,pot,rap): #pragma: no cover r= rap-t**2.#part of the transformation return 2.*t/_JRAxiIntegrand(r,E,L,pot) def _IAxiIntegrandSmall(t,E,L,pot,rperi): #pragma: no cover r= rperi+t**2.#part of the transformation return 2.*t/_JRAxiIntegrand(r,E,L,pot)/r**2. def _IAxiIntegrandLarge(t,E,L,pot,rap): #pragma: no cover r= rap-t**2.#part of the transformation return 2.*t/_JRAxiIntegrand(r,E,L,pot)/r**2. def _rapRperiAxiEq(R,E,L,pot): """The vr=0 equation that needs to be solved to find apo- and pericenter""" return E-potentialAxi(R,pot)-L**2./2./R**2. def _rapRperiAxiFindStart(R,E,L,pot,rap=False,startsign=1.): """ NAME: _rapRperiAxiFindStart PURPOSE: Find adequate start or end points to solve for rap and rperi INPUT: R - Galactocentric radius E - energy L - angular momentum pot - potential rap - if True, find the rap end-point startsign= set to -1 if the function is not positive (due to gamma) OUTPUT: rstart or rend HISTORY: 2010-12-01 - Written - Bovy (NYU) """ if rap: rtry= 2.*R else: rtry= R/2. while startsign*_rapRperiAxiEq(rtry,E,L,pot) > 0. \ and rtry > 0.000000001: if rap: if rtry > 100.: #pragma: no cover raise UnboundError("Orbit seems to be unbound") rtry*= 2. else: rtry/= 2. if rtry < 0.000000001: return 0. return rtry ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleHarmonic.py0000644000175100001710000000772214327773303022074 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleHarmonic # # Calculate actions-angle coordinates for the harmonic-oscillator # # methods: # __call__: returns (j) # actionsFreqs: returns (j,omega) # actionsFreqsAngles: returns (j,omega,a) # ############################################################################### import numpy from ..util import conversion from .actionAngle import actionAngle class actionAngleHarmonic(actionAngle): """Action-angle formalism for the one-dimensional harmonic oscillator""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleHarmonic object INPUT: omega= frequencies (can be Quantity) ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) OUTPUT: instance HISTORY: 2018-04-08 - Written - Bovy (Uoft) """ actionAngle.__init__(self, ro=kwargs.get('ro',None),vo=kwargs.get('vo',None)) if not 'omega' in kwargs: #pragma: no cover raise OSError("Must specify omega= for actionAngleHarmonic") self._omega= conversion.parse_frequency(kwargs.get('omega'), ro=self._ro,vo=self._vo) return None def _evaluate(self,*args,**kwargs): """ NAME: __call__ (_evaluate) PURPOSE: evaluate the action INPUT: Either: a) x,vx: 1) floats: phase-space value for single object (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) OUTPUT: action HISTORY: 2018-04-08 - Written - Bovy (UofT) """ if len(args) == 2: # x,vx x,vx= args return (vx**2./self._omega+self._omega*x**2.)/2. else: # pragma: no cover raise ValueError('actionAngleHarmonic __call__ input not understood') def _actionsFreqs(self,*args,**kwargs): """ NAME: actionsFreqs (_actionsFreqs) PURPOSE: evaluate the action and frequency INPUT: Either: a) x,vx: 1) floats: phase-space value for single object (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) OUTPUT: action,frequency HISTORY: 2018-04-08 - Written - Bovy (UofT) """ if len(args) == 2: # x,vx x,vx= args return ((vx**2./self._omega+self._omega*x**2.)/2., self._omega*numpy.ones_like(x)) else: # pragma: no cover raise ValueError('actionAngleHarmonic __call__ input not understood') def _actionsFreqsAngles(self,*args,**kwargs): """ NAME: actionsFreqsAngles (_actionsFreqsAngles) PURPOSE: evaluate the action, frequency, and angle INPUT: Either: a) x,vx: 1) floats: phase-space value for single object (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) OUTPUT: action,frequency,angle HISTORY: 2018-04-08 - Written - Bovy (UofT) """ if len(args) == 2: # x,vx x,vx= args return ((vx**2./self._omega+self._omega*x**2.)/2., self._omega*numpy.ones_like(x), numpy.arctan2(self._omega*x,vx)) else: # pragma: no cover raise ValueError('actionAngleHarmonic __call__ input not understood') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleHarmonicInverse.py0000644000175100001710000000546614327773303023433 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleHarmonicInverse # # Calculate (x,v) coordinates for the harmonic oscillator from # given actions-angle coordinates # ############################################################################### import numpy from ..util import conversion from .actionAngleInverse import actionAngleInverse class actionAngleHarmonicInverse(actionAngleInverse): """Inverse action-angle formalism for the one-dimensional harmonic oscillator""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleHarmonicInverse object INPUT: omega= frequency (can be Quantity) ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) OUTPUT: instance HISTORY: 2018-04-08 - Started - Bovy (UofT) """ actionAngleInverse.__init__(self,*args,**kwargs) if not 'omega' in kwargs: #pragma: no cover raise OSError("Must specify omega= for actionAngleHarmonic") omega= conversion.parse_frequency(kwargs.get('omega'), ro=self._ro,vo=self._vo) self._omega= omega return None def _evaluate(self,j,angle,**kwargs): """ NAME: __call__ PURPOSE: evaluate the phase-space coordinates (x,v) for a number of angles on a single torus INPUT: j - action (scalar) angle - angle (array [N]) OUTPUT: [x,vx] HISTORY: 2018-04-08 - Written - Bovy (UofT) """ return self._xvFreqs(j,angle,**kwargs)[:2] def _xvFreqs(self,j,angle,**kwargs): """ NAME: xvFreqs PURPOSE: evaluate the phase-space coordinates (x,v) for a number of angles on a single torus as well as the frequency INPUT: j - action (scalar) angle - angle (array [N]) OUTPUT: ([x,vx],Omega) HISTORY: 2018-04-08 - Written - Bovy (UofT) """ amp= numpy.sqrt(2.*j/self._omega) x= amp*numpy.sin(angle) vx= amp*self._omega*numpy.cos(angle) return (x,vx,self._omega) def _Freqs(self,j,**kwargs): """ NAME: Freqs PURPOSE: return the frequency corresponding to a torus INPUT: j - action (scalar) OUTPUT: (Omega) HISTORY: 2018-04-08 - Written - Bovy (UofT) """ return self._omega ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleInverse.py0000644000175100001710000000676114327773303021751 0ustar00runnerdocker############################################################################### # actionAngleInverse.py: top-level class with common routines for inverse # actionAngle methods ############################################################################### from galpy.util.conversion import (actionAngleInverse_physical_input, physical_conversion_actionAngleInverse) from .actionAngle import actionAngle class actionAngleInverse(actionAngle): """actionAngleInverse; top-level class with common routines for inverse actionAngle methods""" def __init__(self,*args,**kwargs): actionAngle.__init__(self, ro=kwargs.get('ro',None),vo=kwargs.get('vo',None)) @actionAngleInverse_physical_input @physical_conversion_actionAngleInverse('__call__',pop=True) def __call__(self,*args,**kwargs): """ NAME: evaluate the phase-space coordinates (x,v) for a number of angles on a single torus INPUT: jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) angler - radial angle (array [N]) anglephi - azimuthal angle (array [N]) anglez - vertical angle (array [N]) Some sub-classes (like actionAngleTorus) have additional parameters: actionAngleTorus: tol= (object-wide value) goal for |dJ|/|J| along the torus OUTPUT: [R,vR,vT,z,vz,phi] HISTORY: 2017-11-14 - Written - Bovy (UofT) """ try: return self._evaluate(*args,**kwargs) except AttributeError: #pragma: no cover raise NotImplementedError("'__call__' method not implemented for this actionAngle module") @actionAngleInverse_physical_input @physical_conversion_actionAngleInverse('xvFreqs',pop=True) def xvFreqs(self,*args,**kwargs): """ NAME: xvFreqs PURPOSE: evaluate the phase-space coordinates (x,v) for a number of angles on a single torus as well as the frequencies INPUT: jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) angler - radial angle (array [N]) anglephi - azimuthal angle (array [N]) anglez - vertical angle (array [N]) OUTPUT: ([R,vR,vT,z,vz,phi],OmegaR,Omegaphi,Omegaz) HISTORY: 2017-11-15 - Written - Bovy (UofT) """ try: return self._xvFreqs(*args,**kwargs) except AttributeError: #pragma: no cover raise NotImplementedError("'xvFreqs' method not implemented for this actionAngle module") @actionAngleInverse_physical_input @physical_conversion_actionAngleInverse('Freqs',pop=True) def Freqs(self,*args,**kwargs): """ NAME: Freqs PURPOSE: return the frequencies corresponding to a torus INPUT: jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) OUTPUT: (OmegaR,Omegaphi,Omegaz) HISTORY: 2017-11-15 - Written - Bovy (UofT) """ try: return self._Freqs(*args,**kwargs) except AttributeError: #pragma: no cover raise NotImplementedError("'Freqs' method not implemented for this actionAngle module") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleIsochrone.py0000644000175100001710000003307014327773303022260 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleIsochrone # # Calculate actions-angle coordinates for the Isochrone potential # # methods: # __call__: returns (jr,lz,jz) # actionsFreqs: returns (jr,lz,jz,Or,Op,Oz) # actionsFreqsAngles: returns (jr,lz,jz,Or,Op,Oz,ar,ap,az) # ############################################################################### import copy import warnings import numpy from ..potential import IsochronePotential from ..util import conversion, galpyWarning from .actionAngle import actionAngle class actionAngleIsochrone(actionAngle): """Action-angle formalism for the isochrone potential, on the Jphi, Jtheta system of Binney & Tremaine (2008)""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleIsochrone object INPUT: Either: b= scale parameter of the isochrone parameter (can be Quantity) ip= instance of a IsochronePotential ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) OUTPUT: instance HISTORY: 2013-09-08 - Written - Bovy (IAS) """ actionAngle.__init__(self, ro=kwargs.get('ro',None),vo=kwargs.get('vo',None)) if not 'b' in kwargs and not 'ip' in kwargs: #pragma: no cover raise OSError("Must specify b= for actionAngleIsochrone") if 'ip' in kwargs: ip= kwargs['ip'] if not isinstance(ip,IsochronePotential): #pragma: no cover raise OSError("'Provided ip= does not appear to be an instance of an IsochronePotential") # Check the units self._pot= ip self._check_consistent_units() self.b= ip.b self.amp= ip._amp else: self.b= conversion.parse_length(kwargs['b'],ro=self._ro) rb= numpy.sqrt(self.b**2.+1.) self.amp= (self.b+rb)**2.*rb self._c= False ext_loaded= False if ext_loaded and (('c' in kwargs and kwargs['c']) or not 'c' in kwargs): #pragma: no cover self._c= True else: self._c= False if not self._c: self._ip= IsochronePotential(amp=self.amp,b=self.b) #Define _pot, because some functions that use actionAngle instances need this self._pot= IsochronePotential(amp=self.amp,b=self.b) # Check the units self._check_consistent_units() return None def _evaluate(self,*args,**kwargs): """ NAME: __call__ (_evaluate) PURPOSE: evaluate the actions (jr,lz,jz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument OUTPUT: (jr,lz,jz) HISTORY: 2013-09-08 - Written - Bovy (IAS) """ if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) if self._c: #pragma: no cover pass else: Lz= R*vT Lx= -z*vT Ly= z*vR-R*vz L2= Lx*Lx+Ly*Ly+Lz*Lz E= self._ip(R,z)+vR**2./2.+vT**2./2.+vz**2./2. L= numpy.sqrt(L2) #Actions Jphi= Lz Jz= L-numpy.fabs(Lz) Jr= self.amp/numpy.sqrt(-2.*E)\ -0.5*(L+numpy.sqrt(L2+4.*self.amp*self.b)) return (Jr,Jphi,Jz) def _actionsFreqs(self,*args,**kwargs): """ NAME: actionsFreqs (_actionsFreqs) PURPOSE: evaluate the actions and frequencies (jr,lz,jz,Omegar,Omegaphi,Omegaz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument OUTPUT: (jr,lz,jz,Omegar,Omegaphi,Omegaz) HISTORY: 2013-09-08 - Written - Bovy (IAS) """ if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) if self._c: #pragma: no cover pass else: Lz= R*vT Lx= -z*vT Ly= z*vR-R*vz L2= Lx*Lx+Ly*Ly+Lz*Lz E= self._ip(R,z)+vR**2./2.+vT**2./2.+vz**2./2. L= numpy.sqrt(L2) #Actions Jphi= Lz Jz= L-numpy.fabs(Lz) Jr= self.amp/numpy.sqrt(-2.*E)\ -0.5*(L+numpy.sqrt(L2+4.*self.amp*self.b)) #Frequencies Omegar= (-2.*E)**1.5/self.amp Omegaz= 0.5*(1.+L/numpy.sqrt(L2+4.*self.amp*self.b))*Omegar Omegaphi= copy.copy(Omegaz) indx= Lz < 0. Omegaphi[indx]*= -1. return (Jr,Jphi,Jz,Omegar,Omegaphi,Omegaz) def _actionsFreqsAngles(self,*args,**kwargs): """ NAME: actionsFreqsAngles (_actionsFreqsAngles) PURPOSE: evaluate the actions, frequencies, and angles (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument OUTPUT: (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) HISTORY: 2013-09-08 - Written - Bovy (IAS) """ if len(args) == 5: #R,vR.vT, z, vz pragma: no cover raise OSError("You need to provide phi when calculating angles") elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz phi= self._eval_phi if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) phi= numpy.array([phi]) if self._c: #pragma: no cover pass else: Lz= R*vT Lx= -z*vT Ly= z*vR-R*vz L2= Lx*Lx+Ly*Ly+Lz*Lz E= self._ip(R,z)+vR**2./2.+vT**2./2.+vz**2./2. L= numpy.sqrt(L2) #Actions Jphi= Lz Jz= L-numpy.fabs(Lz) Jr= self.amp/numpy.sqrt(-2.*E)\ -0.5*(L+numpy.sqrt(L2+4.*self.amp*self.b)) #Frequencies Omegar= (-2.*E)**1.5/self.amp Omegaz= 0.5*(1.+L/numpy.sqrt(L2+4.*self.amp*self.b))*Omegar Omegaphi= copy.copy(Omegaz) indx= Lz < 0. Omegaphi[indx]*= -1. #Angles c= -self.amp/2./E-self.b e2= 1.-L2/self.amp/c*(1.+self.b/c) e= numpy.sqrt(e2) if self.b == 0.: coseta= 1/e*(1.-numpy.sqrt(R**2.+z**2.)/c) else: s= 1.+numpy.sqrt(1.+(R**2.+z**2.)/self.b**2.) coseta= 1/e*(1.-self.b/c*(s-2.)) pindx= (coseta > 1.) coseta[pindx]= 1. pindx= (coseta < -1.) coseta[pindx]= -1. eta= numpy.arccos(coseta) costheta= z/numpy.sqrt(R**2.+z**2.) sintheta= R/numpy.sqrt(R**2.+z**2.) vrindx= (vR*sintheta+vz*costheta) < 0. eta[vrindx]= 2.*numpy.pi-eta[vrindx] angler= eta-e*c/(c+self.b)*numpy.sin(eta) tan11= numpy.arctan(numpy.sqrt((1.+e)/(1.-e))*numpy.tan(0.5*eta)) tan12= numpy.arctan(numpy.sqrt((1.+e+2.*self.b/c)/(1.-e+2.*self.b/c))*numpy.tan(0.5*eta)) vzindx= (-vz*sintheta+vR*costheta) > 0. tan11[tan11 < 0.]+= numpy.pi tan12[tan12 < 0.]+= numpy.pi pindx= (Lz/L > 1.) Lz[pindx]= L[pindx] pindx= (Lz/L < -1.) Lz[pindx]= -L[pindx] sini= numpy.sqrt(L**2.-Lz**2.)/L tani= numpy.sqrt(L**2.-Lz**2.)/Lz sinpsi= costheta/sini pindx= (sinpsi > 1.)*numpy.isfinite(sinpsi) sinpsi[pindx]= 1. pindx= (sinpsi < -1.)*numpy.isfinite(sinpsi) sinpsi[pindx]= -1. psi= numpy.arcsin(sinpsi) psi[vzindx]= numpy.pi-psi[vzindx] # For non-inclined orbits, we set Omega=0 by convention psi[True^numpy.isfinite(psi)]= phi[True^numpy.isfinite(psi)] psi= psi % (2.*numpy.pi) anglez= psi+Omegaz/Omegar*angler\ -tan11-1./numpy.sqrt(1.+4*self.amp*self.b/L2)*tan12 sinu= z/R/tani pindx= (sinu > 1.)*numpy.isfinite(sinu) sinu[pindx]= 1. pindx= (sinu < -1.)*numpy.isfinite(sinu) sinu[pindx]= -1. u= numpy.arcsin(sinu) u[vzindx]= numpy.pi-u[vzindx] # For non-inclined orbits, we set Omega=0 by convention u[True^numpy.isfinite(u)]= phi[True^numpy.isfinite(u)] Omega= phi-u anglephi= Omega anglephi[indx]-= anglez[indx] anglephi[True^indx]+= anglez[True^indx] angler= angler % (2.*numpy.pi) anglephi= anglephi % (2.*numpy.pi) anglez= anglez % (2.*numpy.pi) return (Jr,Jphi,Jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) def _EccZmaxRperiRap(self,*args,**kwargs): """ NAME: _EccZmaxRperiRap PURPOSE: evaluate the eccentricity, maximum height above the plane, peri- and apocenter for an isochrone potential INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument OUTPUT: (e,zmax,rperi,rap) HISTORY: 2017-12-22 - Written - Bovy (UofT) """ if len(args) == 5: #R,vR.vT, z, vz pragma: no cover R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) if self._c: #pragma: no cover pass else: Lz= R*vT Lx= -z*vT Ly= z*vR-R*vz L2= Lx*Lx+Ly*Ly+Lz*Lz E= self._ip(R,z)+vR**2./2.+vT**2./2.+vz**2./2. if self.b == 0: warnings.warn("zmax for point-mass (b=0) isochrone potential is only approximate, because it assumes that zmax is attained at rap, which is not necessarily the case",galpyWarning) a= -self.amp/2./E me2= L2/self.amp/a e= numpy.sqrt(1.-me2) rperi= a*(1.-e) rap= a*(1.+e) else: smin= 0.5*((2.*E-self.amp/self.b)\ +numpy.sqrt((2.*E-self.amp/self.b)**2. +2.*E*(4.*self.amp/self.b+L2/self.b**2.)))/E smax= 2.-self.amp/E/self.b-smin rperi= smin*numpy.sqrt(1.-2./smin)*self.b rap= smax*numpy.sqrt(1.-2./smax)*self.b return ((rap-rperi)/(rap+rperi),rap*numpy.sqrt(1.-Lz**2./L2), rperi,rap) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleIsochroneApprox.py0000644000175100001710000010740414327773303023455 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleIsochroneApprox # # Calculate actions-angle coordinates for any potential by using # an isochrone potential as an approximate potential and using # a Fox & Binney (2013?) + torus machinery-like algorithm # (angle-fit) (Bovy 2014) # # methods: # __call__: returns (jr,lz,jz) # actionsFreqs: returns (jr,lz,jz,Or,Op,Oz) # actionsFreqsAngles: returns (jr,lz,jz,Or,Op,Oz,ar,ap,az) # ############################################################################### import warnings import numpy from numpy import linalg from scipy import optimize from ..potential import (IsochronePotential, MWPotential, _isNonAxi, dvcircdR, vcirc) from ..potential.Potential import flatten as flatten_potential from ..util import conversion, galpyWarning, plot from ..util.conversion import (physical_conversion, potential_physical_input, time_in_Gyr) from .actionAngle import actionAngle from .actionAngleIsochrone import actionAngleIsochrone _TWOPI= 2.*numpy.pi _ANGLETOL= 0.02 #tolerance for deciding whether full angle range is covered class actionAngleIsochroneApprox(actionAngle): """Action-angle formalism using an isochrone potential as an approximate potential and using a Fox & Binney (2014?) like algorithm to calculate the actions using orbit integrations and a torus-machinery-like angle-fit to get the angles and frequencies (Bovy 2014)""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleIsochroneApprox object INPUT: Either: b= scale parameter of the isochrone parameter (can be Quantity) ip= instance of a IsochronePotential aAI= instance of an actionAngleIsochrone pot= potential to calculate action-angle variables for tintJ= (default: 100) time to integrate orbits for to estimate actions (can be Quantity) ntintJ= (default: 10000) number of time-integration points integrate_method= (default: 'dopr54_c') integration method to use dt= (None) orbit.integrate dt keyword (for fixed stepsize integration) maxn= (default: 3) Default value for all methods when using a grid in vec(n) up to this n (zero-based) ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) OUTPUT: instance HISTORY: 2013-09-10 - Written - Bovy (IAS) """ actionAngle.__init__(self, ro=kwargs.get('ro',None),vo=kwargs.get('vo',None)) if not 'pot' in kwargs: #pragma: no cover raise OSError("Must specify pot= for actionAngleIsochroneApprox") self._pot= flatten_potential(kwargs['pot']) if self._pot == MWPotential: warnings.warn("Use of MWPotential as a Milky-Way-like potential is deprecated; galpy.potential.MWPotential2014, a potential fit to a large variety of dynamical constraints (see Bovy 2015), is the preferred Milky-Way-like potential in galpy", galpyWarning) if not 'b' in kwargs and not 'ip' in kwargs \ and not 'aAI' in kwargs: #pragma: no cover raise OSError("Must specify b=, ip=, or aAI= for actionAngleIsochroneApprox") if 'aAI' in kwargs: if not isinstance(kwargs['aAI'],actionAngleIsochrone): #pragma: no cover raise OSError("'Provided aAI= does not appear to be an instance of an actionAngleIsochrone") self._aAI= kwargs['aAI'] elif 'ip' in kwargs: ip= kwargs['ip'] if not isinstance(ip,IsochronePotential): #pragma: no cover raise OSError("'Provided ip= does not appear to be an instance of an IsochronePotential") self._aAI= actionAngleIsochrone(ip=ip) else: b= conversion.parse_length(kwargs['b'],ro=self._ro) self._aAI= actionAngleIsochrone(ip=IsochronePotential(b=b, normalize=1.)) self._tintJ= kwargs.get('tintJ',100.) self._tintJ= conversion.parse_time(self._tintJ,ro=self._ro,vo=self._vo) self._ntintJ= kwargs.get('ntintJ',10000) self._integrate_dt= kwargs.get('dt',None) self._tsJ= numpy.linspace(0.,self._tintJ,self._ntintJ) self._integrate_method= kwargs.get('integrate_method','dopr54_c') self._maxn= kwargs.get('maxn',3) self._c= False ext_loaded= False if ext_loaded and (('c' in kwargs and kwargs['c']) or not 'c' in kwargs): #pragma: no cover self._c= True else: self._c= False # Check the units self._check_consistent_units() return None def _evaluate(self,*args,**kwargs): """ NAME: __call__ (_evaluate) PURPOSE: evaluate the actions (jr,lz,jz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument cumul= if True, return the cumulative average actions (to look at convergence) OUTPUT: (jr,lz,jz) HISTORY: 2013-09-10 - Written - Bovy (IAS) """ R,vR,vT,z,vz,phi= self._parse_args(False,False,*args) if self._c: #pragma: no cover pass else: #Use self._aAI to calculate the actions and angles in the isochrone potential acfs= self._aAI._actionsFreqsAngles(R.flatten(), vR.flatten(), vT.flatten(), z.flatten(), vz.flatten(), phi.flatten()) jrI= numpy.reshape(acfs[0],R.shape)[:,:-1] jzI= numpy.reshape(acfs[2],R.shape)[:,:-1] anglerI= numpy.reshape(acfs[6],R.shape) anglezI= numpy.reshape(acfs[8],R.shape) if numpy.any((numpy.fabs(numpy.amax(anglerI,axis=1)-_TWOPI) > _ANGLETOL)\ *(numpy.fabs(numpy.amin(anglerI,axis=1)) > _ANGLETOL)): #pragma: no cover warnings.warn("Full radial angle range not covered for at least one object; actions are likely not reliable",galpyWarning) if numpy.any((numpy.fabs(numpy.amax(anglezI,axis=1)-_TWOPI) > _ANGLETOL)\ *(numpy.fabs(numpy.amin(anglezI,axis=1)) > _ANGLETOL)): #pragma: no cover warnings.warn("Full vertical angle range not covered for at least one object; actions are likely not reliable",galpyWarning) danglerI= ((numpy.roll(anglerI,-1,axis=1)-anglerI) % _TWOPI)[:,:-1] danglezI= ((numpy.roll(anglezI,-1,axis=1)-anglezI) % _TWOPI)[:,:-1] if kwargs.get('cumul',False): sumFunc= numpy.cumsum else: sumFunc= numpy.sum jr= sumFunc(jrI*danglerI,axis=1)/sumFunc(danglerI,axis=1) jz= sumFunc(jzI*danglezI,axis=1)/sumFunc(danglezI,axis=1) if _isNonAxi(self._pot): lzI= numpy.reshape(acfs[1],R.shape)[:,:-1] anglephiI= numpy.reshape(acfs[7],R.shape) danglephiI= ((numpy.roll(anglephiI,-1,axis=1)-anglephiI) % _TWOPI)[:,:-1] if numpy.any((numpy.fabs(numpy.amax(anglephiI,axis=1)-_TWOPI) > _ANGLETOL)\ *(numpy.fabs(numpy.amin(anglephiI,axis=1)) > _ANGLETOL)): #pragma: no cover warnings.warn("Full azimuthal angle range not covered for at least one object; actions are likely not reliable",galpyWarning) lz= sumFunc(lzI*danglephiI,axis=1)/sumFunc(danglephiI,axis=1) else: lz= R[:,0]*vT[:,0] return (jr,lz,jz) def _actionsFreqs(self,*args,**kwargs): """ NAME: actionsFreqs (_actionsFreqs) PURPOSE: evaluate the actions and frequencies (jr,lz,jz,Omegar,Omegaphi,Omegaz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument maxn= (default: object-wide default) Use a grid in vec(n) up to this n (zero-based) ts= if set, the phase-space points correspond to these times (IF NOT SET, WE ASSUME THAT ts IS THAT THAT IS ASSOCIATED WITH THIS OBJECT) _firstFlip= (False) if True and Orbits are given, the backward part of the orbit is integrated first and stored in the Orbit object OUTPUT: (jr,lz,jz,Omegar,Omegaphi,Omegaz) HISTORY: 2013-09-10 - Written - Bovy (IAS) """ acfs= self._actionsFreqsAngles(*args,**kwargs) return (acfs[0],acfs[1],acfs[2],acfs[3],acfs[4],acfs[5]) def _actionsFreqsAngles(self,*args,**kwargs): """ NAME: actionsFreqsAngles (_actionsFreqsAngles) PURPOSE: evaluate the actions, frequencies, and angles (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument maxn= (default: object-wide default) Use a grid in vec(n) up to this n (zero-based) ts= if set, the phase-space points correspond to these times (IF NOT SET, WE ASSUME THAT ts IS THAT THAT IS ASSOCIATED WITH THIS OBJECT) _firstFlip= (False) if True and Orbits are given, the backward part of the orbit is integrated first and stored in the Orbit object OUTPUT: (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) HISTORY: 2013-09-10 - Written - Bovy (IAS) """ from ..orbit import Orbit _firstFlip= kwargs.get('_firstFlip',False) #If the orbit was already integrated, set ts to the integration times if isinstance(args[0],Orbit) and hasattr(args[0],'orbit') \ and not 'ts' in kwargs: kwargs['ts']= args[0].t elif (isinstance(args[0],list) and isinstance(args[0][0],Orbit)) \ and hasattr(args[0][0],'orbit') \ and not 'ts' in kwargs: kwargs['ts']= args[0][0].t R,vR,vT,z,vz,phi= self._parse_args(True,_firstFlip,*args) if 'ts' in kwargs and not kwargs['ts'] is None: ts= kwargs['ts'] ts= conversion.parse_time(ts,ro=self._ro,vo=self._vo) else: ts= numpy.empty(R.shape[1]) ts[self._ntintJ-1:]= self._tsJ ts[:self._ntintJ-1]= -self._tsJ[1:][::-1] maxn= kwargs.get('maxn',self._maxn) if self._c: #pragma: no cover pass else: #Use self._aAI to calculate the actions and angles in the isochrone potential if '_acfs' in kwargs: acfs= kwargs['_acfs'] else: acfs= self._aAI._actionsFreqsAngles(R.flatten(), vR.flatten(), vT.flatten(), z.flatten(), vz.flatten(), phi.flatten()) jrI= numpy.reshape(acfs[0],R.shape)[:,:-1] jzI= numpy.reshape(acfs[2],R.shape)[:,:-1] anglerI= numpy.reshape(acfs[6],R.shape) anglezI= numpy.reshape(acfs[8],R.shape) if numpy.any((numpy.fabs(numpy.amax(anglerI,axis=1)-_TWOPI) > _ANGLETOL)\ *(numpy.fabs(numpy.amin(anglerI,axis=1)) > _ANGLETOL)): #pragma: no cover warnings.warn("Full radial angle range not covered for at least one object; actions are likely not reliable",galpyWarning) if numpy.any((numpy.fabs(numpy.amax(anglezI,axis=1)-_TWOPI) > _ANGLETOL)\ *(numpy.fabs(numpy.amin(anglezI,axis=1)) > _ANGLETOL)): #pragma: no cover warnings.warn("Full vertical angle range not covered for at least one object; actions are likely not reliable",galpyWarning) danglerI= ((numpy.roll(anglerI,-1,axis=1)-anglerI) % _TWOPI)[:,:-1] danglezI= ((numpy.roll(anglezI,-1,axis=1)-anglezI) % _TWOPI)[:,:-1] jr= numpy.sum(jrI*danglerI,axis=1)/numpy.sum(danglerI,axis=1) jz= numpy.sum(jzI*danglezI,axis=1)/numpy.sum(danglezI,axis=1) if _isNonAxi(self._pot): #pragma: no cover lzI= numpy.reshape(acfs[1],R.shape)[:,:-1] anglephiI= numpy.reshape(acfs[7],R.shape) if numpy.any((numpy.fabs(numpy.amax(anglephiI,axis=1)-_TWOPI) > _ANGLETOL)\ *(numpy.fabs(numpy.amin(anglephiI,axis=1)) > _ANGLETOL)): #pragma: no cover warnings.warn("Full azimuthal angle range not covered for at least one object; actions are likely not reliable",galpyWarning) danglephiI= ((numpy.roll(anglephiI,-1,axis=1)-anglephiI) % _TWOPI)[:,:-1] lz= numpy.sum(lzI*danglephiI,axis=1)/numpy.sum(danglephiI,axis=1) else: lz= R[:,len(ts)//2]*vT[:,len(ts)//2] #Now do an 'angle-fit' angleRT= dePeriod(numpy.reshape(acfs[6],R.shape)) acfs7= numpy.reshape(acfs[7],R.shape) negFreqIndx= numpy.median(acfs7-numpy.roll(acfs7,1,axis=1),axis=1) < 0. #anglephi is decreasing anglephiT= numpy.empty(acfs7.shape) anglephiT[negFreqIndx,:]= dePeriod(_TWOPI-acfs7[negFreqIndx,:]) negFreqPhi= numpy.zeros(R.shape[0],dtype='bool') negFreqPhi[negFreqIndx]= True anglephiT[True^negFreqIndx,:]= dePeriod(acfs7[True^negFreqIndx,:]) angleZT= dePeriod(numpy.reshape(acfs[8],R.shape)) #Write the angle-fit as Y=AX, build A and Y nt= len(ts) no= R.shape[0] #remove 0,0,0 and half-plane if _isNonAxi(self._pot): nn= (2*maxn-1)**2*maxn-(maxn-1)*(2*maxn-1)-maxn else: nn= maxn*(2*maxn-1)-maxn A= numpy.zeros((no,nt,2+nn)) A[:,:,0]= 1. A[:,:,1]= ts #sorting the phi and Z grids this way makes it easy to exclude the origin phig= list(numpy.arange(-maxn+1,maxn,1)) phig.sort(key = lambda x: abs(x)) phig= numpy.array(phig,dtype='int') if _isNonAxi(self._pot): grid= numpy.meshgrid(numpy.arange(maxn),phig,phig) else: grid= numpy.meshgrid(numpy.arange(maxn),phig) gridR= grid[0].T.flatten()[1:] #remove 0,0,0 gridZ= grid[1].T.flatten()[1:] mask = numpy.ones(len(gridR),dtype=bool) # excludes axis that is not in half-space if _isNonAxi(self._pot): gridphi= grid[2].T.flatten()[1:] mask= True\ ^(gridR == 0)*((gridphi < 0)+((gridphi==0)*(gridZ < 0))) else: mask[:2*maxn-3:2]= False gridR= gridR[mask] gridZ= gridZ[mask] tangleR= numpy.tile(angleRT.T,(nn,1,1)).T tgridR= numpy.tile(gridR,(no,nt,1)) tangleZ= numpy.tile(angleZT.T,(nn,1,1)).T tgridZ= numpy.tile(gridZ,(no,nt,1)) if _isNonAxi(self._pot): gridphi= gridphi[mask] tgridphi= numpy.tile(gridphi,(no,nt,1)) tanglephi= numpy.tile(anglephiT.T,(nn,1,1)).T sinnR= numpy.sin(tgridR*tangleR+tgridphi*tanglephi+tgridZ*tangleZ) else: sinnR= numpy.sin(tgridR*tangleR+tgridZ*tangleZ) A[:,:,2:]= sinnR #Matrix magic atainv= numpy.empty((no,2+nn,2+nn)) AT= numpy.transpose(A,axes=(0,2,1)) for ii in range(no): atainv[ii,:,:,]= linalg.inv(numpy.dot(AT[ii,:,:],A[ii,:,:])) ATAR= numpy.sum(AT*numpy.transpose(numpy.tile(angleRT,(2+nn,1,1)),axes=(1,0,2)),axis=2) ATAT= numpy.sum(AT*numpy.transpose(numpy.tile(anglephiT,(2+nn,1,1)),axes=(1,0,2)),axis=2) ATAZ= numpy.sum(AT*numpy.transpose(numpy.tile(angleZT,(2+nn,1,1)),axes=(1,0,2)),axis=2) angleR= numpy.sum(atainv[:,0,:]*ATAR,axis=1) OmegaR= numpy.sum(atainv[:,1,:]*ATAR,axis=1) anglephi= numpy.sum(atainv[:,0,:]*ATAT,axis=1) Omegaphi= numpy.sum(atainv[:,1,:]*ATAT,axis=1) angleZ= numpy.sum(atainv[:,0,:]*ATAZ,axis=1) OmegaZ= numpy.sum(atainv[:,1,:]*ATAZ,axis=1) Omegaphi[negFreqIndx]= -Omegaphi[negFreqIndx] anglephi[negFreqIndx]= _TWOPI-anglephi[negFreqIndx] if kwargs.get('_retacfs',False): return (jr,lz,jz,OmegaR,Omegaphi,OmegaZ, #pragma: no cover angleR % _TWOPI, anglephi % _TWOPI, angleZ % _TWOPI,acfs) else: return (jr,lz,jz,OmegaR,Omegaphi,OmegaZ, angleR % _TWOPI, anglephi % _TWOPI, angleZ % _TWOPI) def plot(self,*args,**kwargs): """ NAME: plot PURPOSE: plot the angles vs. each other, to check whether the isochrone approximation is good INPUT: Either: a) R,vR,vT,z,vz: floats: phase-space value for single object b) Orbit instance type= ('araz') type of plot to make a) 'araz': az vs. ar, with color-coded aphi b) 'araphi': aphi vs. ar, with color-coded az c) 'azaphi': aphi vs. az, with color-coded ar d) 'jr': cumulative average of jr with time, to assess convergence e) 'lz': same as 'jr' but for lz f) 'jz': same as 'jr' but for jz deperiod= (False), if True, de-period the angles downsample= (False) if True, downsample what's plotted to 400 points +plot kwargs OUTPUT: plot to output HISTORY: 2013-09-10 - Written - Bovy (IAS) """ #Kwargs type= kwargs.pop('type','araz') deperiod= kwargs.pop('deperiod',False) downsample= kwargs.pop('downsample',False) #Parse input R,vR,vT,z,vz,phi= self._parse_args('a' in type,False,*args) #Use self._aAI to calculate the actions and angles in the isochrone potential acfs= self._aAI._actionsFreqsAngles(R.flatten(), vR.flatten(), vT.flatten(), z.flatten(), vz.flatten(), phi.flatten()) if type == 'jr' or type == 'lz' or type == 'jz': jrI= numpy.reshape(acfs[0],R.shape)[:,:-1] jzI= numpy.reshape(acfs[2],R.shape)[:,:-1] anglerI= numpy.reshape(acfs[6],R.shape) anglezI= numpy.reshape(acfs[8],R.shape) danglerI= ((numpy.roll(anglerI,-1,axis=1)-anglerI) % _TWOPI)[:,:-1] danglezI= ((numpy.roll(anglezI,-1,axis=1)-anglezI) % _TWOPI)[:,:-1] if True: sumFunc= numpy.cumsum jr= sumFunc(jrI*danglerI,axis=1)/sumFunc(danglerI,axis=1) jz= sumFunc(jzI*danglezI,axis=1)/sumFunc(danglezI,axis=1) lzI= numpy.reshape(acfs[1],R.shape)[:,:-1] anglephiI= numpy.reshape(acfs[7],R.shape) danglephiI= ((numpy.roll(anglephiI,-1,axis=1)-anglephiI) % _TWOPI)[:,:-1] lz= sumFunc(lzI*danglephiI,axis=1)/sumFunc(danglephiI,axis=1) from ..orbit import Orbit if isinstance(args[0],Orbit) and hasattr(args[0],'t'): ts= args[0].t[:-1] else: ts= self._tsJ[:-1] if type == 'jr': if downsample: plotx= ts[::int(round(self._ntintJ//400))] ploty= jr[0,::int(round(self._ntintJ//400))]/jr[0,-1] plotz= anglerI[0,:-1:int(round(self._ntintJ//400))] else: plotx= ts ploty= jr[0,:]/jr[0,-1] plotz= anglerI[0,:-1] plot.plot(plotx,ploty, c=plotz, s=20., scatter=True, edgecolor='none', xlabel=r'$t$', ylabel=r'$J^A_R / \langle J^A_R \rangle$', clabel=r'$\theta^A_R$', vmin=0.,vmax=2.*numpy.pi, crange=[0.,2.*numpy.pi], colorbar=True, **kwargs) elif type == 'lz': if downsample: plotx= ts[::int(round(self._ntintJ//400))] ploty= lz[0,::int(round(self._ntintJ//400))]/lz[0,-1] plotz= anglephiI[0,:-1:int(round(self._ntintJ//400))] else: plotx= ts ploty= lz[0,:]/lz[0,-1] plotz= anglephiI[0,:-1] plot.plot(plotx,ploty,c=plotz,s=20., scatter=True, edgecolor='none', xlabel=r'$t$', ylabel=r'$L^A_Z / \langle L^A_Z \rangle$', clabel=r'$\theta^A_\phi$', vmin=0.,vmax=2.*numpy.pi, crange=[0.,2.*numpy.pi], colorbar=True, **kwargs) elif type == 'jz': if downsample: plotx= ts[::int(round(self._ntintJ//400))] ploty= jz[0,::int(round(self._ntintJ//400))]/jz[0,-1] plotz= anglezI[0,:-1:int(round(self._ntintJ//400))] else: plotx= ts ploty= jz[0,:]/jz[0,-1] plotz= anglezI[0,:-1] plot.plot(plotx,ploty,c=plotz,s=20., scatter=True, edgecolor='none', xlabel=r'$t$', ylabel=r'$J^A_Z / \langle J^A_Z \rangle$', clabel=r'$\theta^A_Z$', vmin=0.,vmax=2.*numpy.pi, crange=[0.,2.*numpy.pi], colorbar=True, **kwargs) else: if deperiod: if 'ar' in type: angleRT= dePeriod(numpy.reshape(acfs[6],R.shape)) else: angleRT= numpy.reshape(acfs[6],R.shape) if 'aphi' in type: acfs7= numpy.reshape(acfs[7],R.shape) negFreqIndx= numpy.median(acfs7-numpy.roll(acfs7,1,axis=1),axis=1) < 0. #anglephi is decreasing anglephiT= numpy.empty(acfs7.shape) anglephiT[negFreqIndx,:]= dePeriod(_TWOPI-acfs7[negFreqIndx,:]) negFreqPhi= numpy.zeros(R.shape[0],dtype='bool') negFreqPhi[negFreqIndx]= True anglephiT[True^negFreqIndx,:]= dePeriod(acfs7[True^negFreqIndx,:]) else: anglephiT= numpy.reshape(acfs[7],R.shape) if 'az' in type: angleZT= dePeriod(numpy.reshape(acfs[8],R.shape)) else: angleZT= numpy.reshape(acfs[8],R.shape) xrange= None yrange= None else: angleRT= numpy.reshape(acfs[6],R.shape) anglephiT= numpy.reshape(acfs[7],R.shape) angleZT= numpy.reshape(acfs[8],R.shape) xrange= [-0.5,2.*numpy.pi+0.5] yrange= [-0.5,2.*numpy.pi+0.5] vmin, vmax= 0.,2.*numpy.pi crange= [vmin,vmax] if type == 'araz': if downsample: plotx= angleRT[0,::int(round(self._ntintJ//400))] ploty= angleZT[0,::int(round(self._ntintJ//400))] plotz= anglephiT[0,::int(round(self._ntintJ//400))] else: plotx= angleRT[0,:] ploty= angleZT[0,:] plotz= anglephiT[0,:] plot.plot(plotx,ploty,c=plotz,s=20., scatter=True, edgecolor='none', xlabel=r'$\theta^A_R$', ylabel=r'$\theta^A_Z$', clabel=r'$\theta^A_\phi$', xrange=xrange,yrange=yrange, vmin=vmin,vmax=vmax, crange=crange, colorbar=True, **kwargs) elif type == 'araphi': if downsample: plotx= angleRT[0,::int(round(self._ntintJ//400))] ploty= anglephiT[0,::int(round(self._ntintJ//400))] plotz= angleZT[0,::int(round(self._ntintJ//400))] else: plotx= angleRT[0,:] ploty= anglephiT[0,:] plotz= angleZT[0,:] plot.plot(plotx,ploty,c=plotz,s=20., scatter=True, edgecolor='none', xlabel=r'$\theta^A_R$', clabel=r'$\theta^A_Z$', ylabel=r'$\theta^A_\phi$', xrange=xrange,yrange=yrange, vmin=vmin,vmax=vmax, crange=crange, colorbar=True, **kwargs) elif type == 'azaphi': if downsample: plotx= angleZT[0,::int(round(self._ntintJ//400))] ploty= anglephiT[0,::int(round(self._ntintJ//400))] plotz= angleRT[0,::int(round(self._ntintJ//400))] else: plotx= angleZT[0,:] ploty= anglephiT[0,:] plotz= angleRT[0,:] plot.plot(plotx,ploty,c=plotz,s=20., scatter=True, edgecolor='none', clabel=r'$\theta^A_R$', xlabel=r'$\theta^A_Z$', ylabel=r'$\theta^A_\phi$', xrange=xrange,yrange=yrange, vmin=vmin,vmax=vmax, crange=crange, colorbar=True, **kwargs) return None def _parse_args(self,freqsAngles=True,_firstFlip=False,*args): """Helper function to parse the arguments to the __call__ and actionsFreqsAngles functions""" from ..orbit import Orbit RasOrbit= False integrated= True #whether the orbit was already integrated when given if len(args) == 5 or len(args) == 3: #pragma: no cover raise OSError("Must specify phi for actionAngleIsochroneApprox") if len(args) == 6 or len(args) == 4: if len(args) == 6: R,vR,vT, z, vz, phi= args else: R,vR,vT, phi= args z, vz= numpy.zeros_like(R), numpy.zeros_like(R) if isinstance(R,float): os= [Orbit([R,vR,vT,z,vz,phi])] RasOrbit= True integrated= False elif len(R.shape) == 1: #not integrated yet os= [Orbit([R[ii],vR[ii],vT[ii],z[ii],vz[ii],phi[ii]]) for ii in range(R.shape[0])] RasOrbit= True integrated= False if isinstance(args[0],Orbit) \ or (isinstance(args[0],list) and isinstance(args[0][0],Orbit)) \ or RasOrbit: if RasOrbit: pass elif not isinstance(args[0],list): os= [args[0]] if os[0].phasedim() == 3 or os[0].phasedim() == 5: #pragma: no cover raise OSError("Must specify phi for actionAngleIsochroneApprox") else: os= args[0] if os[0].phasedim() == 3 or os[0].phasedim() == 5: #pragma: no cover raise OSError("Must specify phi for actionAngleIsochroneApprox") self._check_consistent_units_orbitInput(os[0]) if not hasattr(os[0],'orbit'): #not integrated yet if _firstFlip: for o in os: o.vxvv[...,1]= -o.vxvv[...,1] o.vxvv[...,2]= -o.vxvv[...,2] o.vxvv[...,4]= -o.vxvv[...,4] [o.integrate(self._tsJ,pot=self._pot, method=self._integrate_method, dt=self._integrate_dt) for o in os] if _firstFlip: for o in os: o.vxvv[...,1]= -o.vxvv[...,1] o.vxvv[...,2]= -o.vxvv[...,2] o.vxvv[...,4]= -o.vxvv[...,4] o.orbit[...,1]= -o.orbit[...,1] o.orbit[...,2]= -o.orbit[...,2] o.orbit[...,4]= -o.orbit[...,4] integrated= False ntJ= os[0].getOrbit().shape[0] no= len(os) R= numpy.empty((no,ntJ)) vR= numpy.empty((no,ntJ)) vT= numpy.empty((no,ntJ)) z= numpy.zeros((no,ntJ))+10.**-7. #To avoid numpy warnings for vz= numpy.zeros((no,ntJ))+10.**-7. #planarOrbits phi= numpy.empty((no,ntJ)) for ii in range(len(os)): this_orbit= os[ii].getOrbit() R[ii,:]= this_orbit[:,0] vR[ii,:]= this_orbit[:,1] vT[ii,:]= this_orbit[:,2] if this_orbit.shape[1] == 6: z[ii,:]= this_orbit[:,3] vz[ii,:]= this_orbit[:,4] phi[ii,:]= this_orbit[:,5] else: phi[ii,:]= this_orbit[:,3] if freqsAngles and not integrated: #also integrate backwards in time, such that the requested point is not at the edge no= R.shape[0] nt= R.shape[1] oR= numpy.empty((no,2*nt-1)) ovR= numpy.empty((no,2*nt-1)) ovT= numpy.empty((no,2*nt-1)) oz= numpy.zeros((no,2*nt-1))+10.**-7. #To avoid numpy warnings for ovz= numpy.zeros((no,2*nt-1))+10.**-7. #planarOrbits ophi= numpy.empty((no,2*nt-1)) if _firstFlip: oR[:,:nt]= R[:,::-1] ovR[:,:nt]= vR[:,::-1] ovT[:,:nt]= vT[:,::-1] oz[:,:nt]= z[:,::-1] ovz[:,:nt]= vz[:,::-1] ophi[:,:nt]= phi[:,::-1] else: oR[:,nt-1:]= R ovR[:,nt-1:]= vR ovT[:,nt-1:]= vT oz[:,nt-1:]= z ovz[:,nt-1:]= vz ophi[:,nt-1:]= phi #load orbits if _firstFlip: os= [Orbit([R[ii,0],vR[ii,0],vT[ii,0],z[ii,0],vz[ii,0],phi[ii,0]]) for ii in range(R.shape[0])] else: os= [Orbit([R[ii,0],-vR[ii,0],-vT[ii,0],z[ii,0],-vz[ii,0],phi[ii,0]]) for ii in range(R.shape[0])] #integrate orbits [o.integrate(self._tsJ,pot=self._pot, method=self._integrate_method, dt=self._integrate_dt) for o in os] #extract phase-space points along the orbit ts= self._tsJ if _firstFlip: for ii in range(no): oR[ii,nt:]= os[ii].R(ts[1:]) #drop t=0, which we have ovR[ii,nt:]= os[ii].vR(ts[1:]) #already ovT[ii,nt:]= os[ii].vT(ts[1:]) # reverse, such that if os[ii].getOrbit().shape[1] == 6: oz[ii,nt:]= os[ii].z(ts[1:]) #everything is in the ovz[ii,nt:]= os[ii].vz(ts[1:]) #right order ophi[ii,nt:]= os[ii].phi(ts[1:]) #! else: for ii in range(no): oR[ii,:nt-1]= os[ii].R(ts[1:])[::-1] #drop t=0, which we have ovR[ii,:nt-1]= -os[ii].vR(ts[1:])[::-1] #already ovT[ii,:nt-1]= -os[ii].vT(ts[1:])[::-1] # reverse, such that if os[ii].getOrbit().shape[1] == 6: oz[ii,:nt-1]= os[ii].z(ts[1:])[::-1] #everything is in the ovz[ii,:nt-1]= -os[ii].vz(ts[1:])[::-1] #right order ophi[ii,:nt-1]= os[ii].phi(ts[1:])[::-1] #! return (oR,ovR,ovT,oz,ovz,ophi) else: return (R,vR,vT,z,vz,phi) @potential_physical_input @physical_conversion('position',pop=True) def estimateBIsochrone(pot,R,z,phi=None): """ NAME: estimateBIsochrone PURPOSE: Estimate a good value for the scale of the isochrone potential by matching the slope of the rotation curve INPUT: pot- Potential instance or list thereof R,z - coordinates (if these are arrays, the median estimated delta is returned, i.e., if this is an orbit) phi= (None) azimuth to use for non-axisymmetric potentials (array if R and z are arrays) OUTPUT: b if 1 R,Z given bmin,bmedian,bmax if multiple R given HISTORY: 2013-09-12 - Written - Bovy (IAS) 2016-02-20 - Changed input order to allow physical conversions - Bovy (UofT) 2016-06-28 - Added phi= keyword for non-axisymmetric potential - Bovy (UofT) """ if pot is None: #pragma: no cover raise OSError("pot= needs to be set to a Potential instance or list thereof") if isinstance(R,numpy.ndarray): if phi is None: phi= [None for r in R] bs= numpy.array([estimateBIsochrone(pot,R[ii],z[ii],phi=phi[ii], use_physical=False) for ii in range(len(R))]) return numpy.array([numpy.amin(bs[True^numpy.isnan(bs)]), numpy.median(bs[True^numpy.isnan(bs)]), numpy.amax(bs[True^numpy.isnan(bs)])]) else: r2= R**2.+z**2 r= numpy.sqrt(r2) dlvcdlr= dvcircdR(pot,r,phi=phi,use_physical=False)/vcirc(pot,r,phi=phi,use_physical=False)*r try: b= optimize.brentq(lambda x: dlvcdlr-(x/numpy.sqrt(r2+x**2.)-0.5*r2/(r2+x**2.)), 0.01,100.) except: #pragma: no cover b= numpy.nan return b def dePeriod(arr): """make an array of periodic angles increase linearly""" diff= arr-numpy.roll(arr,1,axis=1) w= diff < -6. addto= numpy.cumsum(w.astype(int),axis=1) return arr+_TWOPI*addto ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleIsochroneInverse.py0000644000175100001710000001666014327773303023622 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleIsochroneInverse # # Calculate (x,v) coordinates for the Isochrone potential from # given actions-angle coordinates # ############################################################################### import numpy from scipy import optimize from ..potential import IsochronePotential from ..util import conversion from .actionAngleInverse import actionAngleInverse class actionAngleIsochroneInverse(actionAngleInverse): """Inverse action-angle formalism for the isochrone potential, on the Jphi, Jtheta system of Binney & Tremaine (2008); following McGill & Binney (1990) for transformations""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleIsochroneInverse object INPUT: Either: b= scale parameter of the isochrone parameter (can be Quantity) ip= instance of a IsochronePotential ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) OUTPUT: instance HISTORY: 2017-11-14 - Started - Bovy (UofT) """ actionAngleInverse.__init__(self,*args,**kwargs) if not 'b' in kwargs and not 'ip' in kwargs: #pragma: no cover raise OSError("Must specify b= for actionAngleIsochrone") if 'ip' in kwargs: ip= kwargs['ip'] if not isinstance(ip,IsochronePotential): #pragma: no cover raise OSError("'Provided ip= does not appear to be an instance of an IsochronePotential") # Check the units self._pot= ip self._check_consistent_units() self.b= ip.b self.amp= ip._amp else: self.b= conversion.parse_length(kwargs['b'],ro=self._ro) rb= numpy.sqrt(self.b**2.+1.) self.amp= (self.b+rb)**2.*rb # In case we ever decide to implement this in C... self._c= False ext_loaded= False if ext_loaded and (('c' in kwargs and kwargs['c']) or not 'c' in kwargs): #pragma: no cover self._c= True else: self._c= False if not self._c: self._ip= IsochronePotential(amp=self.amp,b=self.b) #Define _pot, because some functions that use actionAngle instances need this self._pot= IsochronePotential(amp=self.amp,b=self.b) # Check the units self._check_consistent_units() return None def _evaluate(self,jr,jphi,jz,angler,anglephi,anglez,**kwargs): """ NAME: __call__ PURPOSE: evaluate the phase-space coordinates (x,v) for a number of angles on a single torus INPUT: jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) angler - radial angle (array [N]) anglephi - azimuthal angle (array [N]) anglez - vertical angle (array [N]) tol= (object-wide value) goal for |dJ|/|J| along the torus OUTPUT: [R,vR,vT,z,vz,phi] HISTORY: 2017-11-14 - Written - Bovy (UofT) """ return self._xvFreqs(jr,jphi,jz,angler,anglephi,anglez,**kwargs)[:6] def _xvFreqs(self,jr,jphi,jz,angler,anglephi,anglez,**kwargs): """ NAME: xvFreqs PURPOSE: evaluate the phase-space coordinates (x,v) for a number of angles on a single torus as well as the frequencies INPUT: jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) angler - radial angle (array [N]) anglephi - azimuthal angle (array [N]) anglez - vertical angle (array [N]) OUTPUT: ([R,vR,vT,z,vz,phi],OmegaR,Omegaphi,Omegaz) HISTORY: 2017-11-15 - Written - Bovy (UofT) """ L= jz+numpy.fabs(jphi) # total angular momentum L2= L**2. sqrtfourbkL2= numpy.sqrt(L2+4.*self.b*self.amp) H= -2.*self.amp**2./(2.*jr+L+sqrtfourbkL2)**2. # Calculate the frequencies omegar= (-2.*H)**1.5/self.amp omegaz= (1.+L/sqrtfourbkL2)/2.*omegar # Start on getting the coordinates a= -self.amp/2./H-self.b ab= a+self.b e= numpy.sqrt(1.+L2/(2.*H*a**2.)) # Solve Kepler's-ish equation; ar must be between 0 and 2pi angler= (numpy.atleast_1d(angler) % (-2.*numpy.pi)) % (2.*numpy.pi) anglephi= numpy.atleast_1d(anglephi) anglez= numpy.atleast_1d(anglez) eta= numpy.empty(len(angler)) for ii,ar in enumerate(angler): try: eta[ii]= optimize.newton(lambda x: x-a*e/ab*numpy.sin(x)-ar, 0., lambda x: 1-a*e/ab*numpy.cos(x)) except RuntimeError: # Newton-Raphson did not converge, this has to work, # bc 0 <= ra < 2pi the following start x have different signs eta[ii]= optimize.brentq(lambda x: x-a*e/ab*numpy.sin(x)-ar, 0.,2.*numpy.pi) coseta= numpy.cos(eta) r= a*numpy.sqrt((1.-e*coseta)*(1.-e*coseta+2.*self.b/a)) vr= numpy.sqrt(self.amp/ab)*a*e*numpy.sin(eta)/r taneta2= numpy.tan(eta/2.) tan11= numpy.arctan(numpy.sqrt((1.+e)/(1.-e))*taneta2) tan12= numpy.arctan(\ numpy.sqrt((a*(1.+e)+2.*self.b)/(a*(1.-e)+2.*self.b))*taneta2) tan11[tan11 < 0.]+= numpy.pi tan12[tan12 < 0.]+= numpy.pi Lambdaeta= tan11+L/sqrtfourbkL2*tan12 psi= anglez-omegaz/omegar*angler+Lambdaeta lowerl= numpy.sqrt(1.-jphi**2./L2) sintheta= numpy.sin(psi)*lowerl costheta= numpy.sqrt(1.-sintheta**2.) vtheta= L*lowerl*numpy.cos(psi)/costheta/r R= r*costheta z= r*sintheta vR= vr*costheta-vtheta*sintheta vz= vr*sintheta+vtheta*costheta sinu= sintheta/costheta*jphi/L/lowerl u= numpy.arcsin(sinu) u[vtheta < 0.]= numpy.pi-u[vtheta < 0.] phi= anglephi-numpy.sign(jphi)*anglez+u # For non-inclined orbits, phi == psi phi[True^numpy.isfinite(phi)]= psi[True^numpy.isfinite(phi)] phi= phi % (2.*numpy.pi) phi[phi < 0.]+= 2.*numpy.pi return (R,vR,jphi/R,z,vz,phi, omegar,numpy.sign(jphi)*omegaz,omegaz) def _Freqs(self,jr,jphi,jz,**kwargs): """ NAME: Freqs PURPOSE: return the frequencies corresponding to a torus INPUT: jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) OUTPUT: (OmegaR,Omegaphi,Omegaz) HISTORY: 2017-11-15 - Written - Bovy (UofT) """ L= jz+numpy.fabs(jphi) # total angular momentum sqrtfourbkL2= numpy.sqrt(L**2.+4.*self.b*self.amp) H= -2.*self.amp**2./(2.*jr+L+sqrtfourbkL2)**2. # Calculate the frequencies omegar= (-2.*H)**1.5/self.amp omegaz= (1.+L/sqrtfourbkL2)/2.*omegar return (omegar,numpy.sign(jphi)*omegaz,omegaz) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleSpherical.py0000644000175100001710000005551314327773303022247 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleSpherical # # methods: # __call__: returns (jr,lz,jz) # actionsFreqs: returns (jr,lz,jz,Or,Op,Oz) # actionsFreqsAngles: returns (jr,lz,jz,Or,Op,Oz,ar,ap,az) # ############################################################################### import copy import numpy from scipy import integrate from ..potential import _dim, epifreq, omegac from ..potential.Potential import _evaluatePotentials from ..potential.Potential import flatten as flatten_potential from .actionAngle import actionAngle from .actionAngleAxi import actionAngleAxi, potentialAxi class actionAngleSpherical(actionAngle): """Action-angle formalism for spherical potentials""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleSpherical object INPUT: pot= a Spherical potential ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) OUTPUT: instance HISTORY: 2013-12-28 - Written - Bovy (IAS) """ actionAngle.__init__(self, ro=kwargs.get('ro',None),vo=kwargs.get('vo',None)) if not 'pot' in kwargs: #pragma: no cover raise OSError("Must specify pot= for actionAngleSpherical") self._pot= flatten_potential(kwargs['pot']) #Also store a 'planar' (2D) version of the potential if _dim(self._pot) == 2: self._2dpot= self._pot elif isinstance(self._pot,list): self._2dpot= [p.toPlanar() for p in self._pot] else: self._2dpot= self._pot.toPlanar() #The following for if we ever implement this code in C self._c= False ext_loaded= False if ext_loaded and (('c' in kwargs and kwargs['c']) or not 'c' in kwargs): self._c= True #pragma: no cover else: self._c= False # Check the units self._check_consistent_units() return None def _evaluate(self,*args,**kwargs): """ NAME: __call__ (_evaluate) PURPOSE: evaluate the actions (jr,lz,jz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument fixed_quad= (False) if True, use n=10 fixed_quad integration scipy.integrate.quadrature or .fixed_quad keywords OUTPUT: (jr,lz,jz) HISTORY: 2013-12-28 - Written - Bovy (IAS) """ fixed_quad= kwargs.pop('fixed_quad',False) if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) if self._c: #pragma: no cover pass else: Lz= R*vT Lx= -z*vT Ly= z*vR-R*vz L2= Lx*Lx+Ly*Ly+Lz*Lz E= _evaluatePotentials(self._pot,R,z)\ +vR**2./2.+vT**2./2.+vz**2./2. L= numpy.sqrt(L2) #Actions Jphi= Lz Jz= L-numpy.fabs(Lz) #Jr requires some more work #Set up an actionAngleAxi object for EL and rap/rperi calculations axiR= numpy.sqrt(R**2.+z**2.) axivT= L/axiR axivR= (R*vR+z*vz)/axiR Jr= [] for ii in range(len(axiR)): axiaA= actionAngleAxi(axiR[ii],axivR[ii],axivT[ii], pot=self._2dpot) (rperi,rap)= axiaA.calcRapRperi() EL= axiaA.calcEL() E, L= EL Jr.append(self._calc_jr(rperi,rap,E,L,fixed_quad,**kwargs)) return (numpy.array(Jr),Jphi,Jz) def _actionsFreqs(self,*args,**kwargs): """ NAME: actionsFreqs (_actionsFreqs) PURPOSE: evaluate the actions and frequencies (jr,lz,jz,Omegar,Omegaphi,Omegaz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument fixed_quad= (False) if True, use n=10 fixed_quad integration scipy.integrate.quadrature or .fixed_quad keywords OUTPUT: (jr,lz,jz,Omegar,Omegaphi,Omegaz) HISTORY: 2013-12-28 - Written - Bovy (IAS) """ fixed_quad= kwargs.pop('fixed_quad',False) if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) if self._c: #pragma: no cover pass else: Lz= R*vT Lx= -z*vT Ly= z*vR-R*vz L2= Lx*Lx+Ly*Ly+Lz*Lz E= _evaluatePotentials(self._pot,R,z)+vR**2./2.+vT**2./2.+vz**2./2. L= numpy.sqrt(L2) #Actions Jphi= Lz Jz= L-numpy.fabs(Lz) #Jr requires some more work #Set up an actionAngleAxi object for EL and rap/rperi calculations axiR= numpy.sqrt(R**2.+z**2.) axivT= L/axiR axivR= (R*vR+z*vz)/axiR Jr= [] Or= [] Op= [] for ii in range(len(axiR)): axiaA= actionAngleAxi(axiR[ii],axivR[ii],axivT[ii], pot=self._2dpot) (rperi,rap)= axiaA.calcRapRperi() EL= axiaA.calcEL() E, L= EL Jr.append(self._calc_jr(rperi,rap,E,L,fixed_quad,**kwargs)) #Radial period if Jr[-1] < 10.**-9.: #Circular orbit Or.append(epifreq(self._pot,axiR[ii],use_physical=False)) Op.append(omegac(self._pot,axiR[ii],use_physical=False)) continue Rmean= numpy.exp((numpy.log(rperi)+numpy.log(rap))/2.) Or.append(self._calc_or(Rmean,rperi,rap,E,L,fixed_quad,**kwargs)) Op.append(self._calc_op(Or[-1],Rmean,rperi,rap,E,L,fixed_quad,**kwargs)) Op= numpy.array(Op) Oz= copy.copy(Op) Op[vT < 0.]*= -1. return (numpy.array(Jr),Jphi,Jz,numpy.array(Or),Op,Oz) def _actionsFreqsAngles(self,*args,**kwargs): """ NAME: actionsFreqsAngles (_actionsFreqsAngles) PURPOSE: evaluate the actions, frequencies, and angles (jr,lz,jz,Omegar,Omegaphi,Omegaz,ar,ap,az) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument fixed_quad= (False) if True, use n=10 fixed_quad integration scipy.integrate.quadrature or .fixed_quad keywords OUTPUT: (jr,lz,jz,Omegar,Omegaphi,Omegaz,ar,aphi,az) HISTORY: 2013-12-29 - Written - Bovy (IAS) """ fixed_quad= kwargs.pop('fixed_quad',False) if len(args) == 5: #R,vR.vT, z, vz pragma: no cover raise OSError("You need to provide phi when calculating angles") elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz phi= self._eval_phi if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) phi= numpy.array([phi]) if self._c: #pragma: no cover pass else: Lz= R*vT Lx= -z*vT Ly= z*vR-R*vz L2= Lx*Lx+Ly*Ly+Lz*Lz E= _evaluatePotentials(self._pot,R,z)+vR**2./2.+vT**2./2.+vz**2./2. L= numpy.sqrt(L2) #Actions Jphi= Lz Jz= L-numpy.fabs(Lz) #Jr requires some more work #Set up an actionAngleAxi object for EL and rap/rperi calculations axiR= numpy.sqrt(R**2.+z**2.) axivT= L/axiR #these are really spherical coords, called axi bc they go in actionAngleAxi axivR= (R*vR+z*vz)/axiR axivz= (z*vR-R*vz)/axiR Jr= [] Or= [] Op= [] ar= [] az= [] #Calculate the longitude of the ascending node asc= self._calc_long_asc(z,R,axivz,phi,Lz,L) for ii in range(len(axiR)): axiaA= actionAngleAxi(axiR[ii],axivR[ii],axivT[ii], pot=self._2dpot) (rperi,rap)= axiaA.calcRapRperi() EL= axiaA.calcEL() E, L= EL Jr.append(self._calc_jr(rperi,rap,E,L,fixed_quad,**kwargs)) #Radial period Rmean= numpy.exp((numpy.log(rperi)+numpy.log(rap))/2.) if Jr[-1] < 10.**-9.: #Circular orbit Or.append(epifreq(self._pot,axiR[ii],use_physical=False)) Op.append(omegac(self._pot,axiR[ii],use_physical=False)) else: Or.append(self._calc_or(Rmean,rperi,rap,E,L,fixed_quad,**kwargs)) Op.append(self._calc_op(Or[-1],Rmean,rperi,rap,E,L,fixed_quad,**kwargs)) #Angles ar.append(self._calc_angler(Or[-1],axiR[ii],Rmean,rperi,rap, E,L,axivR[ii],fixed_quad,**kwargs)) az.append(self._calc_anglez(Or[-1],Op[-1],ar[-1], z[ii],axiR[ii], Rmean,rperi,rap,E,L,Lz[ii], axivR[ii],axivz[ii],phi[ii], fixed_quad,**kwargs)) Op= numpy.array(Op) Oz= copy.copy(Op) Op[vT < 0.]*= -1. ap= copy.copy(asc) ar= numpy.array(ar) az= numpy.array(az) ap[vT < 0.]-= az[vT < 0.] ap[vT >= 0.]+= az[vT >= 0.] ar= ar % (2.*numpy.pi) ap= ap % (2.*numpy.pi) az= az % (2.*numpy.pi) return (numpy.array(Jr),Jphi,Jz,numpy.array(Or),Op,Oz, ar,ap,az) def _EccZmaxRperiRap(self,*args,**kwargs): """ NAME: EccZmaxRperiRap (_EccZmaxRperiRap) PURPOSE: evaluate the eccentricity, maximum height above the plane, peri- and apocenter for a spherical potential INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument OUTPUT: (e,zmax,rperi,rap) HISTORY: 2017-12-22 - Written - Bovy (UofT) """ if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) if self._c: #pragma: no cover pass else: Lz= R*vT Lx= -z*vT Ly= z*vR-R*vz L2= Lx*Lx+Ly*Ly+Lz*Lz L= numpy.sqrt(L2) #Set up an actionAngleAxi object for EL and rap/rperi calculations axiR= numpy.sqrt(R**2.+z**2.) axivT= L/axiR axivR= (R*vR+z*vz)/axiR rperi, rap= [], [] for ii in range(len(axiR)): axiaA= actionAngleAxi(axiR[ii],axivR[ii],axivT[ii], pot=self._2dpot) trperi,trap= axiaA.calcRapRperi() rperi.append(trperi) rap.append(trap) rperi= numpy.array(rperi) rap= numpy.array(rap) return ((rap-rperi)/(rap+rperi),rap*numpy.sqrt(1.-Lz**2./L2), rperi,rap) def _calc_jr(self,rperi,rap,E,L,fixed_quad,**kwargs): if fixed_quad: return integrate.fixed_quad(_JrSphericalIntegrand, rperi,rap, args=(E,L,self._2dpot), n=10, **kwargs)[0]/numpy.pi else: return (numpy.array(integrate.quad(_JrSphericalIntegrand, rperi,rap, args=(E,L,self._2dpot), **kwargs)))[0]/numpy.pi def _calc_or(self,Rmean,rperi,rap,E,L,fixed_quad,**kwargs): Tr= 0. if Rmean > rperi and not fixed_quad: Tr+= numpy.array(integrate.quadrature(_TrSphericalIntegrandSmall, 0.,numpy.sqrt(Rmean-rperi), args=(E,L,self._2dpot, rperi), **kwargs))[0] elif Rmean > rperi and fixed_quad: Tr+= integrate.fixed_quad(_TrSphericalIntegrandSmall, 0.,numpy.sqrt(Rmean-rperi), args=(E,L,self._2dpot, rperi), n=10,**kwargs)[0] if Rmean < rap and not fixed_quad: Tr+= numpy.array(integrate.quadrature(_TrSphericalIntegrandLarge, 0.,numpy.sqrt(rap-Rmean), args=(E,L,self._2dpot, rap), **kwargs))[0] elif Rmean < rap and fixed_quad: Tr+= integrate.fixed_quad(_TrSphericalIntegrandLarge, 0.,numpy.sqrt(rap-Rmean), args=(E,L,self._2dpot, rap), n=10,**kwargs)[0] Tr= 2.*Tr return 2.*numpy.pi/Tr def _calc_op(self,Or,Rmean,rperi,rap,E,L,fixed_quad,**kwargs): #Azimuthal period I= 0. if Rmean > rperi and not fixed_quad: I+= numpy.array(integrate.quadrature(_ISphericalIntegrandSmall, 0.,numpy.sqrt(Rmean-rperi), args=(E,L,self._2dpot, rperi), **kwargs))[0] elif Rmean > rperi and fixed_quad: I+= integrate.fixed_quad(_ISphericalIntegrandSmall, 0.,numpy.sqrt(Rmean-rperi), args=(E,L,self._2dpot,rperi), n=10,**kwargs)[0] if Rmean < rap and not fixed_quad: I+= numpy.array(integrate.quadrature(_ISphericalIntegrandLarge, 0.,numpy.sqrt(rap-Rmean), args=(E,L,self._2dpot, rap), **kwargs))[0] elif Rmean < rap and fixed_quad: I+= integrate.fixed_quad(_ISphericalIntegrandLarge, 0.,numpy.sqrt(rap-Rmean), args=(E,L,self._2dpot,rap), n=10,**kwargs)[0] I*= 2*L return I*Or/2./numpy.pi def _calc_long_asc(self,z,R,axivz,phi,Lz,L): i= numpy.arccos(Lz/L) sinu= z/R/numpy.tan(i) pindx= (sinu > 1.)*numpy.isfinite(sinu) sinu[pindx]= 1. pindx= (sinu < -1.)*numpy.isfinite(sinu) sinu[pindx]= -1. u= numpy.arcsin(sinu) vzindx= axivz > 0. u[vzindx]= numpy.pi-u[vzindx] # For non-inclined orbits, we set Omega=0 by convention u[True^numpy.isfinite(u)]= phi[True^numpy.isfinite(u)] return phi-u def _calc_angler(self,Or,r,Rmean,rperi,rap,E,L,vr,fixed_quad,**kwargs): if r < Rmean: if r > rperi and not fixed_quad: wr= Or*integrate.quadrature(_TrSphericalIntegrandSmall, 0.,numpy.sqrt(r-rperi), args=(E,L,self._2dpot,rperi), **kwargs)[0] elif r > rperi and fixed_quad: wr= Or*integrate.fixed_quad(_TrSphericalIntegrandSmall, 0.,numpy.sqrt(r-rperi), args=(E,L,self._2dpot,rperi), n=10,**kwargs)[0] else: wr= 0. if vr < 0.: wr= 2*numpy.pi-wr else: if r < rap and not fixed_quad: wr= Or*integrate.quadrature(_TrSphericalIntegrandLarge, 0.,numpy.sqrt(rap-r), args=(E,L,self._2dpot,rap), **kwargs)[0] elif r < rap and fixed_quad: wr= Or*integrate.fixed_quad(_TrSphericalIntegrandLarge, 0.,numpy.sqrt(rap-r), args=(E,L,self._2dpot,rap), n=10,**kwargs)[0] else: wr= numpy.pi if vr < 0.: wr= numpy.pi+wr else: wr= numpy.pi-wr return wr def _calc_anglez(self,Or,Op,ar,z,r,Rmean,rperi,rap,E,L,Lz,vr,axivz,phi, fixed_quad,**kwargs): #First calculate psi i= numpy.arccos(Lz/L) sinpsi= z/r/numpy.sin(i) if numpy.isfinite(sinpsi): if sinpsi > 1.: sinpsi= 1. elif sinpsi < -1.: sinpsi= -1. psi= numpy.arcsin(sinpsi) if axivz > 0.: psi= numpy.pi-psi else: psi= phi psi= psi % (2.*numpy.pi) #Calculate dSr/dL dpsi= Op/Or*2.*numpy.pi #this is the full I integral if r < Rmean: if not fixed_quad: wz= L*integrate.quadrature(_ISphericalIntegrandSmall, 0.,numpy.sqrt(r-rperi), args=(E,L,self._2dpot, rperi), **kwargs)[0] elif fixed_quad: wz= L*integrate.fixed_quad(_ISphericalIntegrandSmall, 0.,numpy.sqrt(r-rperi), args=(E,L,self._2dpot, rperi), n=10,**kwargs)[0] if vr < 0.: wz= dpsi-wz else: if not fixed_quad: wz= L*integrate.quadrature(_ISphericalIntegrandLarge, 0.,numpy.sqrt(rap-r), args=(E,L,self._2dpot, rap), **kwargs)[0] elif fixed_quad: wz= L*integrate.fixed_quad(_ISphericalIntegrandLarge, 0.,numpy.sqrt(rap-r), args=(E,L,self._2dpot, rap), n=10,**kwargs)[0] if vr < 0.: wz= dpsi/2.+wz else: wz= dpsi/2.-wz #Add everything wz= -wz+psi+Op/Or*ar return wz def _JrSphericalIntegrand(r,E,L,pot): """The J_r integrand""" return numpy.sqrt(2.*(E-potentialAxi(r,pot))-L**2./r**2.) def _TrSphericalIntegrandSmall(t,E,L,pot,rperi): r= rperi+t**2.#part of the transformation return 2.*t/_JrSphericalIntegrand(r,E,L,pot) def _TrSphericalIntegrandLarge(t,E,L,pot,rap): r= rap-t**2.#part of the transformation return 2.*t/_JrSphericalIntegrand(r,E,L,pot) def _ISphericalIntegrandSmall(t,E,L,pot,rperi): r= rperi+t**2.#part of the transformation return 2.*t/_JrSphericalIntegrand(r,E,L,pot)/r**2. def _ISphericalIntegrandLarge(t,E,L,pot,rap): r= rap-t**2.#part of the transformation return 2.*t/_JrSphericalIntegrand(r,E,L,pot)/r**2. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleStaeckel.py0000644000175100001710000013672414327773303022074 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleStaeckel # # Use Binney (2012; MNRAS 426, 1324)'s Staeckel approximation for # calculating the actions # # methods: # __call__: returns (jr,lz,jz) # ############################################################################### import copy import warnings import numpy from scipy import integrate, optimize from ..potential import (DiskSCFPotential, MWPotential, SCFPotential, epifreq, evaluateR2derivs, evaluateRzderivs, evaluatez2derivs, omegac, verticalfreq) from ..potential.Potential import (_check_c, _evaluatePotentials, _evaluateRforces, _evaluatezforces) from ..potential.Potential import flatten as flatten_potential from ..util import coords # for prolate confocal transforms from ..util import conversion, galpyWarning from ..util.conversion import physical_conversion, potential_physical_input from . import actionAngleStaeckel_c from .actionAngle import UnboundError, actionAngle from .actionAngleStaeckel_c import _ext_loaded as ext_loaded class actionAngleStaeckel(actionAngle): """Action-angle formalism for axisymmetric potentials using Binney (2012)'s Staeckel approximation""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleStaeckel object INPUT: pot= potential or list of potentials (3D) delta= focus (can be Quantity) useu0 - use u0 to calculate dV (NOT recommended) c= if True, always use C for calculations order= (10) number of points to use in the Gauss-Legendre numerical integration of the relevant action, frequency, and angle integrals ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) OUTPUT: instance HISTORY: 2012-11-27 - Written - Bovy (IAS) """ actionAngle.__init__(self, ro=kwargs.get('ro',None),vo=kwargs.get('vo',None)) if not 'pot' in kwargs: #pragma: no cover raise OSError("Must specify pot= for actionAngleStaeckel") self._pot= flatten_potential(kwargs['pot']) if self._pot == MWPotential: warnings.warn("Use of MWPotential as a Milky-Way-like potential is deprecated; galpy.potential.MWPotential2014, a potential fit to a large variety of dynamical constraints (see Bovy 2015), is the preferred Milky-Way-like potential in galpy", galpyWarning) if not 'delta' in kwargs: #pragma: no cover raise OSError("Must specify delta= for actionAngleStaeckel") if ext_loaded and (('c' in kwargs and kwargs['c']) or not 'c' in kwargs): self._c= _check_c(self._pot) if 'c' in kwargs and kwargs['c'] and not self._c: warnings.warn("C module not used because potential does not have a C implementation",galpyWarning) #pragma: no cover else: self._c= False self._useu0= kwargs.get('useu0',False) self._delta= kwargs['delta'] self._order= kwargs.get('order',10) self._delta= conversion.parse_length(self._delta,ro=self._ro) # Check the units self._check_consistent_units() return None def _evaluate(self,*args,**kwargs): """ NAME: __call__ (_evaluate) PURPOSE: evaluate the actions (jr,lz,jz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument delta= (object-wide default) can be used to override the object-wide focal length; can also be an array with length N to allow different delta for different phase-space points u0= (None) if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed) c= (object-wide default, bool) True/False to override the object-wide setting for whether or not to use the C implementation order= (object-wide default, int) number of points to use in the Gauss-Legendre numerical integration of the relevant action integrals When not using C: fixed_quad= (False) if True, use Gaussian quadrature (scipy.integrate.fixed_quad instead of scipy.integrate.quad) scipy.integrate.fixed_quad or .quad keywords OUTPUT: (jr,lz,jz) HISTORY: 2012-11-27 - Written - Bovy (IAS) 2017-12-27 - Allowed individual delta for each point - Bovy (UofT) """ delta= kwargs.pop('delta',self._delta) order= kwargs.get('order',self._order) if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) if ((self._c and not ('c' in kwargs and not kwargs['c']))\ or (ext_loaded and ('c' in kwargs and kwargs['c']))) \ and _check_c(self._pot): Lz= R*vT if self._useu0: #First calculate u0 if 'u0' in kwargs: u0= numpy.asarray(kwargs['u0']) else: E= numpy.array([_evaluatePotentials(self._pot,R[ii],z[ii]) +vR[ii]**2./2.+vz[ii]**2./2.+vT[ii]**2./2. for ii in range(len(R))]) u0= actionAngleStaeckel_c.actionAngleStaeckel_calcu0(\ E,Lz,self._pot,delta)[0] kwargs.pop('u0',None) else: u0= None jr, jz, err= actionAngleStaeckel_c.actionAngleStaeckel_c(\ self._pot,delta,R,vR,vT,z,vz,u0=u0,order=order) if err == 0: return (jr,Lz,jz) else: #pragma: no cover raise RuntimeError("C-code for calculation actions failed; try with c=False") else: if 'c' in kwargs and kwargs['c'] and not self._c: #pragma: no cover warnings.warn("C module not used because potential does not have a C implementation",galpyWarning) kwargs.pop('c',None) if len(R) > 1: ojr= numpy.zeros(len(R)) olz= numpy.zeros(len(R)) ojz= numpy.zeros(len(R)) for ii in range(len(R)): targs= (R[ii],vR[ii],vT[ii],z[ii],vz[ii]) tkwargs= copy.copy(kwargs) try: tkwargs['delta']= delta[ii] except (TypeError,IndexError): tkwargs['delta']= delta tjr,tlz,tjz= self(*targs,**tkwargs) ojr[ii]= tjr ojz[ii]= tjz olz[ii]= tlz return (ojr,olz,ojz) else: #Set up the actionAngleStaeckelSingle object aASingle= actionAngleStaeckelSingle(R[0],vR[0],vT[0], z[0],vz[0],pot=self._pot, delta=delta) return (numpy.atleast_1d(aASingle.JR(**copy.copy(kwargs))), numpy.atleast_1d(aASingle._R*aASingle._vT), numpy.atleast_1d(aASingle.Jz(**copy.copy(kwargs)))) def _actionsFreqs(self,*args,**kwargs): """ NAME: actionsFreqs (_actionsFreqs) PURPOSE: evaluate the actions and frequencies (jr,lz,jz,Omegar,Omegaphi,Omegaz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument delta= (object-wide default) can be used to override the object-wide focal length; can also be an array with length N to allow different delta for different phase-space points u0= (None) if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed) c= (object-wide default, bool) True/False to override the object-wide setting for whether or not to use the C implementation order= (10) number of points to use in the Gauss-Legendre numerical integration of the relevant action and frequency integrals When not using C: fixed_quad= (False) if True, use Gaussian quadrature (scipy.integrate.fixed_quad instead of scipy.integrate.quad) scipy.integrate.fixed_quad or .quad keywords OUTPUT: (jr,lz,jz,Omegar,Omegaphi,Omegaz) HISTORY: 2013-08-28 - Written - Bovy (IAS) """ delta= kwargs.pop('delta',self._delta) order= kwargs.get('order',self._order) if ((self._c and not ('c' in kwargs and not kwargs['c']))\ or (ext_loaded and ('c' in kwargs and kwargs['c']))) \ and _check_c(self._pot): if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) Lz= R*vT if self._useu0: #First calculate u0 if 'u0' in kwargs: u0= numpy.asarray(kwargs['u0']) else: E= numpy.array([_evaluatePotentials(self._pot,R[ii],z[ii]) +vR[ii]**2./2.+vz[ii]**2./2.+vT[ii]**2./2. for ii in range(len(R))]) u0= actionAngleStaeckel_c.actionAngleStaeckel_calcu0(\ E,Lz,self._pot,delta)[0] kwargs.pop('u0',None) else: u0= None jr, jz, Omegar, Omegaphi, Omegaz, err= actionAngleStaeckel_c.actionAngleFreqStaeckel_c(\ self._pot,delta,R,vR,vT,z,vz,u0=u0,order=order) # Adjustments for close-to-circular orbits indx= numpy.isnan(Omegar)*(jr < 10.**-3.)+numpy.isnan(Omegaz)*(jz < 10.**-3.) #Close-to-circular and close-to-the-plane orbits if numpy.sum(indx) > 0: Omegar[indx]= [epifreq(self._pot,r,use_physical=False) for r in R[indx]] Omegaphi[indx]= [omegac(self._pot,r,use_physical=False) for r in R[indx]] Omegaz[indx]= [verticalfreq(self._pot,r,use_physical=False) for r in R[indx]] if err == 0: return (jr,Lz,jz,Omegar,Omegaphi,Omegaz) else: #pragma: no cover raise RuntimeError("C-code for calculation actions failed; try with c=False") else: if 'c' in kwargs and kwargs['c'] and not self._c: #pragma: no cover warnings.warn("C module not used because potential does not have a C implementation",galpyWarning) raise NotImplementedError("actionsFreqs with c=False not implemented; maybe you meant to install the C extension?") def _actionsFreqsAngles(self,*args,**kwargs): """ NAME: actionsFreqsAngles (_actionsFreqsAngles) PURPOSE: evaluate the actions, frequencies, and angles (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument delta= (object-wide default) can be used to override the object-wide focal length; can also be an array with length N to allow different delta for different phase-space points u0= (None) if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed) c= (object-wide default, bool) True/False to override the object-wide setting for whether or not to use the C implementation order= (10) number of points to use in the Gauss-Legendre numerical integration of the relevant action, frequency, and angle integrals When not using C: fixed_quad= (False) if True, use Gaussian quadrature (scipy.integrate.fixed_quad instead of scipy.integrate.quad) scipy.integrate.fixed_quad or .quad keywords OUTPUT: (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) HISTORY: 2013-08-28 - Written - Bovy (IAS) """ delta= kwargs.pop('delta',self._delta) order= kwargs.get('order',self._order) if ((self._c and not ('c' in kwargs and not kwargs['c']))\ or (ext_loaded and ('c' in kwargs and kwargs['c']))) \ and _check_c(self._pot): if len(args) == 5: #R,vR.vT, z, vz pragma: no cover raise OSError("Must specify phi") elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz phi= self._eval_phi if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) phi= numpy.array([phi]) Lz= R*vT if self._useu0: #First calculate u0 if 'u0' in kwargs: u0= numpy.asarray(kwargs['u0']) else: E= numpy.array([_evaluatePotentials(self._pot,R[ii],z[ii]) +vR[ii]**2./2.+vz[ii]**2./2.+vT[ii]**2./2. for ii in range(len(R))]) u0= actionAngleStaeckel_c.actionAngleStaeckel_calcu0(\ E,Lz,self._pot,delta)[0] kwargs.pop('u0',None) else: u0= None jr, jz, Omegar, Omegaphi, Omegaz, angler, anglephi,anglez, err= actionAngleStaeckel_c.actionAngleFreqAngleStaeckel_c(\ self._pot,delta,R,vR,vT,z,vz,phi,u0=u0,order=order) # Adjustments for close-to-circular orbits indx= numpy.isnan(Omegar)*(jr < 10.**-3.)+numpy.isnan(Omegaz)*(jz < 10.**-3.) #Close-to-circular and close-to-the-plane orbits if numpy.sum(indx) > 0: Omegar[indx]= [epifreq(self._pot,r,use_physical=False) for r in R[indx]] Omegaphi[indx]= [omegac(self._pot,r,use_physical=False) for r in R[indx]] Omegaz[indx]= [verticalfreq(self._pot,r,use_physical=False) for r in R[indx]] if err == 0: return (jr,Lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) else: raise RuntimeError("C-code for calculation actions failed; try with c=False") #pragma: no cover else: #pragma: no cover if 'c' in kwargs and kwargs['c'] and not self._c: #pragma: no cover warnings.warn("C module not used because potential does not have a C implementation",galpyWarning) raise NotImplementedError("actionsFreqs with c=False not implemented; maybe you meant to install the C extension?") def _EccZmaxRperiRap(self,*args,**kwargs): """ NAME: EccZmaxRperiRap (_EccZmaxRperiRap) PURPOSE: evaluate the eccentricity, maximum height above the plane, peri- and apocenter in the Staeckel approximation INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument delta= (object-wide default) can be used to override the object-wide focal length; can also be an array with length N to allow different delta for different phase-space points u0= (None) if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed) c= (object-wide default, bool) True/False to override the object-wide setting for whether or not to use the C implementation OUTPUT: (e,zmax,rperi,rap) HISTORY: 2017-12-12 - Written - Bovy (UofT) """ delta= kwargs.get('delta',self._delta) umin, umax, vmin= self._uminumaxvmin(*args,**kwargs) rperi= coords.uv_to_Rz(umin,numpy.pi/2.,delta=delta)[0] rap_tmp, zmax= coords.uv_to_Rz(umax,vmin,delta=delta) rap= numpy.sqrt(rap_tmp**2.+zmax**2.) e= (rap-rperi)/(rap+rperi) return (e,zmax,rperi,rap) def _uminumaxvmin(self,*args,**kwargs): """ NAME: _uminumaxvmin PURPOSE: evaluate u_min, u_max, and v_min INPUT: Either: a) R,vR,vT,z,vz b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well c= True/False; overrides the object's c= keyword to use C or not OUTPUT: (umin,umax,vmin) HISTORY: 2017-12-12 - Written - Bovy (UofT) """ delta= kwargs.pop('delta',self._delta) if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz if isinstance(R,float): R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) if ((self._c and not ('c' in kwargs and not kwargs['c']))\ or (ext_loaded and ('c' in kwargs and kwargs['c']))) \ and _check_c(self._pot): Lz= R*vT if self._useu0: #First calculate u0 if 'u0' in kwargs: u0= numpy.asarray(kwargs['u0']) else: E= numpy.array([_evaluatePotentials(self._pot,R[ii],z[ii]) +vR[ii]**2./2.+vz[ii]**2./2.+vT[ii]**2./2. for ii in range(len(R))]) u0= actionAngleStaeckel_c.actionAngleStaeckel_calcu0(\ E,Lz,self._pot,delta)[0] kwargs.pop('u0',None) else: u0= None umin, umax, vmin, err= \ actionAngleStaeckel_c.actionAngleUminUmaxVminStaeckel_c(\ self._pot,delta,R,vR,vT,z,vz,u0=u0) if err == 0: return (umin,umax,vmin) else: #pragma: no cover raise RuntimeError("C-code for calculation actions failed; try with c=False") else: if 'c' in kwargs and kwargs['c'] and not self._c: #pragma: no cover warnings.warn("C module not used because potential does not have a C implementation",galpyWarning) kwargs.pop('c',None) if len(R) > 1: oumin= numpy.zeros(len(R)) oumax= numpy.zeros(len(R)) ovmin= numpy.zeros(len(R)) for ii in range(len(R)): targs= (R[ii],vR[ii],vT[ii],z[ii],vz[ii]) tkwargs= copy.copy(kwargs) try: tkwargs['delta']= delta[ii] except TypeError: tkwargs['delta']= delta tumin,tumax,tvmin= self._uminumaxvmin(*targs,**tkwargs) oumin[ii]= tumin oumax[ii]= tumax ovmin[ii]= tvmin return (oumin,oumax,ovmin) else: #Set up the actionAngleStaeckelSingle object aASingle= actionAngleStaeckelSingle(R[0],vR[0],vT[0], z[0],vz[0],pot=self._pot, delta=delta) umin, umax= aASingle.calcUminUmax() vmin= aASingle.calcVmin() return (numpy.atleast_1d(umin), numpy.atleast_1d(umax), numpy.atleast_1d(vmin)) class actionAngleStaeckelSingle(actionAngle): """Action-angle formalism for axisymmetric potentials using Binney (2012)'s Staeckel approximation""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleStaeckelSingle object INPUT: Either: a) R,vR,vT,z,vz b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well pot= potential or list of potentials OUTPUT: HISTORY: 2012-11-27 - Written - Bovy (IAS) """ self._parse_eval_args(*args,_noOrbUnitsCheck=True,**kwargs) self._R= self._eval_R self._vR= self._eval_vR self._vT= self._eval_vT self._z= self._eval_z self._vz= self._eval_vz if not 'pot' in kwargs: #pragma: no cover raise OSError("Must specify pot= for actionAngleStaeckelSingle") self._pot= kwargs['pot'] if not 'delta' in kwargs: #pragma: no cover raise OSError("Must specify delta= for actionAngleStaeckel") self._delta= kwargs['delta'] #Pre-calculate everything self._ux, self._vx= coords.Rz_to_uv(self._R,self._z, delta=self._delta) self._sinvx= numpy.sin(self._vx) self._cosvx= numpy.cos(self._vx) self._coshux= numpy.cosh(self._ux) self._sinhux= numpy.sinh(self._ux) self._pux= self._delta*(self._vR*self._coshux*self._sinvx +self._vz*self._sinhux*self._cosvx) self._pvx= self._delta*(self._vR*self._sinhux*self._cosvx -self._vz*self._coshux*self._sinvx) EL= self.calcEL() self._E= EL[0] self._Lz= EL[1] #Determine umin and umax self._u0= kwargs.pop('u0',self._ux) #u0 as defined by Binney does not matter for a #single action evaluation, so we don't determine it here self._sinhu0= numpy.sinh(self._u0) self._potu0v0= potentialStaeckel(self._u0,self._vx, self._pot,self._delta) self._I3U= self._E*self._sinhux**2.-self._pux**2./2./self._delta**2.\ -self._Lz**2./2./self._delta**2./self._sinhux**2. self._potupi2= potentialStaeckel(self._ux,numpy.pi/2., self._pot,self._delta) dV= (self._coshux**2.*self._potupi2 -(self._sinhux**2.+self._sinvx**2.) *potentialStaeckel(self._ux,self._vx, self._pot,self._delta)) self._I3V= -self._E*self._sinvx**2.+self._pvx**2./2./self._delta**2.\ +self._Lz**2./2./self._delta**2./self._sinvx**2.\ -dV self.calcUminUmax() self.calcVmin() return None def angleR(self,**kwargs): """ NAME: AngleR PURPOSE: Calculate the radial angle INPUT: scipy.integrate.quadrature keywords OUTPUT: w_R(R,vT,vT) in radians + estimate of the error (does not include TR error) HISTORY: 2012-11-27 - Written - Bovy (IAS) """ raise NotImplementedError("'angleR' not yet implemented for Staeckel approximation") def TR(self,**kwargs): """ NAME: TR PURPOSE: Calculate the radial period for a power-law rotation curve INPUT: scipy.integrate.quadrature keywords OUTPUT: T_R(R,vT,vT)*vc/ro + estimate of the error HISTORY: 2012-11-27 - Written - Bovy (IAS) """ raise NotImplementedError("'TR' not implemented yet for Staeckel approximation") def Tphi(self,**kwargs): """ NAME: Tphi PURPOSE: Calculate the azimuthal period INPUT: +scipy.integrate.quadrature keywords OUTPUT: T_phi(R,vT,vT)/ro/vc + estimate of the error HISTORY: 2012-11-27 - Written - Bovy (IAS) """ raise NotImplementedError("'Tphi' not implemented yet for Staeckel approxximation") def I(self,**kwargs): """ NAME: I PURPOSE: Calculate I, the 'ratio' between the radial and azimutha period INPUT: +scipy.integrate.quadrature keywords OUTPUT: I(R,vT,vT) + estimate of the error HISTORY: 2012-11-27 - Written - Bovy (IAS) """ raise NotImplementedError("'I' not implemented yet for Staeckel approxximation") def Jphi(self): #pragma: no cover """ NAME: Jphi PURPOSE: Calculate the azimuthal action INPUT: OUTPUT: J_R(R,vT,vT)/ro/vc HISTORY: 2012-11-27 - Written - Bovy (IAS) """ return self._R*self._vT def JR(self,**kwargs): """ NAME: JR PURPOSE: Calculate the radial action INPUT: fixed_quad= (False) if True, use n=10 fixed_quad +scipy.integrate.quad keywords OUTPUT: J_R(R,vT,vT)/ro/vc + estimate of the error (nan for fixed_quad) HISTORY: 2012-11-27 - Written - Bovy (IAS) """ if hasattr(self,'_JR'): #pragma: no cover return self._JR umin, umax= self.calcUminUmax() #print self._ux, self._pux, (umax-umin)/umax if (umax-umin)/umax < 10.**-6: return numpy.array([0.]) order= kwargs.pop('order',10) if kwargs.pop('fixed_quad',False): # factor in next line bc integrand=/2delta^2 self._JR= 1./numpy.pi*numpy.sqrt(2.)*self._delta\ *integrate.fixed_quad(_JRStaeckelIntegrand, umin,umax, args=(self._E,self._Lz,self._I3U, self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot), n=order, **kwargs)[0] else: self._JR= 1./numpy.pi*numpy.sqrt(2.)*self._delta\ *integrate.quad(_JRStaeckelIntegrand, umin,umax, args=(self._E,self._Lz,self._I3U, self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot), **kwargs)[0] return self._JR def Jz(self,**kwargs): """ NAME: Jz PURPOSE: Calculate the vertical action INPUT: fixed_quad= (False) if True, use n=10 fixed_quad +scipy.integrate.quad keywords OUTPUT: J_z(R,vT,vT)/ro/vc + estimate of the error HISTORY: 2012-11-27 - Written - Bovy (IAS) """ if hasattr(self,'_JZ'): #pragma: no cover return self._JZ vmin= self.calcVmin() if (numpy.pi/2.-vmin) < 10.**-7: return numpy.array([0.]) order= kwargs.pop('order',10) if kwargs.pop('fixed_quad',False): # factor in next line bc integrand=/2delta^2 self._JZ= 2./numpy.pi*numpy.sqrt(2.)*self._delta \ *integrate.fixed_quad(_JzStaeckelIntegrand, vmin,numpy.pi/2, args=(self._E,self._Lz,self._I3V, self._delta, self._ux,self._coshux**2., self._sinhux**2., self._potupi2,self._pot), n=order, **kwargs)[0] else: # factor in next line bc integrand=/2delta^2 self._JZ= 2./numpy.pi*numpy.sqrt(2.)*self._delta \ *integrate.quad(_JzStaeckelIntegrand, vmin,numpy.pi/2, args=(self._E,self._Lz,self._I3V, self._delta, self._ux,self._coshux**2., self._sinhux**2., self._potupi2,self._pot), **kwargs)[0] return self._JZ def calcEL(self,**kwargs): """ NAME: calcEL PURPOSE: calculate the energy and angular momentum INPUT: scipy.integrate.quadrature keywords OUTPUT: (E,L) HISTORY: 2012-11-27 - Written - Bovy (IAS) """ E,L= calcELStaeckel(self._R,self._vR,self._vT,self._z,self._vz, self._pot) return (E,L) def calcUminUmax(self,**kwargs): """ NAME: calcUminUmax PURPOSE: calculate the u 'apocenter' and 'pericenter' INPUT: OUTPUT: (umin,umax) HISTORY: 2012-11-27 - Written - Bovy (IAS) """ if hasattr(self,'_uminumax'): #pragma: no cover return self._uminumax E, L= self._E, self._Lz # Calculate value of the integrand at current point, to check whether # we are at a turning point current_val= _JRStaeckelIntegrandSquared(self._ux, E,L,self._I3U,self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot) if numpy.fabs(self._pux) < 1e-7 \ or numpy.fabs(current_val) < 1e-10: #We are at umin or umax eps= 10.**-8. peps= _JRStaeckelIntegrandSquared(self._ux+eps, E,L,self._I3U,self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot) meps= _JRStaeckelIntegrandSquared(self._ux-eps, E,L,self._I3U,self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot) if peps < 0. and meps > 0.: #we are at umax umax= self._ux rstart,prevr= _uminUmaxFindStart(self._ux, E,L,self._I3U,self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot) if rstart == 0.: umin= 0. else: try: umin= optimize.brentq(_JRStaeckelIntegrandSquared, rstart,self._ux-eps, (E,L,self._I3U,self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot), maxiter=200) except RuntimeError: #pragma: no cover raise UnboundError("Orbit seems to be unbound") elif peps > 0. and meps < 0.: #we are at umin umin= self._ux rend,prevr= _uminUmaxFindStart(self._ux, E,L,self._I3U,self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot, umax=True) umax= optimize.brentq(_JRStaeckelIntegrandSquared, self._ux+eps,rend, (E,L,self._I3U,self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot), maxiter=200) else: #circular orbit umin= self._ux umax= self._ux else: rstart,prevr= _uminUmaxFindStart(self._ux, E,L,self._I3U,self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot) if rstart == 0.: umin= 0. else: if numpy.fabs(prevr-self._ux) < 10.**-2.: rup= self._ux else: rup= prevr try: umin= optimize.brentq(_JRStaeckelIntegrandSquared, rstart,rup, (E,L,self._I3U,self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot), maxiter=200) except RuntimeError: #pragma: no cover raise UnboundError("Orbit seems to be unbound") rend,prevr= _uminUmaxFindStart(self._ux, E,L,self._I3U,self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot, umax=True) umax= optimize.brentq(_JRStaeckelIntegrandSquared, prevr,rend, (E,L,self._I3U,self._delta, self._u0,self._sinhu0**2., self._vx,self._sinvx**2., self._potu0v0,self._pot), maxiter=200) self._uminumax= (umin,umax) return self._uminumax def calcVmin(self,**kwargs): """ NAME: calcVmin PURPOSE: calculate the v 'pericenter' INPUT: OUTPUT: vmin HISTORY: 2012-11-28 - Written - Bovy (IAS) """ if hasattr(self,'_vmin'): #pragma: no cover return self._vmin E, L= self._E, self._Lz if numpy.fabs(self._pvx) < 10.**-7.: #We are at vmin or vmax eps= 10.**-8. peps= _JzStaeckelIntegrandSquared(self._vx+eps, E,L,self._I3V,self._delta, self._ux,self._coshux**2., self._sinhux**2., self._potupi2,self._pot) meps= _JzStaeckelIntegrandSquared(self._vx-eps, E,L,self._I3V,self._delta, self._ux,self._coshux**2., self._sinhux**2., self._potupi2,self._pot) if peps < 0. and meps > 0.: #we are at vmax, which cannot happen rstart= _vminFindStart(self._vx, E,L,self._I3V,self._delta, self._ux,self._coshux**2., self._sinhux**2., self._potupi2,self._pot) if rstart == 0.: vmin= 0. else: try: vmin= optimize.brentq(_JzStaeckelIntegrandSquared, rstart,self._vx-eps, (E,L,self._I3V,self._delta, self._ux,self._coshux**2., self._sinhux**2., self._potupi2,self._pot), maxiter=200) except RuntimeError: #pragma: no cover raise UnboundError("Orbit seems to be unbound") elif peps > 0. and meps < 0.: #we are at vmin vmin= self._vx else: #planar orbit vmin= self._vx else: rstart= _vminFindStart(self._vx, E,L,self._I3V,self._delta, self._ux,self._coshux**2., self._sinhux**2., self._potupi2,self._pot) if rstart == 0.: vmin= 0. else: try: vmin= optimize.brentq(_JzStaeckelIntegrandSquared, rstart,rstart/0.9, (E,L,self._I3V,self._delta, self._ux,self._coshux**2., self._sinhux**2., self._potupi2,self._pot), maxiter=200) except RuntimeError: #pragma: no cover raise UnboundError("Orbit seems to be unbound") self._vmin= vmin return self._vmin def calcELStaeckel(R,vR,vT,z,vz,pot,vc=1.,ro=1.): """ NAME: calcELStaeckel PURPOSE: calculate the energy and angular momentum INPUT: R - Galactocentric radius (/ro) vR - radial part of the velocity (/vc) vT - azimuthal part of the velocity (/vc) vc - circular velocity ro - reference radius OUTPUT: (E,L) HISTORY: 2012-11-30 - Written - Bovy (IAS) """ return (_evaluatePotentials(pot,R,z)+vR**2./2.+vT**2./2.+vz**2./2.,R*vT) def potentialStaeckel(u,v,pot,delta): """ NAME: potentialStaeckel PURPOSE: return the potential INPUT: u - confocal u v - confocal v pot - potential delta - focus OUTPUT: Phi(u,v) HISTORY: 2012-11-29 - Written - Bovy (IAS) """ R,z= coords.uv_to_Rz(u,v,delta=delta) return _evaluatePotentials(pot,R,z) def FRStaeckel(u,v,pot,delta): #pragma: no cover because unused """ NAME: FRStaeckel PURPOSE: return the radial force INPUT: u - confocal u v - confocal v pot - potential delta - focus OUTPUT: FR(u,v) HISTORY: 2012-11-30 - Written - Bovy (IAS) """ R,z= coords.uv_to_Rz(u,v,delta=delta) return _evaluateRforces(pot,R,z) def FZStaeckel(u,v,pot,delta): #pragma: no cover because unused """ NAME: FZStaeckel PURPOSE: return the vertical force INPUT: u - confocal u v - confocal v pot - potential delta - focus OUTPUT: FZ(u,v) HISTORY: 2012-11-30 - Written - Bovy (IAS) """ R,z= coords.uv_to_Rz(u,v,delta=delta) return _evaluatezforces(pot,R,z) def _JRStaeckelIntegrand(u,E,Lz,I3U,delta,u0,sinh2u0,v0,sin2v0, potu0v0,pot): return numpy.sqrt(_JRStaeckelIntegrandSquared(u,E,Lz,I3U,delta,u0,sinh2u0, v0,sin2v0, potu0v0,pot)) def _JRStaeckelIntegrandSquared(u,E,Lz,I3U,delta,u0,sinh2u0,v0,sin2v0, potu0v0,pot): #potu0v0= potentialStaeckel(u0,v0,pot,delta) """The J_R integrand: p^2_u(u)/2/delta^2""" sinh2u= numpy.sinh(u)**2. dU= (sinh2u+sin2v0)*potentialStaeckel(u,v0, pot,delta)\ -(sinh2u0+sin2v0)*potu0v0 return E*sinh2u-I3U-dU-Lz**2./2./delta**2./sinh2u def _JzStaeckelIntegrand(v,E,Lz,I3V,delta,u0,cosh2u0,sinh2u0, potu0pi2,pot): return numpy.sqrt(_JzStaeckelIntegrandSquared(v,E,Lz,I3V,delta,u0,cosh2u0, sinh2u0, potu0pi2,pot)) def _JzStaeckelIntegrandSquared(v,E,Lz,I3V,delta,u0,cosh2u0,sinh2u0, potu0pi2,pot): #potu0pi2= potentialStaeckel(u0,numpy.pi/2.,pot,delta) """The J_z integrand: p_v(v)/2/delta^2""" sin2v= numpy.sin(v)**2. dV= cosh2u0*potu0pi2\ -(sinh2u0+sin2v)*potentialStaeckel(u0,v,pot,delta) return E*sin2v+I3V+dV-Lz**2./2./delta**2./sin2v def _uminUmaxFindStart(u, E,Lz,I3U,delta,u0,sinh2u0,v0,sin2v0, potu0v0,pot,umax=False): """ NAME: _uminUmaxFindStart PURPOSE: Find adequate start or end points to solve for umin and umax INPUT: same as JRStaeckelIntegrandSquared OUTPUT: rstart or rend HISTORY: 2012-11-30 - Written - Bovy (IAS) """ if umax: utry= u*1.1 else: utry= u*0.9 prevu= u while _JRStaeckelIntegrandSquared(utry, E,Lz,I3U,delta,u0,sinh2u0,v0,sin2v0, potu0v0,pot) >= 0. \ and utry > 0.000000001: prevu= utry if umax: if utry > 100.: raise UnboundError("Orbit seems to be unbound") utry*= 1.1 else: utry*= 0.9 if utry < 0.000000001: return (0.,prevu) return (utry,prevu) def _vminFindStart(v,E,Lz,I3V,delta,u0,cosh2u0,sinh2u0, potu0pi2,pot): """ NAME: _vminFindStart PURPOSE: Find adequate start point to solve for vmin INPUT: same as JzStaeckelIntegrandSquared OUTPUT: rstart HISTORY: 2012-11-28 - Written - Bovy (IAS) """ vtry= 0.9*v while _JzStaeckelIntegrandSquared(vtry, E,Lz,I3V,delta,u0,cosh2u0,sinh2u0, potu0pi2,pot) >= 0. \ and vtry > 0.000000001: vtry*= 0.9 if vtry < 0.000000001: return 0. return vtry @potential_physical_input @physical_conversion('position',pop=True) def estimateDeltaStaeckel(pot,R,z,no_median=False,delta0=1e-6): """ NAME: estimateDeltaStaeckel PURPOSE: Estimate a good value for delta using eqn. (9) in Sanders (2012) INPUT: pot - Potential instance or list thereof R,z- coordinates (if these are arrays, the median estimated delta is returned, i.e., if this is an orbit) no_median - (False) if True, and input is array, return all calculated values of delta (useful for quickly estimating delta for many phase space points) delta0= (1e-6) value to return when delta -10.**-10.) + pot_includes_scf) delta2[indx]= delta0**2. if not no_median: delta2= numpy.median(delta2[True^numpy.isnan(delta2)]) else: delta2= (z**2.-R**2. #eqn. (9) has a sign error +(3.*R*_evaluatezforces(pot,R,z) -3.*z*_evaluateRforces(pot,R,z) +R*z*(evaluateR2derivs(pot,R,z,use_physical=False) -evaluatez2derivs(pot,R,z,use_physical=False)))/evaluateRzderivs(pot,R,z,use_physical=False)) if delta2 < delta0**2. and (delta2 > -10.**-10. or pot_includes_scf): delta2= delta0**2. return numpy.sqrt(delta2) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleStaeckelGrid.py0000644000175100001710000007721014327773303022674 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleStaeckelGrid # # build grid in integrals of motion to quickly evaluate # actionAngleStaeckel # # methods: # __call__: returns (jr,lz,jz) # ############################################################################### import numpy from scipy import interpolate, ndimage, optimize from .. import potential from ..potential.Potential import _evaluatePotentials from ..potential.Potential import flatten as flatten_potential from ..util import conversion, coords, multi from . import actionAngleStaeckel, actionAngleStaeckel_c from .actionAngle import actionAngle from .actionAngleStaeckel_c import _ext_loaded as ext_loaded _PRINTOUTSIDEGRID= False class actionAngleStaeckelGrid(actionAngle): """Action-angle formalism for axisymmetric potentials using Binney (2012)'s Staeckel approximation, grid-based interpolation""" def __init__(self,pot=None,delta=None,Rmax=5., nE=25,npsi=25,nLz=30,numcores=1, interpecc=False, **kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleStaeckelGrid object INPUT: pot= potential or list of potentials delta= focus of prolate confocal coordinate system (can be Quantity) Rmax = Rmax for building grids (natural units) nE=, npsi=, nLz= grid size interpecc= (False) if True, also interpolate the approximate eccentricity, zmax, rperi, and rapo numcores= number of cpus to use to parallellize ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) OUTPUT: instance HISTORY: 2012-11-29 - Written - Bovy (IAS) 2017-12-15 - Written - Bovy (UofT) """ actionAngle.__init__(self, ro=kwargs.get('ro',None),vo=kwargs.get('vo',None)) if pot is None: raise OSError("Must specify pot= for actionAngleStaeckelGrid") self._pot= flatten_potential(pot) if delta is None: raise OSError("Must specify delta= for actionAngleStaeckelGrid") if ext_loaded and 'c' in kwargs and kwargs['c']: self._c= True else: self._c= False self._delta= conversion.parse_length(delta,ro=self._ro) self._Rmax= Rmax self._Rmin= 0.01 #Set up the actionAngleStaeckel object that we will use to interpolate self._aA= actionAngleStaeckel.actionAngleStaeckel(pot=self._pot,delta=self._delta,c=self._c) #Build grid self._Lzmin= 0.01 self._Lzs= numpy.linspace(self._Lzmin, self._Rmax\ *potential.vcirc(self._pot,self._Rmax), nLz) self._Lzmax= self._Lzs[-1] self._nLz= nLz #Calculate E_c(R=RL), energy of circular orbit self._RL= numpy.array([potential.rl(self._pot,l) for l in self._Lzs]) self._RLInterp= interpolate.InterpolatedUnivariateSpline(self._Lzs, self._RL,k=3) self._ERL= _evaluatePotentials(self._pot,self._RL, numpy.zeros(self._nLz))\ +self._Lzs**2./2./self._RL**2. self._ERLmax= numpy.amax(self._ERL)+1. self._ERLInterp= interpolate.InterpolatedUnivariateSpline(self._Lzs, numpy.log(-(self._ERL-self._ERLmax)),k=3) self._Ramax= 200./8. self._ERa= _evaluatePotentials(self._pot,self._Ramax,0.) +self._Lzs**2./2./self._Ramax**2. #self._EEsc= numpy.array([self._ERL[ii]+potential.vesc(self._pot,self._RL[ii])**2./4. for ii in range(nLz)]) self._ERamax= numpy.amax(self._ERa)+1. self._ERaInterp= interpolate.InterpolatedUnivariateSpline(self._Lzs, numpy.log(-(self._ERa-self._ERamax)),k=3) y= numpy.linspace(0.,1.,nE) self._nE= nE psis= numpy.linspace(0.,1.,npsi)*numpy.pi/2. self._npsi= npsi jr= numpy.zeros((nLz,nE,npsi)) jz= numpy.zeros((nLz,nE,npsi)) u0= numpy.zeros((nLz,nE)) jrLzE= numpy.zeros(nLz) jzLzE= numpy.zeros(nLz) #First calculate u0 thisLzs= (numpy.tile(self._Lzs,(nE,1)).T).flatten() thisERL= (numpy.tile(self._ERL,(nE,1)).T).flatten() thisERa= (numpy.tile(self._ERa,(nE,1)).T).flatten() this= (numpy.tile(y,(nLz,1))).flatten() thisE= _invEfunc(_Efunc(thisERa,thisERL)+this*(_Efunc(thisERL,thisERL)-_Efunc(thisERa,thisERL)),thisERL) if isinstance(self._pot,potential.interpRZPotential) and hasattr(self._pot,'_origPot'): u0pot= self._pot._origPot else: u0pot= self._pot if self._c: mu0= actionAngleStaeckel_c.actionAngleStaeckel_calcu0(thisE,thisLzs, u0pot, self._delta)[0] else: if numcores > 1: mu0= multi.parallel_map((lambda x: self.calcu0(thisE[x], thisLzs[x])), range(nE*nLz), numcores=numcores) else: mu0= list(map((lambda x: self.calcu0(thisE[x], thisLzs[x])), range(nE*nLz))) u0= numpy.reshape(mu0,(nLz,nE)) thisR= self._delta*numpy.sinh(u0) thisv= numpy.reshape(self.vatu0(thisE.flatten(),thisLzs.flatten(), u0.flatten(), thisR.flatten()),(nLz,nE)) self.thisv= thisv #reshape thisLzs= numpy.reshape(thisLzs,(nLz,nE)) thispsi= numpy.tile(psis,(nLz,nE,1)).flatten() thisLzs= numpy.tile(thisLzs.T,(npsi,1,1)).T.flatten() thisR= numpy.tile(thisR.T,(npsi,1,1)).T.flatten() thisv= numpy.tile(thisv.T,(npsi,1,1)).T.flatten() mjr, mlz, mjz= self._aA(thisR, #R thisv*numpy.cos(thispsi), #vR thisLzs/thisR, #vT numpy.zeros(len(thisR)), #z thisv*numpy.sin(thispsi), #vz fixed_quad=True) if interpecc: mecc, mzmax, mrperi, mrap=\ self._aA.EccZmaxRperiRap(thisR, #R thisv*numpy.cos(thispsi), #vR thisLzs/thisR, #vT numpy.zeros(len(thisR)), #z thisv*numpy.sin(thispsi)) #vz if isinstance(self._pot,potential.interpRZPotential) and hasattr(self._pot,'_origPot'): #Interpolated potentials have problems with extreme orbits indx= (mjr == 9999.99) indx+= (mjz == 9999.99) #Re-calculate these using the original potential, hopefully not too slow tmpaA= actionAngleStaeckel.actionAngleStaeckel(pot=self._pot._origPot,delta=self._delta,c=self._c) mjr[indx], dumb, mjz[indx]= tmpaA(thisR[indx], #R thisv[indx]*numpy.cos(thispsi[indx]), #vR thisLzs[indx]/thisR[indx], #vT numpy.zeros(numpy.sum(indx)), #z thisv[indx]*numpy.sin(thispsi[indx]), #vz fixed_quad=True) if interpecc: mecc[indx], mzmax[indx], mrperi[indx], mrap[indx]=\ self._aA.EccZmaxRperiRap(thisR[indx], #R thisv[indx]*numpy.cos(thispsi[indx]), #vR thisLzs[indx]/thisR[indx], #vT numpy.zeros(numpy.sum(indx)), #z thisv[indx]*numpy.sin(thispsi[indx])) #vz jr= numpy.reshape(mjr,(nLz,nE,npsi)) jz= numpy.reshape(mjz,(nLz,nE,npsi)) if interpecc: ecc= numpy.reshape(mecc,(nLz,nE,npsi)) zmax= numpy.reshape(mzmax,(nLz,nE,npsi)) rperi= numpy.reshape(mrperi,(nLz,nE,npsi)) rap= numpy.reshape(mrap,(nLz,nE,npsi)) zmaxLzE= numpy.zeros(nLz) rperiLzE= numpy.zeros(nLz) rapLzE= numpy.zeros(nLz) for ii in range(nLz): jrLzE[ii]= numpy.nanmax(jr[ii,(jr[ii,:,:] != 9999.99)])#:,:]) jzLzE[ii]= numpy.nanmax(jz[ii,(jz[ii,:,:] != 9999.99)])#:,:]) if interpecc: zmaxLzE[ii]= numpy.amax(zmax[ii,numpy.isfinite(zmax[ii])]) rperiLzE[ii]= numpy.amax(rperi[ii,numpy.isfinite(rperi[ii])]) rapLzE[ii]= numpy.amax(rap[ii,numpy.isfinite(rap[ii])]) jrLzE[(jrLzE == 0.)]= numpy.nanmin(jrLzE[(jrLzE > 0.)]) jzLzE[(jzLzE == 0.)]= numpy.nanmin(jzLzE[(jzLzE > 0.)]) if interpecc: zmaxLzE[(zmaxLzE == 0.)]= numpy.nanmin(zmaxLzE[(zmaxLzE > 0.)]) rperiLzE[(rperiLzE == 0.)]= numpy.nanmin(rperiLzE[(rperiLzE > 0.)]) rapLzE[(rapLzE == 0.)]= numpy.nanmin(rapLzE[(rapLzE > 0.)]) for ii in range(nLz): jr[ii,:,:]/= jrLzE[ii] jz[ii,:,:]/= jzLzE[ii] if interpecc: zmax[ii,:,:]/= zmaxLzE[ii] rperi[ii,:,:]/= rperiLzE[ii] rap[ii,:,:]/= rapLzE[ii] #Deal w/ 9999.99 jr[(jr > 1.)]= 1. jz[(jz > 1.)]= 1. #Deal w/ NaN jr[numpy.isnan(jr)]= 0. jz[numpy.isnan(jz)]= 0. if interpecc: ecc[(ecc < 0.)]= 0. ecc[(ecc > 1.)]= 1. ecc[numpy.isnan(ecc)]= 0. ecc[numpy.isinf(ecc)]= 1. zmax[(zmax > 1.)]= 1. zmax[numpy.isnan(zmax)]= 0. zmax[numpy.isinf(zmax)]= 1. rperi[(rperi > 1.)]= 1. rperi[numpy.isnan(rperi)]= 0. rperi[numpy.isinf(rperi)]= 0. # typically orbits that can reach 0 rap[(rap > 1.)]= 1. rap[numpy.isnan(rap)]= 0. rap[numpy.isinf(rap)]= 1. #First interpolate the maxima self._jr= jr self._jz= jz self._u0= u0 self._jrLzE= jrLzE self._jzLzE= jzLzE self._jrLzInterp= interpolate.InterpolatedUnivariateSpline(self._Lzs, numpy.log(jrLzE+10.**-5.),k=3) self._jzLzInterp= interpolate.InterpolatedUnivariateSpline(self._Lzs, numpy.log(jzLzE+10.**-5.),k=3) if interpecc: self._ecc= ecc self._zmax= zmax self._rperi= rperi self._rap= rap self._zmaxLzE= zmaxLzE self._rperiLzE= rperiLzE self._rapLzE= rapLzE self._zmaxLzInterp=\ interpolate.InterpolatedUnivariateSpline(self._Lzs, numpy.log(zmaxLzE+10.**-5.),k=3) self._rperiLzInterp=\ interpolate.InterpolatedUnivariateSpline(self._Lzs, numpy.log(rperiLzE+10.**-5.),k=3) self._rapLzInterp=\ interpolate.InterpolatedUnivariateSpline(self._Lzs, numpy.log(rapLzE+10.**-5.),k=3) #Interpolate u0 self._logu0Interp= interpolate.RectBivariateSpline(self._Lzs, y, numpy.log(u0), kx=3,ky=3,s=0.) #spline filter jr and jz, such that they can be used with ndimage.map_coordinates self._jrFiltered= ndimage.spline_filter(numpy.log(self._jr+10.**-10.),order=3) self._jzFiltered= ndimage.spline_filter(numpy.log(self._jz+10.**-10.),order=3) if interpecc: self._eccFiltered= ndimage.spline_filter(numpy.log(self._ecc+10.**-10.),order=3) self._zmaxFiltered= ndimage.spline_filter(numpy.log(self._zmax+10.**-10.),order=3) self._rperiFiltered= ndimage.spline_filter(numpy.log(self._rperi+10.**-10.),order=3) self._rapFiltered= ndimage.spline_filter(numpy.log(self._rap+10.**-10.),order=3) # Check the units self._check_consistent_units() return None def _evaluate(self,*args,**kwargs): """ NAME: __call__ (_evaluate) PURPOSE: evaluate the actions (jr,lz,jz) INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument Keywords for actionAngleStaeckel.__call__ for off-the-grid evaluations OUTPUT: (jr,lz,jz) HISTORY: 2012-11-29 - Written - Bovy (IAS) """ if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz Lz= R*vT Phi= _evaluatePotentials(self._pot,R,z) E= Phi+vR**2./2.+vT**2./2.+vz**2./2. thisERL= -numpy.exp(self._ERLInterp(Lz))+self._ERLmax thisERa= -numpy.exp(self._ERaInterp(Lz))+self._ERamax if isinstance(R,numpy.ndarray): indx= ((E-thisERa)/(thisERL-thisERa) > 1.)\ *(((E-thisERa)/(thisERL-thisERa)-1.) < 10.**-2.) E[indx]= thisERL[indx] indx= ((E-thisERa)/(thisERL-thisERa) < 0.)\ *((E-thisERa)/(thisERL-thisERa) > -10.**-2.) E[indx]= thisERa[indx] indx= (Lz < self._Lzmin) indx+= (Lz > self._Lzmax) indx+= ((E-thisERa)/(thisERL-thisERa) > 1.) indx+= ((E-thisERa)/(thisERL-thisERa) < 0.) indxc= True^indx jr= numpy.empty(R.shape) jz= numpy.empty(R.shape) if numpy.sum(indxc) > 0: u0= numpy.exp(self._logu0Interp.ev(Lz[indxc], (_Efunc(E[indxc],thisERL[indxc])-_Efunc(thisERa[indxc],thisERL[indxc]))/(_Efunc(thisERL[indxc],thisERL[indxc])-_Efunc(thisERa[indxc],thisERL[indxc])))) sinh2u0= numpy.sinh(u0)**2. thisEr= self.Er(R[indxc],z[indxc],vR[indxc],vz[indxc], E[indxc],Lz[indxc],sinh2u0,u0) thisEz= self.Ez(R[indxc],z[indxc],vR[indxc],vz[indxc], E[indxc],Lz[indxc],sinh2u0,u0) thisv2= self.vatu0(E[indxc],Lz[indxc],u0,self._delta*numpy.sinh(u0),retv2=True) cos2psi= 2.*thisEr/thisv2/(1.+sinh2u0) #latter is cosh2u0 cos2psi[(cos2psi > 1.)*(cos2psi < 1.+10.**-5.)]= 1. indxCos2psi= (cos2psi > 1.) indxCos2psi+= (cos2psi < 0.) indxc[indxc]= True^indxCos2psi#Handle these two cases as off-grid indx= True^indxc psi= numpy.arccos(numpy.sqrt(cos2psi[True^indxCos2psi])) coords= numpy.empty((3,numpy.sum(indxc))) coords[0,:]= (Lz[indxc]-self._Lzmin)/(self._Lzmax-self._Lzmin)*(self._nLz-1.) y= (_Efunc(E[indxc],thisERL[indxc])-_Efunc(thisERa[indxc],thisERL[indxc]))/(_Efunc(thisERL[indxc],thisERL[indxc])-_Efunc(thisERa[indxc],thisERL[indxc])) coords[1,:]= y*(self._nE-1.) coords[2,:]= psi/numpy.pi*2.*(self._npsi-1.) jr[indxc]= (numpy.exp(ndimage.interpolation.map_coordinates(self._jrFiltered, coords, order=3, prefilter=False))-10.**-10.)*(numpy.exp(self._jrLzInterp(Lz[indxc]))-10.**-5.) #Switch to Ez-calculated psi sin2psi= 2.*thisEz[True^indxCos2psi]/thisv2[True^indxCos2psi]/(1.+sinh2u0[True^indxCos2psi]) #latter is cosh2u0 sin2psi[(sin2psi > 1.)*(sin2psi < 1.+10.**-5.)]= 1. indxSin2psi= (sin2psi > 1.) indxSin2psi+= (sin2psi < 0.) indxc[indxc]= True^indxSin2psi#Handle these two cases as off-grid indx= True^indxc psiz= numpy.arcsin(numpy.sqrt(sin2psi[True^indxSin2psi])) newcoords= numpy.empty((3,numpy.sum(indxc))) newcoords[0:2,:]= coords[0:2,True^indxSin2psi] newcoords[2,:]= psiz/numpy.pi*2.*(self._npsi-1.) jz[indxc]= (numpy.exp(ndimage.interpolation.map_coordinates(self._jzFiltered, newcoords, order=3, prefilter=False))-10.**-10.)*(numpy.exp(self._jzLzInterp(Lz[indxc]))-10.**-5.) if numpy.sum(indx) > 0: jrindiv, lzindiv, jzindiv= self._aA(R[indx], vR[indx], vT[indx], z[indx], vz[indx], **kwargs) jr[indx]= jrindiv jz[indx]= jzindiv """ jrindiv= numpy.empty(numpy.sum(indx)) jzindiv= numpy.empty(numpy.sum(indx)) for ii in range(numpy.sum(indx)): try: thisaA= actionAngleStaeckel.actionAngleStaeckelSingle(\ R[indx][ii], #R vR[indx][ii], #vR vT[indx][ii], #vT z[indx][ii], #z vz[indx][ii], #vz pot=self._pot,delta=self._delta) jrindiv[ii]= thisaA.JR(fixed_quad=True)[0] jzindiv[ii]= thisaA.Jz(fixed_quad=True)[0] except (UnboundError,OverflowError): jrindiv[ii]= numpy.nan jzindiv[ii]= numpy.nan jr[indx]= jrindiv jz[indx]= jzindiv """ else: jr,Lz, jz= self(numpy.array([R]), numpy.array([vR]), numpy.array([vT]), numpy.array([z]), numpy.array([vz]), **kwargs) return (jr[0],Lz[0],jz[0]) jr[jr < 0.]= 0. jz[jz < 0.]= 0. return (jr,R*vT,jz) def Jz(self,*args,**kwargs): """ NAME: Jz PURPOSE: evaluate the action jz INPUT: Either: a) R,vR,vT,z,vz b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well scipy.integrate.quadrature keywords OUTPUT: jz HISTORY: 2012-07-30 - Written - Bovy (IAS@MPIA) """ return self(*args,**kwargs)[2] def JR(self,*args,**kwargs): """ NAME: JR PURPOSE: evaluate the action jr INPUT: Either: a) R,vR,vT,z,vz b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well scipy.integrate.quadrature keywords OUTPUT: jr HISTORY: 2012-07-30 - Written - Bovy (IAS@MPIA) """ return self(*args,**kwargs)[0] def _EccZmaxRperiRap(self,*args,**kwargs): """ NAME: EccZmaxRperiRap (_EccZmaxRperiRap) PURPOSE: evaluate the eccentricity, maximum height above the plane, peri- and apocenter in the Staeckel approximation INPUT: Either: a) R,vR,vT,z,vz[,phi]: 1) floats: phase-space value for single object (phi is optional) (each can be a Quantity) 2) numpy.ndarray: [N] phase-space values for N objects (each can be a Quantity) b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well as the second argument OUTPUT: (e,zmax,rperi,rap) HISTORY: 2017-12-15 - Written - Bovy (UofT) """ if len(args) == 5: #R,vR.vT, z, vz R,vR,vT, z, vz= args elif len(args) == 6: #R,vR.vT, z, vz, phi R,vR,vT, z, vz, phi= args else: self._parse_eval_args(*args) R= self._eval_R vR= self._eval_vR vT= self._eval_vT z= self._eval_z vz= self._eval_vz Lz= R*vT Phi= _evaluatePotentials(self._pot,R,z) E= Phi+vR**2./2.+vT**2./2.+vz**2./2. thisERL= -numpy.exp(self._ERLInterp(Lz))+self._ERLmax thisERa= -numpy.exp(self._ERaInterp(Lz))+self._ERamax if isinstance(R,numpy.ndarray): indx= ((E-thisERa)/(thisERL-thisERa) > 1.)\ *(((E-thisERa)/(thisERL-thisERa)-1.) < 10.**-2.) E[indx]= thisERL[indx] indx= ((E-thisERa)/(thisERL-thisERa) < 0.)\ *((E-thisERa)/(thisERL-thisERa) > -10.**-2.) E[indx]= thisERa[indx] indx= (Lz < self._Lzmin) indx+= (Lz > self._Lzmax) indx+= ((E-thisERa)/(thisERL-thisERa) > 1.) indx+= ((E-thisERa)/(thisERL-thisERa) < 0.) indxc= True^indx ecc= numpy.empty(R.shape) zmax= numpy.empty(R.shape) rperi= numpy.empty(R.shape) rap= numpy.empty(R.shape) if numpy.sum(indxc) > 0: u0= numpy.exp(self._logu0Interp.ev(Lz[indxc], (_Efunc(E[indxc],thisERL[indxc])-_Efunc(thisERa[indxc],thisERL[indxc]))/(_Efunc(thisERL[indxc],thisERL[indxc])-_Efunc(thisERa[indxc],thisERL[indxc])))) sinh2u0= numpy.sinh(u0)**2. thisEr= self.Er(R[indxc],z[indxc],vR[indxc],vz[indxc], E[indxc],Lz[indxc],sinh2u0,u0) thisEz= self.Ez(R[indxc],z[indxc],vR[indxc],vz[indxc], E[indxc],Lz[indxc],sinh2u0,u0) thisv2= self.vatu0(E[indxc],Lz[indxc],u0,self._delta*numpy.sinh(u0),retv2=True) cos2psi= 2.*thisEr/thisv2/(1.+sinh2u0) #latter is cosh2u0 cos2psi[(cos2psi > 1.)*(cos2psi < 1.+10.**-5.)]= 1. indxCos2psi= (cos2psi > 1.) indxCos2psi+= (cos2psi < 0.) indxc[indxc]= True^indxCos2psi#Handle these two cases as off-grid indx= True^indxc psi= numpy.arccos(numpy.sqrt(cos2psi[True^indxCos2psi])) coords= numpy.empty((3,numpy.sum(indxc))) coords[0,:]= (Lz[indxc]-self._Lzmin)/(self._Lzmax-self._Lzmin)*(self._nLz-1.) y= (_Efunc(E[indxc],thisERL[indxc])-_Efunc(thisERa[indxc],thisERL[indxc]))/(_Efunc(thisERL[indxc],thisERL[indxc])-_Efunc(thisERa[indxc],thisERL[indxc])) coords[1,:]= y*(self._nE-1.) coords[2,:]= psi/numpy.pi*2.*(self._npsi-1.) ecc[indxc]= (numpy.exp(ndimage.interpolation.map_coordinates(self._eccFiltered, coords, order=3, prefilter=False))-10.**-10.) rperi[indxc]= (numpy.exp(ndimage.interpolation.map_coordinates(self._rperiFiltered, coords, order=3, prefilter=False))-10.**-10.)*(numpy.exp(self._rperiLzInterp(Lz[indxc]))-10.**-5.) # We do rap below with zmax #Switch to Ez-calculated psi sin2psi= 2.*thisEz[True^indxCos2psi]/thisv2[True^indxCos2psi]/(1.+sinh2u0[True^indxCos2psi]) #latter is cosh2u0 sin2psi[(sin2psi > 1.)*(sin2psi < 1.+10.**-5.)]= 1. indxSin2psi= (sin2psi > 1.) indxSin2psi+= (sin2psi < 0.) indxc[indxc]= True^indxSin2psi#Handle these two cases as off-grid indx= True^indxc psiz= numpy.arcsin(numpy.sqrt(sin2psi[True^indxSin2psi])) newcoords= numpy.empty((3,numpy.sum(indxc))) newcoords[0:2,:]= coords[0:2,True^indxSin2psi] newcoords[2,:]= psiz/numpy.pi*2.*(self._npsi-1.) zmax[indxc]= (numpy.exp(ndimage.interpolation.map_coordinates(self._zmaxFiltered, newcoords, order=3, prefilter=False))-10.**-10.)*(numpy.exp(self._zmaxLzInterp(Lz[indxc]))-10.**-5.) rap[indxc]= (numpy.exp(ndimage.interpolation.map_coordinates(self._rapFiltered, newcoords, order=3, prefilter=False))-10.**-10.)*(numpy.exp(self._rapLzInterp(Lz[indxc]))-10.**-5.) if numpy.sum(indx) > 0: eccindiv, zmaxindiv, rperiindiv, rapindiv=\ self._aA.EccZmaxRperiRap(R[indx], vR[indx], vT[indx], z[indx], vz[indx], **kwargs) ecc[indx]= eccindiv zmax[indx]= zmaxindiv rperi[indx]= rperiindiv rap[indx]= rapindiv else: ecc,zmax,rperi,rap= self.EccZmaxRperiRap(numpy.array([R]), numpy.array([vR]), numpy.array([vT]), numpy.array([z]), numpy.array([vz]), **kwargs) return (ecc[0],zmax[0],rperi[0],rap[0]) ecc[ecc < 0.]= 0. zmax[zmax < 0.]= 0. rperi[rperi < 0.]= 0. rap[rap < 0.]= 0. return (ecc,zmax,rperi,rap) def vatu0(self,E,Lz,u0,R,retv2=False): """ NAME: vatu0 PURPOSE: calculate the velocity at u0 INPUT: E - energy Lz - angular momentum u0 - u0 R - radius corresponding to u0,pi/2. retv2= (False), if True return v^2 OUTPUT: velocity HISTORY: 2012-11-29 - Written - Bovy (IAS) """ v2= (2.*(E-actionAngleStaeckel.potentialStaeckel(u0,numpy.pi/2., self._pot, self._delta)) -Lz**2./R**2.) if retv2: return v2 v2[(v2 < 0.)*(v2 > -10.**-7.)]= 0. return numpy.sqrt(v2) def calcu0(self,E,Lz): """ NAME: calcu0 PURPOSE: calculate the minimum of the u potential INPUT: E - energy Lz - angular momentum OUTPUT: u0 HISTORY: 2012-11-29 - Written - Bovy (IAS) """ logu0= optimize.brent(_u0Eq, args=(self._delta,self._pot, E,Lz**2./2.)) return numpy.exp(logu0) def Er(self,R,z,vR,vz,E,Lz,sinh2u0,u0): """ NAME: Er PURPOSE: calculate the 'radial energy' INPUT: R, z, vR, vz - coordinates E - energy Lz - angular momentum sinh2u0, u0 - sinh^2 and u0 OUTPUT: Er HISTORY: 2012-11-29 - Written - Bovy (IAS) """ u,v= coords.Rz_to_uv(R,z,self._delta) pu= (vR*numpy.cosh(u)*numpy.sin(v) +vz*numpy.sinh(u)*numpy.cos(v)) #no delta, bc we will divide it out out= (pu**2./2.+Lz**2./2./self._delta**2.*(1./numpy.sinh(u)**2.-1./sinh2u0) -E*(numpy.sinh(u)**2.-sinh2u0) +(numpy.sinh(u)**2.+1.)*actionAngleStaeckel.potentialStaeckel(u,numpy.pi/2.,self._pot,self._delta) -(sinh2u0+1.)*actionAngleStaeckel.potentialStaeckel(u0,numpy.pi/2.,self._pot,self._delta)) # +(numpy.sinh(u)**2.+numpy.sin(v)**2.)*actionAngleStaeckel.potentialStaeckel(u,v,self._pot,self._delta) # -(sinh2u0+numpy.sin(v)**2.)*actionAngleStaeckel.potentialStaeckel(u0,v,self._pot,self._delta)) return out def Ez(self,R,z,vR,vz,E,Lz,sinh2u0,u0): """ NAME: Ez PURPOSE: calculate the 'vertical energy' INPUT: R, z, vR, vz - coordinates E - energy Lz - angular momentum sinh2u0, u0 - sinh^2 and u0 OUTPUT: Ez HISTORY: 2012-12-23 - Written - Bovy (IAS) """ u,v= coords.Rz_to_uv(R,z,self._delta) pv= (vR*numpy.sinh(u)*numpy.cos(v) -vz*numpy.cosh(u)*numpy.sin(v)) #no delta, bc we will divide it out out= (pv**2./2.+Lz**2./2./self._delta**2.*(1./numpy.sin(v)**2.-1.) -E*(numpy.sin(v)**2.-1.) -(sinh2u0+1.)*actionAngleStaeckel.potentialStaeckel(u0,numpy.pi/2.,self._pot,self._delta) +(sinh2u0+numpy.sin(v)**2.)*actionAngleStaeckel.potentialStaeckel(u0,v,self._pot,self._delta)) return out def _u0Eq(logu,delta,pot,E,Lz22): """The equation that needs to be minimized to find u0""" u= numpy.exp(logu) sinh2u= numpy.sinh(u)**2. cosh2u= numpy.cosh(u)**2. dU= cosh2u*actionAngleStaeckel.potentialStaeckel(u,numpy.pi/2.,pot,delta) return -(E*sinh2u-dU-Lz22/delta**2./sinh2u) def _Efunc(E,*args): """Function to apply to the energy in building the grid (e.g., if this is a log, then the grid will be logarithmic""" # return ((E-args[0]))**0.5 return numpy.log(E-args[0]+10.**-10.) def _invEfunc(Ef,*args): """Inverse of Efunc""" # return Ef**2.+args[0] return numpy.exp(Ef)+args[0]-10.**-10. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleStaeckel_c.py0000644000175100001710000006002214327773303022361 0ustar00runnerdockerimport ctypes import ctypes.util import numpy from numpy.ctypeslib import ndpointer from ..util import _load_extension_libs, coords _lib, _ext_loaded= _load_extension_libs.load_libgalpy() def actionAngleStaeckel_c(pot,delta,R,vR,vT,z,vz,u0=None,order=10): """ NAME: actionAngleStaeckel_c PURPOSE: Use C to calculate actions using the Staeckel approximation INPUT: pot - Potential or list of such instances delta - focal length of prolate spheroidal coordinates R, vR, vT, z, vz - coordinates (arrays) u0= (None) if set, u0 to use order= (10) order of Gauss-Legendre integration of the relevant integrals OUTPUT: (jr,jz,err) jr,jz : array, shape (len(R)) err - non-zero if error occurred HISTORY: 2012-12-01 - Written - Bovy (IAS) """ if u0 is None: u0, dummy= coords.Rz_to_uv(R,z,delta=numpy.atleast_1d(delta)) #Parse the potential from ..orbit.integrateFullOrbit import _parse_pot from ..orbit.integratePlanarOrbit import _prep_tfuncs npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot,potforactions=True) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Parse delta delta= numpy.atleast_1d(delta) ndelta= len(delta) #Set up result arrays jr= numpy.empty(len(R)) jz= numpy.empty(len(R)) err= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') actionAngleStaeckel_actionsFunc= _lib.actionAngleStaeckel_actions actionAngleStaeckel_actionsFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements, first store old order f_cont= [R.flags['F_CONTIGUOUS'], vR.flags['F_CONTIGUOUS'], vT.flags['F_CONTIGUOUS'], z.flags['F_CONTIGUOUS'], vz.flags['F_CONTIGUOUS'], u0.flags['F_CONTIGUOUS'], delta.flags['F_CONTIGUOUS']] R= numpy.require(R,dtype=numpy.float64,requirements=['C','W']) vR= numpy.require(vR,dtype=numpy.float64,requirements=['C','W']) vT= numpy.require(vT,dtype=numpy.float64,requirements=['C','W']) z= numpy.require(z,dtype=numpy.float64,requirements=['C','W']) vz= numpy.require(vz,dtype=numpy.float64,requirements=['C','W']) u0= numpy.require(u0,dtype=numpy.float64,requirements=['C','W']) delta= numpy.require(delta,dtype=numpy.float64,requirements=['C','W']) jr= numpy.require(jr,dtype=numpy.float64,requirements=['C','W']) jz= numpy.require(jz,dtype=numpy.float64,requirements=['C','W']) #Run the C code actionAngleStaeckel_actionsFunc(len(R), R, vR, vT, z, vz, u0, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_int(ndelta), delta, ctypes.c_int(order), jr, jz, ctypes.byref(err)) #Reset input arrays if f_cont[0]: R= numpy.asfortranarray(R) if f_cont[1]: vR= numpy.asfortranarray(vR) if f_cont[2]: vT= numpy.asfortranarray(vT) if f_cont[3]: z= numpy.asfortranarray(z) if f_cont[4]: vz= numpy.asfortranarray(vz) if f_cont[5]: u0= numpy.asfortranarray(u0) if f_cont[6]: delta= numpy.asfortranarray(delta) return (jr,jz,err.value) def actionAngleStaeckel_calcu0(E,Lz,pot,delta): """ NAME: actionAngleStaeckel_calcu0 PURPOSE: Use C to calculate u0 in the Staeckel approximation INPUT: E, Lz - energy and angular momentum pot - Potential or list of such instances delta - focal length of prolate spheroidal coordinates OUTPUT: (u0,err) u0 : array, shape (len(E)) err - non-zero if error occurred HISTORY: 2012-12-03 - Written - Bovy (IAS) """ #Parse the potential from ..orbit.integrateFullOrbit import _parse_pot from ..orbit.integratePlanarOrbit import _prep_tfuncs npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot,potforactions=True) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Set up result arrays u0= numpy.empty(len(E)) err= ctypes.c_int(0) #Parse delta delta= numpy.atleast_1d(delta) ndelta= len(delta) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') actionAngleStaeckel_actionsFunc= _lib.calcu0 actionAngleStaeckel_actionsFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements, first store old order f_cont= [E.flags['F_CONTIGUOUS'], Lz.flags['F_CONTIGUOUS'], delta.flags['F_CONTIGUOUS']] E= numpy.require(E,dtype=numpy.float64,requirements=['C','W']) Lz= numpy.require(Lz,dtype=numpy.float64,requirements=['C','W']) delta= numpy.require(delta,dtype=numpy.float64,requirements=['C','W']) u0= numpy.require(u0,dtype=numpy.float64,requirements=['C','W']) #Run the C code actionAngleStaeckel_actionsFunc(len(E), E, Lz, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_int(ndelta), delta, u0, ctypes.byref(err)) #Reset input arrays if f_cont[0]: E= numpy.asfortranarray(E) if f_cont[1]: Lz= numpy.asfortranarray(Lz) if f_cont[2]: delta= numpy.asfortranarray(delta) return (u0,err.value) def actionAngleFreqStaeckel_c(pot,delta,R,vR,vT,z,vz,u0=None,order=10): """ NAME: actionAngleFreqStaeckel_c PURPOSE: Use C to calculate actions and frequencies using the Staeckel approximation INPUT: pot - Potential or list of such instances delta - focal length of prolate spheroidal coordinates R, vR, vT, z, vz - coordinates (arrays) u0= (None) if set, u0 to use order= (10) order of Gauss-Legendre integration of the relevant integrals OUTPUT: (jr,jz,Omegar,Omegaphi,Omegaz,err) jr,jz,Omegar,Omegaphi,Omegaz : array, shape (len(R)) err - non-zero if error occurred HISTORY: 2013-08-23 - Written - Bovy (IAS) """ if u0 is None: u0, dummy= coords.Rz_to_uv(R,z,delta=numpy.atleast_1d(delta)) #Parse the potential from ..orbit.integrateFullOrbit import _parse_pot from ..orbit.integratePlanarOrbit import _prep_tfuncs npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot,potforactions=True) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Parse delta delta= numpy.atleast_1d(delta) ndelta= len(delta) #Set up result arrays jr= numpy.empty(len(R)) jz= numpy.empty(len(R)) Omegar= numpy.empty(len(R)) Omegaphi= numpy.empty(len(R)) Omegaz= numpy.empty(len(R)) err= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') actionAngleStaeckel_actionsFunc= _lib.actionAngleStaeckel_actionsFreqs actionAngleStaeckel_actionsFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements, first store old order f_cont= [R.flags['F_CONTIGUOUS'], vR.flags['F_CONTIGUOUS'], vT.flags['F_CONTIGUOUS'], z.flags['F_CONTIGUOUS'], vz.flags['F_CONTIGUOUS'], u0.flags['F_CONTIGUOUS'], delta.flags['F_CONTIGUOUS']] R= numpy.require(R,dtype=numpy.float64,requirements=['C','W']) vR= numpy.require(vR,dtype=numpy.float64,requirements=['C','W']) vT= numpy.require(vT,dtype=numpy.float64,requirements=['C','W']) z= numpy.require(z,dtype=numpy.float64,requirements=['C','W']) vz= numpy.require(vz,dtype=numpy.float64,requirements=['C','W']) u0= numpy.require(u0,dtype=numpy.float64,requirements=['C','W']) delta= numpy.require(delta,dtype=numpy.float64,requirements=['C','W']) jr= numpy.require(jr,dtype=numpy.float64,requirements=['C','W']) jz= numpy.require(jz,dtype=numpy.float64,requirements=['C','W']) Omegar= numpy.require(Omegar,dtype=numpy.float64,requirements=['C','W']) Omegaphi= numpy.require(Omegaphi,dtype=numpy.float64, requirements=['C','W']) Omegaz= numpy.require(Omegaz,dtype=numpy.float64,requirements=['C','W']) #Run the C code actionAngleStaeckel_actionsFunc(len(R), R, vR, vT, z, vz, u0, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_int(ndelta), delta, ctypes.c_int(order), jr, jz, Omegar, Omegaphi, Omegaz, ctypes.byref(err)) #Reset input arrays if f_cont[0]: R= numpy.asfortranarray(R) if f_cont[1]: vR= numpy.asfortranarray(vR) if f_cont[2]: vT= numpy.asfortranarray(vT) if f_cont[3]: z= numpy.asfortranarray(z) if f_cont[4]: vz= numpy.asfortranarray(vz) if f_cont[5]: u0= numpy.asfortranarray(u0) if f_cont[6]: delta= numpy.asfortranarray(delta) return (jr,jz,Omegar,Omegaphi,Omegaz,err.value) def actionAngleFreqAngleStaeckel_c(pot,delta,R,vR,vT,z,vz,phi, u0=None,order=10): """ NAME: actionAngleFreqAngleStaeckel_c PURPOSE: Use C to calculate actions, frequencies, and angles using the Staeckel approximation INPUT: pot - Potential or list of such instances delta - focal length of prolate spheroidal coordinates R, vR, vT, z, vz, phi - coordinates (arrays) u0= (None) if set, u0 to use order= (10) order of Gauss-Legendre integration of the relevant integrals OUTPUT: (jr,jz,Omegar,Omegaphi,Omegaz,Angler,Anglephi,Anglez,err) jr,jz,Omegar,Omegaphi,Omegaz,Angler,Anglephi,Anglez : array, shape (len(R)) err - non-zero if error occurred HISTORY: 2013-08-27 - Written - Bovy (IAS) """ if u0 is None: u0, dummy= coords.Rz_to_uv(R,z,delta=numpy.atleast_1d(delta)) #Parse the potential from ..orbit.integrateFullOrbit import _parse_pot from ..orbit.integratePlanarOrbit import _prep_tfuncs npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot,potforactions=True) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Parse delta delta= numpy.atleast_1d(delta) ndelta= len(delta) #Set up result arrays jr= numpy.empty(len(R)) jz= numpy.empty(len(R)) Omegar= numpy.empty(len(R)) Omegaphi= numpy.empty(len(R)) Omegaz= numpy.empty(len(R)) Angler= numpy.empty(len(R)) Anglephi= numpy.empty(len(R)) Anglez= numpy.empty(len(R)) err= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') actionAngleStaeckel_actionsFunc= _lib.actionAngleStaeckel_actionsFreqsAngles actionAngleStaeckel_actionsFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements, first store old order f_cont= [R.flags['F_CONTIGUOUS'], vR.flags['F_CONTIGUOUS'], vT.flags['F_CONTIGUOUS'], z.flags['F_CONTIGUOUS'], vz.flags['F_CONTIGUOUS'], u0.flags['F_CONTIGUOUS'], delta.flags['F_CONTIGUOUS']] R= numpy.require(R,dtype=numpy.float64,requirements=['C','W']) vR= numpy.require(vR,dtype=numpy.float64,requirements=['C','W']) vT= numpy.require(vT,dtype=numpy.float64,requirements=['C','W']) z= numpy.require(z,dtype=numpy.float64,requirements=['C','W']) vz= numpy.require(vz,dtype=numpy.float64,requirements=['C','W']) u0= numpy.require(u0,dtype=numpy.float64,requirements=['C','W']) delta= numpy.require(delta,dtype=numpy.float64,requirements=['C','W']) jr= numpy.require(jr,dtype=numpy.float64,requirements=['C','W']) jz= numpy.require(jz,dtype=numpy.float64,requirements=['C','W']) Omegar= numpy.require(Omegar,dtype=numpy.float64,requirements=['C','W']) Omegaphi= numpy.require(Omegaphi,dtype=numpy.float64, requirements=['C','W']) Omegaz= numpy.require(Omegaz,dtype=numpy.float64,requirements=['C','W']) Angler= numpy.require(Angler,dtype=numpy.float64,requirements=['C','W']) Anglephi= numpy.require(Anglephi,dtype=numpy.float64, requirements=['C','W']) Anglez= numpy.require(Anglez,dtype=numpy.float64,requirements=['C','W']) #Run the C code actionAngleStaeckel_actionsFunc(len(R), R, vR, vT, z, vz, u0, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_int(ndelta), delta, ctypes.c_int(order), jr, jz, Omegar, Omegaphi, Omegaz, Angler, Anglephi, Anglez, ctypes.byref(err)) #Reset input arrays if f_cont[0]: R= numpy.asfortranarray(R) if f_cont[1]: vR= numpy.asfortranarray(vR) if f_cont[2]: vT= numpy.asfortranarray(vT) if f_cont[3]: z= numpy.asfortranarray(z) if f_cont[4]: vz= numpy.asfortranarray(vz) if f_cont[5]: u0= numpy.asfortranarray(u0) if f_cont[6]: delta= numpy.asfortranarray(delta) badAngle = Anglephi != 9999.99 Anglephi[badAngle]= (Anglephi[badAngle] + phi[badAngle] % (2.*numpy.pi)) % (2.*numpy.pi) Anglephi[Anglephi < 0.]+= 2.*numpy.pi return (jr,jz,Omegar,Omegaphi,Omegaz,Angler, Anglephi,Anglez,err.value) def actionAngleUminUmaxVminStaeckel_c(pot,delta,R,vR,vT,z,vz,u0=None): """ NAME: actionAngleUminUmaxVminStaeckel_c PURPOSE: Use C to calculate umin, umax, and vmin using the Staeckel approximation INPUT: pot - Potential or list of such instances delta - focal length of prolate spheroidal coordinates R, vR, vT, z, vz - coordinates (arrays) OUTPUT: (umin,umax,vmin,err) umin,umax,vmin : array, shape (len(R)) err - non-zero if error occurred HISTORY: 2017-12-12 - Written - Bovy (UofT) """ if u0 is None: u0, dummy= coords.Rz_to_uv(R,z,delta=numpy.atleast_1d(delta)) #Parse the potential from ..orbit.integrateFullOrbit import _parse_pot from ..orbit.integratePlanarOrbit import _prep_tfuncs npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot,potforactions=True) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Parse delta delta= numpy.atleast_1d(delta) ndelta= len(delta) #Set up result arrays umin= numpy.empty(len(R)) umax= numpy.empty(len(R)) vmin= numpy.empty(len(R)) err= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') actionAngleStaeckel_actionsFunc= _lib.actionAngleStaeckel_uminUmaxVmin actionAngleStaeckel_actionsFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements, first store old order f_cont= [R.flags['F_CONTIGUOUS'], vR.flags['F_CONTIGUOUS'], vT.flags['F_CONTIGUOUS'], z.flags['F_CONTIGUOUS'], vz.flags['F_CONTIGUOUS'], u0.flags['F_CONTIGUOUS'], delta.flags['F_CONTIGUOUS']] R= numpy.require(R,dtype=numpy.float64,requirements=['C','W']) vR= numpy.require(vR,dtype=numpy.float64,requirements=['C','W']) vT= numpy.require(vT,dtype=numpy.float64,requirements=['C','W']) z= numpy.require(z,dtype=numpy.float64,requirements=['C','W']) vz= numpy.require(vz,dtype=numpy.float64,requirements=['C','W']) u0= numpy.require(u0,dtype=numpy.float64,requirements=['C','W']) delta= numpy.require(delta,dtype=numpy.float64,requirements=['C','W']) umin= numpy.require(umin,dtype=numpy.float64,requirements=['C','W']) umax= numpy.require(umax,dtype=numpy.float64,requirements=['C','W']) vmin= numpy.require(vmin,dtype=numpy.float64,requirements=['C','W']) #Run the C code actionAngleStaeckel_actionsFunc(len(R), R, vR, vT, z, vz, u0, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_int(ndelta), delta, umin, umax, vmin, ctypes.byref(err)) #Reset input arrays if f_cont[0]: R= numpy.asfortranarray(R) if f_cont[1]: vR= numpy.asfortranarray(vR) if f_cont[2]: vT= numpy.asfortranarray(vT) if f_cont[3]: z= numpy.asfortranarray(z) if f_cont[4]: vz= numpy.asfortranarray(vz) if f_cont[5]: u0= numpy.asfortranarray(u0) if f_cont[6]: delta= numpy.asfortranarray(delta) return (umin,umax,vmin,err.value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleTorus.py0000644000175100001710000002246114327773303021445 0ustar00runnerdocker############################################################################### # class: actionAngleTorus # # Use McMillan, Binney, and Dehnen's Torus code to calculate (x,v) # given actions and angles # # ############################################################################### import warnings import numpy from ..potential import MWPotential, _isNonAxi from ..potential.Potential import _check_c from ..potential.Potential import flatten as flatten_potential from ..util import galpyWarning from . import actionAngleTorus_c from .actionAngleTorus_c import _ext_loaded as ext_loaded _autofit_errvals= {} _autofit_errvals[-1]= 'something wrong with input, usually bad starting values for the parameters' _autofit_errvals[-2]= 'Fit failed the goal by a factor <= 2' _autofit_errvals[-3]= 'Fit failed the goal by more than 2' _autofit_errvals[-4]= 'Fit aborted: serious problems occured' class actionAngleTorus: """Action-angle formalism using the Torus machinery""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleTorus object INPUT: pot= potential or list of potentials (3D) tol= default tolerance to use when fitting tori (|dJ|/J) dJ= default action difference when computing derivatives (Hessian or Jacobian) OUTPUT: instance HISTORY: 2015-08-07 - Written - Bovy (UofT) """ if not 'pot' in kwargs: #pragma: no cover raise OSError("Must specify pot= for actionAngleTorus") self._pot= flatten_potential(kwargs['pot']) if _isNonAxi(self._pot): raise RuntimeError("actionAngleTorus for non-axisymmetric potentials is not supported") if self._pot == MWPotential: warnings.warn("Use of MWPotential as a Milky-Way-like potential is deprecated; galpy.potential.MWPotential2014, a potential fit to a large variety of dynamical constraints (see Bovy 2015), is the preferred Milky-Way-like potential in galpy", galpyWarning) if ext_loaded: self._c= _check_c(self._pot) if not self._c: raise RuntimeError('The given potential is not fully implemented in C; using the actionAngleTorus code is not supported in pure Python') else:# pragma: no cover raise RuntimeError('actionAngleTorus instances cannot be used, because the actionAngleTorus_c extension failed to load') self._tol= kwargs.get('tol',0.001) self._dJ= kwargs.get('dJ',0.001) return None def __call__(self,jr,jphi,jz,angler,anglephi,anglez,**kwargs): """ NAME: __call__ PURPOSE: evaluate the phase-space coordinates (x,v) for a number of angles on a single torus INPUT: jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) angler - radial angle (array [N]) anglephi - azimuthal angle (array [N]) anglez - vertical angle (array [N]) tol= (object-wide value) goal for |dJ|/|J| along the torus OUTPUT: [R,vR,vT,z,vz,phi] HISTORY: 2015-08-07 - Written - Bovy (UofT) """ out= actionAngleTorus_c.actionAngleTorus_xvFreqs_c(\ self._pot, jr,jphi,jz, angler,anglephi,anglez, tol=kwargs.get('tol',self._tol)) if out[9] != 0: warnings.warn("actionAngleTorus' AutoFit exited with non-zero return status %i: %s" % (out[9],_autofit_errvals[out[9]]), galpyWarning) return numpy.array(out[:6]).T def xvFreqs(self,jr,jphi,jz,angler,anglephi,anglez,**kwargs): """ NAME: xvFreqs PURPOSE: evaluate the phase-space coordinates (x,v) for a number of angles on a single torus as well as the frequencies INPUT: jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) angler - radial angle (array [N]) anglephi - azimuthal angle (array [N]) anglez - vertical angle (array [N]) tol= (object-wide value) goal for |dJ|/|J| along the torus OUTPUT: ([R,vR,vT,z,vz,phi],OmegaR,Omegaphi,Omegaz,AutoFit error message) HISTORY: 2015-08-07 - Written - Bovy (UofT) """ out= actionAngleTorus_c.actionAngleTorus_xvFreqs_c(\ self._pot, jr,jphi,jz, angler,anglephi,anglez, tol=kwargs.get('tol',self._tol)) if out[9] != 0: warnings.warn("actionAngleTorus' AutoFit exited with non-zero return status %i: %s" % (out[9],_autofit_errvals[out[9]]), galpyWarning) return (numpy.array(out[:6]).T,out[6],out[7],out[8],out[9]) def Freqs(self,jr,jphi,jz,**kwargs): """ NAME: Freqs PURPOSE: return the frequencies corresponding to a torus INPUT: jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) tol= (object-wide value) goal for |dJ|/|J| along the torus OUTPUT: (OmegaR,Omegaphi,Omegaz) HISTORY: 2015-08-07 - Written - Bovy (UofT) """ out= actionAngleTorus_c.actionAngleTorus_Freqs_c(\ self._pot, jr,jphi,jz, tol=kwargs.get('tol',self._tol)) if out[3] != 0: warnings.warn("actionAngleTorus' AutoFit exited with non-zero return status %i: %s" % (out[3],_autofit_errvals[out[3]]), galpyWarning) return out def hessianFreqs(self,jr,jphi,jz,**kwargs): """ NAME: hessianFreqs PURPOSE: return the Hessian d Omega / d J and frequencies Omega corresponding to a torus INPUT: jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) tol= (object-wide value) goal for |dJ|/|J| along the torus dJ= (object-wide value) action difference when computing derivatives (Hessian or Jacobian) nosym= (False) if True, don't explicitly symmetrize the Hessian (good to check errors) OUTPUT: (dO/dJ,Omegar,Omegaphi,Omegaz,Autofit error message) HISTORY: 2016-07-15 - Written - Bovy (UofT) """ out= actionAngleTorus_c.actionAngleTorus_hessian_c(\ self._pot, jr,jphi,jz, tol=kwargs.get('tol',self._tol), dJ=kwargs.get('dJ',self._dJ)) if out[4] != 0: warnings.warn("actionAngleTorus' AutoFit exited with non-zero return status %i: %s" % (out[4],_autofit_errvals[out[4]]), galpyWarning) # Re-arrange frequencies and actions to r,phi,z out[0][:,:]= out[0][:,[0,2,1]] out[0][:,:]= out[0][[0,2,1]] if kwargs.get('nosym',False): return out else :# explicitly symmetrize return (0.5*(out[0]+out[0].T),out[1],out[2],out[3],out[4]) def xvJacobianFreqs(self,jr,jphi,jz,angler,anglephi,anglez,**kwargs): """ NAME: xvJacobianFreqs PURPOSE: return [R,vR,vT,z,vz,phi], the Jacobian d [R,vR,vT,z,vz,phi] / d (J,angle), the Hessian dO/dJ, and frequencies Omega corresponding to a torus at multiple sets of angles INPUT: jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) angler - radial angle (array [N]) anglephi - azimuthal angle (array [N]) anglez - vertical angle (array [N]) tol= (object-wide value) goal for |dJ|/|J| along the torus dJ= (object-wide value) action difference when computing derivatives (Hessian or Jacobian) nosym= (False) if True, don't explicitly symmetrize the Hessian (good to check errors) OUTPUT: ([R,vR,vT,z,vz,phi], [N,6] array d[R,vR,vT,z,vz,phi]/d[J,angle], --> (N,6,6) array dO/dJ, --> (3,3) array Omegar,Omegaphi,Omegaz, [N] arrays Autofit error message) HISTORY: 2016-07-19 - Written - Bovy (UofT) """ out= actionAngleTorus_c.actionAngleTorus_jacobian_c(\ self._pot, jr,jphi,jz, angler,anglephi,anglez, tol=kwargs.get('tol',self._tol), dJ=kwargs.get('dJ',self._dJ)) if out[11] != 0: warnings.warn("actionAngleTorus' AutoFit exited with non-zero return status %i: %s" % (out[11],_autofit_errvals[out[11]]), galpyWarning) # Re-arrange actions,angles to r,phi,z out[6][:,:,:]= out[6][:,:,[0,2,1,3,5,4]] out[7][:,:]= out[7][:,[0,2,1]] out[7][:,:]= out[7][[0,2,1]] # Re-arrange x,v to R,vR,vT,z,vz,phi out[6][:,:]= out[6][:,[0,3,5,1,4,2]] if not kwargs.get('nosym',False): # explicitly symmetrize out[7][:]= 0.5*(out[7]+out[7].T) return (numpy.array(out[:6]).T,out[6],out[7], out[8],out[9],out[10],out[11]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleTorus_c.py0000644000175100001710000003647614327773303021762 0ustar00runnerdockerimport ctypes import ctypes.util import numpy from numpy.ctypeslib import ndpointer from ..util import _load_extension_libs _lib, _ext_loaded= _load_extension_libs.load_libgalpy_actionAngleTorus() def actionAngleTorus_xvFreqs_c(pot,jr,jphi,jz, angler,anglephi,anglez, tol=0.003): """ NAME: actionAngleTorus_xvFreqs_c PURPOSE: compute configuration (x,v) and frequencies of a set of angles on a single torus INPUT: pot - Potential object or list thereof jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) angler - radial angle (array [N]) anglephi - azimuthal angle (array [N]) anglez - vertical angle (array [N]) tol= (0.003) goal for |dJ|/|J| along the torus OUTPUT: (R,vR,vT,z,vz,phi,Omegar,Omegaphi,Omegaz,flag) HISTORY: 2015-08-05/07 - Written - Bovy (UofT) """ #Parse the potential from ..orbit.integrateFullOrbit import _parse_pot from ..orbit.integratePlanarOrbit import _prep_tfuncs npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot,potfortorus=True) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Set up result arrays R= numpy.empty(len(angler)) vR= numpy.empty(len(angler)) vT= numpy.empty(len(angler)) z= numpy.empty(len(angler)) vz= numpy.empty(len(angler)) phi= numpy.empty(len(angler)) Omegar= numpy.empty(1) Omegaphi= numpy.empty(1) Omegaz= numpy.empty(1) flag= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') actionAngleTorus_xvFreqsFunc= _lib.actionAngleTorus_xvFreqs actionAngleTorus_xvFreqsFunc.argtypes=\ [ctypes.c_double, ctypes.c_double, ctypes.c_double, ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_double, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements, first store old order f_cont= [angler.flags['F_CONTIGUOUS'], anglephi.flags['F_CONTIGUOUS'], anglez.flags['F_CONTIGUOUS']] angler= numpy.require(angler,dtype=numpy.float64,requirements=['C','W']) anglephi= numpy.require(anglephi,dtype=numpy.float64,requirements=['C','W']) anglez= numpy.require(anglez,dtype=numpy.float64,requirements=['C','W']) R= numpy.require(R,dtype=numpy.float64,requirements=['C','W']) vR= numpy.require(vR,dtype=numpy.float64,requirements=['C','W']) vT= numpy.require(vT,dtype=numpy.float64,requirements=['C','W']) z= numpy.require(z,dtype=numpy.float64,requirements=['C','W']) vz= numpy.require(vz,dtype=numpy.float64,requirements=['C','W']) phi= numpy.require(phi,dtype=numpy.float64,requirements=['C','W']) Omegar= numpy.require(Omegar,dtype=numpy.float64,requirements=['C','W']) Omegaphi= numpy.require(Omegaphi,dtype=numpy.float64,requirements=['C','W']) Omegaz= numpy.require(Omegaz,dtype=numpy.float64,requirements=['C','W']) #Run the C code actionAngleTorus_xvFreqsFunc(ctypes.c_double(jr), ctypes.c_double(jphi), ctypes.c_double(jz), ctypes.c_int(len(angler)), angler, anglephi, anglez, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(tol), R,vR,vT,z,vz,phi, Omegar,Omegaphi,Omegaz, ctypes.byref(flag)) #Reset input arrays if f_cont[0]: angler= numpy.asfortranarray(angler) if f_cont[1]: anglephi= numpy.asfortranarray(anglephi) if f_cont[2]: anglez= numpy.asfortranarray(anglez) return (R,vR,vT,z,vz,phi,Omegar[0],Omegaphi[0],Omegaz[0],flag.value) def actionAngleTorus_Freqs_c(pot,jr,jphi,jz, tol=0.003): """ NAME: actionAngleTorus_Freqs_c PURPOSE: compute frequencies on a single torus INPUT: pot - Potential object or list thereof jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) tol= (0.003) goal for |dJ|/|J| along the torus OUTPUT: (Omegar,Omegaphi,Omegaz,flag) HISTORY: 2015-08-05/07 - Written - Bovy (UofT) """ #Parse the potential from ..orbit.integrateFullOrbit import _parse_pot from ..orbit.integratePlanarOrbit import _prep_tfuncs npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot,potfortorus=True) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Set up result Omegar= numpy.empty(1) Omegaphi= numpy.empty(1) Omegaz= numpy.empty(1) flag= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') actionAngleTorus_FreqsFunc= _lib.actionAngleTorus_Freqs actionAngleTorus_FreqsFunc.argtypes=\ [ctypes.c_double, ctypes.c_double, ctypes.c_double, ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_double, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements Omegar= numpy.require(Omegar,dtype=numpy.float64,requirements=['C','W']) Omegaphi= numpy.require(Omegaphi,dtype=numpy.float64,requirements=['C','W']) Omegaz= numpy.require(Omegaz,dtype=numpy.float64,requirements=['C','W']) #Run the C code actionAngleTorus_FreqsFunc(ctypes.c_double(jr), ctypes.c_double(jphi), ctypes.c_double(jz), ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(tol), Omegar,Omegaphi,Omegaz, ctypes.byref(flag)) return (Omegar[0],Omegaphi[0],Omegaz[0],flag.value) def actionAngleTorus_hessian_c(pot,jr,jphi,jz, tol=0.003,dJ=0.001): """ NAME: actionAngleTorus_hessian_c PURPOSE: compute dO/dJ on a single torus INPUT: pot - Potential object or list thereof jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) tol= (0.003) goal for |dJ|/|J| along the torus dJ= (0.001) action difference when computing derivatives (Hessian or Jacobian) OUTPUT: (dO/dJ,Omegar,Omegaphi,Omegaz,Autofit error flag) Note: dO/dJ is *not* symmetrized here HISTORY: 2016-07-15 - Written - Bovy (UofT) """ #Parse the potential from ..orbit.integrateFullOrbit import _parse_pot from ..orbit.integratePlanarOrbit import _prep_tfuncs npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot,potfortorus=True) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Set up result dOdJT= numpy.empty(9) Omegar= numpy.empty(1) Omegaphi= numpy.empty(1) Omegaz= numpy.empty(1) flag= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') actionAngleTorus_HessFunc= _lib.actionAngleTorus_hessianFreqs actionAngleTorus_HessFunc.argtypes=\ [ctypes.c_double, ctypes.c_double, ctypes.c_double, ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_double, ctypes.c_double, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements dOdJT= numpy.require(dOdJT,dtype=numpy.float64,requirements=['C','W']) Omegar= numpy.require(Omegar,dtype=numpy.float64,requirements=['C','W']) Omegaphi= numpy.require(Omegaphi,dtype=numpy.float64,requirements=['C','W']) Omegaz= numpy.require(Omegaz,dtype=numpy.float64,requirements=['C','W']) #Run the C code actionAngleTorus_HessFunc(ctypes.c_double(jr), ctypes.c_double(jphi), ctypes.c_double(jz), ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(tol), ctypes.c_double(dJ), dOdJT, Omegar,Omegaphi,Omegaz, ctypes.byref(flag)) return (dOdJT.reshape((3,3)).T,Omegar[0],Omegaphi[0],Omegaz[0],flag.value) def actionAngleTorus_jacobian_c(pot,jr,jphi,jz,angler,anglephi,anglez, tol=0.003,dJ=0.001): """ NAME: actionAngleTorus_jacobian_c PURPOSE: compute d(x,v)/d(J,theta) on a single torus, also compute dO/dJ and the frequencies INPUT: pot - Potential object or list thereof jr - radial action (scalar) jphi - azimuthal action (scalar) jz - vertical action (scalar) angler - radial angle (array [N]) anglephi - azimuthal angle (array [N]) anglez - vertical angle (array [N]) tol= (0.003) goal for |dJ|/|J| along the torus dJ= (0.001) action difference when computing derivatives (Hessian or Jacobian) OUTPUT: (d[R,vR,vT,z,vz,phi]/d[J,theta], Omegar,Omegaphi,Omegaz, Autofit error message) Note: dO/dJ is *not* symmetrized here HISTORY: 2016-07-19 - Written - Bovy (UofT) """ #Parse the potential from ..orbit.integrateFullOrbit import _parse_pot from ..orbit.integratePlanarOrbit import _prep_tfuncs npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot,potfortorus=True) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Set up result R= numpy.empty(len(angler)) vR= numpy.empty(len(angler)) vT= numpy.empty(len(angler)) z= numpy.empty(len(angler)) vz= numpy.empty(len(angler)) phi= numpy.empty(len(angler)) dxvOdJaT= numpy.empty(36*len(angler)) dOdJT= numpy.empty(9) Omegar= numpy.empty(1) Omegaphi= numpy.empty(1) Omegaz= numpy.empty(1) flag= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') actionAngleTorus_JacFunc= _lib.actionAngleTorus_jacobianFreqs actionAngleTorus_JacFunc.argtypes=\ [ctypes.c_double, ctypes.c_double, ctypes.c_double, ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_double, ctypes.c_double, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements, first store old order f_cont= [angler.flags['F_CONTIGUOUS'], anglephi.flags['F_CONTIGUOUS'], anglez.flags['F_CONTIGUOUS']] angler= numpy.require(angler,dtype=numpy.float64,requirements=['C','W']) anglephi= numpy.require(anglephi,dtype=numpy.float64,requirements=['C','W']) anglez= numpy.require(anglez,dtype=numpy.float64,requirements=['C','W']) R= numpy.require(R,dtype=numpy.float64,requirements=['C','W']) vR= numpy.require(vR,dtype=numpy.float64,requirements=['C','W']) vT= numpy.require(vT,dtype=numpy.float64,requirements=['C','W']) z= numpy.require(z,dtype=numpy.float64,requirements=['C','W']) vz= numpy.require(vz,dtype=numpy.float64,requirements=['C','W']) phi= numpy.require(phi,dtype=numpy.float64,requirements=['C','W']) dxvOdJaT= numpy.require(dxvOdJaT, dtype=numpy.float64,requirements=['C','W']) dOdJT= numpy.require(dOdJT,dtype=numpy.float64,requirements=['C','W']) Omegar= numpy.require(Omegar,dtype=numpy.float64,requirements=['C','W']) Omegaphi= numpy.require(Omegaphi,dtype=numpy.float64,requirements=['C','W']) Omegaz= numpy.require(Omegaz,dtype=numpy.float64,requirements=['C','W']) #Run the C code actionAngleTorus_JacFunc(ctypes.c_double(jr), ctypes.c_double(jphi), ctypes.c_double(jz), ctypes.c_int(len(angler)), angler, anglephi, anglez, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(tol), ctypes.c_double(dJ), R,vR,vT,z,vz,phi, dxvOdJaT, dOdJT, Omegar,Omegaphi,Omegaz, ctypes.byref(flag)) #Reset input arrays if f_cont[0]: angler= numpy.asfortranarray(angler) if f_cont[1]: anglephi= numpy.asfortranarray(anglephi) if f_cont[2]: anglez= numpy.asfortranarray(anglez) dxvOdJaT= numpy.reshape(dxvOdJaT,(len(angler),6,6),order='C') dxvOdJa= numpy.swapaxes(dxvOdJaT,1,2) return (R,vR,vT,z,vz,phi, dxvOdJa, dOdJT.reshape((3,3)).T, Omegar[0],Omegaphi[0],Omegaz[0], flag.value) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667233486.697467 galpy-1.8.1/galpy/actionAngle/actionAngleTorus_c_ext/0000755000175100001710000000000014327773317022255 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleTorus_c_ext/galpyPot.h0000644000175100001710000000306614327773303024225 0ustar00runnerdocker/***************************************************************************//**\file galpyPot.h \brief Contains class galpyPotential. General interface to galpy potentials * * * galpyPot.h * * * * C++ code written by Jo Bovy, 2015 * *******************************************************************************/ #ifndef __GALPY_GALPYPOT_H__ #define __GALPY_GALPYPOT_H__ #include "Potential.h" #include /** \brief A general interface to galpy potentials in C++ */ // Class declaration class galpyPotential : public Potential { int nargs; struct potentialArg * potentialArgs; void error(const char*) const; public: galpyPotential(int,struct potentialArg *); double operator() (const double, const double) const; double operator() (const double, double&, double&) const;//?? double operator() (const double, const double, double&, double&) const; double operator() (const double, const double, double&, double&, double&, double&, double&) const;//?? double RfromLc(const double, double* = 0) const; double LfromRc(const double, double* = 0) const; Frequencies KapNuOm(const double) const; }; inline galpyPotential::galpyPotential(int na, struct potentialArg * inPotentialArgs) : nargs(na), potentialArgs(inPotentialArgs) { } #endif /* galpyPot.h */ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngleVertical.py0000644000175100001710000001454514327773303022106 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleVertical # # methods: # Jz # anglez # Tz # calczmax # calcEz ############################################################################### import numpy from scipy import integrate, optimize from ..potential.linearPotential import evaluatelinearPotentials from .actionAngle import actionAngle class actionAngleVertical(actionAngle): """Action-angle formalism for vertical integral using the adiabatic approximation""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize an actionAngleVertical object INPUT: Either: a) z,vz b) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well pot= potential or list of potentials (planarPotentials) OUTPUT: HISTORY: 2012-06-01 - Written - Bovy (IAS) """ self._parse_eval_args(*args,_noOrbUnitsCheck=True,**kwargs) self._z= self._eval_z self._vz= self._eval_vz if not 'pot' in kwargs: #pragma: no cover raise OSError("Must specify pot= for actionAngleVertical") self._verticalpot= kwargs['pot'] return None def Jz(self,**kwargs): """ NAME: Jz PURPOSE: Calculate the vertical action INPUT: +scipy.integrate.quad keywords OUTPUT: J_z(z,vz)/ro/vc + estimate of the error HISTORY: 2012-06-01 - Written - Bovy (IAS) """ if hasattr(self,'_Jz'): return self._Jz zmax= self.calczmax() if zmax == -9999.99: return numpy.array([9999.99,numpy.nan]) Ez= calcEz(self._z,self._vz,self._verticalpot) self._Jz= 2.*integrate.quad(_JzIntegrand,0.,zmax, args=(Ez,self._verticalpot), **kwargs)[0]/numpy.pi return self._Jz def Tz(self,**kwargs): #pragma: no cover """ NAME: Tz PURPOSE: Calculate the vertical period INPUT: +scipy.integrate.quad keywords OUTPUT: T_z(z,vz)*vc/ro + estimate of the error HISTORY: 2012-06-01 - Written - Bovy (IAS) """ if hasattr(self,'_Tz'): return self._Tz zmax= self.calczmax() Ez= calcEz(self._z,self._vz,self._verticalpot) self._Tz= 4.*integrate.quad(_TzIntegrand,0.,zmax, args=(Ez,self._verticalpot), **kwargs)[0] return self._Tz def anglez(self,**kwargs): #pragma: no cover """ NAME: anglez PURPOSE: Calculate the vertical angle INPUT: +scipy.integrate.quad keywords OUTPUT: angle_z(z,vz)*vc/ro + estimate of the error HISTORY: 2012-06-01 - Written - Bovy (IAS) """ if hasattr(self,'_anglez'): return self._anglez zmax= self.calczmax() Ez= calcEz(self._z,self._vz,self._verticalpot) Tz= self.Tz(**kwargs) self._anglez= 2.*numpy.pi*(numpy.array(integrate.quad(_TzIntegrand,0.,numpy.fabs(self._z), args=(Ez,self._verticalpot), **kwargs)))/Tz[0] if self._z >= 0. and self._vz >= 0.: pass elif self._z >= 0. and self._vz < 0.: self._anglez[0]= numpy.pi-self._anglez[0] elif self._z < 0. and self._vz <= 0.: self._anglez[0]= numpy.pi+self._anglez[0] else: self._anglez[0]= 2.*numpy.pi-self._anglez[0] return self._anglez def calczmax(self): """ NAME: calczmax PURPOSE: calculate the maximum height INPUT: OUTPUT: zmax HISTORY: 2012-06-01 - Written - Bovy (IAS) """ if hasattr(self,'_zmax'): #pragma: no cover return self._zmax Ez= calcEz(self._z,self._vz,self._verticalpot) if self._vz == 0.: #We are exactly at the maximum height zmax= numpy.fabs(self._z) else: zstart= self._z try: zend= _zmaxFindStart(self._z,Ez,self._verticalpot) except OverflowError: #pragma: no cover zmax= -9999.99 else: zmax= optimize.brentq(_zmaxEq,zstart,zend, (Ez,self._verticalpot)) self._zmax= zmax return self._zmax def _zmaxEq(z,Ez,pot): """The vz=0 equation that needs to be solved to find zmax""" return Ez-potentialVertical(z,pot) def calcEz(z,vz,pot): """ NAME: calcEz PURPOSE: calculate the vertical energy INPUT: z - height (/ro) vz - vertical part of the velocity (/vc) pot - potential OUTPUT: Ez HISTORY: 2012-06-01 - Written - Bovy (IAS) """ return potentialVertical(z,pot)+vz**2./2. def potentialVertical(z,pot): """ NAME: potentialVertical PURPOSE: return the potential INPUT: z - height (/ro) pot - potential OUTPUT: Phi_z(z) HISTORY: 2012-06-01 - Written - Bovy (IAS) """ return evaluatelinearPotentials(pot,z,use_physical=False) def _JzIntegrand(z,Ez,pot): """The J_z integrand""" return numpy.sqrt(2.*(Ez-potentialVertical(z,pot))) def _TzIntegrand(z,Ez,pot): #pragma: no cover """The T_z integrand""" return 1./_JzIntegrand(z,Ez,pot) def _zmaxFindStart(z,Ez,pot): """ NAME: _zmaxFindStart PURPOSE: Find adequate end point to solve for zmax INPUT: z - height Ez - vertical energy pot - potential OUTPUT: zend HISTORY: 2012-06-01 - Written - Bovy (IAS) """ if z == 0.: ztry= 0.00001 else: ztry= 2.*numpy.fabs(z) while (Ez-potentialVertical(ztry,pot)) > 0.: ztry*= 2. if ztry > 100.: #pragma: no cover raise OverflowError return ztry ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667233486.7014673 galpy-1.8.1/galpy/actionAngle/actionAngle_c_ext/0000755000175100001710000000000014327773317021220 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngle_c_ext/actionAngle.h0000644000175100001710000000144614327773303023615 0ustar00runnerdocker/* C code for actionAngle calculations */ #ifndef __GALPY_ACTIONANGLE_H__ #define __GALPY_ACTIONANGLE_H__ #ifdef __cplusplus extern "C" { #endif #ifdef _WIN32 #include #endif #include #include #include #include "interp_2d.h" /* Macro for dealing with potentially unused variables due to OpenMP */ /* If we're not using GNU C, elide __attribute__ if it doesn't exist*/ #ifndef __has_attribute // Compatibility with non-clang compilers. #define __has_attribute(x) 0 #endif #if defined(__GNUC__) || __has_attribute(unused) # define UNUSED __attribute__((unused)) #else # define UNUSED /*NOTHING*/ #endif /* Structure declarations */ struct pragmasolver{ gsl_root_fsolver *s; }; #ifdef __cplusplus } #endif #endif /* actionAngle.h */ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngle_c_ext/actionAngleAdiabatic.c0000644000175100001710000004535714327773303025403 0ustar00runnerdocker/* C code for the adiabatic approximation */ #include #include #include #include #include #include #include #include #include #ifdef _OPENMP #include #endif #define CHUNKSIZE 10 //Potentials #include #include #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif //Macros to export functions in DLL on different OS #if defined(_WIN32) #define EXPORT __declspec(dllexport) #elif defined(__GNUC__) #define EXPORT __attribute__((visibility("default"))) #else // Just do nothing? #define EXPORT #endif /* Structure Declarations */ struct JRAdiabaticArg{ double ER; double Lz22; int nargs; struct potentialArg * actionAngleArgs; }; struct JzAdiabaticArg{ double Ez; double R; int nargs; struct potentialArg * actionAngleArgs; }; /* Function Declarations */ EXPORT void actionAngleAdiabatic_RperiRapZmax(int,double *,double *,double *,double *, double *,int,int *,double *,tfuncs_type_arr,double, double *,double *,double *,int *); EXPORT void actionAngleAdiabatic_actions(int,double *,double *,double *,double *, double *,int,int *,double *,tfuncs_type_arr,double, double *,double *,int *); void calcJRAdiabatic(int,double *,double *,double *,double *,double *, int,struct potentialArg *,int); void calcJzAdiabatic(int,double *,double *,double *,double *,int, struct potentialArg *,int); void calcRapRperi(int,double *,double *,double *,double *,double *, int,struct potentialArg *); void calcZmax(int,double *,double *,double *,double *,int, struct potentialArg *); double JRAdiabaticIntegrandSquared(double,void *); double JRAdiabaticIntegrand(double,void *); double JzAdiabaticIntegrandSquared(double,void *); double JzAdiabaticIntegrand(double,void *); double evaluateVerticalPotentials(double, double,int, struct potentialArg *); /* Actual functions, inlines first */ static inline void calcEREzL(int ndata, double *R, double *vR, double *vT, double *z, double *vz, double *ER, double *Ez, double *Lz, int nargs, struct potentialArg * actionAngleArgs){ int ii; UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) private(ii) for (ii=0; ii < ndata; ii++){ *(ER+ii)= evaluatePotentials(*(R+ii),0., nargs,actionAngleArgs) + 0.5 * *(vR+ii) * *(vR+ii) + 0.5 * *(vT+ii) * *(vT+ii); *(Ez+ii)= evaluateVerticalPotentials(*(R+ii),*(z+ii), nargs,actionAngleArgs) + 0.5 * *(vz+ii) * *(vz+ii); *(Lz+ii)= *(R+ii) * *(vT+ii); } } /* MAIN FUNCTIONS */ void actionAngleAdiabatic_RperiRapZmax(int ndata, double *R, double *vR, double *vT, double *z, double *vz, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double gamma, double *rperi, double *rap, double *zmax, int * err){ int ii; //Set up the potentials struct potentialArg * actionAngleArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,actionAngleArgs,&pot_type,&pot_args,&pot_tfuncs); //ER, Ez, Lz double *ER= (double *) malloc ( ndata * sizeof(double) ); double *Ez= (double *) malloc ( ndata * sizeof(double) ); double *Lz= (double *) malloc ( ndata * sizeof(double) ); calcEREzL(ndata,R,vR,vT,z,vz,ER,Ez,Lz,npot,actionAngleArgs); //Calculate peri and apocenters double *jz= (double *) malloc ( ndata * sizeof(double) ); calcZmax(ndata,zmax,z,R,Ez,npot,actionAngleArgs); calcJzAdiabatic(ndata,jz,zmax,R,Ez,npot,actionAngleArgs,10); //Adjust planar effective potential for gamma UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) private(ii) for (ii=0; ii < ndata; ii++){ *(Lz+ii)= fabs( *(Lz+ii) ) + gamma * *(jz+ii); *(ER+ii)+= 0.5 * *(Lz+ii) * *(Lz+ii) / *(R+ii) / *(R+ii) - 0.5 * *(vT+ii) * *(vT+ii); } calcRapRperi(ndata,rperi,rap,R,ER,Lz,npot,actionAngleArgs); free_potentialArgs(npot,actionAngleArgs); free(actionAngleArgs); free(ER); free(Ez); free(Lz); free(jz); } void actionAngleAdiabatic_actions(int ndata, double *R, double *vR, double *vT, double *z, double *vz, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double gamma, double *jr, double *jz, int * err){ int ii; //Set up the potentials struct potentialArg * actionAngleArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,actionAngleArgs,&pot_type,&pot_args,&pot_tfuncs); //ER, Ez, Lz double *ER= (double *) malloc ( ndata * sizeof(double) ); double *Ez= (double *) malloc ( ndata * sizeof(double) ); double *Lz= (double *) malloc ( ndata * sizeof(double) ); calcEREzL(ndata,R,vR,vT,z,vz,ER,Ez,Lz,npot,actionAngleArgs); //Calculate peri and apocenters double *rperi= (double *) malloc ( ndata * sizeof(double) ); double *rap= (double *) malloc ( ndata * sizeof(double) ); double *zmax= (double *) malloc ( ndata * sizeof(double) ); calcZmax(ndata,zmax,z,R,Ez,npot,actionAngleArgs); calcJzAdiabatic(ndata,jz,zmax,R,Ez,npot,actionAngleArgs,10); //Adjust planar effective potential for gamma UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) private(ii) for (ii=0; ii < ndata; ii++){ *(Lz+ii)= fabs( *(Lz+ii) ) + gamma * *(jz+ii); *(ER+ii)+= 0.5 * *(Lz+ii) * *(Lz+ii) / *(R+ii) / *(R+ii) - 0.5 * *(vT+ii) * *(vT+ii); } calcRapRperi(ndata,rperi,rap,R,ER,Lz,npot,actionAngleArgs); calcJRAdiabatic(ndata,jr,rperi,rap,ER,Lz,npot,actionAngleArgs,10); free_potentialArgs(npot,actionAngleArgs); free(actionAngleArgs); free(ER); free(Ez); free(Lz); free(rperi); free(rap); free(zmax); } void calcJRAdiabatic(int ndata, double * jr, double * rperi, double * rap, double * ER, double * Lz, int nargs, struct potentialArg * actionAngleArgs, int order){ int ii, tid, nthreads; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif gsl_function * JRInt= (gsl_function *) malloc ( nthreads * sizeof(gsl_function) ); struct JRAdiabaticArg * params= (struct JRAdiabaticArg *) malloc ( nthreads * sizeof (struct JRAdiabaticArg) ); for (tid=0; tid < nthreads; tid++){ (params+tid)->nargs= nargs; (params+tid)->actionAngleArgs= actionAngleArgs; } //Setup integrator gsl_integration_glfixed_table * T= gsl_integration_glfixed_table_alloc (order); UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) \ private(tid,ii) \ shared(jr,rperi,rap,JRInt,params,T,ER,Lz) for (ii=0; ii < ndata; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif if ( *(rperi+ii) == -9999.99 || *(rap+ii) == -9999.99 ){ *(jr+ii)= 9999.99; continue; } if ( (*(rap+ii) - *(rperi+ii)) / *(rap+ii) < 0.000001 ){//circular *(jr+ii) = 0.; continue; } //Setup function (params+tid)->ER= *(ER+ii); (params+tid)->Lz22= 0.5 * *(Lz+ii) * *(Lz+ii); (JRInt+tid)->function = &JRAdiabaticIntegrand; (JRInt+tid)->params = params+tid; //Integrate *(jr+ii)= gsl_integration_glfixed (JRInt+tid,*(rperi+ii),*(rap+ii),T) * sqrt(2.) / M_PI; } free(JRInt); free(params); gsl_integration_glfixed_table_free ( T ); } void calcJzAdiabatic(int ndata, double * jz, double * zmax, double * R, double * Ez, int nargs, struct potentialArg * actionAngleArgs, int order){ int ii, tid, nthreads; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif gsl_function * JzInt= (gsl_function *) malloc ( nthreads * sizeof(gsl_function) ); struct JzAdiabaticArg * params= (struct JzAdiabaticArg *) malloc ( nthreads * sizeof (struct JzAdiabaticArg) ); for (tid=0; tid < nthreads; tid++){ (params+tid)->nargs= nargs; (params+tid)->actionAngleArgs= actionAngleArgs; } //Setup integrator gsl_integration_glfixed_table * T= gsl_integration_glfixed_table_alloc (order); UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) \ private(tid,ii) \ shared(jz,zmax,JzInt,params,T,Ez,R) for (ii=0; ii < ndata; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif if ( *(zmax+ii) == -9999.99 ){ *(jz+ii)= 9999.99; continue; } if ( *(zmax+ii) < 0.000001 ){//circular *(jz+ii) = 0.; continue; } //Setup function (params+tid)->Ez= *(Ez+ii); (params+tid)->R= *(R+ii); (JzInt+tid)->function = &JzAdiabaticIntegrand; (JzInt+tid)->params = params+tid; //Integrate *(jz+ii)= gsl_integration_glfixed (JzInt+tid,0.,*(zmax+ii),T) * 2 * sqrt(2.) / M_PI; } free(JzInt); free(params); gsl_integration_glfixed_table_free ( T ); } void calcRapRperi(int ndata, double * rperi, double * rap, double * R, double * ER, double * Lz, int nargs, struct potentialArg * actionAngleArgs){ int ii, tid, nthreads; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif double peps, meps; gsl_function * JRRoot= (gsl_function *) malloc ( nthreads * sizeof(gsl_function) ); struct JRAdiabaticArg * params= (struct JRAdiabaticArg *) malloc ( nthreads * sizeof (struct JRAdiabaticArg) ); //Setup solver int status; int iter, max_iter = 100; const gsl_root_fsolver_type *T; double R_lo, R_hi; struct pragmasolver *s= (struct pragmasolver *) malloc ( nthreads * sizeof (struct pragmasolver) ); T = gsl_root_fsolver_brent; for (tid=0; tid < nthreads; tid++){ (params+tid)->nargs= nargs; (params+tid)->actionAngleArgs= actionAngleArgs; (s+tid)->s= gsl_root_fsolver_alloc (T); } UNUSED int chunk= CHUNKSIZE; gsl_set_error_handler_off(); #pragma omp parallel for schedule(static,chunk) \ private(tid,ii,iter,status,R_lo,R_hi,meps,peps) \ shared(rperi,rap,JRRoot,params,s,R,ER,Lz,max_iter) for (ii=0; ii < ndata; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif //Setup function (params+tid)->ER= *(ER+ii); (params+tid)->Lz22= 0.5 * *(Lz+ii) * *(Lz+ii); (JRRoot+tid)->params = params+tid; (JRRoot+tid)->function = &JRAdiabaticIntegrandSquared; //Find starting points for minimum peps= GSL_FN_EVAL(JRRoot+tid,*(R+ii)+0.0000001); meps= GSL_FN_EVAL(JRRoot+tid,*(R+ii)-0.0000001); if ( fabs(GSL_FN_EVAL(JRRoot+tid,*(R+ii))) < 0.0000001 && peps*meps < 0 ){ //we are at rap or rperi if ( peps < 0. && meps > 0. ) {//rap *(rap+ii)= *(R+ii); R_lo= 0.9 * (*(R+ii) - 0.0000001); R_hi= *(R+ii) - 0.00000001; while ( GSL_FN_EVAL(JRRoot+tid,R_lo) >= 0. && R_lo > 0.000000001){ R_hi= R_lo; //this makes sure that brent evaluates using previous R_lo*= 0.9; } //Find root status = gsl_root_fsolver_set ((s+tid)->s, JRRoot+tid, R_lo, R_hi); if (status == GSL_EINVAL) { *(rperi+ii) = 0.;//Assume zero if below 0.000000001 continue; } iter= 0; do { iter++; status = gsl_root_fsolver_iterate ((s+tid)->s); R_lo = gsl_root_fsolver_x_lower ((s+tid)->s); R_hi = gsl_root_fsolver_x_upper ((s+tid)->s); status = gsl_root_test_interval (R_lo, R_hi, 9.9999999999999998e-13, 4.4408920985006262e-16); } while (status == GSL_CONTINUE && iter < max_iter); // LCOV_EXCL_START if (status == GSL_EINVAL) {//Shouldn't ever get here *(rperi+ii) = -9999.99; *(rap+ii) = -9999.99; continue; } // LCOV_EXCL_STOP *(rperi+ii) = gsl_root_fsolver_root ((s+tid)->s); } else {// JB: Should catch all: if ( peps > 0. && meps < 0. ){//rperi *(rperi+ii)= *(R+ii); R_lo= *(R+ii) + 0.0000001; R_hi= 1.1 * (*(R+ii) + 0.0000001); while ( GSL_FN_EVAL(JRRoot+tid,R_hi) >= 0. && R_hi < 37.5) { R_lo= R_hi; //this makes sure that brent evaluates using previous R_hi*= 1.1; } //Find root status = gsl_root_fsolver_set ((s+tid)->s, JRRoot+tid, R_lo, R_hi); if (status == GSL_EINVAL) { *(rperi+ii) = -9999.99; *(rap+ii) = -9999.99; continue; } iter= 0; do { iter++; status = gsl_root_fsolver_iterate ((s+tid)->s); R_lo = gsl_root_fsolver_x_lower ((s+tid)->s); R_hi = gsl_root_fsolver_x_upper ((s+tid)->s); status = gsl_root_test_interval (R_lo, R_hi, 9.9999999999999998e-13, 4.4408920985006262e-16); } while (status == GSL_CONTINUE && iter < max_iter); // LCOV_EXCL_START if (status == GSL_EINVAL) {//Shouldn't ever get here *(rperi+ii) = -9999.99; *(rap+ii) = -9999.99; continue; } // LCOV_EXCL_STOP *(rap+ii) = gsl_root_fsolver_root ((s+tid)->s); } } else if ( fabs(peps) < 0.00000001 && fabs(meps) < 0.00000001 && peps <= 0 && meps <= 0 ) {//circular *(rperi+ii) = *(R+ii); *(rap+ii) = *(R+ii); } else { R_lo= 0.9 * *(R+ii); R_hi= *(R+ii); while ( GSL_FN_EVAL(JRRoot+tid,R_lo) >= 0. && R_lo > 0.000000001){ R_hi= R_lo; //this makes sure that brent evaluates using previous R_lo*= 0.9; } R_hi= (R_lo < 0.9 * *(R+ii)) ? R_lo / 0.9 / 0.9: *(R+ii); //Find root status = gsl_root_fsolver_set ((s+tid)->s, JRRoot+tid, R_lo, R_hi); if (status == GSL_EINVAL) { *(rperi+ii) = 0.;//Assume zero if below 0.000000001 } else { iter= 0; do { iter++; status = gsl_root_fsolver_iterate ((s+tid)->s); R_lo = gsl_root_fsolver_x_lower ((s+tid)->s); R_hi = gsl_root_fsolver_x_upper ((s+tid)->s); status = gsl_root_test_interval (R_lo, R_hi, 9.9999999999999998e-13, 4.4408920985006262e-16); } while (status == GSL_CONTINUE && iter < max_iter); // LCOV_EXCL_START if (status == GSL_EINVAL) {//Shouldn't ever get here *(rperi+ii) = -9999.99; *(rap+ii) = -9999.99; continue; } // LCOV_EXCL_STOP *(rperi+ii) = gsl_root_fsolver_root ((s+tid)->s); } //Find starting points for maximum R_lo= *(R+ii); R_hi= 1.1 * *(R+ii); while ( GSL_FN_EVAL(JRRoot+tid,R_hi) > 0. && R_hi < 37.5) { R_lo= R_hi; //this makes sure that brent evaluates using previous R_hi*= 1.1; } R_lo= (R_hi > 1.1 * *(R+ii)) ? R_hi / 1.1 / 1.1: *(R+ii); //Find root status = gsl_root_fsolver_set ((s+tid)->s, JRRoot+tid, R_lo, R_hi); if (status == GSL_EINVAL) { *(rperi+ii) = -9999.99; *(rap+ii) = -9999.99; continue; } iter= 0; do { iter++; status = gsl_root_fsolver_iterate ((s+tid)->s); R_lo = gsl_root_fsolver_x_lower ((s+tid)->s); R_hi = gsl_root_fsolver_x_upper ((s+tid)->s); status = gsl_root_test_interval (R_lo, R_hi, 9.9999999999999998e-13, 4.4408920985006262e-16); } while (status == GSL_CONTINUE && iter < max_iter); // LCOV_EXCL_START if (status == GSL_EINVAL) {//Shouldn't ever get here *(rperi+ii) = -9999.99; *(rap+ii) = -9999.99; continue; } // LCOV_EXCL_STOP *(rap+ii) = gsl_root_fsolver_root ((s+tid)->s); } } gsl_set_error_handler (NULL); for (tid=0; tid < nthreads; tid++) gsl_root_fsolver_free( (s+tid)->s); free(s); free(JRRoot); free(params); } void calcZmax(int ndata, double * zmax, double * z, double * R, double * Ez, int nargs, struct potentialArg * actionAngleArgs){ int ii, tid, nthreads; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif gsl_function * JzRoot= (gsl_function *) malloc ( nthreads * sizeof(gsl_function) ); struct JzAdiabaticArg * params= (struct JzAdiabaticArg *) malloc ( nthreads * sizeof (struct JzAdiabaticArg) ); //Setup solver int status; int iter, max_iter = 100; const gsl_root_fsolver_type *T; double z_lo, z_hi; struct pragmasolver *s= (struct pragmasolver *) malloc ( nthreads * sizeof (struct pragmasolver) ); T = gsl_root_fsolver_brent; for (tid=0; tid < nthreads; tid++){ (params+tid)->nargs= nargs; (params+tid)->actionAngleArgs= actionAngleArgs; (s+tid)->s= gsl_root_fsolver_alloc (T); } UNUSED int chunk= CHUNKSIZE; gsl_set_error_handler_off(); #pragma omp parallel for schedule(static,chunk) \ private(tid,ii,iter,status,z_lo,z_hi) \ shared(zmax,JzRoot,params,s,z,Ez,R,max_iter) for (ii=0; ii < ndata; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif //Setup function (params+tid)->Ez= *(Ez+ii); (params+tid)->R= *(R+ii); (JzRoot+tid)->function = &JzAdiabaticIntegrandSquared; (JzRoot+tid)->params = params+tid; //Find starting points for minimum if ( fabs(GSL_FN_EVAL(JzRoot+tid,*(z+ii))) < 0.0000001){ //we are at zmax *(zmax+ii)= fabs( *(z+ii) ); } else { z_lo= fabs ( *(z+ii) ); z_hi= ( *(z+ii) == 0. ) ? 0.1: 1.1 * fabs( *(z+ii) ); while ( GSL_FN_EVAL(JzRoot+tid,z_hi) >= 0. && z_hi < 37.5) { z_lo= z_hi; //this makes sure that brent evaluates using previous z_hi*= 1.1; } //Find root status = gsl_root_fsolver_set ((s+tid)->s, JzRoot+tid, z_lo, z_hi); if (status == GSL_EINVAL) { *(zmax+ii) = -9999.99; continue; } iter= 0; do { iter++; status = gsl_root_fsolver_iterate ((s+tid)->s); z_lo = gsl_root_fsolver_x_lower ((s+tid)->s); z_hi = gsl_root_fsolver_x_upper ((s+tid)->s); status = gsl_root_test_interval (z_lo, z_hi, 9.9999999999999998e-13, 4.4408920985006262e-16); } while (status == GSL_CONTINUE && iter < max_iter); // LCOV_EXCL_START if (status == GSL_EINVAL) {//Shouldn't ever get here *(zmax+ii) = -9999.99; continue; } // LCOV_EXCL_STOP *(zmax+ii) = gsl_root_fsolver_root ((s+tid)->s); } } gsl_set_error_handler (NULL); for (tid=0; tid < nthreads; tid++) gsl_root_fsolver_free( (s+tid)->s); free(s); free(JzRoot); free(params); } double JRAdiabaticIntegrand(double R, void * p){ return sqrt(JRAdiabaticIntegrandSquared(R,p)); } double JRAdiabaticIntegrandSquared(double R, void * p){ struct JRAdiabaticArg * params= (struct JRAdiabaticArg *) p; return params->ER - evaluatePotentials(R,0.,params->nargs,params->actionAngleArgs) - params->Lz22 / R / R; } double JzAdiabaticIntegrand(double z, void * p){ return sqrt(JzAdiabaticIntegrandSquared(z,p)); } double JzAdiabaticIntegrandSquared(double z, void * p){ struct JzAdiabaticArg * params= (struct JzAdiabaticArg *) p; return params->Ez - evaluateVerticalPotentials(params->R,z, params->nargs, params->actionAngleArgs); } double evaluateVerticalPotentials(double R, double z, int nargs, struct potentialArg * actionAngleArgs){ return evaluatePotentials(R,z,nargs,actionAngleArgs) -evaluatePotentials(R,0.,nargs,actionAngleArgs); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/actionAngle/actionAngle_c_ext/actionAngleStaeckel.c0000644000175100001710000021156014327773303025264 0ustar00runnerdocker/* C code for Binney (2012)'s Staeckel approximation code */ #ifdef _WIN32 #include #endif #include #include #include #include #include #include #include #include #include #ifdef _OPENMP #include #endif #define CHUNKSIZE 10 //Potentials #include #include #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif //Macros to export functions in DLL on different OS #if defined(_WIN32) #define EXPORT __declspec(dllexport) #elif defined(__GNUC__) #define EXPORT __attribute__((visibility("default"))) #else // Just do nothing? #define EXPORT #endif /* Structure Declarations */ struct JRStaeckelArg{ double E; double Lz22delta; double I3U; double delta; double u0; double sinh2u0; double v0; double sin2v0; double potu0v0; int nargs; struct potentialArg * actionAngleArgs; }; struct JzStaeckelArg{ double E; double Lz22delta; double I3V; double delta; double u0; double cosh2u0; double sinh2u0; double potupi2; int nargs; struct potentialArg * actionAngleArgs; }; struct dJRStaeckelArg{ double E; double Lz22delta; double I3U; double delta; double u0; double sinh2u0; double v0; double sin2v0; double potu0v0; double umin; double umax; int nargs; struct potentialArg * actionAngleArgs; }; struct dJzStaeckelArg{ double E; double Lz22delta; double I3V; double delta; double u0; double cosh2u0; double sinh2u0; double potupi2; double vmin; int nargs; struct potentialArg * actionAngleArgs; }; struct u0EqArg{ double E; double Lz22delta; double delta; int nargs; struct potentialArg * actionAngleArgs; }; /* Function Declarations */ EXPORT void calcu0(int,double *,double *,int,int *,double *,tfuncs_type_arr, int,double*,double *,int *); EXPORT void actionAngleStaeckel_uminUmaxVmin(int,double *,double *,double *,double *, double *,double *,int,int *,double *,tfuncs_type_arr, int,double *,double *, double *,double *,int *); EXPORT void actionAngleStaeckel_actions(int,double *,double *,double *,double *, double *,double *,int,int *,double *,tfuncs_type_arr,int, double *,int,double *,double *,int *); EXPORT void actionAngleStaeckel_actionsFreqsAngles(int,double *,double *,double *, double *,double *,double *, int,int *,double *,tfuncs_type_arr, int,double *,int,double *,double *, double *,double *,double *, double *,double *,double *,int *); EXPORT void actionAngleStaeckel_actionsFreqs(int,double *,double *,double *,double *, double *,double *,int,int *,double *,tfuncs_type_arr, int,double *,int,double *,double *, double *,double *,double *,int *); void calcAnglesStaeckel(int,double *,double *,double *,double *,double *, double *,double *,double *,double *,double *,double *, double *,double *,double *,double *,double *,double *, double *,double *,double *,double *,double *,double *, double *,int,double *,double *,double *,double *, double *,double *,double *,double *,double *,double *, int,struct potentialArg *,int); void calcFreqsFromDerivsStaeckel(int,double *,double *,double *, double *,double *,double *, double *,double *,double *,double *); void calcdI3dJFromDerivsStaeckel(int,double *,double *,double *,double *, double *,double *,double *,double *); void calcJRStaeckel(int,double *,double *,double *,double *,double *,double *, int,double *,double *,double *,double *,double *,double *, int,struct potentialArg *,int); void calcJzStaeckel(int,double *,double *,double *,double *,double *,int, double *,double *,double *,double *,double *,int, struct potentialArg *,int); void calcdJRStaeckel(int,double *,double *,double *,double *,double *, double *,double *,double *,int, double *,double *,double *,double *,double *,double *,int, struct potentialArg *,int); void calcdJzStaeckel(int,double *,double *,double *,double *,double *, double *,double *,int,double *,double *,double *,double *, double *,int, struct potentialArg *,int); void calcUminUmax(int,double *,double *,double *,double *,double *,double *, double *,int,double *,double *,double *,double *,double *, double *,int,struct potentialArg *); void calcVmin(int,double *,double *,double *,double *,double *,double *,int, double *,double *,double *,double *,double *,int, struct potentialArg *); double JRStaeckelIntegrandSquared(double,void *); double JRStaeckelIntegrand(double,void *); double JzStaeckelIntegrandSquared(double,void *); double JzStaeckelIntegrand(double,void *); double dJRdEStaeckelIntegrand(double,void *); double dJRdELowStaeckelIntegrand(double,void *); double dJRdEHighStaeckelIntegrand(double,void *); double dJRdLzStaeckelIntegrand(double,void *); double dJRdLzLowStaeckelIntegrand(double,void *); double dJRdLzHighStaeckelIntegrand(double,void *); double dJRdI3StaeckelIntegrand(double,void *); double dJRdI3LowStaeckelIntegrand(double,void *); double dJRdI3HighStaeckelIntegrand(double,void *); double dJzdEStaeckelIntegrand(double,void *); double dJzdELowStaeckelIntegrand(double,void *); double dJzdEHighStaeckelIntegrand(double,void *); double dJzdLzStaeckelIntegrand(double,void *); double dJzdLzLowStaeckelIntegrand(double,void *); double dJzdLzHighStaeckelIntegrand(double,void *); double dJzdI3StaeckelIntegrand(double,void *); double dJzdI3LowStaeckelIntegrand(double,void *); double dJzdI3HighStaeckelIntegrand(double,void *); double u0Equation(double,void *); double evaluatePotentials(double,double,int, struct potentialArg *); double evaluatePotentialsUV(double,double,double,int,struct potentialArg *); /* Actual functions, inlines first */ static inline void uv_to_Rz(double u, double v, double * R, double *z, double delta){ *R= delta * sinh(u) * sin(v); *z= delta * cosh(u) * cos(v); } static inline void Rz_to_uv_vec(int ndata, double *R, double *z, double *u, double *v, int ndelta, double * delta){ int ii; double d12, d22, coshu, cosv,tdelta; int delta_stride= ndelta == 1 ? 0 : 1; for (ii=0; ii < ndata; ii++) { tdelta= *(delta+ii*delta_stride); d12= (*(z+ii)+tdelta)*(*(z+ii)+tdelta)+(*(R+ii))*(*(R+ii)); d22= (*(z+ii)-tdelta)*(*(z+ii)-tdelta)+(*(R+ii))*(*(R+ii)); coshu= 0.5/tdelta*(sqrt(d12)+sqrt(d22)); cosv= 0.5/tdelta*(sqrt(d12)-sqrt(d22)); *u++= acosh(coshu); *v++= acos(cosv); } u-= ndata; v-= ndata; } static inline void calcEL(int ndata, double *R, double *vR, double *vT, double *z, double *vz, double *E, double *Lz, int nargs, struct potentialArg * actionAngleArgs){ int ii; for (ii=0; ii < ndata; ii++){ *(E+ii)= evaluatePotentials(*(R+ii),*(z+ii), nargs,actionAngleArgs) + 0.5 * *(vR+ii) * *(vR+ii) + 0.5 * *(vT+ii) * *(vT+ii) + 0.5 * *(vz+ii) * *(vz+ii); *(Lz+ii)= *(R+ii) * *(vT+ii); } } /* MAIN FUNCTIONS */ void calcu0(int ndata, double *E, double *Lz, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, int ndelta, double * delta, double *u0, int * err){ int ii; //Set up the potentials struct potentialArg * actionAngleArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,actionAngleArgs,&pot_type,&pot_args,&pot_tfuncs); //setup the function to be minimized gsl_function u0Eq; struct u0EqArg * params= (struct u0EqArg *) malloc ( sizeof (struct u0EqArg) ); params->nargs= npot; params->actionAngleArgs= actionAngleArgs; //Setup solver int status; int iter, max_iter = 100; const gsl_min_fminimizer_type *T; gsl_min_fminimizer *s; double u_guess, u_lo, u_hi; T = gsl_min_fminimizer_brent; s = gsl_min_fminimizer_alloc (T); u0Eq.function = &u0Equation; int delta_stride= ndelta == 1 ? 0 : 1; for (ii=0; ii < ndata; ii++){ //Setup function params->delta= *(delta+ii*delta_stride); params->E= *(E+ii); params->Lz22delta= 0.5 * *(Lz+ii) * *(Lz+ii) / *(delta+ii*delta_stride) / *(delta+ii*delta_stride); u0Eq.params = params; //Find starting points for minimum u_guess= 1.; u_lo= 0.001; u_hi= 100.; gsl_set_error_handler_off(); status = gsl_min_fminimizer_set (s, &u0Eq, u_guess, u_lo, u_hi); if (status == GSL_EINVAL) { *(u0+ii)= u_hi; gsl_set_error_handler (NULL); continue; } gsl_set_error_handler (NULL); iter= 0; do { iter++; status = gsl_min_fminimizer_iterate (s); u_guess = gsl_min_fminimizer_x_minimum (s); u_lo = gsl_min_fminimizer_x_lower (s); u_hi = gsl_min_fminimizer_x_upper (s); status = gsl_min_test_interval (u_lo, u_hi, 9.9999999999999998e-13, 4.4408920985006262e-16); } while (status == GSL_CONTINUE && iter < max_iter); *(u0+ii)= gsl_min_fminimizer_x_minimum (s); } gsl_min_fminimizer_free (s); free(params); free_potentialArgs(npot,actionAngleArgs); free(actionAngleArgs); *err= status; } void actionAngleStaeckel_uminUmaxVmin(int ndata, double *R, double *vR, double *vT, double *z, double *vz, double *u0, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, int ndelta, double * delta, double *umin, double *umax, double *vmin, int * err){ // Just copied this over from actionAngleStaeckel_actions below, not elegant // but does the job... int ii; double tdelta; //Set up the potentials struct potentialArg * actionAngleArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,actionAngleArgs,&pot_type,&pot_args,&pot_tfuncs); //E,Lz double *E= (double *) malloc ( ndata * sizeof(double) ); double *Lz= (double *) malloc ( ndata * sizeof(double) ); calcEL(ndata,R,vR,vT,z,vz,E,Lz,npot,actionAngleArgs); //Calculate all necessary parameters double *ux= (double *) malloc ( ndata * sizeof(double) ); double *vx= (double *) malloc ( ndata * sizeof(double) ); Rz_to_uv_vec(ndata,R,z,ux,vx,ndelta,delta); double *coshux= (double *) malloc ( ndata * sizeof(double) ); double *sinhux= (double *) malloc ( ndata * sizeof(double) ); double *sinvx= (double *) malloc ( ndata * sizeof(double) ); double *cosvx= (double *) malloc ( ndata * sizeof(double) ); double *pux= (double *) malloc ( ndata * sizeof(double) ); double *pvx= (double *) malloc ( ndata * sizeof(double) ); double *sinh2u0= (double *) malloc ( ndata * sizeof(double) ); double *cosh2u0= (double *) malloc ( ndata * sizeof(double) ); double *v0= (double *) malloc ( ndata * sizeof(double) ); double *sin2v0= (double *) malloc ( ndata * sizeof(double) ); double *potu0v0= (double *) malloc ( ndata * sizeof(double) ); double *potupi2= (double *) malloc ( ndata * sizeof(double) ); double *I3U= (double *) malloc ( ndata * sizeof(double) ); double *I3V= (double *) malloc ( ndata * sizeof(double) ); int delta_stride= ndelta == 1 ? 0 : 1; UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) private(ii,tdelta) for (ii=0; ii < ndata; ii++){ tdelta= *(delta+ii*delta_stride); *(coshux+ii)= cosh(*(ux+ii)); *(sinhux+ii)= sinh(*(ux+ii)); *(cosvx+ii)= cos(*(vx+ii)); *(sinvx+ii)= sin(*(vx+ii)); *(pux+ii)= tdelta * (*(vR+ii) * *(coshux+ii) * *(sinvx+ii) + *(vz+ii) * *(sinhux+ii) * *(cosvx+ii)); *(pvx+ii)= tdelta * (*(vR+ii) * *(sinhux+ii) * *(cosvx+ii) - *(vz+ii) * *(coshux+ii) * *(sinvx+ii)); *(sinh2u0+ii)= sinh(*(u0+ii)) * sinh(*(u0+ii)); *(cosh2u0+ii)= cosh(*(u0+ii)) * cosh(*(u0+ii)); *(v0+ii)= 0.5 * M_PI; //*(vx+ii); *(sin2v0+ii)= sin(*(v0+ii)) * sin(*(v0+ii)); *(potu0v0+ii)= evaluatePotentialsUV(*(u0+ii),*(v0+ii),tdelta, npot,actionAngleArgs); *(I3U+ii)= *(E+ii) * *(sinhux+ii) * *(sinhux+ii) - 0.5 * *(pux+ii) * *(pux+ii) / tdelta / tdelta - 0.5 * *(Lz+ii) * *(Lz+ii) / tdelta / tdelta / *(sinhux+ii) / *(sinhux+ii) - ( *(sinhux+ii) * *(sinhux+ii) + *(sin2v0+ii)) *evaluatePotentialsUV(*(ux+ii),*(v0+ii),tdelta, npot,actionAngleArgs) + ( *(sinh2u0+ii) + *(sin2v0+ii) )* *(potu0v0+ii); *(potupi2+ii)= evaluatePotentialsUV(*(u0+ii),0.5 * M_PI,tdelta, npot,actionAngleArgs); *(I3V+ii)= - *(E+ii) * *(sinvx+ii) * *(sinvx+ii) + 0.5 * *(pvx+ii) * *(pvx+ii) / tdelta / tdelta + 0.5 * *(Lz+ii) * *(Lz+ii) / tdelta / tdelta / *(sinvx+ii) / *(sinvx+ii) - *(cosh2u0+ii) * *(potupi2+ii) + ( *(sinh2u0+ii) + *(sinvx+ii) * *(sinvx+ii)) * evaluatePotentialsUV(*(u0+ii),*(vx+ii),tdelta, npot,actionAngleArgs); } //Calculate 'peri' and 'apo'centers calcUminUmax(ndata,umin,umax,ux,pux,E,Lz,I3U,ndelta,delta,u0,sinh2u0,v0, sin2v0,potu0v0,npot,actionAngleArgs); calcVmin(ndata,vmin,vx,pvx,E,Lz,I3V,ndelta,delta,u0,cosh2u0,sinh2u0,potupi2, npot,actionAngleArgs); //Free free_potentialArgs(npot,actionAngleArgs); free(actionAngleArgs); free(E); free(Lz); free(ux); free(vx); free(coshux); free(sinhux); free(sinvx); free(cosvx); free(pux); free(pvx); free(sinh2u0); free(cosh2u0); free(v0); free(sin2v0); free(potu0v0); free(potupi2); free(I3U); free(I3V); } void actionAngleStaeckel_actions(int ndata, double *R, double *vR, double *vT, double *z, double *vz, double *u0, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, int ndelta, double * delta, int order, double *jr, double *jz, int * err){ int ii; double tdelta; //Set up the potentials struct potentialArg * actionAngleArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,actionAngleArgs,&pot_type,&pot_args,&pot_tfuncs); //E,Lz double *E= (double *) malloc ( ndata * sizeof(double) ); double *Lz= (double *) malloc ( ndata * sizeof(double) ); calcEL(ndata,R,vR,vT,z,vz,E,Lz,npot,actionAngleArgs); //Calculate all necessary parameters double *ux= (double *) malloc ( ndata * sizeof(double) ); double *vx= (double *) malloc ( ndata * sizeof(double) ); Rz_to_uv_vec(ndata,R,z,ux,vx,ndelta,delta); double *coshux= (double *) malloc ( ndata * sizeof(double) ); double *sinhux= (double *) malloc ( ndata * sizeof(double) ); double *sinvx= (double *) malloc ( ndata * sizeof(double) ); double *cosvx= (double *) malloc ( ndata * sizeof(double) ); double *pux= (double *) malloc ( ndata * sizeof(double) ); double *pvx= (double *) malloc ( ndata * sizeof(double) ); double *sinh2u0= (double *) malloc ( ndata * sizeof(double) ); double *cosh2u0= (double *) malloc ( ndata * sizeof(double) ); double *v0= (double *) malloc ( ndata * sizeof(double) ); double *sin2v0= (double *) malloc ( ndata * sizeof(double) ); double *potu0v0= (double *) malloc ( ndata * sizeof(double) ); double *potupi2= (double *) malloc ( ndata * sizeof(double) ); double *I3U= (double *) malloc ( ndata * sizeof(double) ); double *I3V= (double *) malloc ( ndata * sizeof(double) ); int delta_stride= ndelta == 1 ? 0 : 1; UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) private(ii,tdelta) for (ii=0; ii < ndata; ii++){ tdelta= *(delta+ii*delta_stride); *(coshux+ii)= cosh(*(ux+ii)); *(sinhux+ii)= sinh(*(ux+ii)); *(cosvx+ii)= cos(*(vx+ii)); *(sinvx+ii)= sin(*(vx+ii)); *(pux+ii)= tdelta * (*(vR+ii) * *(coshux+ii) * *(sinvx+ii) + *(vz+ii) * *(sinhux+ii) * *(cosvx+ii)); *(pvx+ii)= tdelta * (*(vR+ii) * *(sinhux+ii) * *(cosvx+ii) - *(vz+ii) * *(coshux+ii) * *(sinvx+ii)); *(sinh2u0+ii)= sinh(*(u0+ii)) * sinh(*(u0+ii)); *(cosh2u0+ii)= cosh(*(u0+ii)) * cosh(*(u0+ii)); *(v0+ii)= 0.5 * M_PI; //*(vx+ii); *(sin2v0+ii)= sin(*(v0+ii)) * sin(*(v0+ii)); *(potu0v0+ii)= evaluatePotentialsUV(*(u0+ii),*(v0+ii),tdelta, npot,actionAngleArgs); *(I3U+ii)= *(E+ii) * *(sinhux+ii) * *(sinhux+ii) - 0.5 * *(pux+ii) * *(pux+ii) / tdelta / tdelta - 0.5 * *(Lz+ii) * *(Lz+ii) / tdelta / tdelta / *(sinhux+ii) / *(sinhux+ii) - ( *(sinhux+ii) * *(sinhux+ii) + *(sin2v0+ii)) *evaluatePotentialsUV(*(ux+ii),*(v0+ii),tdelta, npot,actionAngleArgs) + ( *(sinh2u0+ii) + *(sin2v0+ii) )* *(potu0v0+ii); *(potupi2+ii)= evaluatePotentialsUV(*(u0+ii),0.5 * M_PI,tdelta, npot,actionAngleArgs); *(I3V+ii)= - *(E+ii) * *(sinvx+ii) * *(sinvx+ii) + 0.5 * *(pvx+ii) * *(pvx+ii) / tdelta / tdelta + 0.5 * *(Lz+ii) * *(Lz+ii) / tdelta / tdelta / *(sinvx+ii) / *(sinvx+ii) - *(cosh2u0+ii) * *(potupi2+ii) + ( *(sinh2u0+ii) + *(sinvx+ii) * *(sinvx+ii)) * evaluatePotentialsUV(*(u0+ii),*(vx+ii),tdelta, npot,actionAngleArgs); } //Calculate 'peri' and 'apo'centers double *umin= (double *) malloc ( ndata * sizeof(double) ); double *umax= (double *) malloc ( ndata * sizeof(double) ); double *vmin= (double *) malloc ( ndata * sizeof(double) ); calcUminUmax(ndata,umin,umax,ux,pux,E,Lz,I3U,ndelta,delta,u0,sinh2u0,v0, sin2v0,potu0v0,npot,actionAngleArgs); calcVmin(ndata,vmin,vx,pvx,E,Lz,I3V,ndelta,delta,u0,cosh2u0,sinh2u0,potupi2, npot,actionAngleArgs); //Calculate the actions calcJRStaeckel(ndata,jr,umin,umax,E,Lz,I3U,ndelta,delta,u0,sinh2u0,v0,sin2v0, potu0v0,npot,actionAngleArgs,order); calcJzStaeckel(ndata,jz,vmin,E,Lz,I3V,ndelta,delta,u0,cosh2u0,sinh2u0, potupi2,npot,actionAngleArgs,order); //Free free_potentialArgs(npot,actionAngleArgs); free(actionAngleArgs); free(E); free(Lz); free(ux); free(vx); free(coshux); free(sinhux); free(sinvx); free(cosvx); free(pux); free(pvx); free(sinh2u0); free(cosh2u0); free(v0); free(sin2v0); free(potu0v0); free(potupi2); free(I3U); free(I3V); free(umin); free(umax); free(vmin); } void calcJRStaeckel(int ndata, double * jr, double * umin, double * umax, double * E, double * Lz, double * I3U, int ndelta, double * delta, double * u0, double * sinh2u0, double * v0, double * sin2v0, double * potu0v0, int nargs, struct potentialArg * actionAngleArgs, int order){ int ii, tid, nthreads; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif gsl_function * JRInt= (gsl_function *) malloc ( nthreads * sizeof(gsl_function) ); struct JRStaeckelArg * params= (struct JRStaeckelArg *) malloc ( nthreads * sizeof (struct JRStaeckelArg) ); for (tid=0; tid < nthreads; tid++){ (params+tid)->nargs= nargs; (params+tid)->actionAngleArgs= actionAngleArgs; } //Setup integrator gsl_integration_glfixed_table * T= gsl_integration_glfixed_table_alloc (order); int delta_stride= ndelta == 1 ? 0 : 1; UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) \ private(tid,ii) \ shared(jr,umin,umax,JRInt,params,T,delta,E,Lz,I3U,u0,sinh2u0,v0,sin2v0,potu0v0) for (ii=0; ii < ndata; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif if ( *(umin+ii) == -9999.99 || *(umax+ii) == -9999.99 ){ *(jr+ii)= 9999.99; continue; } if ( (*(umax+ii) - *(umin+ii)) / *(umax+ii) < 0.000001 ){//circular *(jr+ii) = 0.; continue; } //Setup function (params+tid)->delta= *(delta+ii*delta_stride); (params+tid)->E= *(E+ii); (params+tid)->Lz22delta= 0.5 * *(Lz+ii) * *(Lz+ii) / *(delta+ii*delta_stride) / *(delta+ii*delta_stride); (params+tid)->I3U= *(I3U+ii); (params+tid)->u0= *(u0+ii); (params+tid)->sinh2u0= *(sinh2u0+ii); (params+tid)->v0= *(v0+ii); (params+tid)->sin2v0= *(sin2v0+ii); (params+tid)->potu0v0= *(potu0v0+ii); (JRInt+tid)->function = &JRStaeckelIntegrand; (JRInt+tid)->params = params+tid; //Integrate *(jr+ii)= gsl_integration_glfixed (JRInt+tid,*(umin+ii),*(umax+ii),T) * sqrt(2.) * *(delta+ii*delta_stride) / M_PI; } free(JRInt); free(params); gsl_integration_glfixed_table_free ( T ); } void calcJzStaeckel(int ndata, double * jz, double * vmin, double * E, double * Lz, double * I3V, int ndelta, double * delta, double * u0, double * cosh2u0, double * sinh2u0, double * potupi2, int nargs, struct potentialArg * actionAngleArgs, int order){ int ii, tid, nthreads; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif gsl_function * JzInt= (gsl_function *) malloc ( nthreads * sizeof(gsl_function) ); struct JzStaeckelArg * params= (struct JzStaeckelArg *) malloc ( nthreads * sizeof (struct JzStaeckelArg) ); for (tid=0; tid < nthreads; tid++){ (params+tid)->nargs= nargs; (params+tid)->actionAngleArgs= actionAngleArgs; } //Setup integrator gsl_integration_glfixed_table * T= gsl_integration_glfixed_table_alloc (order); int delta_stride= ndelta == 1 ? 0 : 1; UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) \ private(tid,ii) \ shared(jz,vmin,JzInt,params,T,delta,E,Lz,I3V,u0,cosh2u0,sinh2u0,potupi2) for (ii=0; ii < ndata; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif if ( *(vmin+ii) == -9999.99 ){ *(jz+ii)= 9999.99; continue; } if ( (0.5 * M_PI - *(vmin+ii)) / M_PI * 2. < 0.000001 ){//circular *(jz+ii) = 0.; continue; } //Setup function (params+tid)->delta= *(delta+ii*delta_stride); (params+tid)->E= *(E+ii); (params+tid)->Lz22delta= 0.5 * *(Lz+ii) * *(Lz+ii) / *(delta+ii*delta_stride) / *(delta+ii*delta_stride); (params+tid)->I3V= *(I3V+ii); (params+tid)->u0= *(u0+ii); (params+tid)->cosh2u0= *(cosh2u0+ii); (params+tid)->sinh2u0= *(sinh2u0+ii); (params+tid)->potupi2= *(potupi2+ii); (JzInt+tid)->function = &JzStaeckelIntegrand; (JzInt+tid)->params = params+tid; //Integrate *(jz+ii)= gsl_integration_glfixed (JzInt+tid,*(vmin+ii),M_PI/2.,T) * 2 * sqrt(2.) * *(delta+ii*delta_stride) / M_PI; } free(JzInt); free(params); gsl_integration_glfixed_table_free ( T ); } void actionAngleStaeckel_actionsFreqs(int ndata, double *R, double *vR, double *vT, double *z, double *vz, double *u0, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, int ndelta, double * delta, int order, double *jr, double *jz, double *Omegar, double *Omegaphi, double *Omegaz, int * err){ int ii; double tdelta; //Set up the potentials struct potentialArg * actionAngleArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,actionAngleArgs,&pot_type,&pot_args,&pot_tfuncs); //E,Lz double *E= (double *) malloc ( ndata * sizeof(double) ); double *Lz= (double *) malloc ( ndata * sizeof(double) ); calcEL(ndata,R,vR,vT,z,vz,E,Lz,npot,actionAngleArgs); //Calculate all necessary parameters double *ux= (double *) malloc ( ndata * sizeof(double) ); double *vx= (double *) malloc ( ndata * sizeof(double) ); Rz_to_uv_vec(ndata,R,z,ux,vx,ndelta,delta); double *coshux= (double *) malloc ( ndata * sizeof(double) ); double *sinhux= (double *) malloc ( ndata * sizeof(double) ); double *sinvx= (double *) malloc ( ndata * sizeof(double) ); double *cosvx= (double *) malloc ( ndata * sizeof(double) ); double *pux= (double *) malloc ( ndata * sizeof(double) ); double *pvx= (double *) malloc ( ndata * sizeof(double) ); double *sinh2u0= (double *) malloc ( ndata * sizeof(double) ); double *cosh2u0= (double *) malloc ( ndata * sizeof(double) ); double *v0= (double *) malloc ( ndata * sizeof(double) ); double *sin2v0= (double *) malloc ( ndata * sizeof(double) ); double *potu0v0= (double *) malloc ( ndata * sizeof(double) ); double *potupi2= (double *) malloc ( ndata * sizeof(double) ); double *I3U= (double *) malloc ( ndata * sizeof(double) ); double *I3V= (double *) malloc ( ndata * sizeof(double) ); int delta_stride= ndelta == 1 ? 0 : 1; UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) private(ii,tdelta) for (ii=0; ii < ndata; ii++){ tdelta= *(delta+ii*delta_stride); *(coshux+ii)= cosh(*(ux+ii)); *(sinhux+ii)= sinh(*(ux+ii)); *(cosvx+ii)= cos(*(vx+ii)); *(sinvx+ii)= sin(*(vx+ii)); *(pux+ii)= tdelta * (*(vR+ii) * *(coshux+ii) * *(sinvx+ii) + *(vz+ii) * *(sinhux+ii) * *(cosvx+ii)); *(pvx+ii)= tdelta * (*(vR+ii) * *(sinhux+ii) * *(cosvx+ii) - *(vz+ii) * *(coshux+ii) * *(sinvx+ii)); *(sinh2u0+ii)= sinh(*(u0+ii)) * sinh(*(u0+ii)); *(cosh2u0+ii)= cosh(*(u0+ii)) * cosh(*(u0+ii)); *(v0+ii)= 0.5 * M_PI; //*(vx+ii); *(sin2v0+ii)= sin(*(v0+ii)) * sin(*(v0+ii)); *(potu0v0+ii)= evaluatePotentialsUV(*(u0+ii),*(v0+ii),tdelta, npot,actionAngleArgs); *(I3U+ii)= *(E+ii) * *(sinhux+ii) * *(sinhux+ii) - 0.5 * *(pux+ii) * *(pux+ii) / tdelta / tdelta - 0.5 * *(Lz+ii) * *(Lz+ii) / tdelta / tdelta / *(sinhux+ii) / *(sinhux+ii) - ( *(sinhux+ii) * *(sinhux+ii) + *(sin2v0+ii)) *evaluatePotentialsUV(*(ux+ii),*(v0+ii),tdelta, npot,actionAngleArgs) + ( *(sinh2u0+ii) + *(sin2v0+ii) )* *(potu0v0+ii); *(potupi2+ii)= evaluatePotentialsUV(*(u0+ii),0.5 * M_PI,tdelta, npot,actionAngleArgs); *(I3V+ii)= - *(E+ii) * *(sinvx+ii) * *(sinvx+ii) + 0.5 * *(pvx+ii) * *(pvx+ii) / tdelta / tdelta + 0.5 * *(Lz+ii) * *(Lz+ii) / tdelta / tdelta / *(sinvx+ii) / *(sinvx+ii) - *(cosh2u0+ii) * *(potupi2+ii) + ( *(sinh2u0+ii) + *(sinvx+ii) * *(sinvx+ii)) * evaluatePotentialsUV(*(u0+ii),*(vx+ii),tdelta, npot,actionAngleArgs); } //Calculate 'peri' and 'apo'centers double *umin= (double *) malloc ( ndata * sizeof(double) ); double *umax= (double *) malloc ( ndata * sizeof(double) ); double *vmin= (double *) malloc ( ndata * sizeof(double) ); calcUminUmax(ndata,umin,umax,ux,pux,E,Lz,I3U,ndelta,delta,u0,sinh2u0,v0, sin2v0,potu0v0,npot,actionAngleArgs); calcVmin(ndata,vmin,vx,pvx,E,Lz,I3V,ndelta,delta,u0,cosh2u0,sinh2u0,potupi2, npot,actionAngleArgs); //Calculate the actions calcJRStaeckel(ndata,jr,umin,umax,E,Lz,I3U,ndelta,delta,u0,sinh2u0,v0,sin2v0, potu0v0,npot,actionAngleArgs,order); calcJzStaeckel(ndata,jz,vmin,E,Lz,I3V,ndelta,delta,u0,cosh2u0,sinh2u0, potupi2,npot,actionAngleArgs,order); //Calculate the derivatives of the actions wrt the integrals of motion double *dJRdE= (double *) malloc ( ndata * sizeof(double) ); double *dJRdLz= (double *) malloc ( ndata * sizeof(double) ); double *dJRdI3= (double *) malloc ( ndata * sizeof(double) ); double *dJzdE= (double *) malloc ( ndata * sizeof(double) ); double *dJzdLz= (double *) malloc ( ndata * sizeof(double) ); double *dJzdI3= (double *) malloc ( ndata * sizeof(double) ); double *detA= (double *) malloc ( ndata * sizeof(double) ); calcdJRStaeckel(ndata,dJRdE,dJRdLz,dJRdI3, umin,umax,E,Lz,I3U,ndelta,delta,u0,sinh2u0,v0,sin2v0, potu0v0,npot,actionAngleArgs,order); calcdJzStaeckel(ndata,dJzdE,dJzdLz,dJzdI3, vmin,E,Lz,I3V,ndelta,delta,u0,cosh2u0,sinh2u0, potupi2,npot,actionAngleArgs,order); calcFreqsFromDerivsStaeckel(ndata,Omegar,Omegaphi,Omegaz,detA, dJRdE,dJRdLz,dJRdI3, dJzdE,dJzdLz,dJzdI3); //Free free_potentialArgs(npot,actionAngleArgs); free(actionAngleArgs); free(E); free(Lz); free(ux); free(vx); free(coshux); free(sinhux); free(sinvx); free(cosvx); free(pux); free(pvx); free(sinh2u0); free(cosh2u0); free(v0); free(sin2v0); free(potu0v0); free(potupi2); free(I3U); free(I3V); free(umin); free(umax); free(vmin); free(dJRdE); free(dJRdLz); free(dJRdI3); free(dJzdE); free(detA); free(dJzdLz); free(dJzdI3); } void actionAngleStaeckel_actionsFreqsAngles(int ndata, double *R, double *vR, double *vT, double *z, double *vz, double *u0, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, int ndelta, double * delta, int order, double *jr, double *jz, double *Omegar, double *Omegaphi, double *Omegaz, double *Angler, double *Anglephi, double *Anglez, int * err){ int ii; double tdelta; //Set up the potentials struct potentialArg * actionAngleArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,actionAngleArgs,&pot_type,&pot_args,&pot_tfuncs); //E,Lz double *E= (double *) malloc ( ndata * sizeof(double) ); double *Lz= (double *) malloc ( ndata * sizeof(double) ); calcEL(ndata,R,vR,vT,z,vz,E,Lz,npot,actionAngleArgs); //Calculate all necessary parameters double *ux= (double *) malloc ( ndata * sizeof(double) ); double *vx= (double *) malloc ( ndata * sizeof(double) ); Rz_to_uv_vec(ndata,R,z,ux,vx,ndelta,delta); double *coshux= (double *) malloc ( ndata * sizeof(double) ); double *sinhux= (double *) malloc ( ndata * sizeof(double) ); double *sinvx= (double *) malloc ( ndata * sizeof(double) ); double *cosvx= (double *) malloc ( ndata * sizeof(double) ); double *pux= (double *) malloc ( ndata * sizeof(double) ); double *pvx= (double *) malloc ( ndata * sizeof(double) ); double *sinh2u0= (double *) malloc ( ndata * sizeof(double) ); double *cosh2u0= (double *) malloc ( ndata * sizeof(double) ); double *v0= (double *) malloc ( ndata * sizeof(double) ); double *sin2v0= (double *) malloc ( ndata * sizeof(double) ); double *potu0v0= (double *) malloc ( ndata * sizeof(double) ); double *potupi2= (double *) malloc ( ndata * sizeof(double) ); double *I3U= (double *) malloc ( ndata * sizeof(double) ); double *I3V= (double *) malloc ( ndata * sizeof(double) ); int delta_stride= ndelta == 1 ? 0 : 1; UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) private(ii,tdelta) for (ii=0; ii < ndata; ii++){ tdelta= *(delta+ii*delta_stride); *(coshux+ii)= cosh(*(ux+ii)); *(sinhux+ii)= sinh(*(ux+ii)); *(cosvx+ii)= cos(*(vx+ii)); *(sinvx+ii)= sin(*(vx+ii)); *(pux+ii)= tdelta * (*(vR+ii) * *(coshux+ii) * *(sinvx+ii) + *(vz+ii) * *(sinhux+ii) * *(cosvx+ii)); *(pvx+ii)= tdelta * (*(vR+ii) * *(sinhux+ii) * *(cosvx+ii) - *(vz+ii) * *(coshux+ii) * *(sinvx+ii)); *(sinh2u0+ii)= sinh(*(u0+ii)) * sinh(*(u0+ii)); *(cosh2u0+ii)= cosh(*(u0+ii)) * cosh(*(u0+ii)); *(v0+ii)= 0.5 * M_PI; //*(vx+ii); *(sin2v0+ii)= sin(*(v0+ii)) * sin(*(v0+ii)); *(potu0v0+ii)= evaluatePotentialsUV(*(u0+ii),*(v0+ii),tdelta, npot,actionAngleArgs); *(I3U+ii)= *(E+ii) * *(sinhux+ii) * *(sinhux+ii) - 0.5 * *(pux+ii) * *(pux+ii) / tdelta / tdelta - 0.5 * *(Lz+ii) * *(Lz+ii) / tdelta / tdelta / *(sinhux+ii) / *(sinhux+ii) - ( *(sinhux+ii) * *(sinhux+ii) + *(sin2v0+ii)) *evaluatePotentialsUV(*(ux+ii),*(v0+ii),tdelta, npot,actionAngleArgs) + ( *(sinh2u0+ii) + *(sin2v0+ii) )* *(potu0v0+ii); *(potupi2+ii)= evaluatePotentialsUV(*(u0+ii),0.5 * M_PI,tdelta, npot,actionAngleArgs); *(I3V+ii)= - *(E+ii) * *(sinvx+ii) * *(sinvx+ii) + 0.5 * *(pvx+ii) * *(pvx+ii) / tdelta / tdelta + 0.5 * *(Lz+ii) * *(Lz+ii) / tdelta / tdelta / *(sinvx+ii) / *(sinvx+ii) - *(cosh2u0+ii) * *(potupi2+ii) + ( *(sinh2u0+ii) + *(sinvx+ii) * *(sinvx+ii)) * evaluatePotentialsUV(*(u0+ii),*(vx+ii),tdelta, npot,actionAngleArgs); } //Calculate 'peri' and 'apo'centers double *umin= (double *) malloc ( ndata * sizeof(double) ); double *umax= (double *) malloc ( ndata * sizeof(double) ); double *vmin= (double *) malloc ( ndata * sizeof(double) ); calcUminUmax(ndata,umin,umax,ux,pux,E,Lz,I3U,ndelta,delta,u0,sinh2u0,v0, sin2v0,potu0v0,npot,actionAngleArgs); calcVmin(ndata,vmin,vx,pvx,E,Lz,I3V,ndelta,delta,u0,cosh2u0,sinh2u0,potupi2, npot,actionAngleArgs); //Calculate the actions calcJRStaeckel(ndata,jr,umin,umax,E,Lz,I3U,ndelta,delta,u0,sinh2u0,v0,sin2v0, potu0v0,npot,actionAngleArgs,order); calcJzStaeckel(ndata,jz,vmin,E,Lz,I3V,ndelta,delta,u0,cosh2u0,sinh2u0, potupi2,npot,actionAngleArgs,order); //Calculate the derivatives of the actions wrt the integrals of motion double *dJRdE= (double *) malloc ( ndata * sizeof(double) ); double *dJRdLz= (double *) malloc ( ndata * sizeof(double) ); double *dJRdI3= (double *) malloc ( ndata * sizeof(double) ); double *dJzdE= (double *) malloc ( ndata * sizeof(double) ); double *dJzdLz= (double *) malloc ( ndata * sizeof(double) ); double *dJzdI3= (double *) malloc ( ndata * sizeof(double) ); double *detA= (double *) malloc ( ndata * sizeof(double) ); calcdJRStaeckel(ndata,dJRdE,dJRdLz,dJRdI3, umin,umax,E,Lz,I3U,ndelta,delta,u0,sinh2u0,v0,sin2v0, potu0v0,npot,actionAngleArgs,order); calcdJzStaeckel(ndata,dJzdE,dJzdLz,dJzdI3, vmin,E,Lz,I3V,ndelta,delta,u0,cosh2u0,sinh2u0, potupi2,npot,actionAngleArgs,order); calcFreqsFromDerivsStaeckel(ndata,Omegar,Omegaphi,Omegaz,detA, dJRdE,dJRdLz,dJRdI3, dJzdE,dJzdLz,dJzdI3); double *dI3dJR= (double *) malloc ( ndata * sizeof(double) ); double *dI3dJz= (double *) malloc ( ndata * sizeof(double) ); double *dI3dLz= (double *) malloc ( ndata * sizeof(double) ); calcdI3dJFromDerivsStaeckel(ndata,dI3dJR,dI3dJz,dI3dLz,detA, dJRdE,dJzdE,dJRdLz,dJzdLz); calcAnglesStaeckel(ndata,Angler,Anglephi,Anglez, Omegar,Omegaphi,Omegaz,dI3dJR,dI3dJz,dI3dLz, dJRdE,dJRdLz,dJRdI3, dJzdE,dJzdLz,dJzdI3, ux,vx,pux,pvx, umin,umax,E,Lz,I3U,ndelta,delta,u0,sinh2u0,v0,sin2v0, potu0v0, vmin,I3V,cosh2u0,potupi2, npot,actionAngleArgs,order); //Free free_potentialArgs(npot,actionAngleArgs); free(actionAngleArgs); free(E); free(Lz); free(ux); free(vx); free(coshux); free(sinhux); free(sinvx); free(cosvx); free(pux); free(pvx); free(sinh2u0); free(cosh2u0); free(v0); free(sin2v0); free(potu0v0); free(potupi2); free(I3U); free(I3V); free(umin); free(umax); free(vmin); free(dJRdE); free(dJRdLz); free(dJRdI3); free(dJzdE); free(dJzdLz); free(dJzdI3); free(detA); free(dI3dJR); free(dI3dJz); free(dI3dLz); } void calcFreqsFromDerivsStaeckel(int ndata, double * Omegar, double * Omegaphi, double * Omegaz, double * detA, double * djrdE, double * djrdLz, double * djrdI3, double * djzdE, double * djzdLz, double * djzdI3){ int ii; UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) \ private(ii) \ shared(Omegar,Omegaphi,Omegaz,djrdE,djrdLz,djrdI3,djzdE,djzdLz,djzdI3,detA) for (ii=0; ii < ndata; ii++){ if ( *(djrdE+ii) == 9999.99 || *(djzdE+ii) == 9999.99 ) { *(Omegar+ii)= 9999.99; *(Omegaz+ii)= 9999.99; *(Omegaphi+ii)= 9999.99; } else { //First calculate the determinant of the relevant matrix *(detA+ii)= *(djrdE+ii) * *(djzdI3+ii) - *(djzdE+ii) * *(djrdI3+ii); //Then calculate the frequencies *(Omegar+ii)= *(djzdI3+ii) / *(detA+ii); *(Omegaz+ii)= - *(djrdI3+ii) / *(detA+ii); *(Omegaphi+ii)= ( *(djrdI3+ii) * *(djzdLz+ii) - *(djzdI3+ii) * *(djrdLz+ii)) / *(detA+ii); } } } void calcdI3dJFromDerivsStaeckel(int ndata, double * dI3dJR, double * dI3dJz, double * dI3dLz, double * detA, double * djrdE, double * djzdE, double * djrdLz, double * djzdLz){ int ii; UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) \ private(ii) \ shared(djrdE,djzdE,djrdLz,djzdLz,dI3dJR,dI3dJz,dI3dLz,detA) for (ii=0; ii < ndata; ii++){ *(dI3dJR+ii)= - *(djzdE+ii) / *(detA+ii); *(dI3dJz+ii)= *(djrdE+ii) / *(detA+ii); *(dI3dLz+ii)= -( *(djrdE+ii) * *(djzdLz+ii) - *(djzdE+ii) * *(djrdLz+ii) ) / *(detA+ii); } } void calcdJRStaeckel(int ndata, double * djrdE, double * djrdLz, double * djrdI3, double * umin, double * umax, double * E, double * Lz, double * I3U, int ndelta, double * delta, double * u0, double * sinh2u0, double * v0, double * sin2v0, double * potu0v0, int nargs, struct potentialArg * actionAngleArgs, int order){ int ii, tid, nthreads; double mid; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif gsl_function * dJRInt= (gsl_function *) malloc ( nthreads * sizeof(gsl_function) ); struct dJRStaeckelArg * params= (struct dJRStaeckelArg *) malloc ( nthreads * sizeof (struct dJRStaeckelArg) ); for (tid=0; tid < nthreads; tid++){ (params+tid)->nargs= nargs; (params+tid)->actionAngleArgs= actionAngleArgs; } //Setup integrator gsl_integration_glfixed_table * T= gsl_integration_glfixed_table_alloc (order); int delta_stride= ndelta == 1 ? 0 : 1; UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) \ private(tid,ii,mid) \ shared(djrdE,djrdLz,djrdI3,umin,umax,dJRInt,params,T,delta,E,Lz,I3U,u0,sinh2u0,v0,sin2v0,potu0v0) for (ii=0; ii < ndata; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif if ( *(umin+ii) == -9999.99 || *(umax+ii) == -9999.99 ){ *(djrdE+ii)= 9999.99; *(djrdLz+ii)= 9999.99; *(djrdI3+ii)= 9999.99; continue; } if ( (*(umax+ii) - *(umin+ii)) / *(umax+ii) < 0.000001 ){//circular *(djrdE+ii) = 0.; *(djrdLz+ii) = 0.; *(djrdI3+ii) = 0.; continue; } //Setup function (params+tid)->delta= *(delta+ii*delta_stride); (params+tid)->E= *(E+ii); (params+tid)->Lz22delta= 0.5 * *(Lz+ii) * *(Lz+ii) / *(delta+ii*delta_stride) / *(delta+ii*delta_stride); (params+tid)->I3U= *(I3U+ii); (params+tid)->u0= *(u0+ii); (params+tid)->sinh2u0= *(sinh2u0+ii); (params+tid)->v0= *(v0+ii); (params+tid)->sin2v0= *(sin2v0+ii); (params+tid)->potu0v0= *(potu0v0+ii); (params+tid)->umin= *(umin+ii); (params+tid)->umax= *(umax+ii); (dJRInt+tid)->function = &dJRdELowStaeckelIntegrand; (dJRInt+tid)->params = params+tid; mid= sqrt( 0.5 * ( *(umax+ii) - *(umin+ii) ) ); //Integrate to get djrdE *(djrdE+ii)= gsl_integration_glfixed (dJRInt+tid,0.,mid,T); (dJRInt+tid)->function = &dJRdEHighStaeckelIntegrand; *(djrdE+ii)+= gsl_integration_glfixed (dJRInt+tid,0.,mid,T); *(djrdE+ii)*= *(delta+ii*delta_stride) / M_PI / sqrt(2.); //then calculate djrdLz (dJRInt+tid)->function = &dJRdLzLowStaeckelIntegrand; *(djrdLz+ii)= gsl_integration_glfixed (dJRInt+tid,0.,mid,T); (dJRInt+tid)->function = &dJRdLzHighStaeckelIntegrand; *(djrdLz+ii)+= gsl_integration_glfixed (dJRInt+tid,0.,mid,T); *(djrdLz+ii)*= - *(Lz+ii) / M_PI / sqrt(2.) / *(delta+ii*delta_stride); //then calculate djrdI3 (dJRInt+tid)->function = &dJRdI3LowStaeckelIntegrand; *(djrdI3+ii)= gsl_integration_glfixed (dJRInt+tid,0.,mid,T); (dJRInt+tid)->function = &dJRdI3HighStaeckelIntegrand; *(djrdI3+ii)+= gsl_integration_glfixed (dJRInt+tid,0.,mid,T); *(djrdI3+ii)*= - *(delta+ii*delta_stride) / M_PI / sqrt(2.); } free(dJRInt); free(params); gsl_integration_glfixed_table_free ( T ); } void calcdJzStaeckel(int ndata, double * djzdE, double * djzdLz, double * djzdI3, double * vmin, double * E, double * Lz, double * I3V, int ndelta, double * delta, double * u0, double * cosh2u0, double * sinh2u0, double * potupi2, int nargs, struct potentialArg * actionAngleArgs, int order){ int ii, tid, nthreads; double mid; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif gsl_function * dJzInt= (gsl_function *) malloc ( nthreads * sizeof(gsl_function) ); struct dJzStaeckelArg * params= (struct dJzStaeckelArg *) malloc ( nthreads * sizeof (struct dJzStaeckelArg) ); for (tid=0; tid < nthreads; tid++){ (params+tid)->nargs= nargs; (params+tid)->actionAngleArgs= actionAngleArgs; } //Setup integrator gsl_integration_glfixed_table * T= gsl_integration_glfixed_table_alloc (order); int delta_stride= ndelta == 1 ? 0 : 1; UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) \ private(tid,ii,mid) \ shared(djzdE,djzdLz,djzdI3,vmin,dJzInt,params,T,delta,E,Lz,I3V,u0,cosh2u0,sinh2u0,potupi2) for (ii=0; ii < ndata; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif if ( *(vmin+ii) == -9999.99 ){ *(djzdE+ii)= 9999.99; *(djzdLz+ii)= 9999.99; *(djzdI3+ii)= 9999.99; continue; } if ( (0.5 * M_PI - *(vmin+ii)) / M_PI * 2. < 0.000001 ){//circular *(djzdE+ii) = 0.; *(djzdLz+ii) = 0.; *(djzdI3+ii) = 0.; continue; } //Setup function (params+tid)->delta= *(delta+ii*delta_stride); (params+tid)->E= *(E+ii); (params+tid)->Lz22delta= 0.5 * *(Lz+ii) * *(Lz+ii) / *(delta+ii*delta_stride) / *(delta+ii*delta_stride); (params+tid)->I3V= *(I3V+ii); (params+tid)->u0= *(u0+ii); (params+tid)->cosh2u0= *(cosh2u0+ii); (params+tid)->sinh2u0= *(sinh2u0+ii); (params+tid)->potupi2= *(potupi2+ii); (params+tid)->vmin= *(vmin+ii); //First calculate dJzdE (dJzInt+tid)->function = &dJzdELowStaeckelIntegrand; (dJzInt+tid)->params = params+tid; mid= sqrt( 0.5 * (M_PI/2. - *(vmin+ii) ) ); //BOVY: pv does not vanish at pi/2, so no need to break up the integral //Integrate *(djzdE+ii)= gsl_integration_glfixed (dJzInt+tid,0.,mid,T); (dJzInt+tid)->function = &dJzdEHighStaeckelIntegrand; *(djzdE+ii)+= gsl_integration_glfixed (dJzInt+tid,0.,mid,T); *(djzdE+ii)*= sqrt(2.) * *(delta+ii*delta_stride) / M_PI; //Then calculate dJzdLz (dJzInt+tid)->function = &dJzdLzLowStaeckelIntegrand; //Integrate *(djzdLz+ii)= gsl_integration_glfixed (dJzInt+tid,0.,mid,T); (dJzInt+tid)->function = &dJzdLzHighStaeckelIntegrand; *(djzdLz+ii)+= gsl_integration_glfixed (dJzInt+tid,0.,mid,T); *(djzdLz+ii)*= - *(Lz+ii) * sqrt(2.) / M_PI / *(delta+ii*delta_stride); //Then calculate dJzdI3 (dJzInt+tid)->function = &dJzdI3LowStaeckelIntegrand; //Integrate *(djzdI3+ii)= gsl_integration_glfixed (dJzInt+tid,0.,mid,T); (dJzInt+tid)->function = &dJzdI3HighStaeckelIntegrand; *(djzdI3+ii)+= gsl_integration_glfixed (dJzInt+tid,0.,mid,T); *(djzdI3+ii)*= sqrt(2.) * *(delta+ii*delta_stride) / M_PI; } free(dJzInt); free(params); gsl_integration_glfixed_table_free ( T ); } void calcAnglesStaeckel(int ndata, double * Angler, double * Anglephi, double * Anglez, double * Omegar, double * Omegaphi, double * Omegaz, double * dI3dJR, double * dI3dJz, double * dI3dLz, double * dJRdE, double * dJRdLz, double * dJRdI3, double * dJzdE, double * dJzdLz, double * dJzdI3, double * ux, double * vx, double * pux, double * pvx, double * umin, double * umax, double * E, double * Lz, double * I3U, int ndelta, double * delta, double * u0, double * sinh2u0, double * v0, double * sin2v0, double * potu0v0, double * vmin, double * I3V, double * cosh2u0, double * potupi2, int nargs, struct potentialArg * actionAngleArgs, int order){ int ii, tid, nthreads; double Or1, Or2, I3r1, I3r2,phitmp; double mid, midpoint; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif gsl_function * AngleuInt= (gsl_function *) malloc ( nthreads * sizeof(gsl_function) ); gsl_function * AnglevInt= (gsl_function *) malloc ( nthreads * sizeof(gsl_function) ); struct dJRStaeckelArg * paramsu= (struct dJRStaeckelArg *) malloc ( nthreads * sizeof (struct dJRStaeckelArg) ); struct dJzStaeckelArg * paramsv= (struct dJzStaeckelArg *) malloc ( nthreads * sizeof (struct dJzStaeckelArg) ); for (tid=0; tid < nthreads; tid++){ (paramsu+tid)->nargs= nargs; (paramsu+tid)->actionAngleArgs= actionAngleArgs; (paramsv+tid)->nargs= nargs; (paramsv+tid)->actionAngleArgs= actionAngleArgs; } //Setup integrator gsl_integration_glfixed_table * T= gsl_integration_glfixed_table_alloc (order); int delta_stride= ndelta == 1 ? 0 : 1; UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) \ private(tid,ii,mid,midpoint,Or1,Or2,I3r1,I3r2,phitmp) \ shared(Angler,Anglephi,Anglez,Omegar,Omegaz,dI3dJR,dI3dJz,umin,umax,AngleuInt,AnglevInt,paramsu,paramsv,T,delta,E,Lz,I3U,u0,sinh2u0,v0,sin2v0,potu0v0,vmin,I3V,cosh2u0,potupi2) for (ii=0; ii < ndata; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif if ( *(umin+ii) == -9999.99 || *(umax+ii) == -9999.99 ){ *(Angler+ii)= 9999.99; *(Anglephi+ii)= 9999.99; *(Anglez+ii)= 9999.99; continue; } if ( (*(umax+ii) - *(umin+ii)) / *(umax+ii) < 0.000001 ){//circular *(Angler+ii) = 0.; *(Anglephi+ii) = 0.; *(Anglez+ii) = 0.; continue; } //Setup u function (paramsu+tid)->delta= *(delta+ii*delta_stride); (paramsu+tid)->E= *(E+ii); (paramsu+tid)->Lz22delta= 0.5 * *(Lz+ii) * *(Lz+ii) / *(delta+ii*delta_stride) / *(delta+ii*delta_stride); (paramsu+tid)->I3U= *(I3U+ii); (paramsu+tid)->u0= *(u0+ii); (paramsu+tid)->sinh2u0= *(sinh2u0+ii); (paramsu+tid)->v0= *(v0+ii); (paramsu+tid)->sin2v0= *(sin2v0+ii); (paramsu+tid)->potu0v0= *(potu0v0+ii); (paramsu+tid)->umin= *(umin+ii); (paramsu+tid)->umax= *(umax+ii); (AngleuInt+tid)->params = paramsu+tid; midpoint= *(umin+ii)+ 0.5 * ( *(umax+ii) - *(umin+ii) ); if ( *(pux+ii) > 0. ) { if ( *(ux+ii) > midpoint ) { mid= sqrt( ( *(umax+ii) - *(ux+ii) ) ); (AngleuInt+tid)->function = &dJRdEHighStaeckelIntegrand; Or1= gsl_integration_glfixed (AngleuInt+tid,0.,mid,T); (AngleuInt+tid)->function = &dJRdI3HighStaeckelIntegrand; I3r1= -gsl_integration_glfixed (AngleuInt+tid,0.,mid,T); (AngleuInt+tid)->function = &dJRdLzHighStaeckelIntegrand; *(Anglephi+ii)= M_PI * *(dJRdLz+ii) + *(Lz+ii) * gsl_integration_glfixed (AngleuInt+tid,0.,mid,T) / *(delta+ii*delta_stride) / sqrt(2.); Or1*= *(delta+ii*delta_stride) / sqrt(2.); I3r1*= *(delta+ii*delta_stride) / sqrt(2.); Or1= M_PI * *(dJRdE+ii) - Or1; I3r1= M_PI * *(dJRdI3+ii) - I3r1; } else { mid= sqrt( ( *(ux+ii) - *(umin+ii) ) ); (AngleuInt+tid)->function = &dJRdELowStaeckelIntegrand; Or1= gsl_integration_glfixed (AngleuInt+tid,0.,mid,T); (AngleuInt+tid)->function = &dJRdI3LowStaeckelIntegrand; I3r1= -gsl_integration_glfixed (AngleuInt+tid,0.,mid,T); (AngleuInt+tid)->function = &dJRdLzLowStaeckelIntegrand; *(Anglephi+ii)= - *(Lz+ii) * gsl_integration_glfixed (AngleuInt+tid,0.,mid,T) / *(delta+ii*delta_stride) / sqrt(2.); Or1*= *(delta+ii*delta_stride) / sqrt(2.); I3r1*= *(delta+ii*delta_stride) / sqrt(2.); } } else { if ( *(ux+ii) > midpoint ) { mid= sqrt( ( *(umax+ii) - *(ux+ii) ) ); (AngleuInt+tid)->function = &dJRdEHighStaeckelIntegrand; Or1= gsl_integration_glfixed (AngleuInt+tid,0.,mid,T); Or1*= *(delta+ii*delta_stride) / sqrt(2.); Or1= M_PI * *(dJRdE+ii) + Or1; (AngleuInt+tid)->function = &dJRdI3HighStaeckelIntegrand; I3r1= -gsl_integration_glfixed (AngleuInt+tid,0.,mid,T); I3r1*= *(delta+ii*delta_stride) / sqrt(2.); I3r1= M_PI * *(dJRdI3+ii) + I3r1; (AngleuInt+tid)->function = &dJRdLzHighStaeckelIntegrand; *(Anglephi+ii)= M_PI * *(dJRdLz+ii) - *(Lz+ii) * gsl_integration_glfixed (AngleuInt+tid,0.,mid,T) / *(delta+ii*delta_stride) / sqrt(2.); } else { mid= sqrt( ( *(ux+ii) - *(umin+ii) ) ); (AngleuInt+tid)->function = &dJRdELowStaeckelIntegrand; Or1= gsl_integration_glfixed (AngleuInt+tid,0.,mid,T); Or1*= *(delta+ii*delta_stride) / sqrt(2.); Or1= 2. * M_PI * *(dJRdE+ii) - Or1; (AngleuInt+tid)->function = &dJRdI3LowStaeckelIntegrand; I3r1= -gsl_integration_glfixed (AngleuInt+tid,0.,mid,T); I3r1*= *(delta+ii*delta_stride) / sqrt(2.); I3r1= 2. * M_PI * *(dJRdI3+ii) - I3r1; (AngleuInt+tid)->function = &dJRdLzLowStaeckelIntegrand; *(Anglephi+ii)= 2. * M_PI * *(dJRdLz+ii) + *(Lz+ii) * gsl_integration_glfixed (AngleuInt+tid,0.,mid,T) / *(delta+ii*delta_stride) / sqrt(2.); } } //Setup v function (paramsv+tid)->delta= *(delta+ii*delta_stride); (paramsv+tid)->E= *(E+ii); (paramsv+tid)->Lz22delta= 0.5 * *(Lz+ii) * *(Lz+ii) / *(delta+ii*delta_stride) / *(delta+ii*delta_stride); (paramsv+tid)->I3V= *(I3V+ii); (paramsv+tid)->u0= *(u0+ii); (paramsv+tid)->cosh2u0= *(cosh2u0+ii); (paramsv+tid)->sinh2u0= *(sinh2u0+ii); (paramsv+tid)->potupi2= *(potupi2+ii); (paramsv+tid)->vmin= *(vmin+ii); (AnglevInt+tid)->params = paramsv+tid; midpoint= *(vmin+ii)+ 0.5 * ( 0.5 * M_PI - *(vmin+ii) ); if ( *(pvx+ii) > 0. ) { if ( *(vx+ii) < midpoint || *(vx+ii) > (M_PI - midpoint) ) { mid = ( *(vx+ii) > 0.5 * M_PI ) ? sqrt( (M_PI - *(vx+ii) - *(vmin+ii))): sqrt( *(vx+ii) - *(vmin+ii)); (AnglevInt+tid)->function = &dJzdELowStaeckelIntegrand; Or2= gsl_integration_glfixed (AnglevInt+tid,0.,mid,T); Or2*= *(delta+ii*delta_stride) / sqrt(2.); (AnglevInt+tid)->function = &dJzdI3LowStaeckelIntegrand; I3r2= gsl_integration_glfixed (AnglevInt+tid,0.,mid,T); I3r2*= *(delta+ii*delta_stride) / sqrt(2.); (AnglevInt+tid)->function = &dJzdLzLowStaeckelIntegrand; phitmp= gsl_integration_glfixed (AnglevInt+tid,0.,mid,T); phitmp*= - *(Lz+ii) / *(delta+ii*delta_stride) / sqrt(2.); if ( *(vx+ii) > 0.5 * M_PI ) { Or2= M_PI * *(dJzdE+ii) - Or2; I3r2= M_PI * *(dJzdI3+ii) - I3r2; phitmp= M_PI * *(dJzdLz+ii) - phitmp; } } else { mid= sqrt( fabs ( 0.5 * M_PI - *(vx+ii) ) ); (AnglevInt+tid)->function = &dJzdEHighStaeckelIntegrand; Or2= gsl_integration_glfixed (AnglevInt+tid,0.,mid,T); Or2*= *(delta+ii*delta_stride) / sqrt(2.); (AnglevInt+tid)->function = &dJzdI3HighStaeckelIntegrand; I3r2= gsl_integration_glfixed (AnglevInt+tid,0.,mid,T); I3r2*= *(delta+ii*delta_stride) / sqrt(2.); (AnglevInt+tid)->function = &dJzdLzHighStaeckelIntegrand; phitmp= gsl_integration_glfixed (AnglevInt+tid,0.,mid,T); phitmp*= - *(Lz+ii) / *(delta+ii*delta_stride) / sqrt(2.); if ( *(vx+ii) > 0.5 * M_PI ) { Or2= 0.5 * M_PI * *(dJzdE+ii) + Or2; I3r2= 0.5 * M_PI * *(dJzdI3+ii) + I3r2; phitmp= 0.5 * M_PI * *(dJzdLz+ii) + phitmp; } else { Or2= 0.5 * M_PI * *(dJzdE+ii) - Or2; I3r2= 0.5 * M_PI * *(dJzdI3+ii) - I3r2; phitmp= 0.5 * M_PI * *(dJzdLz+ii) - phitmp; } } } else { if ( *(vx+ii) < midpoint || *(vx+ii) > (M_PI - midpoint)) { mid = ( *(vx+ii) > 0.5 * M_PI ) ? sqrt( (M_PI - *(vx+ii) - *(vmin+ii))): sqrt( *(vx+ii) - *(vmin+ii)); (AnglevInt+tid)->function = &dJzdELowStaeckelIntegrand; Or2= gsl_integration_glfixed (AnglevInt+tid,0.,mid,T); Or2*= *(delta+ii*delta_stride) / sqrt(2.); (AnglevInt+tid)->function = &dJzdI3LowStaeckelIntegrand; I3r2= gsl_integration_glfixed (AnglevInt+tid,0.,mid,T); I3r2*= *(delta+ii*delta_stride) / sqrt(2.); (AnglevInt+tid)->function = &dJzdLzLowStaeckelIntegrand; phitmp= gsl_integration_glfixed (AnglevInt+tid,0.,mid,T); phitmp*= - *(Lz+ii) / *(delta+ii*delta_stride) / sqrt(2.); if ( *(vx+ii) < 0.5 * M_PI ) { Or2= 2. * M_PI * *(dJzdE+ii) - Or2; I3r2= 2. * M_PI * *(dJzdI3+ii) - I3r2; phitmp= 2. * M_PI * *(dJzdLz+ii) - phitmp; } else { Or2= M_PI * *(dJzdE+ii) + Or2; I3r2= M_PI * *(dJzdI3+ii) + I3r2; phitmp= M_PI * *(dJzdLz+ii) + phitmp; } } else { mid= sqrt( fabs ( 0.5 * M_PI - *(vx+ii) ) ); (AnglevInt+tid)->function = &dJzdEHighStaeckelIntegrand; Or2= gsl_integration_glfixed (AnglevInt+tid,0.,mid,T); Or2*= *(delta+ii*delta_stride) / sqrt(2.); (AnglevInt+tid)->function = &dJzdI3HighStaeckelIntegrand; I3r2= gsl_integration_glfixed (AnglevInt+tid,0.,mid,T); I3r2*= *(delta+ii*delta_stride) / sqrt(2.); (AnglevInt+tid)->function = &dJzdLzHighStaeckelIntegrand; phitmp= gsl_integration_glfixed (AnglevInt+tid,0.,mid,T); phitmp*= - *(Lz+ii) / *(delta+ii*delta_stride) / sqrt(2.); if ( *(vx+ii) < 0.5 * M_PI ) { Or2= 1.5 * M_PI * *(dJzdE+ii) + Or2; I3r2= 1.5 * M_PI * *(dJzdI3+ii) + I3r2; phitmp= 1.5 * M_PI * *(dJzdLz+ii) + phitmp; } else { Or2= 1.5 * M_PI * *(dJzdE+ii) - Or2; I3r2= 1.5 * M_PI * *(dJzdI3+ii) - I3r2; phitmp= 1.5 * M_PI * *(dJzdLz+ii) - phitmp; } } } *(Angler+ii)= *(Omegar+ii) * ( Or1 + Or2 ) + *(dI3dJR+ii) * ( I3r1 + I3r2 ); // In Binney (2012) Anglez starts at zmax/vmin and v_z < 0 / v_v > 0; // Put this on the same system as Isochrone and Spherical angles +pi/2 *(Anglez+ii)= *(Omegaz+ii) * ( Or1 + Or2 ) + *(dI3dJz+ii) * ( I3r1 + I3r2 ) + 0.5 * M_PI; *(Anglephi+ii)+= phitmp; *(Anglephi+ii)+= *(Omegaphi+ii) * ( Or1 + Or2 ) + *(dI3dLz+ii) * ( I3r1 + I3r2 ); *(Angler+ii)= fmod(*(Angler+ii),2. * M_PI); *(Anglez+ii)= fmod(*(Anglez+ii),2. * M_PI); while ( *(Angler+ii) < 0. ) *(Angler+ii)+= 2. * M_PI; while ( *(Anglez+ii) < 0. ) *(Anglez+ii)+= 2. * M_PI; while ( *(Angler+ii) > 2. * M_PI ) *(Angler+ii)-= 2. * M_PI; while ( *(Anglez+ii) > 2. * M_PI ) *(Anglez+ii)-= 2. * M_PI; } free(AngleuInt); free(AnglevInt); free(paramsu); free(paramsv); gsl_integration_glfixed_table_free ( T ); } void calcUminUmax(int ndata, double * umin, double * umax, double * ux, double * pux, double * E, double * Lz, double * I3U, int ndelta, double * delta, double * u0, double * sinh2u0, double * v0, double * sin2v0, double * potu0v0, int nargs, struct potentialArg * actionAngleArgs){ int ii, tid, nthreads; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif double peps, meps; gsl_function * JRRoot= (gsl_function *) malloc ( nthreads * sizeof(gsl_function) ); struct JRStaeckelArg * params= (struct JRStaeckelArg *) malloc ( nthreads * sizeof (struct JRStaeckelArg) ); //Setup solver int status; int iter, max_iter = 100; const gsl_root_fsolver_type *T; struct pragmasolver *s= (struct pragmasolver *) malloc ( nthreads * sizeof (struct pragmasolver) );; double u_lo, u_hi; T = gsl_root_fsolver_brent; for (tid=0; tid < nthreads; tid++){ (params+tid)->nargs= nargs; (params+tid)->actionAngleArgs= actionAngleArgs; (s+tid)->s= gsl_root_fsolver_alloc (T); } int delta_stride= ndelta == 1 ? 0 : 1; UNUSED int chunk= CHUNKSIZE; gsl_set_error_handler_off(); #pragma omp parallel for schedule(static,chunk) \ private(tid,ii,iter,status,u_lo,u_hi,meps,peps) \ shared(umin,umax,JRRoot,params,s,ux,delta,E,Lz,I3U,u0,sinh2u0,v0,sin2v0,potu0v0,max_iter) for (ii=0; ii < ndata; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif //Setup function (params+tid)->delta= *(delta+ii*delta_stride); (params+tid)->E= *(E+ii); (params+tid)->Lz22delta= 0.5 * *(Lz+ii) * *(Lz+ii) / *(delta+ii*delta_stride) / *(delta+ii*delta_stride); (params+tid)->I3U= *(I3U+ii); (params+tid)->u0= *(u0+ii); (params+tid)->sinh2u0= *(sinh2u0+ii); (params+tid)->v0= *(v0+ii); (params+tid)->sin2v0= *(sin2v0+ii); (params+tid)->potu0v0= *(potu0v0+ii); (JRRoot+tid)->function = &JRStaeckelIntegrandSquared; (JRRoot+tid)->params = params+tid; //Find starting points for minimum peps= GSL_FN_EVAL(JRRoot+tid,*(ux+ii)+0.000001); meps= GSL_FN_EVAL(JRRoot+tid,*(ux+ii)-0.000001); if ( fabs(GSL_FN_EVAL(JRRoot+tid,*(ux+ii))) < 0.0000001 && peps*meps < 0. ){ //we are at umin or umax if ( peps < 0. && meps > 0. ) {//umax *(umax+ii)= *(ux+ii); u_lo= 0.9 * (*(ux+ii) - 0.000001); u_hi= *(ux+ii) - 0.0000001; while ( GSL_FN_EVAL(JRRoot+tid,u_lo) >= 0. && u_lo > 0.000000001){ u_hi= u_lo; //this makes sure that brent evaluates using previous u_lo*= 0.9; } //Find root status = gsl_root_fsolver_set ((s+tid)->s, JRRoot+tid, u_lo, u_hi); if (status == GSL_EINVAL) { *(umin+ii) = 0.;//Assume zero if below 0.000000001 } else { iter= 0; do { iter++; status = gsl_root_fsolver_iterate ((s+tid)->s); u_lo = gsl_root_fsolver_x_lower ((s+tid)->s); u_hi = gsl_root_fsolver_x_upper ((s+tid)->s); status = gsl_root_test_interval (u_lo, u_hi, 9.9999999999999998e-13, 4.4408920985006262e-16); } while (status == GSL_CONTINUE && iter < max_iter); // LCOV_EXCL_START if (status == GSL_EINVAL) {//Shouldn't ever get here *(umin+ii) = -9999.99; *(umax+ii) = -9999.99; continue; } // LCOV_EXCL_STOP *(umin+ii) = gsl_root_fsolver_root ((s+tid)->s); } } else {// JB: Should catch all: if ( peps > 0. && meps < 0. ){//umin *(umin+ii)= *(ux+ii); u_lo= *(ux+ii) + 0.000001; u_hi= 1.1 * (*(ux+ii) + 0.000001); while ( GSL_FN_EVAL(JRRoot+tid,u_hi) >= 0. && u_hi < asinh(37.5/ *(delta+ii*delta_stride))) { u_lo= u_hi; //this makes sure that brent evaluates using previous u_hi*= 1.1; } //Find root status = gsl_root_fsolver_set ((s+tid)->s, JRRoot+tid, u_lo, u_hi); if (status == GSL_EINVAL) { *(umin+ii) = -9999.99; *(umax+ii) = -9999.99; continue; } iter= 0; do { iter++; status = gsl_root_fsolver_iterate ((s+tid)->s); u_lo = gsl_root_fsolver_x_lower ((s+tid)->s); u_hi = gsl_root_fsolver_x_upper ((s+tid)->s); status = gsl_root_test_interval (u_lo, u_hi, 9.9999999999999998e-13, 4.4408920985006262e-16); } while (status == GSL_CONTINUE && iter < max_iter); // LCOV_EXCL_START if (status == GSL_EINVAL) {//Shouldn't ever get here *(umin+ii) = -9999.99; *(umax+ii) = -9999.99; continue; } // LCOV_EXCL_STOP *(umax+ii) = gsl_root_fsolver_root ((s+tid)->s); } } else if ( fabs(peps) < 0.00000001 && fabs(meps) < 0.00000001 && peps <= 0 && meps <= 0 ) {//circular *(umin+ii) = *(ux+ii); *(umax+ii) = *(ux+ii); } else { u_lo= 0.9 * *(ux+ii); u_hi= *(ux+ii); while ( GSL_FN_EVAL(JRRoot+tid,u_lo) >= 0. && u_lo > 0.000000001){ u_hi= u_lo; //this makes sure that brent evaluates using previous u_lo*= 0.9; } u_hi= (u_lo < 0.9 * *(ux+ii)) ? u_lo / 0.9 / 0.9: *(ux+ii); //Find root status = gsl_root_fsolver_set ((s+tid)->s, JRRoot+tid, u_lo, u_hi); if (status == GSL_EINVAL) { *(umin+ii) = 0.;//Assume zero if below 0.000000001 } else { iter= 0; do { iter++; status = gsl_root_fsolver_iterate ((s+tid)->s); u_lo = gsl_root_fsolver_x_lower ((s+tid)->s); u_hi = gsl_root_fsolver_x_upper ((s+tid)->s); status = gsl_root_test_interval (u_lo, u_hi, 9.9999999999999998e-13, 4.4408920985006262e-16); } while (status == GSL_CONTINUE && iter < max_iter); // LCOV_EXCL_START if (status == GSL_EINVAL) {//Shouldn't ever get here *(umin+ii) = -9999.99; *(umax+ii) = -9999.99; continue; } // LCOV_EXCL_STOP *(umin+ii) = gsl_root_fsolver_root ((s+tid)->s); } //Find starting points for maximum u_lo= *(ux+ii); u_hi= 1.1 * *(ux+ii); while ( GSL_FN_EVAL(JRRoot+tid,u_hi) > 0. && u_hi < asinh(37.5/ *(delta+ii*delta_stride))) { u_lo= u_hi; //this makes sure that brent evaluates using previous u_hi*= 1.1; } u_lo= (u_hi > 1.1 * *(ux+ii)) ? u_hi / 1.1 / 1.1: *(ux+ii); //Find root status = gsl_root_fsolver_set ((s+tid)->s, JRRoot+tid, u_lo, u_hi); if (status == GSL_EINVAL) { *(umin+ii) = -9999.99; *(umax+ii) = -9999.99; continue; } iter= 0; do { iter++; status = gsl_root_fsolver_iterate ((s+tid)->s); u_lo = gsl_root_fsolver_x_lower ((s+tid)->s); u_hi = gsl_root_fsolver_x_upper ((s+tid)->s); status = gsl_root_test_interval (u_lo, u_hi, 9.9999999999999998e-13, 4.4408920985006262e-16); } while (status == GSL_CONTINUE && iter < max_iter); // LCOV_EXCL_START if (status == GSL_EINVAL) {//Shouldn't ever get here *(umin+ii) = -9999.99; *(umax+ii) = -9999.99; continue; } // LCOV_EXCL_STOP *(umax+ii) = gsl_root_fsolver_root ((s+tid)->s); } } gsl_set_error_handler (NULL); for (tid=0; tid < nthreads; tid++) gsl_root_fsolver_free( (s+tid)->s); free(s); free(JRRoot); free(params); } void calcVmin(int ndata, double * vmin, double * vx, double * pvx, double * E, double * Lz, double * I3V, int ndelta, double * delta, double * u0, double * cosh2u0, double * sinh2u0, double * potupi2, int nargs, struct potentialArg * actionAngleArgs){ int ii, tid, nthreads; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif gsl_function * JzRoot= (gsl_function *) malloc ( nthreads * sizeof(gsl_function) ); struct JzStaeckelArg * params= (struct JzStaeckelArg *) malloc ( nthreads * sizeof (struct JzStaeckelArg) ); //Setup solver int status; int iter, max_iter = 100; const gsl_root_fsolver_type *T; struct pragmasolver *s= (struct pragmasolver *) malloc ( nthreads * sizeof (struct pragmasolver) );; double v_lo, v_hi; T = gsl_root_fsolver_brent; for (tid=0; tid < nthreads; tid++){ (params+tid)->nargs= nargs; (params+tid)->actionAngleArgs= actionAngleArgs; (s+tid)->s= gsl_root_fsolver_alloc (T); } int delta_stride= ndelta == 1 ? 0 : 1; UNUSED int chunk= CHUNKSIZE; gsl_set_error_handler_off(); #pragma omp parallel for schedule(static,chunk) \ private(tid,ii,iter,status,v_lo,v_hi) \ shared(vmin,JzRoot,params,s,vx,delta,E,Lz,I3V,u0,cosh2u0,sinh2u0,potupi2,max_iter) for (ii=0; ii < ndata; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif //Setup function (params+tid)->delta= *(delta+ii*delta_stride); (params+tid)->E= *(E+ii); (params+tid)->Lz22delta= 0.5 * *(Lz+ii) * *(Lz+ii) / *(delta+ii*delta_stride) / *(delta+ii*delta_stride); (params+tid)->I3V= *(I3V+ii); (params+tid)->u0= *(u0+ii); (params+tid)->cosh2u0= *(cosh2u0+ii); (params+tid)->sinh2u0= *(sinh2u0+ii); (params+tid)->potupi2= *(potupi2+ii); (JzRoot+tid)->function = &JzStaeckelIntegrandSquared; (JzRoot+tid)->params = params+tid; //Find starting points for minimum if ( fabs(GSL_FN_EVAL(JzRoot+tid,*(vx+ii))) < 0.0000001) //we are at vmin *(vmin+ii)= ( *(vx+ii) > 0.5 * M_PI ) ? M_PI - *(vx+ii): *(vx+ii); else { if ( *(vx+ii) > 0.5 * M_PI ){ v_lo= 0.9 * ( M_PI - *(vx+ii) ); v_hi= M_PI - *(vx+ii); } else { v_lo= 0.9 * *(vx+ii); v_hi= *(vx+ii); } while ( GSL_FN_EVAL(JzRoot+tid,v_lo) >= 0. && v_lo > 0.000000001){ v_hi= v_lo; //this makes sure that brent evaluates using previous v_lo*= 0.9; } //Find root status = gsl_root_fsolver_set ((s+tid)->s, JzRoot+tid, v_lo, v_hi); if (status == GSL_EINVAL) { *(vmin+ii) = -9999.99; continue; } iter= 0; do { iter++; status = gsl_root_fsolver_iterate ((s+tid)->s); v_lo = gsl_root_fsolver_x_lower ((s+tid)->s); v_hi = gsl_root_fsolver_x_upper ((s+tid)->s); status = gsl_root_test_interval (v_lo, v_hi, 9.9999999999999998e-13, 4.4408920985006262e-16); } while (status == GSL_CONTINUE && iter < max_iter); // LCOV_EXCL_START if (status == GSL_EINVAL) {//Shouldn't ever get here *(vmin+ii) = -9999.99; continue; } // LCOV_EXCL_STOP *(vmin+ii) = gsl_root_fsolver_root ((s+tid)->s); fflush(stdout); } } gsl_set_error_handler (NULL); for (tid=0; tid < nthreads; tid++) gsl_root_fsolver_free( (s+tid)->s); free(s); free(JzRoot); free(params); } double JRStaeckelIntegrand(double u, void * p){ double out= JRStaeckelIntegrandSquared(u,p); if ( out <= 0.) return 0.; else return sqrt(out); } double JRStaeckelIntegrandSquared(double u, void * p){ struct JRStaeckelArg * params= (struct JRStaeckelArg *) p; double sinh2u= sinh(u) * sinh(u); double dU= (sinh2u+params->sin2v0) *evaluatePotentialsUV(u,params->v0,params->delta, params->nargs,params->actionAngleArgs) - (params->sinh2u0+params->sin2v0)*params->potu0v0; return params->E * sinh2u - params->I3U - dU - params->Lz22delta / sinh2u; } double JRStaeckelIntegrandSquared4dJR(double u, void * p){ struct dJRStaeckelArg * params= (struct dJRStaeckelArg *) p; double sinh2u= sinh(u) * sinh(u); double dU= (sinh2u+params->sin2v0) *evaluatePotentialsUV(u,params->v0,params->delta, params->nargs,params->actionAngleArgs) - (params->sinh2u0+params->sin2v0)*params->potu0v0; return params->E * sinh2u - params->I3U - dU - params->Lz22delta / sinh2u; } double JzStaeckelIntegrand(double v, void * p){ double out= JzStaeckelIntegrandSquared(v,p); if ( out <= 0. ) return 0.; else return sqrt(out); } double JzStaeckelIntegrandSquared(double v, void * p){ struct JzStaeckelArg * params= (struct JzStaeckelArg *) p; double sin2v= sin(v) * sin(v); double dV= params->cosh2u0 * params->potupi2 - (params->sinh2u0+sin2v) *evaluatePotentialsUV(params->u0,v,params->delta, params->nargs,params->actionAngleArgs); return params->E * sin2v + params->I3V + dV - params->Lz22delta / sin2v; } double JzStaeckelIntegrandSquared4dJz(double v, void * p){ struct dJzStaeckelArg * params= (struct dJzStaeckelArg *) p; double sin2v= sin(v) * sin(v); double dV= params->cosh2u0 * params->potupi2 - (params->sinh2u0+sin2v) *evaluatePotentialsUV(params->u0,v,params->delta, params->nargs,params->actionAngleArgs); return params->E * sin2v + params->I3V + dV - params->Lz22delta / sin2v; } double dJRdELowStaeckelIntegrand(double t, void * p){ struct dJRStaeckelArg * params= (struct dJRStaeckelArg *) p; double u= params->umin + t * t; return 2. * t * dJRdEStaeckelIntegrand(u,p); } double dJRdEHighStaeckelIntegrand(double t, void * p){ struct dJRStaeckelArg * params= (struct dJRStaeckelArg *) p; double u= params->umax - t * t; return 2. * t * dJRdEStaeckelIntegrand(u,p); } double dJRdEStaeckelIntegrand(double u, void * p){ double out= JRStaeckelIntegrandSquared4dJR(u,p); if ( out <= 0. ) return 0.; else return sinh(u)*sinh(u)/sqrt(out); } double dJRdLzLowStaeckelIntegrand(double t, void * p){ struct dJRStaeckelArg * params= (struct dJRStaeckelArg *) p; double u= params->umin + t * t; return 2. * t * dJRdLzStaeckelIntegrand(u,p); } double dJRdLzHighStaeckelIntegrand(double t, void * p){ struct dJRStaeckelArg * params= (struct dJRStaeckelArg *) p; double u= params->umax - t * t; return 2. * t * dJRdLzStaeckelIntegrand(u,p); } double dJRdLzStaeckelIntegrand(double u, void * p){ double out= JRStaeckelIntegrandSquared4dJR(u,p); if ( out <= 0. ) return 0.; else return 1./sinh(u)/sinh(u)/sqrt(out); } double dJRdI3LowStaeckelIntegrand(double t, void * p){ struct dJRStaeckelArg * params= (struct dJRStaeckelArg *) p; double u= params->umin + t * t; return 2. * t * dJRdI3StaeckelIntegrand(u,p); } double dJRdI3HighStaeckelIntegrand(double t, void * p){ struct dJRStaeckelArg * params= (struct dJRStaeckelArg *) p; double u= params->umax - t * t; return 2. * t * dJRdI3StaeckelIntegrand(u,p); } double dJRdI3StaeckelIntegrand(double u, void * p){ double out= JRStaeckelIntegrandSquared4dJR(u,p); if ( out <= 0. ) return 0.; else return 1./sqrt(out); } double dJzdELowStaeckelIntegrand(double t, void * p){ struct dJzStaeckelArg * params= (struct dJzStaeckelArg *) p; double v= params->vmin + t * t; return 2. * t * dJzdEStaeckelIntegrand(v,p); } double dJzdEHighStaeckelIntegrand(double t, void * p){ double v= M_PI/2. - t * t; return 2. * t * dJzdEStaeckelIntegrand(v,p); } double dJzdEStaeckelIntegrand(double v, void * p){ double out= JzStaeckelIntegrandSquared4dJz(v,p); if ( out <= 0. ) return 0.; else return sin(v)*sin(v)/sqrt(out); } double dJzdLzLowStaeckelIntegrand(double t, void * p){ struct dJzStaeckelArg * params= (struct dJzStaeckelArg *) p; double v= params->vmin + t * t; return 2. * t * dJzdLzStaeckelIntegrand(v,p); } double dJzdLzHighStaeckelIntegrand(double t, void * p){ double v= M_PI/2. - t * t; return 2. * t * dJzdLzStaeckelIntegrand(v,p); } double dJzdLzStaeckelIntegrand(double v, void * p){ double out= JzStaeckelIntegrandSquared4dJz(v,p); if ( out <= 0. ) return 0.; else return 1./sin(v)/sin(v)/sqrt(out); } double dJzdI3LowStaeckelIntegrand(double t, void * p){ struct dJzStaeckelArg * params= (struct dJzStaeckelArg *) p; double v= params->vmin + t * t; return 2. * t * dJzdI3StaeckelIntegrand(v,p); } double dJzdI3HighStaeckelIntegrand(double t, void * p){ double v= M_PI/2. - t * t; return 2. * t * dJzdI3StaeckelIntegrand(v,p); } double dJzdI3StaeckelIntegrand(double v, void * p){ double out= JzStaeckelIntegrandSquared4dJz(v,p); if ( out <= 0. ) return 0.; else return 1./sqrt(out); } double u0Equation(double u, void * p){ struct u0EqArg * params= (struct u0EqArg *) p; double sinh2u= sinh(u) * sinh(u); double cosh2u= cosh(u) * cosh(u); double dU= cosh2u * evaluatePotentialsUV(u,0.5*M_PI,params->delta, params->nargs,params->actionAngleArgs); return -(params->E*sinh2u-dU-params->Lz22delta/sinh2u); } double evaluatePotentialsUV(double u, double v, double delta, int nargs, struct potentialArg * actionAngleArgs){ double R,z; uv_to_Rz(u,v,&R,&z,delta); return evaluatePotentials(R,z,nargs,actionAngleArgs); } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667233486.7014673 galpy-1.8.1/galpy/df/0000755000175100001710000000000014327773317013757 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/__init__.py0000644000175100001710000000435514327773303016072 0ustar00runnerdockerfrom . import (constantbetadf, constantbetaHernquistdf, diskdf, eddingtondf, evolveddiskdf, isotropicHernquistdf, isotropicNFWdf, isotropicPlummerdf, jeans, kingdf, osipkovmerrittdf, osipkovmerrittHernquistdf, osipkovmerrittNFWdf, quasiisothermaldf, sphericaldf, streamdf, streamgapdf, streamspraydf, surfaceSigmaProfile) # # Functions # impulse_deltav_plummer= streamgapdf.impulse_deltav_plummer impulse_deltav_plummer_curvedstream= streamgapdf.impulse_deltav_plummer_curvedstream impulse_deltav_hernquist= streamgapdf.impulse_deltav_hernquist impulse_deltav_hernquist_curvedstream= streamgapdf.impulse_deltav_hernquist_curvedstream impulse_deltav_general= streamgapdf.impulse_deltav_general impulse_deltav_general_curvedstream= streamgapdf.impulse_deltav_general_curvedstream impulse_deltav_general_orbitintegration= streamgapdf.impulse_deltav_general_orbitintegration impulse_deltav_general_fullplummerintegration= streamgapdf.impulse_deltav_general_fullplummerintegration impulse_deltav_plummerstream= streamgapdf.impulse_deltav_plummerstream impulse_deltav_plummerstream_curvedstream= streamgapdf.impulse_deltav_plummerstream_curvedstream # # Classes # shudf= diskdf.shudf dehnendf= diskdf.dehnendf schwarzschilddf= diskdf.schwarzschilddf DFcorrection= diskdf.DFcorrection diskdf= diskdf.diskdf evolveddiskdf= evolveddiskdf.evolveddiskdf expSurfaceSigmaProfile= surfaceSigmaProfile.expSurfaceSigmaProfile surfaceSigmaProfile= surfaceSigmaProfile.surfaceSigmaProfile quasiisothermaldf= quasiisothermaldf.quasiisothermaldf streamdf= streamdf.streamdf streamgapdf= streamgapdf.streamgapdf sphericaldf= sphericaldf.sphericaldf eddingtondf= eddingtondf.eddingtondf isotropicHernquistdf= isotropicHernquistdf.isotropicHernquistdf constantbetaHernquistdf= constantbetaHernquistdf.constantbetaHernquistdf osipkovmerrittHernquistdf= osipkovmerrittHernquistdf.osipkovmerrittHernquistdf kingdf= kingdf.kingdf isotropicPlummerdf= isotropicPlummerdf.isotropicPlummerdf isotropicNFWdf= isotropicNFWdf.isotropicNFWdf osipkovmerrittdf= osipkovmerrittdf.osipkovmerrittdf osipkovmerrittNFWdf= osipkovmerrittNFWdf.osipkovmerrittNFWdf constantbetadf= constantbetadf.constantbetadf streamspraydf= streamspraydf.streamspraydf ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/constantbetaHernquistdf.py0000644000175100001710000000664014327773303021234 0ustar00runnerdocker# Class that implements the anisotropic spherical Hernquist DF with constant # beta parameter import numpy import scipy.integrate import scipy.special from ..potential import HernquistPotential, evaluatePotentials from ..util import conversion from .constantbetadf import _constantbetadf 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): """ NAME: __init__ PURPOSE: Initialize a Hernquist DF with constant anisotropy INPUT: pot - Hernquist potential which determines the DF beta - anisotropy parameter OUTPUT: None HISTORY: 2020-07-22 - Written - Lane (UofT) """ assert isinstance(pot,HernquistPotential),'pot= must be potential.HernquistPotential' _constantbetadf.__init__(self,pot=pot,beta=beta,ro=ro,vo=vo) self._psi0= -evaluatePotentials(self._pot,0,0,use_physical=False) self._potInf= 0. self._GMa = self._psi0*self._pot.a**2. # Final factor is mass to make the DF that of the mass density 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)\ /self._GMa**(1.5-self._beta)\ *self._psi0*self._pot.a def fE(self,E): """ NAME: fE PURPOSE Calculate the energy portion of a Hernquist distribution function INPUT: E - The energy (can be Quantity) OUTPUT: fE - The value of the energy portion of the DF HISTORY: 2020-07-22 - Written """ 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, all adjusted such that DF = mass den if self._beta == 0.: # isotropic case sqrtEtilde= numpy.sqrt(Etilde) fE= self._psi0*self._pot.a\ /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.*self._pot.a) elif self._beta == -0.5: fE= ((20.*Etilde**3.-20.*Etilde**4.+6.*Etilde**5.)\ /(1.-Etilde)**4)/(4.*numpy.pi**3.*self._GMa*self._pot.a) else: 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]''' return self._pot.a*numpy.sqrt(ms)/(1-numpy.sqrt(ms)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/constantbetadf.py0000644000175100001710000003141614327773303017330 0ustar00runnerdocker# Class that implements DFs of the form f(E,L) = L^{-2\beta} f(E) with constant # beta anisotropy parameter import numpy from scipy import integrate, interpolate, special from ..potential.Potential import _evaluatePotentials from ..util import conversion from ..util._optional_deps import _JAX_LOADED from .sphericaldf import anisotropicsphericaldf, sphericaldf if _JAX_LOADED: from jax import grad, vmap # This is the general constantbeta superclass, implementation of general # formula can be found following this class 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,denspot=None,beta=None,rmax=None, scale=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a spherical DF with constant anisotropy parameter INPUT: pot - Spherical potential which determines the DF 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. Not necessary, and will also be overridden by value from pot if available. """ anisotropicsphericaldf.__init__(self,pot=pot,denspot=denspot, rmax=rmax,scale=scale,ro=ro,vo=vo) self._beta= 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 _sample_eta(self,r,n=1): """Sample the angle eta which defines radial vs tangential velocities""" if not hasattr(self,'_coseta_icmf_interp'): # Cumulative dist for cos(eta) = # 0.5 + x 2F1(0.5,beta,1.5,x^2)/sqrt(pi)/Gamma(1-beta)*Gamma(1.5-beta) cosetas= numpy.linspace(-1.,1.,20001) coseta_cmf= cosetas*special.hyp2f1(0.5,self._beta,1.5,cosetas**2.)\ /numpy.sqrt(numpy.pi)/special.gamma(1.-self._beta)\ *special.gamma(1.5-self._beta)+0.5 self._coseta_icmf_interp= interpolate.interp1d(\ coseta_cmf,cosetas, bounds_error=False,fill_value='extrapolate') return numpy.arccos(self._coseta_icmf_interp(\ numpy.random.uniform(size=n))) def _p_v_at_r(self,v,r): if hasattr(self,'_fE_interp'): return self._fE_interp(_evaluatePotentials(self._pot,r,0)\ +0.5*v**2.)*v**(2.-2.*self._beta) else: return self.fE(_evaluatePotentials(self._pot,r,0)\ +0.5*v**2.)*v**(2.-2.*self._beta) def _vmomentdensity(self,r,n,m): if m%2 == 1 or n%2 == 1: return 0. 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) +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.)/\ special.gamma(0.5*(m+n-2.*self._beta+3.)) class constantbetadf(_constantbetadf): """Class that implements DFs of the form :math:`f(E,L) = L^{-2\\beta} f_1(E)` with constant :math:`\\beta` anisotropy parameter for a given density profile""" def __init__(self,pot=None,denspot=None,beta=0.,twobeta=None, rmax=None,scale=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a spherical DF with constant anisotropy parameter 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) beta= (0.) anisotropy parameter twobeta= (None) twice the anisotropy parameter (useful for \beta = half-integer, which is a special case); has priority over beta 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-14 - Written - Bovy (UofT) """ if not _JAX_LOADED: # pragma: no cover raise ImportError("galpy.df.constantbetadf requires the google/jax library") # Parse twobeta if not twobeta is None: beta= twobeta/2. else: twobeta= 2.*beta _constantbetadf.__init__(self,pot=pot,denspot=denspot,beta=beta, rmax=rmax,scale=scale,ro=ro,vo=vo) self._twobeta= twobeta self._halfint= False if isinstance(self._twobeta,int) and self._twobeta % 2 == 1: self._halfint= True self._m= (3-self._twobeta)//2 ii= self._m-1 # Compute d^m (dens x r^2beta) / d Psi^m as successive # d / dr ( ...) / F_r func= lambda r: self._denspot._ddenstwobetadr(r,beta=self._beta)\ /self._pot._rforce_jax(r) while ii > 0: func= lambda r,func=func: grad(func)(r)\ /self._pot._rforce_jax(r) ii-= 1 else: self._m= int(numpy.floor(1.5-self._beta)) self._alpha= 1.5-self._beta-self._m self._fE_prefactor= 2.**self._beta/(2.*numpy.pi)**1.5\ /special.gamma(1.-self._alpha)/special.gamma(1.-self._beta) ii= self._m # Similar d^m (dens x r^2beta) / d Psi^m as above, # but because integral necessary now is over psi, we can omit # the final 1/Fr to do the integral over r if ii == 0: func= lambda r: self._denspot._ddenstwobetadr(r, beta=self._beta) else: func= lambda r: self._denspot._ddenstwobetadr(r, beta=self._beta)\ /self._pot._rforce_jax(r) while ii > 0: if ii == 1: func= lambda r,func=func: grad(func)(r) else: func= lambda r,func=func: grad(func)(r)\ /self._pot._rforce_jax(r) ii-= 1 self._gradfunc= vmap(func) # Min and max energy self._potInf= _evaluatePotentials(self._pot,self._rmax,0) self._Emin= _evaluatePotentials(self._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) # Build interpolator for the lower limit of the integration (near the # 1/(Phi-E)^alpha divergence; at the end, we slightly adjust it up # to be sure to be above the point where things go haywire... if not self._halfint: Es= numpy.linspace(self._Emin, self._potInf+1e-3*(self._Emin-self._potInf),51) guesspow= -17 guesst= 10.**(guesspow*(1.-self._alpha)) startt= numpy.ones_like(Es)*guesst startval= numpy.zeros_like(Es) while numpy.any(startval == 0.): guesspow+= 1 guesst= 10.**(guesspow*(1.-self._alpha)) indx= startval == 0. startt[indx]= guesst startval[indx]= _fEintegrand_smallr(startt[indx], self._pot,Es[indx], self._gradfunc, self._alpha, self._rphi(Es[indx])) self._logstartt= interpolate.InterpolatedUnivariateSpline(\ Es,numpy.log10(startt)+10./3.*(1.-self._alpha),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 constant-beta distribution function INPUT: E - The energy (can be Quantity) OUTPUT: fE - The value of the energy portion of the DF HISTORY: 2021-02-14 - Written - Bovy (UofT) """ Eint= numpy.atleast_1d(conversion.parse_energy(E,vo=self._vo)) out= numpy.zeros_like(Eint) indx= (Eint < self._potInf)*(Eint >= self._Emin) if self._halfint: # fE is simply given by the relevant derivative out[indx]= self._gradfunc(self._rphi(Eint[indx])) return out\ /(2.*numpy.pi**1.5*2**(0.5-self._beta)*special.gamma(1.-self._beta)) else: # Now need to integrate to get fE # 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.quadrature( lambda t: _fEintegrand_smallr(t,self._pot,tE,self._gradfunc, self._alpha,self._rphi(tE)), 10.**self._logstartt(tE),self._rphi(tE)**(1.-self._alpha))[0] for tE in Eint[indx]]) # Add constant part at the beginning out[indx]+= 10.**self._logstartt(Eint[indx])\ *_fEintegrand_smallr(10.**self._logstartt(Eint[indx]), self._pot,Eint[indx], self._gradfunc, self._alpha, self._rphi(Eint[indx])) # 2nd half of the integral out[indx]+= numpy.array([integrate.quadrature( lambda t: _fEintegrand_larger(t,self._pot,tE,self._gradfunc, self._alpha), 0.,0.5/self._rphi(tE))[0] for tE in Eint[indx]]) return -out*self._fE_prefactor def _fEintegrand_raw(r,pot,E,dmp1nudrmp1,alpha): # The 'raw', i.e., direct integrand in the constant-beta inversion out= numpy.zeros_like(r) # Avoid JAX item assignment issues #print("r",r,dmp1nudrmp1(r),(_evaluatePotentials(pot,r,0)-E)) out[:]= dmp1nudrmp1(r)/(_evaluatePotentials(pot,r,0)-E)**alpha out[True^numpy.isfinite(out)]= 0. # assume these are where denom is slightly neg. return out def _fEintegrand_smallr(t,pot,E,dmp1nudrmp1,alpha,rmin): # The integrand at small r, using transformation to deal with divergence #print("t",t,rmin,t**(1./(1.-alpha))+rmin) return 1./(1.-alpha)*t**(alpha/(1.-alpha))\ *_fEintegrand_raw(t**(1./(1.-alpha))+rmin,pot,E,dmp1nudrmp1,alpha) def _fEintegrand_larger(t,pot,E,dmp1nudrmp1,alpha): # The integrand at large r, using transformation to deal with infinity return 1./t**2*_fEintegrand_raw(1./t,pot,E,dmp1nudrmp1,alpha) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667233486.7054672 galpy-1.8.1/galpy/df/data/0000755000175100001710000000000014327773317014670 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000022400000000000010213 xustar00126 path=galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.2500_0.7500_0.0125_0.0000_151_5.0000_20.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.2500_0.7500_0.0125_0.0000_10000644000175100001710000001647614327773303031066 0ustar00runnerdocker(lp1 (lp2 F0.99807830382513363 aF0.99665116796745967 aa(lp3 F0.99879250968562705 aF0.99785671025953526 aa(lp4 F0.99935596126860826 aF0.99884759448410143 aa(lp5 F0.99979224526395516 aF0.99965445897334237 aa(lp6 F1.0001219677777138 aF1.0003042536926749 aa(lp7 F1.0003630399574011 aF1.000820584083457 aa(lp8 F1.0005309498169943 aF1.0012240539775163 aa(lp9 F1.0006390274365282 aF1.0015325842166944 aa(lp10 F1.0006986896904693 aF1.0017617091509132 aa(lp11 F1.0007196678858152 aF1.0019248493297441 aa(lp12 F1.0007102149397071 aF1.0020335599088974 aa(lp13 F1.0006772930875432 aF1.0020977555141428 aa(lp14 F1.0006267423274577 aF1.0021259124984956 aa(lp15 F1.0005634307332654 aF1.0021252499764948 aa(lp16 F1.0004913878079971 aF1.0021018911449109 aa(lp17 F1.000413922243268 aF1.0020610064935096 aa(lp18 F1.0003337254673854 aF1.0020069405131562 aa(lp19 F1.0002529623598733 aF1.0019433234672401 aa(lp20 F1.0001733504481998 aF1.0018731697214558 aa(lp21 F1.0000962288339512 aF1.0017989640331169 aa(lp22 F1.0000226180010365 aF1.0017227370984354 aa(lp23 F0.99995327156167269 aF1.0016461315507041 aa(lp24 F0.9998887209030195 aF1.001570459493264 aa(lp25 F0.9998293135969164 aF1.0014967525481018 aa(lp26 F0.99977524635017812 aF1.001425805305163 aa(lp27 F0.9997265931855065 aF1.0013582129598753 aa(lp28 F0.99968332946385574 aF1.0012944038484879 aa(lp29 F0.99964535229603657 aF1.0012346675068755 aa(lp30 F0.99961249781433481 aF1.0011791788108908 aa(lp31 F0.99958455573226523 aF1.0011280186929812 aa(lp32 F0.99956128155457957 aF1.0010811918693188 aa(lp33 F0.99954240676539474 aF1.001038641964219 aa(lp34 F0.99952764727628707 aF1.0010002643686866 aa(lp35 F0.99951671037927292 aF1.0009659171336085 aa(lp36 F0.99950930041812369 aF1.0009354301565765 aa(lp37 F0.99950512337338482 aF1.0009086128939155 aa(lp38 F0.99950389050990263 aF1.0008852607993697 aa(lp39 F0.9995053212408922 aF1.0008651606621732 aa(lp40 F0.99950914532560109 aF1.0008480950053136 aa(lp41 F0.99951510449773184 aF1.0008338456677126 aa(lp42 F0.99952295363917265 aF1.0008221967002484 aa(lp43 F0.99953246154124809 aF1.0008129366668279 aa(lp44 F0.99954341136482538 aF1.0008058604446539 aa(lp45 F0.99955560081517691 aF1.0008007705994402 aa(lp46 F0.99956884211483932 aF1.000797478403846 aa(lp47 F0.99958296179683603 aF1.0007958045524952 aa(lp48 F0.99959780035363055 aF1.0007955796313752 aa(lp49 F0.99961321180047891 aF1.0007966443733767 aa(lp50 F0.99962906313053312 aF1.0007988497508735 aa(lp51 F0.99964523372836533 aF1.000802056924307 aa(lp52 F0.99966161474516069 aF1.0008061370815877 aa(lp53 F0.99967810843442662 aF1.0008109711861035 aa(lp54 F0.99969462747699889 aF1.0008164496692291 aa(lp55 F0.99971109432914906 aF1.0008224720462733 aa(lp56 F0.9997274405298201 aF1.0008289465221269 aa(lp57 F0.99974360607012935 aF1.0008357895519202 aa(lp58 F0.99975953872801415 aF1.0008429253941979 aa(lp59 F0.99977519348487609 aF1.0008502856548018 aa(lp60 F0.99979053191717704 aF1.000857808823669 aa(lp61 F0.99980552162423353 aF1.0008654398327343 aa(lp62 F0.99982013570265049 aF1.0008731295905657 aa(lp63 F0.99983435227581119 aF1.0008808345785818 aa(lp64 F0.99984815393855664 aF1.0008885163971184 aa(lp65 F0.99986152738374756 aF1.0008961413951285 aa(lp66 F0.99987446293675897 aF1.0009036802601574 aa(lp67 F0.99988695422999263 aF1.0009111076722443 aa(lp68 F0.99989899773749646 aF1.000918401947573 aa(lp69 F0.99991059258461101 aF1.0009255447042877 aa(lp70 F0.99992174010073254 aF1.0009325205939403 aa(lp71 F0.99993244357863098 aF1.0009393169646901 aa(lp72 F0.99994270813935315 aF1.0009459236220171 aa(lp73 F0.99995254027699665 aF1.0009523325808696 aa(lp74 F0.99996194780687708 aF1.0009585378220593 aa(lp75 F0.99997093963100547 aF1.0009645351076866 aa(lp76 F0.99997952555579839 aF1.0009703217235453 aa(lp77 F0.99998771603391789 aF1.0009758963761004 aa(lp78 F0.99999552221455346 aF1.0009812589540603 aa(lp79 F1.0000029556761474 aF1.0009864104454482 aa(lp80 F1.0000100283835944 aF1.0009913527434635 aa(lp81 F1.0000167523912817 aF1.0009960885209104 aa(lp82 F1.0000231401161011 aF1.0010006211665912 aa(lp83 F1.0000292038782048 aF1.0010049546482784 aa(lp84 F1.0000349561331001 aF1.001009093394662 aa(lp85 F1.0000404091409685 aF1.0010130422222461 aa(lp86 F1.0000455752139064 aF1.0010168063386968 aa(lp87 F1.0000504663389451 aF1.001020391153377 aa(lp88 F1.0000550942850541 aF1.0010238022247986 aa(lp89 F1.00005947063654 aF1.0010270454197201 aa(lp90 F1.0000636072363149 aF1.0010301265163055 aa(lp91 F1.0000675143674558 aF1.0010330514673123 aa(lp92 F1.0000712034910961 aF1.0010358261526511 aa(lp93 F1.0000746844627499 aF1.0010384564322674 aa(lp94 F1.0000779677104075 aF1.0010409483669265 aa(lp95 F1.0000810630539572 aF1.0010433074810969 aa(lp96 F1.0000839797900745 aF1.0010455396003057 aa(lp97 F1.0000867269886544 aF1.0010476503669132 aa(lp98 F1.0000893136854505 aF1.0010496451245214 aa(lp99 F1.0000917480519855 aF1.0010515293061815 aa(lp100 F1.0000940381462913 aF1.0010533080932618 aa(lp101 F1.0000961909240824 aF1.0010549866361209 aa(lp102 F1.0000982153488209 aF1.001056569394998 aa(lp103 F1.000100117074592 aF1.0010580616513098 aa(lp104 F1.00010190406376 aF1.0010594675365947 aa(lp105 F1.0001035810967576 aF1.0010607915106786 aa(lp106 F1.0001051554249214 aF1.0010620380048647 aa(lp107 F1.0001066331299535 aF1.0010632112051281 aa(lp108 F1.000108019414705 aF1.0010643142883069 aa(lp109 F1.0001093180581713 aF1.0010653516906753 aa(lp110 F1.0001105365633158 aF1.0010663264856088 aa(lp111 F1.0001116793871991 aF1.0010672426455605 aa(lp112 F1.0001127486539829 aF1.0010681029007533 aa(lp113 F1.0001137504233313 aF1.0010689105189472 aa(lp114 F1.0001146889841834 aF1.0010696687475242 aa(lp115 F1.0001155680095066 aF1.0010703795537292 aa(lp116 F1.0001163894910108 aF1.0010710471742859 aa(lp117 F1.0001171591433686 aF1.0010716726825601 aa(lp118 F1.0001178774286081 aF1.0010722588952241 aa(lp119 F1.0001185506343324 aF1.0010728083681495 aa(lp120 F1.0001191803480745 aF1.0010733226837523 aa(lp121 F1.000119767138391 aF1.0010738049952996 aa(lp122 F1.0001203163284047 aF1.0010742557859649 aa(lp123 F1.0001208297633288 aF1.001074678756859 aa(lp124 F1.000121310219581 aF1.0010750741978447 aa(lp125 F1.000121761508926 aF1.0010754430893081 aa(lp126 F1.0001221730220644 aF1.0010757901922311 aa(lp127 F1.0001225624227918 aF1.001076113115994 aa(lp128 F1.0001229295696796 aF1.0010764160957455 aa(lp129 F1.0001232673001443 aF1.0010766975292151 aa(lp130 F1.0001235822778345 aF1.0010769618513145 aa(lp131 F1.0001238791988802 aF1.0010772091239908 aa(lp132 F1.0001241565231251 aF1.0010774383186325 aa(lp133 F1.0001244165804457 aF1.0010776532647419 aa(lp134 F1.0001246598626821 aF1.0010778544587162 aa(lp135 F1.0001248850414617 aF1.0010780416305025 aa(lp136 F1.0001250876186707 aF1.0010782171099926 aa(lp137 F1.0001252867297001 aF1.0010783795874003 aa(lp138 F1.0001254527992907 aF1.0010785324658358 aa(lp139 F1.0001256408804291 aF1.0010786755582719 aa(lp140 F1.0001257779484911 aF1.0010788060353104 aa(lp141 F1.0001259180912907 aF1.0010789285178829 aa(lp142 F1.0001260560511447 aF1.0010790455850036 aa(lp143 F1.0001262045540233 aF1.0010791560985852 aa(lp144 F1.0001263291205589 aF1.0010792510284021 aa(lp145 F1.0001264306321609 aF1.0010793484268623 aa(lp146 F1.0001265129477435 aF1.0010794336952156 aa(lp147 F1.0001266213985636 aF1.001079507815432 aa(lp148 F1.0001267152378297 aF1.0010795850540803 aa(lp149 F1.0001268173684306 aF1.0010796563934625 aa(lp150 F1.0001268524751608 aF1.0010797258444397 aa(lp151 F1.0001269178553707 aF1.0010797875439543 aa(lp152 F1.0001269834918931 aF1.0010798390051854 aa. ././@PaxHeader0000000000000000000000000000022400000000000010213 xustar00126 path=galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.2500_0.7500_0.2000_0.0000_151_5.0000_20.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.2500_0.7500_0.2000_0.0000_10000644000175100001710000001674014327773303031052 0ustar00runnerdocker(lp1 (lp2 F0.8698398204958413 aF0.61389563056799479 aa(lp3 F0.75359141114144645 aF0.34684196579658749 aa(lp4 F0.72849304523064373 aF0.39608170627468015 aa(lp5 F0.79344985710644023 aF0.48213217823515964 aa(lp6 F0.89494468126604632 aF0.61549681723892125 aa(lp7 F1.0138255581325004 aF0.76278112038330348 aa(lp8 F1.1056131527359223 aF0.92735440170208339 aa(lp9 F1.181793704481261 aF1.0790659564298939 aa(lp10 F1.2294526413402513 aF1.2104116896112114 aa(lp11 F1.2413457349825145 aF1.3246006805228809 aa(lp12 F1.2320840647833438 aF1.4114458603796829 aa(lp13 F1.2128355941695408 aF1.463312675030519 aa(lp14 F1.1859890611690467 aF1.4835565459953535 aa(lp15 F1.1521217001455462 aF1.4805486884322037 aa(lp16 F1.1132362365155073 aF1.4624250458811205 aa(lp17 F1.0729342037425353 aF1.4346415652150382 aa(lp18 F1.034751993331344 aF1.4005845794469496 aa(lp19 F1.0010581898565984 aF1.3626424422121777 aa(lp20 F0.97281719474540318 aF1.3228854212775567 aa(lp21 F0.949949712654929 aF1.283167615368173 aa(lp22 F0.93178773618980526 aF1.2450199920140583 aa(lp23 F0.9174302705698143 aF1.2095622558093293 aa(lp24 F0.90599920959592906 aF1.1774383305175045 aa(lp25 F0.89677158547017199 aF1.1488262892075605 aa(lp26 F0.88919282694464341 aF1.1235485765387974 aa(lp27 F0.88284523246554392 aF1.101210039316594 aa(lp28 F0.87740856819662083 aF1.0813372684990339 aa(lp29 F0.87263808301877477 aF1.0634881455530605 aa(lp30 F0.86836779658803653 aF1.0473193771350793 aa(lp31 F0.8645248818977217 aF1.0326118104445128 aa(lp32 F0.86113880607351456 aF1.0192641586832321 aa(lp33 F0.85833313548593171 aF1.007262891868576 aa(lp34 F0.8562953728504733 aF0.99663773230650532 aa(lp35 F0.85522583676825292 aF0.987414896268071 aa(lp36 F0.85527529403785352 aF0.97957783418299504 aa(lp37 F0.85648696430430449 aF0.97304259377801494 aa(lp38 F0.85875871720618036 aF0.96765119091617569 aa(lp39 F0.8618374878834032 aF0.96318438634494885 aa(lp40 F0.86535185454213581 aF0.95939167216232124 aa(lp41 F0.86887980643709495 aF0.95603301842865174 aa(lp42 F0.87203890625447966 aF0.95292301134437907 aa(lp43 F0.87457716358486504 aF0.94996638077392037 aa(lp44 F0.87643896156374324 aF0.94717392445094895 aa(lp45 F0.87778380499874087 aF0.9446521136294449 aa(lp46 F0.8789477095629793 aF0.94256671517484947 aa(lp47 F0.88035403202458806 aF0.9410883417897925 aa(lp48 F0.88239604559914375 aF0.94033381801997529 aa(lp49 F0.8853230348511465 aF0.94031931287016046 aa(lp50 F0.8891615524960762 aF0.94094045442272234 aa(lp51 F0.89369741269622005 aF0.94198775039989546 aa(lp52 F0.89852885760716195 aF0.9431971548721475 aa(lp53 F0.90318198528470561 aF0.94432293155360758 aa(lp54 F0.90725667032193957 aF0.9452095597337028 aa(lp55 F0.91055406833939467 aF0.94583613637530839 aa(lp56 F0.91313668271404769 aF0.94631439380948268 aa(lp57 F0.91529417678278679 aF0.94683900569082913 aa(lp58 F0.91742589459342316 aF0.94760859101382533 aa(lp59 F0.91988618732943028 aF0.94874803772593652 aa(lp60 F0.9228543394204205 aF0.95026260162482556 aa(lp61 F0.92628044982481583 aF0.95204156104989801 aa(lp62 F0.92992715744380061 aF0.95390886083928472 aa(lp63 F0.93348625591868173 aF0.95569770428484946 aa(lp64 F0.93671542387196194 aF0.95731459670663543 aa(lp65 F0.93953064747757864 aF0.95876368957629543 aa(lp66 F0.94201375051697778 aF0.96012315303791729 aa(lp67 F0.94434043178793936 aF0.96149103142291381 aa(lp68 F0.94667605862626658 aF0.96293211637358678 aa(lp69 F0.9490980282575795 aF0.96445275365187433 aa(lp70 F0.9515806075503771 aF0.96601128760714317 aa(lp71 F0.95403747924249327 aF0.96755105301460953 aa(lp72 F0.95638547911784588 aF0.96903247909172718 aa(lp73 F0.95858800507984754 aF0.97044640608088506 aa(lp74 F0.96065832728663758 aF0.97180621214996099 aa(lp75 F0.96263297125503233 aF0.97313003337230519 aa(lp76 F0.96454136672677593 aF0.97442722655713832 aa(lp77 F0.96639209781594526 aF0.97569590474768819 aa(lp78 F0.96817812249075574 aF0.97692869452156816 aa(lp79 F0.96988967419216243 aF0.97811948395757597 aa(lp80 F0.97152282328579076 aF0.97926623148380121 aa(lp81 F0.97307989696646502 aF0.98036981129331335 aa(lp82 F0.97456551531660174 aF0.98143168370167766 aa(lp83 F0.97598334508829321 aF0.98245263512824543 aa(lp84 F0.97733549563693212 aF0.98343286675631281 aa(lp85 F0.9786234467218361 aF0.98437255434027082 aa(lp86 F0.97984887283723299 aF0.98527216112688487 aa(lp87 F0.98101379919832354 aF0.9861324157323702 aa(lp88 F0.98212040458855154 aF0.98695418332645302 aa(lp89 F0.98317085796807435 aF0.98773839101158156 aa(lp90 F0.98416728433693335 aF0.98848601498679922 aa(lp91 F0.98511178401007948 aF0.98919808332998949 aa(lp92 F0.98600644284187045 aF0.98987567086374129 aa(lp93 F0.98685332622171085 aF0.99051988788508871 aa(lp94 F0.98765446971121595 aF0.9911318690591896 aa(lp95 F0.98841187192786539 aF0.99171276431720834 aa(lp96 F0.98912748973277154 aF0.99226373128344714 aa(lp97 F0.98980323425341954 aF0.99278592854460002 aa(lp98 F0.99044096747346966 aF0.9932805097105678 aa(lp99 F0.99104249934448252 aF0.99374861817201998 aa(lp100 F0.99160958553788547 aF0.99419138258147799 aa(lp101 F0.9921439256118062 aF0.9946099129630932 aa(lp102 F0.99264716182332313 aF0.99500529740397481 aa(lp103 F0.9931208781487233 aF0.9953785992800932 aa(lp104 F0.99356659994863861 aF0.99573085497829794 aa(lp105 F0.99398579370349294 aF0.99606307201051925 aa(lp106 F0.99437986736030581 aF0.99637622758746691 aa(lp107 F0.99475017062531357 aF0.99667126745765555 aa(lp108 F0.99509799579077907 aF0.99694910514655732 aa(lp109 F0.99542457854192634 aF0.99721062139654226 aa(lp110 F0.99573109909103663 aF0.99745666391356469 aa(lp111 F0.99601868335793542 aF0.99768804727123539 aa(lp112 F0.9962884043230622 aF0.997905553052993 aa(lp113 F0.99654128350786186 aF0.99810993012370464 aa(lp114 F0.99677829244532712 aF0.99830189505017042 aa(lp115 F0.99700035432707468 aF0.99848213264248209 aa(lp116 F0.99720834556729621 aF0.99865129659991203 aa(lp117 F0.99740309756644119 aF0.99881001021258964 aa(lp118 F0.99758539828871606 aF0.9989588671887617 aa(lp119 F0.9977559940444346 aF0.99909843244184737 aa(lp120 F0.99791559112857209 aF0.99922924304450367 aa(lp121 F0.99806485747104112 aF0.99935180909119814 aa(lp122 F0.99820442438110357 aF0.99946661467494791 aa(lp123 F0.9983348880866465 aF0.99957411882338343 aa(lp124 F0.99845681141113618 aF0.99967475645851533 aa(lp125 F0.99857072522999701 aF0.99976893937709788 aa(lp126 F0.99867713005777403 aF0.99985705719390572 aa(lp127 F0.99877649763518173 aF0.99993947827902929 aa(lp128 F0.9988692721334268 aF1.0000165507136205 aa(lp129 F0.99895587169682198 aF1.000088603181323 aa(lp130 F0.99903668974419968 aF1.0001559458915517 aa(lp131 F0.99911209627274644 aF1.0002188714458999 aa(lp132 F0.99918243907595317 aF1.0002776556302699 aa(lp133 F0.99924804493594643 aF1.0003325583682174 aa(lp134 F0.99930922074516171 aF1.0003838243787353 aa(lp135 F0.99936625467646234 aF1.0004316840606331 aa(lp136 F0.99941941708674409 aF1.0004763541274151 aa(lp137 F0.99946896172016719 aF1.0005180384232668 aa(lp138 F0.99951512634282036 aF1.0005569285484028 aa(lp139 F0.99955813416449535 aF1.0005932045302193 aa(lp140 F0.99959819388466253 aF1.0006270354605615 aa(lp141 F0.99963550149129654 aF1.0006585801114334 aa(lp142 F0.99967024027437879 aF1.0006879874839953 aa(lp143 F0.9997025819220694 aF1.0007153973432237 aa(lp144 F0.99973268703422347 aF1.0007409407936279 aa(lp145 F0.99976070580410215 aF1.0007647407383669 aa(lp146 F0.99978677893134282 aF1.0007869123817328 aa(lp147 F0.99981103806432214 aF1.0008075635861691 aa(lp148 F0.99983360594528625 aF1.0008267955418098 aa(lp149 F0.99985459743010285 aF1.0008447028006702 aa(lp150 F0.99987411987263297 aF1.0008613740028254 aa(lp151 F0.99989227362365074 aF1.0008768919782502 aa(lp152 F0.99990915299906113 aF1.0008913346130446 aa. ././@PaxHeader0000000000000000000000000000022400000000000010213 xustar00126 path=galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_0.6667_0.0125_0.0000_151_5.0000_20.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_0.6667_0.0125_0.0000_10000644000175100001710000001644214327773303031101 0ustar00runnerdocker(lp1 (lp2 F0.99727009731943717 aF0.99490273120018602 aa(lp3 F0.99821246266699548 aF0.9966355477260419 aa(lp4 F0.99895435261721321 aF0.99803602489691889 aa(lp5 F0.99952866418136332 aF0.99915585734449264 aa(lp6 F0.99996394372701003 aF1.0000399914567288 aa(lp7 F1.0002847892979174 aF1.0007272781469003 aa(lp8 F1.0005122498647241 aF1.001251141810453 aa(lp9 F1.0006642302854702 aF1.0016402053258358 aa(lp10 F1.0007558684181024 aF1.0019188716916008 aa(lp11 F1.0007998888053022 aF1.0021078566164181 aa(lp12 F1.0008069238828619 aF1.0022246696058021 aa(lp13 F1.0007858035951727 aF1.0022840448000085 aa(lp14 F1.0007438129833126 aF1.0022983235593221 aa(lp15 F1.0006869195332837 aF1.0022777920589527 aa(lp16 F1.000619972272959 aF1.0022309775088027 aa(lp17 F1.0005468750878264 aF1.0021649068531766 aa(lp18 F1.0004707367845829 aF1.002085331771243 aa(lp19 F1.0003940004266985 aF1.0019969236601578 aa(lp20 F1.0003185543395561 aF1.0019034420536388 aa(lp21 F1.0002458270251873 aF1.0018078796594863 aa(lp22 F1.000176868029613 aF1.0017125869113135 aa(lp23 F1.0001124166070721 aF1.0016193786402614 aa(lp24 F1.0000529598256409 aF1.0015296251930041 aa(lp25 F0.99999878156883004 aF1.0014443300581455 aa(lp26 F0.9999500037136585 aF1.0013641958200279 aa(lp27 F0.99990662060331348 aF1.0012896800385145 aa(lp28 F0.99986852778700941 aF1.0012210424511734 aa(lp29 F0.9998355458736613 aF1.0011583847178673 aa(lp30 F0.99980744022774659 aF1.001101683768391 aa(lp31 F0.99978393713792701 aF1.0010508196740355 aa(lp32 F0.99976473700174617 aF1.0010055988403064 aa(lp33 F0.99974952499002812 aF1.0009657732119961 aa(lp34 F0.99973797959113153 aF1.0009310560841451 aa(lp35 F0.99972977937662633 aF1.0009011350350416 aa(lp36 F0.99972460828193199 aF1.000875682421519 aa(lp37 F0.99972215964377686 aF1.000854363816744 aa(lp38 F0.99972213921822983 aF1.0008368447176919 aa(lp39 F0.99972426734702313 aF1.0008227958003966 aa(lp40 F0.99972828043386663 aF1.0008118969640682 aa(lp41 F0.99973393185259096 aF1.0008038403641599 aa(lp42 F0.99974099240590097 aF1.0007983326163368 aa(lp43 F0.99974925041562579 aF1.0007950963097136 aa(lp44 F0.99975851152527229 aF1.000793870963673 aa(lp45 F0.99976859828825437 aF1.0007944135269102 aa(lp46 F0.99977934957644765 aF1.000796498519275 aa(lp47 F0.99979061988056472 aF1.0007999178786455 aa(lp48 F0.99980227850079662 aF1.0008044805866356 aa(lp49 F0.99981420870424531 aF1.0008100121244445 aa(lp50 F0.99982630683520879 aF1.0008163537991273 aa(lp51 F0.99983848140590392 aF1.0008233619751667 aa(lp52 F0.99985065222765124 aF1.0008309072566206 aa(lp53 F0.99986274949017717 aF1.0008388736191809 aa(lp54 F0.99987471293073926 aF1.0008471575321021 aa(lp55 F0.9998864910099774 aF1.0008556670860393 aa(lp56 F0.99989804010534256 aF1.0008643211171744 aa(lp57 F0.99990932378595521 aF1.0008730483605279 aa(lp58 F0.99992031207425236 aF1.000881786644958 aa(lp59 F0.99993098082362808 aF1.0008904820983866 aa(lp60 F0.99994131108316808 aF1.0008990884070166 aa(lp61 F0.99995128850372339 aF1.0009075661125511 aa(lp62 F0.99996090284031136 aF1.0009158819439969 aa(lp63 F0.99997014748744695 aF1.0009240082165121 aa(lp64 F0.99997901893598451 aF1.0009319222287147 aa(lp65 F0.99998751647399153 aF1.0009396057518709 aa(lp66 F0.99999564174240907 aF1.0009470445079669 aa(lp67 F1.0000033984463026 aF1.00095422775703 aa(lp68 F1.0000107920023367 aF1.000961147842494 aa(lp69 F1.000017829314833 aF1.0009677998206965 aa(lp70 F1.0000245184786889 aF1.0009741811324635 aa(lp71 F1.0000308685688974 aF1.0009802912416723 aa(lp72 F1.0000368895057763 aF1.0009861314104163 aa(lp73 F1.0000425918035811 aF1.0009917044078693 aa(lp74 F1.0000479864059242 aF1.0009970142477165 aa(lp75 F1.0000530846572131 aF1.0010020660864252 aa(lp76 F1.0000578981541361 aF1.0010068658873363 aa(lp77 F1.0000624383945471 aF1.0010114203872955 aa(lp78 F1.000066717077281 aF1.0010157368748835 aa(lp79 F1.000070745929849 aF1.0010198230802632 aa(lp80 F1.0000745363388712 aF1.0010236870895106 aa(lp81 F1.00007809954751 aF1.0010273372237197 aa(lp82 F1.0000814467601093 aF1.0010307818973236 aa(lp83 F1.0000845887592698 aF1.0010340296016005 aa(lp84 F1.000087536081568 aF1.0010370889688567 aa(lp85 F1.0000902988394555 aF1.001039968367591 aa(lp86 F1.0000928871897969 aF1.0010426761386815 aa(lp87 F1.0000953105631716 aF1.0010452206152973 aa(lp88 F1.0000975775605312 aF1.0010476098589638 aa(lp89 F1.0000996977003669 aF1.0010498516001241 aa(lp90 F1.0001016797145992 aF1.0010519537012448 aa(lp91 F1.0001035304314652 aF1.0010539233445539 aa(lp92 F1.0001052588607764 aF1.001055767947493 aa(lp93 F1.0001068713622321 aF1.0010574940371477 aa(lp94 F1.000108375406465 aF1.0010591086443341 aa(lp95 F1.0001097776237513 aF1.0010606178319961 aa(lp96 F1.0001110840599943 aF1.0010620279583722 aa(lp97 F1.0001123012751019 aF1.0010633444045336 aa(lp98 F1.000113434316823 aF1.0010645735407331 aa(lp99 F1.0001144896759413 aF1.001065719603476 aa(lp100 F1.0001154706412698 aF1.0010667885505558 aa(lp101 F1.0001163821194927 aF1.001067784551551 aa(lp102 F1.0001172307557509 aF1.0010687122738353 aa(lp103 F1.0001180186286589 aF1.0010695763186199 aa(lp104 F1.0001187520866293 aF1.0010703802540546 aa(lp105 F1.000119429919833 aF1.0010711284719123 aa(lp106 F1.0001200609551533 aF1.0010718239347192 aa(lp107 F1.0001206470177095 aF1.0010724710279686 aa(lp108 F1.0001211909822321 aF1.0010730720584657 aa(lp109 F1.0001216927953782 aF1.0010736308153416 aa(lp110 F1.0001221598156809 aF1.001074148592588 aa(lp111 F1.0001225940088079 aF1.0010746302082201 aa(lp112 F1.0001229939704932 aF1.0010750769124199 aa(lp113 F1.0001233651496197 aF1.0010754907584405 aa(lp114 F1.0001237087743902 aF1.0010758760720684 aa(lp115 F1.0001240302809846 aF1.0010762311205792 aa(lp116 F1.0001243231821995 aF1.0010765613511798 aa(lp117 F1.0001245971666166 aF1.0010768684680567 aa(lp118 F1.0001248451498344 aF1.0010771518473816 aa(lp119 F1.0001250802062824 aF1.0010774133479514 aa(lp120 F1.0001252983295161 aF1.0010776573989257 aa(lp121 F1.0001254961802482 aF1.0010778807607996 aa(lp122 F1.0001256763515138 aF1.0010780908687069 aa(lp123 F1.0001258495372241 aF1.0010782814628418 aa(lp124 F1.000126011160845 aF1.0010784589105852 aa(lp125 F1.0001261546931328 aF1.0010786246723764 aa(lp126 F1.0001262805872699 aF1.0010787761972417 aa(lp127 F1.000126399371174 aF1.0010789178333936 aa(lp128 F1.0001265267203192 aF1.0010790469653272 aa(lp129 F1.0001266291140563 aF1.00107916602785 aa(lp130 F1.0001267232006565 aF1.0010792748440658 aa(lp131 F1.000126810318809 aF1.0010793836184351 aa(lp132 F1.00012688966802 aF1.0010794765824331 aa(lp133 F1.0001269793139638 aF1.0010795637128982 aa(lp134 F1.000127054732048 aF1.0010796492871277 aa(lp135 F1.0001271227598034 aF1.0010797162312828 aa(lp136 F1.0001271707957811 aF1.001079785214537 aa(lp137 F1.0001272479400889 aF1.0010798452187009 aa(lp138 F1.0001272437839068 aF1.0010799064573543 aa(lp139 F1.0001273306328906 aF1.0010799565514132 aa(lp140 F1.0001273288189449 aF1.001080015719263 aa(lp141 F1.0001273555223427 aF1.001080064412011 aa(lp142 F1.0001273815600842 aF1.0010800941716886 aa(lp143 F1.0001274781750937 aF1.0010801548169583 aa(lp144 F1.0001275208218485 aF1.0010801758813366 aa(lp145 F1.000127537212202 aF1.0010802190499495 aa(lp146 F1.0001275445640641 aF1.0010802413473638 aa(lp147 F1.000127604738791 aF1.0010802482828494 aa(lp148 F1.0001276440812976 aF1.0010802936632186 aa(lp149 F1.0001277045356598 aF1.0010803280825422 aa(lp150 F1.0001275857369347 aF1.0010803396195218 aa(lp151 F1.0001276118896525 aF1.0010803502848042 aa(lp152 F1.0001275707536303 aF1.0010803866714342 aa. ././@PaxHeader0000000000000000000000000000022400000000000010213 xustar00126 path=galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_0.6667_0.2000_0.0000_151_5.0000_20.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_0.6667_0.2000_0.0000_10000644000175100001710000001672214327773303031074 0ustar00runnerdocker(lp1 (lp2 F0.8688590418257689 aF0.55778549189531212 aa(lp3 F0.66943380441103129 aF0.12284826849669657 aa(lp4 F0.58290489091702324 aF0.18187241858293712 aa(lp5 F0.65050161179680077 aF0.29330535277798186 aa(lp6 F0.78866181043084838 aF0.45936432596346044 aa(lp7 F0.96342508885332345 aF0.64856878326608169 aa(lp8 F1.0907238958886873 aF0.88003455204867254 aa(lp9 F1.1988528349277414 aF1.1043207718857919 aa(lp10 F1.2755138951623797 aF1.2968253452738454 aa(lp11 F1.303964520738534 aF1.4594631346822839 aa(lp12 F1.3007494906009569 aF1.5777599762295036 aa(lp13 F1.281859761132244 aF1.6412564263038232 aa(lp14 F1.2531523643117308 aF1.6549256309738198 aa(lp15 F1.2169283914028151 aF1.6316567792359653 aa(lp16 F1.1751064382217726 aF1.5857846834683442 aa(lp17 F1.1306068093335104 aF1.528811336290933 aa(lp18 F1.0870491409924439 aF1.4681741365882113 aa(lp19 F1.0476107928236424 aF1.4082153417884984 aa(lp20 F1.0143518591369065 aF1.3513713438083266 aa(lp21 F0.98811743349448766 aF1.2989591062524188 aa(lp22 F0.96876012339423057 aF1.2515836624218746 aa(lp23 F0.95539604533643052 aF1.209423367501594 aa(lp24 F0.94669297823045018 aF1.1723708750098085 aa(lp25 F0.94115791299862506 aF1.140090619338012 aa(lp26 F0.93737205708079097 aF1.1120738247625306 aa(lp27 F0.93416541745161796 aF1.0877085346469368 aa(lp28 F0.93071996303736115 aF1.0663745114697929 aa(lp29 F0.92661223838772577 aF1.0475302720827488 aa(lp30 F0.92180217316818591 aF1.0307737403696271 aa(lp31 F0.9165729088078709 aF1.0158630373304429 aa(lp32 F0.91143029667447439 aF1.0027009744106965 aa(lp33 F0.90697821464184458 aF0.99128893786581207 aa(lp34 F0.90378924121222404 aF0.9816611525081006 aa(lp35 F0.90228656968384879 aF0.97381496861539985 aa(lp36 F0.90265169331509432 aF0.96765440504951694 aa(lp37 F0.90477152391776683 aF0.96296098318425349 aa(lp38 F0.90823799269073135 aF0.95940113477001487 aa(lp39 F0.91240992852271907 aF0.95657202143932929 aa(lp40 F0.91653706731769924 aF0.95407724622568635 aa(lp41 F0.91992840777323015 aF0.95161389365445781 aa(lp42 F0.9221261839864312 aF0.94904457992459323 aa(lp43 F0.92303295425900245 aF0.9464290110706951 aa(lp44 F0.92294427216128549 aF0.94399937742179896 aa(lp45 F0.92246591415970203 aF0.94208211936970931 aa(lp46 F0.92233400236628948 aF0.94098685440580399 aa(lp47 F0.9231904433074537 aF0.94089541830554502 aa(lp48 F0.92538207226842528 aF0.94178606370917994 aa(lp49 F0.9288468973910412 aF0.94342075798113445 aa(lp50 F0.93312962984938397 aF0.94540702250133346 aa(lp51 F0.93753327082011706 aF0.94732116281427625 aa(lp52 F0.94136601606123749 aF0.94885169310715289 aa(lp53 F0.94419500274736079 aF0.94990467645036569 aa(lp54 F0.94599903452125078 aF0.95062163385616005 aa(lp55 F0.94714680361920178 aF0.95129883053012576 aa(lp56 F0.94820820972602016 aF0.95224457174020161 aa(lp57 F0.94968936010648697 aF0.95364129621719007 aa(lp58 F0.9518180255791473 aF0.95547510666204605 aa(lp59 F0.95447768408907818 aF0.95756216115168213 aa(lp60 F0.95731262521117366 aF0.95965367554438097 aa(lp61 F0.95993897596721778 aF0.96156110783212723 aa(lp62 F0.96214083119151683 aF0.96323282536263677 aa(lp63 F0.96394393897270847 aF0.96474495204512456 aa(lp64 F0.96554126222491909 aF0.96622417461480103 aa(lp65 F0.9671398046783658 aF0.96776046974896202 aa(lp66 F0.96883788926605685 aF0.96936441731067913 aa(lp67 F0.97060239307582263 aF0.97098466570364306 aa(lp68 F0.97233483598959725 aF0.9725584942967439 aa(lp69 F0.97395763864046214 aF0.97405283535553444 aa(lp70 F0.97545566513201454 aF0.97547114668567314 aa(lp71 F0.97685928132874777 aF0.97683337978183615 aa(lp72 F0.97820300334196097 aF0.97815360124815964 aa(lp73 F0.97950008154920964 aF0.97943249163420287 aa(lp74 F0.98074511313621926 aF0.98066356608808503 aa(lp75 F0.9819294223769478 aF0.98184173213417947 aa(lp76 F0.98305066238449945 aF0.98296629487805076 aa(lp77 F0.98411201282185201 aF0.98403902058252612 aa(lp78 F0.98511752541456465 aF0.98506164765544058 aa(lp79 F0.98606978499202924 aF0.98603519975177845 aa(lp80 F0.98697038793133285 aF0.98696049721912216 aa(lp81 F0.98782095196158826 aF0.98783860572264159 aa(lp82 F0.98862339975946045 aF0.98867085638091956 aa(lp83 F0.98937977654918019 aF0.98945870891083909 aa(lp84 F0.99009210020782179 aF0.99020367667351694 aa(lp85 F0.99076234347165748 aF0.9909073144805477 aa(lp86 F0.99139245070600446 aF0.99157121417424454 aa(lp87 F0.99198433832301425 aF0.99219699164745634 aa(lp88 F0.99253988577819063 aF0.99278627208605708 aa(lp89 F0.99306092808194379 aF0.99334067769234691 aa(lp90 F0.9935492507683108 aF0.99386181771077386 aa(lp91 F0.99400658606183789 aF0.99435127994619954 aa(lp92 F0.9944346097870862 aF0.99481062355156213 aa(lp93 F0.99483493889272789 aF0.99524137300561266 aa(lp94 F0.99520912976738296 aF0.99564501317551768 aa(lp95 F0.99555867705853873 aF0.99602298534582956 aa(lp96 F0.99588501309015731 aF0.99637668411178071 aa(lp97 F0.99618950775230608 aF0.99670745503650982 aa(lp98 F0.99647346875015219 aF0.99701659297881695 aa(lp99 F0.99673814228639779 aF0.99730534101071333 aa(lp100 F0.99698471396589461 aF0.99757488983961862 aa(lp101 F0.99721431002783667 aF0.997826377686721 aa(lp102 F0.99742799876808796 aF0.99806089052295621 aa(lp103 F0.99762679208864513 aF0.99827946264527967 aa(lp104 F0.99781164729812488 aF0.99848307750863052 aa(lp105 F0.99798346884964362 aF0.99867266878306016 aa(lp106 F0.99814311039579284 aF0.9988491216079014 aa(lp107 F0.99829137664207235 aF0.99901327396653794 aa(lp108 F0.99842902543086176 aF0.99916591820451828 aa(lp109 F0.9985567697179637 aF0.99930780260665364 aa(lp110 F0.99867527967215397 aF0.99943963305729633 aa(lp111 F0.99878518461840815 aF0.99956207473392078 aa(lp112 F0.99888707499914065 aF0.99967575382954277 aa(lp113 F0.99898150444460343 aF0.99978125925455363 aa(lp114 F0.99906899154313333 aF0.99987914437325198 aa(lp115 F0.9991500217848357 aF0.99996992870540335 aa(lp116 F0.99922504928637379 aF1.0000540995771618 aa(lp117 F0.99929449861179542 aF1.0001321137866246 aa(lp118 F0.9993587663616601 aF1.0002043991595988 aa(lp119 F0.99941822286338211 aF1.000271356179421 aa(lp120 F0.99947321363854369 aF1.0003333593754202 aa(lp121 F0.99952406090824197 aF1.0003907588696357 aa(lp122 F0.99957106502099702 aF1.0004438817079968 aa(lp123 F0.99961450578803024 aF1.0004930332484296 aa(lp124 F0.99965464375898216 aF1.0005384983789449 aa(lp125 F0.99969172135066153 aF1.0005805428102867 aa(lp126 F0.99972596404565672 aF1.0006194141613216 aa(lp127 F0.99975758172149753 aF1.0006553431721337 aa(lp128 F0.99978676923648802 aF1.0006885446382059 aa(lp129 F0.99981370760634969 aF1.0007192185059557 aa(lp130 F0.99983856507918956 aF1.0007475507987731 aa(lp131 F0.99986149783349498 aF1.0007737144293289 aa(lp132 F0.99988265058141856 aF1.0007978702424511 aa(lp133 F0.99990215788662051 aF1.000820167555001 aa(lp134 F0.99992014427090481 aF1.0008407451440615 aa(lp135 F0.99993672521267063 aF1.0008597318206378 aa(lp136 F0.9999520078485441 aF1.0008772470727745 aa(lp137 F0.99996609133197922 aF1.0008934019221136 aa(lp138 F0.99997906730755703 aF1.0009082990864318 aa(lp139 F0.99999102142294116 aF1.0009220339767941 aa(lp140 F1.0000020315298759 aF1.0009346950387101 aa(lp141 F1.0000121711395518 aF1.0009463640423359 aa(lp142 F1.0000215073834577 aF1.0009571169687543 aa(lp143 F1.0000301027831031 aF1.0009670240101216 aa(lp144 F1.0000380146468038 aF1.0009761501336367 aa(lp145 F1.0000452960900137 aF1.0009845556689527 aa(lp146 F1.0000519964175874 aF1.0009922960466437 aa(lp147 F1.0000581616220625 aF1.0009994228738013 aa(lp148 F1.0000638333516962 aF1.0010059837301 aa(lp149 F1.0000690501250422 aF1.0010120228975106 aa(lp150 F1.000073847443248 aF1.0010175805889392 aa(lp151 F1.0000782593385193 aF1.0010226949478027 aa(lp152 F1.0000823158507319 aF1.0010274002084016 aa. ././@PaxHeader0000000000000000000000000000022400000000000010213 xustar00126 path=galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.0125_0.0000_151_5.0000_20.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.0125_0.0000_10000644000175100001710000001646714327773303031060 0ustar00runnerdocker(lp1 (lp2 F0.9990743950728167 aF0.99880468037221271 aa(lp3 F0.99935807285038303 aF0.99928119890865896 aa(lp4 F0.99959597270678402 aF0.99969281812756849 aa(lp5 F0.99979360960209707 aF1.0000466679138915 aa(lp6 F0.99995594760501638 aF1.0003492022297855 aa(lp7 F1.000087445583314 aF1.0006062530463655 aa(lp8 F1.0001920997722569 aF1.0008230824508781 aa(lp9 F1.0002734843788637 aF1.0010044310067099 aa(lp10 F1.0003347891952021 aF1.0011545629292211 aa(lp11 F1.0003788547816843 aF1.0012773080858932 aa(lp12 F1.0004082050166614 aF1.0013761009054434 aa(lp13 F1.000425077196915 aF1.0014540163422774 aa(lp14 F1.0004314497520188 aF1.0015138030306727 aa(lp15 F1.0004290676990244 aF1.0015579137837352 aa(lp16 F1.0004194659664856 aF1.0015885335948713 aa(lp17 F1.0004039907097484 aF1.0016076052992113 aa(lp18 F1.0003838187549321 aF1.0016168530516127 aa(lp19 F1.000359975302709 aF1.0016178037759287 aa(lp20 F1.0003333500135978 aF1.0016118067313999 aa(lp21 F1.0003047116050514 aF1.0016000513399856 aa(lp22 F1.0002747210709848 aF1.0015835834077584 aa(lp23 F1.000243943641844 aF1.0015633198678788 aa(lp24 F1.0002128595843238 aF1.0015400621661958 aa(lp25 F1.0001818739432402 aF1.0015145083999091 aa(lp26 F1.0001513253161711 aF1.0014872643141399 aa(lp27 F1.0001214937455138 aF1.0014588532552264 aa(lp28 F1.0000926078084147 aF1.0014297251680222 aa(lp29 F1.0000648509759531 aF1.0014002647236757 aa(lp30 F1.0000383673114877 aF1.0013707986516669 aa(lp31 F1.0000132665662624 aF1.0013416023503501 aa(lp32 F0.99998962873178265 aF1.0013129058375967 aa(lp33 F0.99996750810119628 aF1.0012848991044991 aa(lp34 F0.99994693688275771 aF1.001257736925081 aa(lp35 F0.99992792841584388 aF1.0012315431723935 aa(lp36 F0.9999104800207903 aF1.0012064146887321 aa(lp37 F0.99989457552592642 aF1.001182424749143 aa(lp38 F0.99988018749818575 aF1.0011596261591591 aa(lp39 F0.99986727921311669 aF1.0011380540201156 aa(lp40 F0.99985580638448146 aF1.001117728194465 aa(lp41 F0.99984571868268879 aF1.0010986555010466 aa(lp42 F0.99983696106597952 aF1.0010808316642932 aa(lp43 F0.99982947493292251 aF1.0010642430453074 aa(lp44 F0.99982319913604623 aF1.0010488681724112 aa(lp45 F0.99981807083994412 aF1.0010346790925966 aa(lp46 F0.99981402628164839 aF1.0010216425657561 aa(lp47 F0.99981100140320756 aF1.0010097211098195 aa(lp48 F0.99980893240147994 aF1.0009988739212676 aa(lp49 F0.99980775618294637 aF1.0009890576754183 aa(lp50 F0.99980741075520674 aF1.000980227224828 aa(lp51 F0.99980783553725094 aF1.0009723362060476 aa(lp52 F0.9998089716317542 aF1.0009653375602683 aa(lp53 F0.9998107620179526 aF1.0009591839825094 aa(lp54 F0.99981315172235774 aF1.0009538283069359 aa(lp55 F0.99981608795337629 aF1.0009492238259972 aa(lp56 F0.99981952017026543 aF1.0009453245671214 aa(lp57 F0.99982340016633597 aF1.0009420855202564 aa(lp58 F0.9998276820707418 aF1.000939462817628 aa(lp59 F0.99983232240432407 aF1.0009374138938154 aa(lp60 F0.99983728003657468 aF1.0009358975948943 aa(lp61 F0.9998425161773995 aF1.0009348742784245 aa(lp62 F0.99984799434337768 aF1.0009343058733522 aa(lp63 F0.99985368030713329 aF1.00093415593917 aa(lp64 F0.99985954203860705 aF1.000934389673324 aa(lp65 F0.99986554964791485 aF1.0009349739515416 aa(lp66 F0.9998716753007082 aF1.0009358773061328 aa(lp67 F0.99987789318083808 aF1.0009370699170554 aa(lp68 F0.99988417935402807 aF1.0009385236096018 aa(lp69 F0.99989051174727317 aF1.0009402117962272 aa(lp70 F0.9998968700347296 aF1.0009421094608091 aa(lp71 F0.9999032355359162 aF1.0009441931180425 aa(lp72 F0.99990959120632139 aF1.0009464407545225 aa(lp73 F0.99991592147297603 aF1.0009488317857498 aa(lp74 F0.99992221219336941 aF1.0009513470074136 aa(lp75 F0.99992845058586211 aF1.0009539685384006 aa(lp76 F0.99993462514483777 aF1.0009566797652818 aa(lp77 F0.99994072552541935 aF1.0009594652912763 aa(lp78 F0.99994674252440263 aF1.0009623108697032 aa(lp79 F0.99995266800490357 aF1.0009652033654866 aa(lp80 F0.99995849478486465 aF1.000968130678983 aa(lp81 F0.99996421659807933 aF1.0009710817115514 aa(lp82 F0.99996982806955348 aF1.0009740462916248 aa(lp83 F0.99997532456121885 aF1.00097701514485 aa(lp84 F0.99998070221889346 aF1.0009799798211849 aa(lp85 F0.99998595779052168 aF1.0009829326682107 aa(lp86 F0.99999108877011189 aF1.0009858667517146 aa(lp87 F0.99999609316496396 aF1.0009887758611984 aa(lp88 F1.0000009694481511 aF1.000991654388989 aa(lp89 F1.0000057166398006 aF1.0009944973854952 aa(lp90 F1.0000103343865976 aF1.0009973004092967 aa(lp91 F1.0000148223144245 aF1.0010000595856374 aa(lp92 F1.0000191808380259 aF1.001002771511839 aa(lp93 F1.0000234104371752 aF1.0010054332367719 aa(lp94 F1.0000275120218223 aF1.0010080422307235 aa(lp95 F1.0000314867199025 aF1.0010105963888944 aa(lp96 F1.0000353359375895 aF1.0010130938715427 aa(lp97 F1.0000390612934089 aF1.0010155332696713 aa(lp98 F1.0000426645683618 aF1.0010179133983232 aa(lp99 F1.0000461477380387 aF1.0010202334368887 aa(lp100 F1.0000495128527902 aF1.0010224927432529 aa(lp101 F1.0000527620095618 aF1.0010246908962246 aa(lp102 F1.0000558979015555 aF1.0010268277489389 aa(lp103 F1.0000589226230696 aF1.0010289033018396 aa(lp104 F1.0000618389501996 aF1.0010309177686121 aa(lp105 F1.0000646490830631 aF1.0010328715041328 aa(lp106 F1.0000673559665465 aF1.0010347649121394 aa(lp107 F1.0000699621401841 aF1.0010365986718481 aa(lp108 F1.0000724702690171 aF1.0010383734957813 aa(lp109 F1.0000748828523898 aF1.001040090141361 aa(lp110 F1.0000772028559133 aF1.0010417495921695 aa(lp111 F1.0000794329486227 aF1.001043352773298 aa(lp112 F1.0000815755804513 aF1.0010449005908573 aa(lp113 F1.0000836335949117 aF1.0010463944177561 aa(lp114 F1.0000856095703832 aF1.0010478352659276 aa(lp115 F1.0000875062662791 aF1.0010492242516769 aa(lp116 F1.0000893257168224 aF1.0010505627058335 aa(lp117 F1.0000910712933413 aF1.001051851737339 aa(lp118 F1.0000927446568273 aF1.0010530927480579 aa(lp119 F1.0000943488799938 aF1.001054286948825 aa(lp120 F1.0000958863249361 aF1.0010554355700636 aa(lp121 F1.000097358861294 aF1.0010565399869951 aa(lp122 F1.0000987690176568 aF1.001057601441915 aa(lp123 F1.0001001191965475 aF1.0010586212618107 aa(lp124 F1.0001014119513434 aF1.0010596004960806 aa(lp125 F1.0001026489172968 aF1.0010605405405797 aa(lp126 F1.0001038314313191 aF1.0010614426386641 aa(lp127 F1.000104962956609 aF1.0010623082759893 aa(lp128 F1.0001060455187143 aF1.0010631383365485 aa(lp129 F1.0001070791988897 aF1.0010639341803096 aa(lp130 F1.0001080676277188 aF1.0010646967683292 aa(lp131 F1.0001090122372336 aF1.0010654277440323 aa(lp132 F1.0001099143548824 aF1.0010661277844481 aa(lp133 F1.0001107767448656 aF1.0010667976880618 aa(lp134 F1.0001115993772434 aF1.0010674394401669 aa(lp135 F1.0001123850607756 aF1.0010680534083465 aa(lp136 F1.0001131344336112 aF1.0010686408270966 aa(lp137 F1.0001138505461997 aF1.0010692026352292 aa(lp138 F1.0001145315836062 aF1.0010697398932693 aa(lp139 F1.00011518452254 aF1.0010702537554002 aa(lp140 F1.0001158034820439 aF1.0010707444614175 aa(lp141 F1.0001163952274192 aF1.0010712143235503 aa(lp142 F1.0001169596816692 aF1.0010716622969136 aa(lp143 F1.0001174988236314 aF1.0010720901495773 aa(lp144 F1.0001180120359334 aF1.0010724990495545 aa(lp145 F1.0001185002539059 aF1.0010728889071521 aa(lp146 F1.0001189641031643 aF1.0010732615300322 aa(lp147 F1.0001194083842913 aF1.0010736169732553 aa(lp148 F1.0001198317701256 aF1.001073956651366 aa(lp149 F1.0001202337344206 aF1.0010742797304999 aa(lp150 F1.0001206134681926 aF1.0010745887891965 aa(lp151 F1.0001209780439861 aF1.0010748825207756 aa(lp152 F1.0001213246786778 aF1.0010751634361053 aa. ././@PaxHeader0000000000000000000000000000022400000000000010213 xustar00126 path=galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.1000_0.0000_151_5.0000_20.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.1000_0.0000_10000644000175100001710000001672014327773303031041 0ustar00runnerdocker(lp1 (lp2 F0.93774745070605769 aF0.86272101934702372 aa(lp3 F0.95220322549967706 aF0.88794351458892307 aa(lp4 F0.96538946394015535 aF0.91110192104591703 aa(lp5 F0.97714270537124237 aF0.93206141077449056 aa(lp6 F0.98737343712633807 aF0.9507787383069034 aa(lp7 F0.99606265633282809 aF0.96727634810348639 aa(lp8 F1.0032483024573122 aF0.98162393953146154 aa(lp9 F1.0090093514414202 aF0.99392712168078368 aa(lp10 F1.0134539980586257 aF1.0043179239293325 aa(lp11 F1.0167087091485743 aF1.0129460659854896 aa(lp12 F1.0189092716732062 aF1.0199713901460354 aa(lp13 F1.0201936023805074 aF1.0255573033039376 aa(lp14 F1.0206961824804803 aF1.029865517013169 aa(lp15 F1.0205441148353034 aF1.0330520278102862 aa(lp16 F1.0198545790777067 aF1.0352642310075006 aa(lp17 F1.0187334041670075 aF1.0366390348194765 aa(lp18 F1.0172745047369245 aF1.0373017908485733 aa(lp19 F1.0155599627953784 aF1.0373658623611315 aa(lp20 F1.0136605389880997 aF1.0369326878822978 aa(lp21 F1.0116364670673033 aF1.0360921840915469 aa(lp22 F1.0095384049467255 aF1.0349233834456282 aa(lp23 F1.0074084440776507 aF1.0334952223656195 aa(lp24 F1.005281125602191 aF1.0318673986526881 aa(lp25 F1.0031844150235898 aF1.0300912564463789 aa(lp26 F1.0011406066948494 aF1.0282106575019183 aa(lp27 F0.99916714855943567 aF1.0262628159520468 aa(lp28 F0.99727737933701499 aF1.0242790804732125 aa(lp29 F0.9954811787984007 aF1.0222856555478592 aa(lp30 F0.99378553530491032 aF1.0203042570515781 aa(lp31 F0.99219503675566978 aF1.0183527030210291 aa(lp32 F0.99071229222412516 aF1.0164454409726869 aa(lp33 F0.98933829171591126 aF1.0145940154231354 aa(lp34 F0.98807271157660892 aF1.0128074804364906 aa(lp35 F0.98691417264544801 aF1.0110927615877519 aa(lp36 F0.98586045746392192 aF1.009454972323953 aa(lp37 F0.98490869227426225 aF1.0078976895475871 aa(lp38 F0.9840554990041962 aF1.0064231930424377 aa(lp39 F0.98329712178286721 aF1.0050326729148518 aa(lp40 F0.98262953190677493 aF1.0037264089270543 aa(lp41 F0.98204851461525577 aF1.002503925224701 aa(lp42 F0.98154974054465527 aF1.0013641235999466 aa(lp43 F0.98112882431074677 aF1.000305398058398 aa(lp44 F0.98078137229239049 aF0.99932573313854023 aa(lp45 F0.9805030213644873 aF0.99842278814168151 aa(lp46 F0.98028947005080358 aF0.99759396914154186 aa(lp47 F0.98013650333621039 aF0.99683649042479328 aa(lp48 F0.98004001218699022 aF0.99614742679349566 aa(lp49 F0.97999600867100611 aF0.9955237579674443 aa(lp50 F0.98000063741671084 aF0.99496240616892806 aa(lp51 F0.98005018403352995 aF0.99446026782524688 aa(lp52 F0.98014108101568209 aF0.99401424018762541 aa(lp53 F0.98026991155139931 aF0.99362124357002701 aa(lp54 F0.98043341160527464 aF0.99327823980384122 aa(lp55 F0.98062847058154801 aF0.99298224743483654 aa(lp56 F0.98085213079666211 aF0.99273035410965504 aa(lp57 F0.9811015860147162 aF0.99251972654145759 aa(lp58 F0.98137417918427239 aF0.99234761839177177 aa(lp59 F0.9816673995588403 aF0.99221137636395407 aa(lp60 F0.98197887930981698 aF0.99210844475173798 aa(lp61 F0.98230638974083373 aF0.99203636867694922 aa(lp62 F0.98264783720130544 aF0.99199279618983383 aa(lp63 F0.98300125875375166 aF0.99197547940141351 aa(lp64 F0.98336481767103179 aF0.99198227479479528 aa(lp65 F0.98373679881753995 aF0.99201114283815794 aa(lp66 F0.9841156039436737 aF0.99206014701073075 aa(lp67 F0.98449974695749243 aF0.9921274523216167 aa(lp68 F0.98488784916041072 aF0.99221132341990681 aa(lp69 F0.98527863452686493 aF0.99231012236432681 aa(lp70 F0.98567092501261966 aF0.99242230610990778 aa(lp71 F0.98606363591874813 aF0.99254642376752178 aa(lp72 F0.98645577134504292 aF0.99268111368485634 aa(lp73 F0.9868464197032879 aF0.99282510039149896 aa(lp74 F0.98723474935404176 aF0.99297719144510954 aa(lp75 F0.98762000434362762 aF0.99313627420424122 aa(lp76 F0.98800150024370847 aF0.99330131256525478 aa(lp77 F0.98837862012171895 aF0.99347134367277645 aa(lp78 F0.98875081063494685 aF0.99364547464183461 aa(lp79 F0.9891175782575542 aF0.99382287929463942 aa(lp80 F0.98947848561230323 aF0.99400279493290844 aa(lp81 F0.98983314797398536 aF0.99418451916454764 aa(lp82 F0.99018122987932689 aF0.99436740678346724 aa(lp83 F0.99052244187608363 aF0.99455086672274018 aa(lp84 F0.99085653741045632 aF0.99473435908357655 aa(lp85 F0.9911833098442957 aF0.99491739224317022 aa(lp86 F0.99150258960598014 aF0.99509952006519597 aa(lp87 F0.99181424147798558 aF0.99528033917708703 aa(lp88 F0.99211816197779912 aF0.99545948637231096 aa(lp89 F0.99241427693535011 aF0.9956366360886898 aa(lp90 F0.99270253912890138 aF0.99581149798828539 aa(lp91 F0.99298292603168037 aF0.99598381465649788 aa(lp92 F0.9932554378012225 aF0.99615335936604554 aa(lp93 F0.9935200951679426 aF0.9963199339719333 aa(lp94 F0.99377693767386133 aF0.99648336689146633 aa(lp95 F0.99402602183114031 aF0.99664351117523209 aa(lp96 F0.9942674194731228 aF0.99680024268829126 aa(lp97 F0.99450121622139998 aF0.99695345837142813 aa(lp98 F0.9947275099675188 aF0.99710307459833536 aa(lp99 F0.99494640955529112 aF0.99724902563009743 aa(lp100 F0.99515803344381792 aF0.99739126213311824 aa(lp101 F0.9953625085465615 aF0.9975297498161988 aa(lp102 F0.99555996912120581 aF0.99766446810201748 aa(lp103 F0.99575055568085402 aF0.99779540892946439 aa(lp104 F0.99593441410986883 aF0.99792257558007791 aa(lp105 F0.99611169468100003 aF0.99804598161389124 aa(lp106 F0.99628255133636701 aF0.99816564985680822 aa(lp107 F0.99644714082840857 aF0.99828161145379501 aa(lp108 F0.99660562209484582 aF0.99839390499170366 aa(lp109 F0.99675815556523384 aF0.99850257568360523 aa(lp110 F0.99690490263195819 aF0.99860767459365274 aa(lp111 F0.99704602506163942 aF0.99870925793994414 aa(lp112 F0.99718168451617684 aF0.99880738643355871 aa(lp113 F0.99731204216866309 aF0.99890212466872108 aa(lp114 F0.99743725821254536 aF0.99899354056354395 aa(lp115 F0.99755749159059415 aF0.999081704834756 aa(lp116 F0.99767289959731309 aF0.99916669053702201 aa(lp117 F0.99778363766789602 aF0.99924857260402333 aa(lp118 F0.99788985904895511 aF0.99932742746046155 aa(lp119 F0.9979917146353231 aF0.99940333264974635 aa(lp120 F0.99808935274584631 aF0.99947636650133653 aa(lp121 F0.99818291893363498 aF0.99954660782484661 aa(lp122 F0.99827255590137431 aF0.99961413562458901 aa(lp123 F0.99835840330587 aF0.99967902886955162 aa(lp124 F0.99844059772309335 aF0.99974136624458776 aa(lp125 F0.99851927249854144 aF0.9998012259641732 aa(lp126 F0.99859455771776373 aF0.99985868558095703 aa(lp127 F0.99866658018498844 aF0.99991382182852762 aa(lp128 F0.99873546333272933 aF0.99996671048013219 aa(lp129 F0.99880132718621828 aF1.0000174262103039 aa(lp130 F0.99886428843844 aF1.0000660425096228 aa(lp131 F0.99892446037764671 aF1.000112631552754 aa(lp132 F0.9989819528988243 aF1.0001572641574561 aa(lp133 F0.99903687258008123 aF1.000200009672054 aa(lp134 F0.99908932263276307 aF1.000240935951894 aa(lp135 F0.99913940299341142 aF1.0002801092848832 aa(lp136 F0.99918721031854218 aF1.0003175943684997 aa(lp137 F0.99923283813969788 aF1.0003534542846457 aa(lp138 F0.9992763766611078 aF1.0003877504670373 aa(lp139 F0.99931791322522945 aF1.0004205426570361 aa(lp140 F0.99935753185438414 aF1.0004518889597631 aa(lp141 F0.99939531383950875 aF1.0004818457890832 aa(lp142 F0.99943133740435797 aF1.0005104679012871 aa(lp143 F0.9994656779626867 aF1.0005378083695791 aa(lp144 F0.9994984081206526 aF1.0005639186232265 aa(lp145 F0.99952959778617068 aF1.000588848465888 aa(lp146 F0.99955931422629496 aF1.0006126460719647 aa(lp147 F0.99958762217826569 aF1.000635358020638 aa(lp148 F0.99961458381722379 aF1.0006570293003725 aa(lp149 F0.99964025885479368 aF1.0006777033948748 aa(lp150 F0.99966470475854186 aF1.0006974222922391 aa(lp151 F0.99968797640940121 aF1.0007162262801157 aa(lp152 F0.99971012806814086 aF1.0007341549505357 aa. ././@PaxHeader0000000000000000000000000000022500000000000010214 xustar00127 path=galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_-0.1000_151_5.0000_20.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_-0.1000_0000644000175100001710000001677114327773303031045 0ustar00runnerdocker(lp1 (lp2 F0.99677544871204127 aF0.99395736532893164 aa(lp3 F0.88646036087442226 aF0.73683434612162624 aa(lp4 F0.89962617516171006 aF0.73936115910065436 aa(lp5 F0.91463908106261449 aF0.76738809887890147 aa(lp6 F0.93775329648196493 aF0.80153419104566948 aa(lp7 F0.96047707659067005 aF0.84051483641812585 aa(lp8 F0.9868109875883011 aF0.88195710407597039 aa(lp9 F1.0124953837776709 aF0.92347345883050735 aa(lp10 F1.0348754784012875 aF0.96449059093064227 aa(lp11 F1.0544236847100423 aF1.002631613527019 aa(lp12 F1.0702884395962236 aF1.0366798637039389 aa(lp13 F1.0814451188079834 aF1.0665864581144047 aa(lp14 F1.0880458432529772 aF1.0921720659422012 aa(lp15 F1.0907968655790998 aF1.1131109881612418 aa(lp16 F1.0902776636131428 aF1.1293607827927454 aa(lp17 F1.0869196271175912 aF1.141207633329028 aa(lp18 F1.0811599584933678 aF1.149108500565347 aa(lp19 F1.0735006529741784 aF1.1535572089104498 aa(lp20 F1.0644728913619521 aF1.1550228785269476 aa(lp21 F1.0545705567028281 aF1.1539444268440759 aa(lp22 F1.0442056653665064 aF1.1507382737897791 aa(lp23 F1.033696911729872 aF1.1458021271205199 aa(lp24 F1.0232810190759063 aF1.1395081269373721 aa(lp25 F1.0131309024535109 aF1.13219348725736 aa(lp26 F1.0033719947608175 aF1.1241534326768767 aa(lp27 F0.99409373009181923 aF1.115637301424574 aa(lp28 F0.98535676326980204 aF1.1068500147474702 aa(lp29 F0.9771973396380923 aF1.0979553320667361 aa(lp30 F0.96963082591069549 aF1.0890818058036438 aa(lp31 F0.96265528377438525 aF1.0803287634038599 aa(lp32 F0.95625551104356266 aF1.0717718351693166 aa(lp33 F0.95040733746624928 aF1.0634680815839315 aa(lp34 F0.94508172511845601 aF1.0554597409968658 aa(lp35 F0.94024829100123641 aF1.0477775917725993 aa(lp36 F0.93587786470299905 aF1.0404433660680987 aa(lp37 F0.93194401565733886 aF1.0334716283564029 aa(lp38 F0.92842359156803578 aF1.0268712023399229 aa(lp39 F0.92529643334548983 aF1.02064621306673 aa(lp40 F0.92254455038503724 aF1.0147968193507517 aa(lp41 F0.92015102587229125 aF1.0093197646898189 aa(lp42 F0.91809890394021099 aF1.0042087883662236 aa(lp43 F0.91637028761029837 aF0.99945497436743658 aa(lp44 F0.91494578241288482 aF0.99504710871059776 aa(lp45 F0.91380435936258853 aF0.9909720919005488 aa(lp46 F0.91292364104422341 aF0.98721541236981702 aa(lp47 F0.91228053899786887 aF0.98376169278767134 aa(lp48 F0.91185214570744522 aF0.98059527190471263 aa(lp49 F0.91161673385125941 aF0.97770076356087376 aa(lp50 F0.91155468847651111 aF0.97506355217469232 aa(lp51 F0.91164924972733064 aF0.97267013435373695 aa(lp52 F0.91188691477956152 aF0.97050829021310703 aa(lp53 F0.91225743904117984 aF0.96856704747913458 aa(lp54 F0.91275342009756866 aF0.96683645488072245 aa(lp55 F0.91336952020684814 aF0.96530720842160544 aa(lp56 F0.91410144672973048 aF0.96397020298870117 aa(lp57 F0.91494485778537171 aF0.96281609786690092 aa(lp58 F0.91589438118955591 aF0.96183498471130702 aa(lp59 F0.91694292179043024 aF0.96101623048918172 aa(lp60 F0.91808138728144073 aF0.96034853491852012 aa(lp61 F0.91929888742580601 aF0.95982019918818162 aa(lp62 F0.92058337132293699 aF0.95941955831685266 aa(lp63 F0.92192257801663313 aF0.95913548993475051 aa(lp64 F0.92330510579385094 aF0.95895789298412604 aa(lp65 F0.92472137477474714 aF0.95887803085120604 aa(lp66 F0.92616427276392888 aF0.9588886594759104 aa(lp67 F0.92762933928522151 aF0.95898390769798081 aa(lp68 F0.92911444432902446 aF0.95915893378177342 aa(lp69 F0.93061903489609532 aF0.95940943523037914 aa(lp70 F0.93214312512059472 aF0.95973112604285526 aa(lp71 F0.9336862688042491 aF0.9601193044850016 aa(lp72 F0.93524675765722209 aF0.96056861110056335 aa(lp73 F0.93682122977147475 aF0.96107303161508306 aa(lp74 F0.93840476612396362 aF0.96162612868369324 aa(lp75 F0.93999142346785491 aF0.9622214328332358 aa(lp76 F0.9415750378582044 aF0.96285287802557884 aa(lp77 F0.94315006567956838 aF0.96351516036628648 aa(lp78 F0.94471222939304023 aF0.96420392595790672 aa(lp79 F0.94625880399510198 aF0.96491574817326231 aa(lp80 F0.94778849630580575 aF0.96564791703449249 aa(lp81 F0.94930099119240463 aF0.96639812020193361 aa(lp82 F0.95079633473912351 aF0.96716411877400421 aa(lp83 F0.9522743571430895 aF0.96794351304027915 aa(lp84 F0.95373430344594556 aF0.96873365429204794 aa(lp85 F0.95517475279132125 aF0.96953170523119969 aa(lp86 F0.95659380225947588 aF0.97033480246522097 aa(lp87 F0.95798940686775147 aF0.97114024767539597 aa(lp88 F0.95935973122282681 aF0.97194565671711031 aa(lp89 F0.9607033903589578 aF0.97274902299051236 aa(lp90 F0.96201951900823568 aF0.97354869002976319 aa(lp91 F0.96330768334868433 aF0.97434326197468724 aa(lp92 F0.96456770327276642 aF0.97513149706477853 aa(lp93 F0.9657994718261983 aF0.97591222483707696 aa(lp94 F0.96700283715822788 aF0.97668430831460096 aa(lp95 F0.9681775712848194 aF0.97744664938557457 aa(lp96 F0.96932340845231824 aF0.97819821927915973 aa(lp97 F0.97044011326738777 aF0.97893809155317313 aa(lp98 F0.9715275390981799 aF0.97966546126532894 aa(lp99 F0.9725856549092724 aF0.98037964532320576 aa(lp100 F0.97361453898871497 aF0.98108006883053067 aa(lp101 F0.97461435372724203 aF0.98176624657182021 aa(lp102 F0.97558531837204243 aF0.98243776731676069 aa(lp103 F0.97652769044744314 aF0.98309428436014912 aa(lp104 F0.97744175869522398 aF0.98373551142810023 aa(lp105 F0.97832784315532551 aF0.98436122119869951 aa(lp106 F0.97918629770300769 aF0.98497124378601097 aa(lp107 F0.98001751059645614 aF0.98556546391852062 aa(lp108 F0.98082190233651223 aF0.98614381694997322 aa(lp109 F0.98159992185307365 aF0.98670628438327268 aa(lp110 F0.98235204253183639 aF0.98725288951243817 aa(lp111 F0.98307875794088195 aF0.98778369360739549 aa(lp112 F0.98378057943729669 aF0.98829879246171404 aa(lp113 F0.98445803330147741 aF0.98879831330973622 aa(lp114 F0.98511165835662173 aF0.98928241183472976 aa(lp115 F0.9857420042221775 aF0.98975126942437119 aa(lp116 F0.98634962871313014 aF0.99020509041063121 aa(lp117 F0.98693509611770835 aF0.99064409956144928 aa(lp118 F0.98749897524293428 aF0.99106853973493703 aa(lp119 F0.98804183754915109 aF0.99147866952683505 aa(lp120 F0.98856425568546458 aF0.99187476135078356 aa(lp121 F0.98906680198249586 aF0.99225709931744033 aa(lp122 F0.98955004671870928 aF0.99262597758274385 aa(lp123 F0.99001455767187541 aF0.99298169854430851 aa(lp124 F0.9904608978374716 aF0.99332457134409036 aa(lp125 F0.99088962558127058 aF0.99365491042108856 aa(lp126 F0.99130129266113098 aF0.99397303415681304 aa(lp127 F0.99169644396616596 aF0.99427926366638431 aa(lp128 F0.99207561678622858 aF0.99457392169337255 aa(lp129 F0.99243933948550711 aF0.99485733154331879 aa(lp130 F0.99278813182187986 aF0.9951298161732568 aa(lp131 F0.99312250365636312 aF0.99539169736544586 aa(lp132 F0.99344295495625923 aF0.99564329488392755 aa(lp133 F0.99374997499262463 aF0.9958849258672523 aa(lp134 F0.99404404270462443 aF0.9961169041594049 aa(lp135 F0.99432562539989067 aF0.99633953973515077 aa(lp136 F0.99459517969449274 aF0.99655313828995051 aa(lp137 F0.99485315038346711 aF0.99675800066727216 aa(lp138 F0.99509997076446588 aF0.99695442264651923 aa(lp139 F0.99533606275242403 aF0.99714269445800263 aa(lp140 F0.9955618362236015 aF0.99732310060805374 aa(lp141 F0.99577768971519132 aF0.99749591957779338 aa(lp142 F0.99598400980331014 aF0.99766142365089616 aa(lp143 F0.99618117163849285 aF0.99781987874439038 aa(lp144 F0.99636953902097147 aF0.99797154427655177 aa(lp145 F0.99654946385413068 aF0.99811667303924934 aa(lp146 F0.99672128752505029 aF0.99825551126432488 aa(lp147 F0.99688533891813114 aF0.99838829822665343 aa(lp148 F0.99704193889667792 aF0.99851526704535032 aa(lp149 F0.99719139130644596 aF0.99863664271593111 aa(lp150 F0.99733400237606196 aF0.99875264692364352 aa(lp151 F0.9974700404896486 aF0.99886348637090705 aa(lp152 F0.99759982842415684 aF0.99896938444039463 aa. ././@PaxHeader0000000000000000000000000000022500000000000010214 xustar00127 path=galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_-0.2000_151_5.0000_20.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_-0.2000_0000644000175100001710000001676414327773303031050 0ustar00runnerdocker(lp1 (lp2 F1.0000858990781711 aF1.0009931120387563 aa(lp3 F0.92592625981440391 aF0.82915993730691895 aa(lp4 F0.92484810309519694 aF0.81036237748218898 aa(lp5 F0.93215049094983893 aF0.81693795559167459 aa(lp6 F0.94717065042472082 aF0.8348506199458684 aa(lp7 F0.9628148770489442 aF0.85838549729896962 aa(lp8 F0.98133738455532582 aF0.88589232003534057 aa(lp9 F1.0012354555390812 aF0.91593212278239478 aa(lp10 F1.0210091016330058 aF0.94740567208563753 aa(lp11 F1.0396550165783913 aF0.97870747756534071 aa(lp12 F1.0559417090487662 aF1.0086787691738917 aa(lp13 F1.0690380098899153 aF1.036497187211266 aa(lp14 F1.0786543234850154 aF1.0614656697565708 aa(lp15 F1.084767591287557 aF1.0830959971653791 aa(lp16 F1.0874914474825195 aF1.1011599684854467 aa(lp17 F1.0870766343343869 aF1.1156350609179646 aa(lp18 F1.083897614403686 aF1.1266329116462028 aa(lp19 F1.0784030725150133 aF1.1343553545369878 aa(lp20 F1.0710624579572523 aF1.1390682382787898 aa(lp21 F1.0623259788109358 aF1.1410821423061843 aa(lp22 F1.0526023937130573 aF1.1407343648607509 aa(lp23 F1.0422498857807942 aF1.1383709966579953 aa(lp24 F1.0315742364958758 aF1.1343313049091084 aa(lp25 F1.0208301637995072 aF1.1289353888877294 aa(lp26 F1.0102239798892003 aF1.1224761394088709 aa(lp27 F0.99991693462714126 aF1.1152144836370075 aa(lp28 F0.99002929939506901 aF1.1073778710721967 aa(lp29 F0.98064504353764659 aF1.0991604555323682 aa(lp30 F0.97181697438948411 aF1.0907251254769623 aa(lp31 F0.96357219273101224 aF1.0822059118870762 aa(lp32 F0.95591753618287689 aF1.0737113664337667 aa(lp33 F0.94884475979894556 aF1.065327742892324 aa(lp34 F0.94233527961247232 aF1.0571224152773353 aa(lp35 F0.93636425165848813 aF1.0491469482259146 aa(lp36 F0.93090394870858684 aF1.0414401691135926 aa(lp37 F0.92592634604204771 aF1.034030790394318 aa(lp38 F0.92140493290182002 aF1.0269395481713119 aa(lp39 F0.9173157854533317 aF1.0201809907933368 aa(lp40 F0.91363797572020677 aF1.0137648568048998 aa(lp41 F0.91035343738885355 aF1.0076970627548987 aa(lp42 F0.90744639332120358 aF1.0019803332807939 aa(lp43 F0.90490249420717128 aF0.99661455003024246 aa(lp44 F0.90270782032931673 aF0.9915969302005383 aa(lp45 F0.90084790016318972 aF0.98692212389988732 aa(lp46 F0.8993068824897883 aF0.9825822949652897 aa(lp47 F0.89806697287952031 aF0.97856726919809778 aa(lp48 F0.89710822544828206 aF0.97486480214641635 aa(lp49 F0.89640872316402964 aF0.97146099485133386 aa(lp50 F0.89594514765215116 aF0.96834084273887644 aa(lp51 F0.89569367284358337 aF0.96548887828061647 aa(lp52 F0.89563106992865449 aF0.96288985531440474 aa(lp53 F0.89573588242129276 aF0.96052939872708132 aa(lp54 F0.89598950779516517 aF0.95839453477129166 aa(lp55 F0.89637701980193529 aF0.95647402805669046 aa(lp56 F0.89688759887513314 aF0.95475846254522223 aa(lp57 F0.89751444837332994 aF0.95324008083877521 aa(lp58 F0.89825422868851101 aF0.9519123440142776 aa(lp59 F0.89910600986956268 aF0.95076932991806118 aa(lp60 F0.90006990845501911 aF0.94980502764040908 aa(lp61 F0.90114559160854946 aF0.949012657417868 aa(lp62 F0.90233088146357288 aF0.94838413556918388 aa(lp63 F0.90362069193106276 aF0.94790978848194285 aa(lp64 F0.90500649032671732 aF0.94757838286831386 aa(lp65 F0.90647639888431786 aF0.94737748736325766 aa(lp66 F0.9080159467624358 aF0.94729411935194463 aa(lp67 F0.909609365275445 aF0.9473155730273467 aa(lp68 F0.91124121064080466 aF0.94743028123938988 aa(lp69 F0.91289802123242692 aF0.94762854729631296 aa(lp70 F0.91456969244455788 aF0.94790299648157261 aa(lp71 F0.91625028909334605 aF0.94824864514304696 aa(lp72 F0.9179381153578734 aF0.94866255963835922 aa(lp73 F0.9196350074659877 aF0.94914316215827332 aa(lp74 F0.92134497815981342 aF0.94968931946757162 aa(lp75 F0.92307248706285461 aF0.95029940346945141 aa(lp76 F0.92482070229987057 aF0.95097052501673496 aa(lp77 F0.92659013013873903 aF0.95169810857649428 aa(lp78 F0.92837791273428627 aF0.95247589983036196 aa(lp79 F0.93017794206420301 aF0.95329639862586779 aa(lp80 F0.93198174499783559 aF0.95415160575175528 aa(lp81 F0.93377990755927942 aF0.95503389839406172 aa(lp82 F0.93556367341747637 aF0.95593681157677146 aa(lp83 F0.93732631339280492 aF0.95685553859508177 aa(lp84 F0.93906393208492511 aF0.95778703672758725 aa(lp85 F0.94077553840022043 aF0.95872973526120253 aa(lp86 F0.94246241359038385 aF0.95968295101278478 aa(lp87 F0.94412700312812747 aF0.96064619116978578 aa(lp88 F0.94577167802831263 aF0.96161854260718316 aa(lp89 F0.94739772256771937 aF0.96259830594653095 aa(lp90 F0.94900480734914294 aF0.96358294691232127 aa(lp91 F0.95059103630097108 aF0.96456933522379773 aa(lp92 F0.95215347159856656 aF0.96555415648527732 aa(lp93 F0.95368890271435192 aF0.96653434449433828 aa(lp94 F0.95519458050024275 aF0.96750739628229343 aa(lp95 F0.95666869061624216 aF0.96847149090098228 aa(lp96 F0.95811046297309199 aF0.96942541140842509 aa(lp97 F0.95951995531215006 aF0.9703683357901367 aa(lp98 F0.96089765414427619 aF0.97129959407431909 aa(lp99 F0.96224407038568782 aF0.9722184801891125 aa(lp100 F0.96355946975364981 aF0.9731241674084441 aa(lp101 F0.96484379559715994 aF0.9740157266786309 aa(lp102 F0.96609675572662712 aF0.97489220958884881 aa(lp103 F0.96731799054064027 aF0.97575274376002141 aa(lp104 F0.96850723193086774 aF0.97659659935638143 aa(lp105 F0.96966439360758727 aF0.97742320936697313 aa(lp106 F0.97078958034579188 aF0.97823215052625834 aa(lp107 F0.97188304046578788 aF0.97902310514220148 aa(lp108 F0.97294510096044562 aF0.97979582480732408 aa(lp109 F0.97397611744130508 aF0.98055010818787047 aa(lp110 F0.97497645194226323 aF0.9812857943176625 aa(lp111 F0.9759464743587779 aF0.98200276552417431 aa(lp112 F0.97688657473981944 aF0.98270095260091173 aa(lp113 F0.97779717415658562 aF0.98338033715003592 aa(lp114 F0.97867872891173791 aF0.98404094980391632 aa(lp115 F0.97953172783710263 aF0.98468286572462593 aa(lp116 F0.98035668660494335 aF0.98530619926563479 aa(lp117 F0.98115414178941562 aF0.98591109927680154 aa(lp118 F0.98192464655103251 aF0.9864977454196765 aa(lp119 F0.98266876778626688 aF0.98706634528223858 aa(lp120 F0.98338708425429233 aF0.98761713182019284 aa(lp121 F0.98408018474882419 aF0.98815036091420316 aa(lp122 F0.98474866614353418 aF0.98866630894690177 aa(lp123 F0.98539313174677223 aF0.98916527041889646 aa(lp124 F0.98601418910744565 aF0.98964755566338225 aa(lp125 F0.98661244848675667 aF0.99011348875504357 aa(lp126 F0.98718852098216114 aF0.99056340548150923 aa(lp127 F0.98774301727371638 aF0.99099765154561759 aa(lp128 F0.98827654602166215 aF0.99141658081619322 aa(lp129 F0.98878971284005424 aF0.99182055372822564 aa(lp130 F0.9892831187889527 aF0.99220993583858685 aa(lp131 F0.98975735974369528 aF0.99258509640592607 aa(lp132 F0.99021302505285635 aF0.99294640711170412 aa(lp133 F0.99065069675400041 aF0.9932942409886899 aa(lp134 F0.99107094910045135 aF0.99362897119223825 aa(lp135 F0.99147434720927563 aF0.99395097017858558 aa(lp136 F0.99186144710697199 aF0.99426060867345556 aa(lp137 F0.99223279477219095 aF0.99455825499532446 aa(lp138 F0.99258892577109248 aF0.99484427415461518 aa(lp139 F0.992930364817454 aF0.99511902739981606 aa(lp140 F0.99325762548473473 aF0.99538287139791648 aa(lp141 F0.99357120981467673 aF0.99563615792095306 aa(lp142 F0.99387160816556774 aF0.99587923319599292 aa(lp143 F0.9941592990078989 aF0.99611243763140822 aa(lp144 F0.99443474866892967 aF0.99633610541239825 aa(lp145 F0.99469841119797753 aF0.99655056405971576 aa(lp146 F0.99495072949465879 aF0.99675613466969426 aa(lp147 F0.99519213094322101 aF0.99695313041127853 aa(lp148 F0.99542303867936555 aF0.9971418592991278 aa(lp149 F0.99564384626772662 aF0.99732261755362794 aa(lp150 F0.99585497067822581 aF0.99749570351415806 aa(lp151 F0.99605674666621391 aF0.9976613870776827 aa(lp152 F0.99624963843319314 aF0.99781998103821667 aa. ././@PaxHeader0000000000000000000000000000022400000000000010213 xustar00126 path=galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_151_5.0000_20.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_10000644000175100001710000001676714327773303031055 0ustar00runnerdocker(lp1 (lp2 F0.81121001069551002 aF0.53887054250829214 aa(lp3 F0.83123467594310818 aF0.58232434668183275 aa(lp4 F0.85741754566491302 aF0.63492574587228501 aa(lp5 F0.88852858677391455 aF0.6952153138029018 aa(lp6 F0.92448155468179383 aF0.75799806098044054 aa(lp7 F0.96164832172117032 aF0.82076087705692746 aa(lp8 F0.99662352877336979 aF0.88175223265275693 aa(lp9 F1.0279084852263898 aF0.93886395247454524 aa(lp10 F1.0540171669520766 aF0.99055964866171131 aa(lp11 F1.0739069407135364 aF1.0361634273401459 aa(lp12 F1.0876558205623683 aF1.0751592198534554 aa(lp13 F1.0958932587589427 aF1.1072320423048643 aa(lp14 F1.0992908932072052 aF1.1325027171090165 aa(lp15 F1.0985315448294117 aF1.1514554782948649 aa(lp16 F1.0943783761386361 aF1.1647296698493419 aa(lp17 F1.0876218913035913 aF1.1730120644469149 aa(lp18 F1.0789982459905336 aF1.177000083624947 aa(lp19 F1.0691392963956363 aF1.1773839132898589 aa(lp20 F1.0585608585453341 aF1.1748255820180813 aa(lp21 F1.0476723649729027 aF1.1699375967893009 aa(lp22 F1.0367921261386066 aF1.163266222842114 aa(lp23 F1.0261608170657035 aF1.1552830874245807 aa(lp24 F1.0159524304105039 aF1.1463852928204767 aa(lp25 F1.006284600391977 aF1.1368995740815366 aa(lp26 F0.99722871070790176 aF1.1270899524349549 aa(lp27 F0.98881953356348595 aF1.1171648575434061 aa(lp28 F0.98106450637754028 aF1.1072862639474139 aa(lp29 F0.97395177385349518 aF1.0975763710116082 aa(lp30 F0.96745697879129888 aF1.0881258658616211 aa(lp31 F0.96154881196479913 aF1.0789997851859932 aa(lp32 F0.95619317027836581 aF1.0702434037062689 aa(lp33 F0.951356069553566 aF1.0618867974299051 aa(lp34 F0.94700549475269191 aF1.0539487023991467 aa(lp35 F0.94311230375217392 aF1.0464393238208149 aa(lp36 F0.93965035115309947 aF1.0393624407070201 aa(lp37 F0.93659603646305845 aF1.032716859439087 aa(lp38 F0.93392747061447801 aF1.0264973825625918 aa(lp39 F0.93162350249514492 aF1.0206954792595908 aa(lp40 F0.92966284019897205 aF1.0152997679649782 aa(lp41 F0.92802339404954193 aF1.0102964628112021 aa(lp42 F0.92668202321527526 aF1.0056698195222078 aa(lp43 F0.9256146866574565 aF1.0014026431717442 aa(lp44 F0.92479696849155713 aF0.99747687305656763 aa(lp45 F0.92420489179699061 aF0.99387417603438988 aa(lp46 F0.92381584601551958 aF0.99057652413374786 aa(lp47 F0.9236094603274958 aF0.98756667595134207 aa(lp48 F0.92356824804562032 aF0.98482851014885209 aa(lp49 F0.92367789639458375 aF0.9823471688370049 aa(lp50 F0.92392713645851343 aF0.98010900328843353 aa(lp51 F0.92430720959488322 aF0.9781013477347128 aa(lp52 F0.92481102505478963 aF0.9763121786886878 aa(lp53 F0.92543216548963958 aF0.97472973911805305 aa(lp54 F0.92616392976370732 aF0.97334221385112818 aa(lp55 F0.92699859841493459 aF0.97213753174100437 aa(lp56 F0.92792706433470096 aF0.97110334091690764 aa(lp57 F0.92893889437307198 aF0.97022716403699305 aa(lp58 F0.93002279630181861 aF0.96949669804293592 aa(lp59 F0.93116737501264735 aF0.96890018689876978 aa(lp60 F0.93236199597643077 aF0.96842677626804929 aa(lp61 F0.93359754845242326 aF0.96806676179600615 aa(lp62 F0.9348669247239485 aF0.96781166834968868 aa(lp63 F0.93616510283972953 aF0.96765413892997654 aa(lp64 F0.93748881997624522 aF0.96758766133667451 aa(lp65 F0.93883592538124871 aF0.96760620501850891 aa(lp66 F0.94020459373632304 aF0.96770385735525288 aa(lp67 F0.94159259336004952 aF0.96787455941426803 aa(lp68 F0.94299680603603842 aF0.96811199521559677 aa(lp69 F0.94441310028520709 aF0.96840965709434013 aa(lp70 F0.94583656468296151 aF0.96876105090204423 aa(lp71 F0.94726200152149365 aF0.96915996843894581 aa(lp72 F0.94868451142984511 aF0.96960073891921461 aa(lp73 F0.95009998726708667 aF0.97007838113777922 aa(lp74 F0.95150537351140596 aF0.97058862088358322 aa(lp75 F0.95289863997170288 aF0.97112777789627758 aa(lp76 F0.95427851150780052 aF0.97169257199710957 aa(lp77 F0.95564407125951778 aF0.97227991793079194 aa(lp78 F0.95699438225956812 aF0.97288677332576834 aa(lp79 F0.95832824591711752 aF0.97351007739559514 aa(lp80 F0.95964415063999176 aF0.97414678101084728 aa(lp81 F0.96094038836785567 aF0.97479393668176006 aa(lp82 F0.96221526066600938 aF0.97544880096551634 aa(lp83 F0.96346727846207547 aF0.97610890661410366 aa(lp84 F0.9646952814004679 aF0.97677208224013201 aa(lp85 F0.96589845011555986 aF0.97743642229982708 aa(lp86 F0.96707623276078614 aF0.97810022866151858 aa(lp87 F0.96822823546817616 aF0.97876195094781016 aa(lp88 F0.96935412775945839 aF0.97942014610985983 aa(lp89 F0.9704535934413675 aF0.98007346402138262 aa(lp90 F0.97152632947460693 aF0.98072065312970813 aa(lp91 F0.97257207413193436 aF0.98136057371174468 aa(lp92 F0.97359063964428227 aF0.98199220730516967 aa(lp93 F0.97458193097462009 aF0.98261465687885807 aa(lp94 F0.97554594599817246 aF0.98322713853022503 aa(lp95 F0.97648276242149001 aF0.98382896912104389 aa(lp96 F0.97739252097086249 aF0.98441955439203432 aa(lp97 F0.97827541202315071 aF0.98499838006890761 aa(lp98 F0.97913166824744113 aF0.98556500604538155 aa(lp99 F0.97996156176121252 aF0.98611906227965473 aa(lp100 F0.98076540290994696 aF0.9866602450614117 aa(lp101 F0.98154353854643894 aF0.98718831283309283 aa(lp102 F0.98229634910907893 aF0.98770308159560627 aa(lp103 F0.98302424480213135 aF0.98820442016284415 aa(lp104 F0.98372766183569749 aF0.98869224567583935 aa(lp105 F0.98440705882728641 aF0.98916651939735922 aa(lp106 F0.98506291378895428 aF0.98962724289772952 aa(lp107 F0.98569572131562311 aF0.9900744544500012 aa(lp108 F0.98630599006600617 aF0.99050822567379826 aa(lp109 F0.98689424032821371 aF0.99092865832367205 aa(lp110 F0.98746100178256124 aF0.99133588128851902 aa(lp111 F0.98800681131630219 aF0.99173004773108875 aa(lp112 F0.98853221102613498 aF0.99211133244750083 aa(lp113 F0.98903774644605014 aF0.99247992938233176 aa(lp114 F0.98952396475524129 aF0.99283604929341396 aa(lp115 F0.98999141328003026 aF0.99317991761182411 aa(lp116 F0.99044063802110283 aF0.99351177242708422 aa(lp117 F0.99087218237659713 aF0.99383186261787659 aa(lp118 F0.99128658589001784 aF0.99414044613589314 aa(lp119 F0.99168438324424746 aF0.99443778841165986 aa(lp120 F0.99206610321284683 aF0.99472416087581106 aa(lp121 F0.99243226781460692 aF0.99499983962413119 aa(lp122 F0.99278339154131512 aF0.99526510416290381 aa(lp123 F0.99311998063669571 aF0.99552023629092723 aa(lp124 F0.99344253251108683 aF0.99576551904954358 aa(lp125 F0.99375153519400117 aF0.99600123580472433 aa(lp126 F0.99404746686936662 aF0.99622766937084561 aa(lp127 F0.9943307955336691 aF0.99644510126539076 aa(lp128 F0.99460197860350563 aF0.9966538109981159 aa(lp129 F0.99486146268996434 aF0.99685407546391425 aa(lp130 F0.9951096833852332 aF0.99704616838957583 aa(lp131 F0.99534706510556947 aF0.99723035984449138 aa(lp132 F0.99557402092697578 aF0.99740691579617458 aa(lp133 F0.99579095261900064 aF0.99757609777239675 aa(lp134 F0.99599825047326185 aF0.99773816248015235 aa(lp135 F0.99619629345915972 aF0.99789336158328934 aa(lp136 F0.99638544908975346 aF0.99804194141067348 aa(lp137 F0.9965660736951838 aF0.99818414281701573 aa(lp138 F0.99673851225516319 aF0.99832020095326668 aa(lp139 F0.99690309882080397 aF0.99845034521765008 aa(lp140 F0.997060156312779 aF0.99857479907620594 aa(lp141 F0.9972099970011622 aF0.99869378006502929 aa(lp142 F0.99735292241558871 aF0.99880749968208604 aa(lp143 F0.99748922371059168 aF0.99891616342525835 aa(lp144 F0.99761918175092312 aF0.99901997072964777 aa(lp145 F0.99774306734029305 aF0.99911911503761897 aa(lp146 F0.99786114159834249 aF0.99921378380935044 aa(lp147 F0.9979736556948795 aF0.9993041585144401 aa(lp148 F0.99808085230194754 aF0.9993904149363747 aa(lp149 F0.998182962858287 aF0.99947272255224506 aa(lp150 F0.99828021476543094 aF0.99955124645075155 aa(lp151 F0.99837281548571366 aF0.99962614255519 aa(lp152 F0.99846099449649439 aF0.99969757236002277 aa. ././@PaxHeader0000000000000000000000000000022400000000000010213 xustar00126 path=galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.1000_151_5.0000_20.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.1000_10000644000175100001710000001700014327773303031033 0ustar00runnerdocker(lp1 (lp2 F0.16937204089567359 aF0.13775002593966373 aa(lp3 F0.74299252316216979 aF0.35134364932722867 aa(lp4 F0.78211600801581249 aF0.51393590177564519 aa(lp5 F0.86226398082828493 aF0.6002743236325081 aa(lp6 F0.89838581750328272 aF0.71574587933567024 aa(lp7 F0.97396806328022389 aF0.79475001767707321 aa(lp8 F1.0071910055553313 aF0.89231290367060567 aa(lp9 F1.0487738293111499 aF0.9694742019620135 aa(lp10 F1.0845870964988373 aF1.0288166718012921 aa(lp11 F1.0996225912944313 aF1.0859979770429751 aa(lp12 F1.1069084198246404 aF1.1337037195071717 aa(lp13 F1.1125414804023823 aF1.1666816694043047 aa(lp14 F1.1140976770599968 aF1.1878390082883543 aa(lp15 F1.1096049690910978 aF1.2015910622504948 aa(lp16 F1.1002316296409307 aF1.2101415736413841 aa(lp17 F1.0884829628272807 aF1.2138871664609712 aa(lp18 F1.0762406881763062 aF1.2129669332433302 aa(lp19 F1.0643057956697493 aF1.2079441613060697 aa(lp20 F1.0527475937116537 aF1.1998059276368775 aa(lp21 F1.0414298226371159 aF1.1896336610101617 aa(lp22 F1.0303162315938919 aF1.1783368987910487 aa(lp23 F1.0195395717191436 aF1.1665417991655955 aa(lp24 F1.0093237466124669 aF1.1546191083491275 aa(lp25 F0.99987730008966536 aF1.1427677191170207 aa(lp26 F0.99132262034808327 aF1.1310981082502247 aa(lp27 F0.98367696538091676 aF1.1196896647740777 aa(lp28 F0.97687442490429677 aF1.1086172338821583 aa(lp29 F0.97080640141921204 aF1.0979549259282906 aa(lp30 F0.96536057286275068 aF1.0877695307870561 aa(lp31 F0.96044676305229415 aF1.0781124890880318 aa(lp32 F0.95600683118735008 aF1.0690152171623335 aa(lp33 F0.95201149661855911 aF1.0604891527447562 aa(lp34 F0.94844985249573455 aF1.0525293052614615 aa(lp35 F0.94531734145877333 aF1.0451193672173877 aa(lp36 F0.94260617920221046 aF1.0382366639009901 aa(lp37 F0.94030012070212743 aF1.0318559577743138 aa(lp38 F0.93837354997951383 aF1.0259517577698516 aa(lp39 F0.93679368278043618 aF1.020499294722423 aa(lp40 F0.93552422889925535 aF1.0154746734997975 aa(lp41 F0.9345291077805945 aF1.0108547002298323 aa(lp42 F0.93377533567653004 aF1.0066167010151252 aa(lp43 F0.93323469228058775 aF1.0027385212325977 aa(lp44 F0.93288424305855144 aF0.99919870565039914 aa(lp45 F0.93270601023819444 aF0.99597676335828378 aa(lp46 F0.93268613391663291 aF0.99305339173401375 aa(lp47 F0.93281380544533488 aF0.99041056149747575 aa(lp48 F0.93308015883254425 aF0.98803142681513445 aa(lp49 F0.93347723176275366 aF0.98590008533958495 aa(lp50 F0.9339970683288904 aF0.98400126135266142 aa(lp51 F0.93463103143392956 aF0.98232001101449573 aa(lp52 F0.93536940008867886 aF0.98084153848687039 aa(lp53 F0.93620131222723368 aF0.97955117600675312 aa(lp54 F0.93711506708876746 aF0.97843453317247231 aa(lp55 F0.93809873009907485 aF0.97747777028314053 aa(lp56 F0.93914090678053996 aF0.97666791355593785 aa(lp57 F0.94023149824116103 aF0.97599311755154816 aa(lp58 F0.94136224445325523 aF0.9754427952040472 aa(lp59 F0.94252691266509359 aF0.97500757174380437 aa(lp60 F0.94372107971001795 aF0.97467907847374413 aa(lp61 F0.94494157748095686 aF0.97444963469937584 aa(lp62 F0.94618575964632778 aF0.97431191477563339 aa(lp63 F0.94745080153425032 aF0.97425868648896674 aa(lp64 F0.94873322454537579 aF0.97428268723821154 aa(lp65 F0.9500287582481256 aF0.97437665830467846 aa(lp66 F0.95133254662328004 aF0.97453350257276194 aa(lp67 F0.95263958940649851 aF0.97474650158492526 aa(lp68 F0.95394525080745307 aF0.97500950692116295 aa(lp69 F0.95524565906980474 aF0.97531704180857237 aa(lp70 F0.95653787698859682 aF0.97566428569031083 aa(lp71 F0.95781981697838248 aF0.97604695803722263 aa(lp72 F0.95908996699826277 aF0.97646115113407883 aa(lp73 F0.96034705036849766 aF0.97690317339641264 aa(lp74 F0.96158974781560325 aF0.97736945118657526 aa(lp75 F0.96281656481610789 aF0.97785650736321883 aa(lp76 F0.96402585762579662 aF0.9783610026521713 aa(lp77 F0.96521596767539974 aF0.97887980491186799 aa(lp78 F0.96638538302223465 aF0.97941004826221112 aa(lp79 F0.96753285355942875 aF0.97994915683978767 aa(lp80 F0.96865742378549502 aF0.9804948287871621 aa(lp81 F0.96975839013213283 aF0.98104499398994105 aa(lp82 F0.97083522035256564 aF0.98159776652509279 aa(lp83 F0.97188747764115802 aF0.98215140891005837 aa(lp84 F0.97291477704236384 aF0.98270431504885836 aa(lp85 F0.97391677850021219 aF0.98325500827495071 aa(lp86 F0.97489320244183941 aF0.98380214557114887 aa(lp87 F0.97584384971921956 aF0.98434451977189485 aa(lp88 F0.97676861155302241 aF0.98488105570953022 aa(lp89 F0.97766746686915051 aF0.98541080094575773 aa(lp90 F0.97854047082581097 aF0.9859329142408173 aa(lp91 F0.97938774164167464 aF0.98644665488759375 aa(lp92 F0.98020945038412388 aF0.98695137416039347 aa(lp93 F0.98100581500869366 aF0.98744650882839058 aa(lp94 F0.98177709675374869 aF0.98793157570305634 aa(lp95 F0.98252359751186036 aF0.98840616635464773 aa(lp96 F0.98324565670302611 aF0.98886994173050768 aa(lp97 F0.98394364729907458 aF0.9893226267157621 aa(lp98 F0.98461797202716939 aF0.98976400487172034 aa(lp99 F0.98526905947882959 aF0.99019391346542607 aa(lp100 F0.98589736043400966 aF0.99061223888588312 aa(lp101 F0.98650334510101534 aF0.9910189123414086 aa(lp102 F0.98708749984667155 aF0.99141390583764677 aa(lp103 F0.98765032473468306 aF0.99179722840952089 aa(lp104 F0.98819233093387093 aF0.99216892253720113 aa(lp105 F0.98871403827832927 aF0.9925290608422479 aa(lp106 F0.98921597321559485 aF0.99287774294670506 aa(lp107 F0.98969866675561691 aF0.99321509258337715 aa(lp108 F0.99016265236365542 aF0.9935412548536936 aa(lp109 F0.99060846472104147 aF0.99385639377482882 aa(lp110 F0.99103663772556239 aF0.99416068984979267 aa(lp111 F0.99144770327544185 aF0.99445433797709193 aa(lp112 F0.99184218993207507 aF0.99473754541713777 aa(lp113 F0.99222062184549931 aF0.99501052993250072 aa(lp114 F0.99258351742121431 aF0.99527351810204978 aa(lp115 F0.99293138881064413 aF0.9955267437357247 aa(lp116 F0.99326474056893344 aF0.99577044646958845 aa(lp117 F0.99358406948608391 aF0.99600487041878039 aa(lp118 F0.99388986335168561 aF0.99623026297467754 aa(lp119 F0.99418260081069254 aF0.99644687377382268 aa(lp120 F0.9944627507944972 aF0.99665495361642087 aa(lp121 F0.99473077186769077 aF0.99685475365844867 aa(lp122 F0.99498711229101877 aF0.99704652453140585 aa(lp123 F0.99523220939683021 aF0.99723051567973275 aa(lp124 F0.99546648956572414 aF0.99740697465607564 aa(lp125 F0.99569036794522436 aF0.99757614659498017 aa(lp126 F0.9959042484680013 aF0.99773827366395018 aa(lp127 F0.99610852358943591 aF0.99789359462397276 aa(lp128 F0.99630357452618179 aF0.99804234445314299 aa(lp129 F0.99648977085206947 aF0.99818475396798956 aa(lp130 F0.99666747110098364 aF0.99832104958946111 aa(lp131 F0.99683702229568316 aF0.99845145301001037 aa(lp132 F0.99699876036885271 aF0.99857618107560875 aa(lp133 F0.99715301020288383 aF0.99869544553726364 aa(lp134 F0.99730008569064754 aF0.99880945296850099 aa(lp135 F0.99744029002478096 aF0.99891840462092762 aa(lp136 F0.99757391593314571 aF0.99902249636497531 aa(lp137 F0.99770124554779194 aF0.99912191863437005 aa(lp138 F0.99782255093943151 aF0.99921685641291325 aa(lp139 F0.99793809426500568 aF0.99930748914663703 aa(lp140 F0.99804812788483166 aF0.99939399086442282 aa(lp141 F0.99815289471402291 aF0.99947653013431192 aa(lp142 F0.99825262831364459 aF0.99955527010704759 aa(lp143 F0.99834755322210167 aF0.9996303686029403 aa(lp144 F0.99843788542737566 aF0.99970197812850581 aa(lp145 F0.99852383209173323 aF0.99977024600103415 aa(lp146 F0.99860559236909008 aF0.99983531440926221 aa(lp147 F0.99868335709890532 aF0.99989732047407553 aa(lp148 F0.99875730958747255 aF0.99995639645412626 aa(lp149 F0.99882762530043767 aF1.0000126695695752 aa(lp150 F0.99889447366986817 aF1.0000662629012498 aa(lp151 F0.99895801261105854 aF1.0001172932647273 aa(lp152 F0.99901840797093222 aF1.0001658784385667 aa. ././@PaxHeader0000000000000000000000000000022400000000000010213 xustar00126 path=galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.2000_151_5.0000_20.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.2000_10000644000175100001710000001676514327773303031055 0ustar00runnerdocker(lp1 (lp2 F0.26242761788829444 aF0.00088007207645892434 aa(lp3 F0.63700147756369196 aF0.17378090987737763 aa(lp4 F0.67378664145500156 aF0.34752932361499389 aa(lp5 F0.79958867839240455 aF0.4891112012266029 aa(lp6 F0.88447428705898956 aF0.64594632189379064 aa(lp7 F0.97163689065021963 aF0.78988975557470986 aa(lp8 F1.0426519440522342 aF0.91226835412180018 aa(lp9 F1.0821797817897225 aF1.026566479786676 aa(lp10 F1.1150321662686531 aF1.1128827500581593 aa(lp11 F1.1375267886141442 aF1.1733864110627992 aa(lp12 F1.1426567864111834 aF1.2204484385059677 aa(lp13 F1.1377685279163852 aF1.2544886378588644 aa(lp14 F1.1299880423511746 aF1.2733425765608386 aa(lp15 F1.1206016603607138 aF1.2792673547095117 aa(lp16 F1.1088889437880416 aF1.2765651576274493 aa(lp17 F1.0947716305743265 aF1.2689576754085563 aa(lp18 F1.0794351376740476 aF1.2583909367843373 aa(lp19 F1.0643594185720662 aF1.2456376326666228 aa(lp20 F1.0505155879464083 aF1.2311313082681241 aa(lp21 F1.0381830198813726 aF1.2154062741641571 aa(lp22 F1.0271996757476869 aF1.1991039379769868 aa(lp23 F1.0172601809811557 aF1.1828468274484401 aa(lp24 F1.0081250118239242 aF1.1671125767676069 aa(lp25 F0.99969569342365217 aF1.1521757359254683 aa(lp26 F0.99197806450364845 aF1.1381340249917624 aa(lp27 F0.98500967101480563 aF1.1249752406652469 aa(lp28 F0.97880315518833838 aF1.1126430213937841 aa(lp29 F0.97332540318786465 aF1.1010809257255845 aa(lp30 F0.96850666421823661 aF1.0902503434096884 aa(lp31 F0.96426334007791936 aF1.080130773720106 aa(lp32 F0.96051984901149823 aF1.0707112104462648 aa(lp33 F0.95722105652188982 aF1.0619801117502474 aa(lp34 F0.95433315590863121 aF1.0539186011018886 aa(lp35 F0.95183626549712508 aF1.0464983482193848 aa(lp36 F0.94971434954523093 aF1.0396831856080386 aa(lp37 F0.94794728345444035 aF1.0334325167969283 aa(lp38 F0.94650744321512792 aF1.0277048820615773 aa(lp39 F0.94536081171494624 aF1.0224606757157155 aa(lp40 F0.94447093231081192 aF1.0176637111858764 aa(lp41 F0.94380357743287957 aF1.0132817603705186 aa(lp42 F0.94333035937323118 aF1.0092863430367132 aa(lp43 F0.9430303057089664 aF1.0056520999084866 aa(lp44 F0.94288932496494449 aF1.0023560122811812 aa(lp45 F0.94289815768943641 aF0.99937665097759065 aa(lp46 F0.94304971896661816 aF0.99669356063596659 aa(lp47 F0.94333668943255222 aF0.99428683613383351 aa(lp48 F0.94374993527468332 aF0.99213692098282025 aa(lp49 F0.94427798472358571 aF0.99022462923690691 aa(lp50 F0.94490746747060417 aF0.98853135593651476 aa(lp51 F0.94562420258231528 aF0.98703940073546359 aa(lp52 F0.94641451922336861 aF0.98573230297079095 aa(lp53 F0.94726641086675445 aF0.98459508495868808 aa(lp54 F0.94817023198955042 aF0.98361432811966309 aa(lp55 F0.94911880866066223 aF0.98277805693263609 aa(lp56 F0.9501070081819275 aF0.98207546387305644 aa(lp57 F0.95113095475196119 aF0.98149655576272221 aa(lp58 F0.9521871552230633 aF0.98103182172316472 aa(lp59 F0.95327179398519979 aF0.9806720093671204 aa(lp60 F0.954380374378209 aF0.98040805510864693 aa(lp61 F0.95550775542320521 aF0.98023115801518512 aa(lp62 F0.95664850154640391 aF0.98013294305556631 aa(lp63 F0.95779737291789435 aF0.98010563361963643 aa(lp64 F0.95894976140583643 aF0.98014216283374889 aa(lp65 F0.96010192826547414 aF0.98023618398720735 aa(lp66 F0.9612509914483357 aF0.98038198625550432 aa(lp67 F0.96239471285907918 aF0.98057435397478421 aa(lp68 F0.96353119945279508 aF0.98080842409059354 aa(lp69 F0.96465864224821829 aF0.98107958699427777 aa(lp70 F0.96577517712114647 aF0.98138345009551109 aa(lp71 F0.96687888469958339 aF0.98171585473605261 aa(lp72 F0.96796788700093883 aF0.98207291824474807 aa(lp73 F0.96904046999976201 aF0.98245106980659302 aa(lp74 F0.97009516973226695 aF0.98284706038548109 aa(lp75 F0.9711307929144567 aF0.98325794390473498 aa(lp76 F0.97214638065758718 aF0.98368104042180415 aa(lp77 F0.97314114611227476 aF0.98411389698156981 aa(lp78 F0.97411441862311132 aF0.98455425804800367 aa(lp79 F0.97506561287922355 aF0.98500004903833061 aa(lp80 F0.97599422327924545 aF0.98544936962102303 aa(lp81 F0.97689983201726049 aF0.9859004903513372 aa(lp82 F0.97778211764597456 aF0.98635184742754134 aa(lp83 F0.97864085628970987 aF0.98680203380047082 aa(lp84 F0.97947591494710928 aF0.98724978769819782 aa(lp85 F0.98028724075930229 aF0.98769398068411163 aa(lp86 F0.98107485045897758 aF0.98813360681599316 aa(lp87 F0.98183882227767683 aF0.98856777342988811 aa(lp88 F0.98257929022188217 aF0.98899569313929436 aa(lp89 F0.98329643958880342 aF0.98941667647984055 aa(lp90 F0.9839905027189717 aF0.9898301248127187 aa(lp91 F0.98466175466977313 aF0.99023552342415089 aa(lp92 F0.98531050858278102 aF0.99063243487402564 aa(lp93 F0.98593711141717244 aF0.99102049270575676 aa(lp94 F0.98654193979768279 aF0.99139939554806955 aa(lp95 F0.98712539626911655 aF0.99176890161174425 aa(lp96 F0.98768790580340304 aF0.99212882352470722 aa(lp97 F0.98822991260965709 aF0.99247902349720596 aa(lp98 F0.98875187705429834 aF0.99281940879506403 aa(lp99 F0.98925427290061363 aF0.99314992747966979 aa(lp100 F0.98973758466870043 aF0.99347056447496684 aa(lp101 F0.99020230524140174 aF0.99378133785942868 aa(lp102 F0.99064893364549844 aF0.99408229542287474 aa(lp103 F0.99107797297858369 aF0.99437351151186537 aa(lp104 F0.99148992862605012 aF0.9946550840162639 aa(lp105 F0.9918853064192148 aF0.99492713166914726 aa(lp106 F0.99226461121054499 aF0.99518979149383757 aa(lp107 F0.99262834544623002 aF0.99544321646283995 aa(lp108 F0.99297700775827213 aF0.99568757333725189 aa(lp109 F0.99331109210720392 aF0.99592304070950688 aa(lp110 F0.99363108642099096 aF0.99614980714327883 aa(lp111 F0.99393747202536908 aF0.99636806954360657 aa(lp112 F0.99423072257286538 aF0.99657803161703873 aa(lp113 F0.99451130356877515 aF0.99677990248366821 aa(lp114 F0.99477967157935321 aF0.99697389542420933 aa(lp115 F0.99503627387890259 aF0.99716022671905802 aa(lp116 F0.99528154789745937 aF0.99733911463743385 aa(lp117 F0.99551592089339558 aF0.99751077847958702 aa(lp118 F0.99573980969772546 aF0.99767543774423406 aa(lp119 F0.99595362042209556 aF0.99783331139242104 aa(lp120 F0.99615774831134352 aF0.99798461715340514 aa(lp121 F0.99635257761490015 aF0.99812957095352506 aa(lp122 F0.99653848154428903 aF0.99826838636156467 aa(lp123 F0.99671582212958587 aF0.99840127416309155 aa(lp124 F0.99688495039734903 aF0.99852844192058932 aa(lp125 F0.99704620626826712 aF0.99865009365815038 aa(lp126 F0.99719991868867375 aF0.99876642953059791 aa(lp127 F0.99734640567903088 aF0.99887764559200443 aa(lp128 F0.99748597460075039 aF0.99898393358076676 aa(lp129 F0.99761892211743808 aF0.99908548071663472 aa(lp130 F0.99774553445260217 aF0.99918246961706914 aa(lp131 F0.99786608763402995 aF0.9992750781147417 aa(lp132 F0.99798084754507654 aF0.99936347924139757 aa(lp133 F0.99809007027143615 aF0.99944784112735541 aa(lp134 F0.99819400221013288 aF0.99952832698921867 aa(lp135 F0.99829288043120867 aF0.99960509511096907 aa(lp136 F0.99838693273971979 aF0.9996782988442473 aa(lp137 F0.99847637807499512 aF0.99974808665198522 aa(lp138 F0.99856142662765079 aF0.99981460212441342 aa(lp139 F0.99864228023713053 aF0.99987798400183581 aa(lp140 F0.99871913249407795 aF0.99993836629815758 aa(lp141 F0.9987921690052679 aF0.9999958783261057 aa(lp142 F0.99886156778381197 aF1.0000506447809765 aa(lp143 F0.9989274992845425 aF1.0001027858413811 aa(lp144 F0.99899012681020549 aF1.0001524172308571 aa(lp145 F0.9990496067763518 aF1.0001996503579194 aa(lp146 F0.99910608873324347 aF1.0002445923857888 aa(lp147 F0.99915971587363706 aF1.0002873463357118 aa(lp148 F0.9992106251027244 aF1.0003280111915995 aa(lp149 F0.99925894717776931 aF1.0003666820450217 aa(lp150 F0.99930480740765359 aF1.0004034502199428 aa(lp151 F0.99934832479266789 aF1.0004384031076614 aa(lp152 F0.99938961545606708 aF1.0004716254769435 aa. ././@PaxHeader0000000000000000000000000000022100000000000010210 xustar00123 path=galpy-1.8.1/galpy/df/data/dfcorrection_shudf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_151_5.0000_20.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_shudf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_151_0000644000175100001710000001702714327773303030706 0ustar00runnerdocker(lp1 (lp2 F1.0247460826575929 aF1.0010805795621271 aa(lp3 F1.033451776586197 aF1.0043404205946462 aa(lp4 F1.0391893047321905 aF1.0046814957691361 aa(lp5 F1.0417622841310203 aF1.0020220865059544 aa(lp6 F1.0410397669035953 aF0.99638284349334005 aa(lp7 F1.0370540193109397 aF0.9878675792043311 aa(lp8 F1.0298069191583163 aF0.97667228429328501 aa(lp9 F1.0193982243013839 aF0.96311166129967651 aa(lp10 F1.0061739316744036 aF0.94760526063491857 aa(lp11 F0.9906800668874467 aF0.93066657437503675 aa(lp12 F0.97352517826898122 aF0.91287766435031159 aa(lp13 F0.9553313630672603 aF0.8948196283962998 aa(lp14 F0.93678171987678283 aF0.87699280831916959 aa(lp15 F0.91862271536381901 aF0.85979127019427293 aa(lp16 F0.90153434884209438 aF0.84353745456588658 aa(lp17 F0.88594709911346947 aF0.82851018605574567 aa(lp18 F0.87193451087708762 aF0.81490691754767641 aa(lp19 F0.85923967774812582 aF0.80274344086308769 aa(lp20 F0.84749315580945983 aF0.79188871470484345 aa(lp21 F0.83662478232414739 aF0.78244055798752232 aa(lp22 F0.82695885006890302 aF0.7747726096112566 aa(lp23 F0.81908729681663739 aF0.76936102659134409 aa(lp24 F0.81368084776922511 aF0.76661153122446646 aa(lp25 F0.81128880085304611 aF0.76673036532195415 aa(lp26 F0.8121646922785325 aF0.76964081898937409 aa(lp27 F0.81614639440679981 aF0.77494634665591078 aa(lp28 F0.82261032102014697 aF0.78194084065474356 aa(lp29 F0.83051480517916976 aF0.78967766951388696 aa(lp30 F0.83853729990566517 aF0.79709918363752885 aa(lp31 F0.84529389490477291 aF0.80321358623787065 aa(lp32 F0.84960346985787394 aF0.80728274088303287 aa(lp33 F0.85073378226730023 aF0.80897001067558472 aa(lp34 F0.84855933412428775 aF0.80840069739754361 aa(lp35 F0.84358306015104001 aF0.80612472990520323 aa(lp36 F0.83681835255613846 aF0.80299407074875162 aa(lp37 F0.82956633987204886 aF0.79998940262113205 aa(lp38 F0.8231517027500086 aF0.7980216417404471 aa(lp39 F0.81867297480099266 aF0.79773820351898672 aa(lp40 F0.81684955944621584 aF0.7994302356907983 aa(lp41 F0.81800571980871961 aF0.80310250518910098 aa(lp42 F0.82210431947762208 aF0.80855162316650375 aa(lp43 F0.82878528943936569 aF0.81540326589520973 aa(lp44 F0.8374147329445758 aF0.82314112966790853 aa(lp45 F0.84715084368431892 aF0.83115901770294176 aa(lp46 F0.85701964789860985 aF0.83882767057167862 aa(lp47 F0.86599500773291316 aF0.8455575227636003 aa(lp48 F0.87308152374219938 aF0.85082935548092942 aa(lp49 F0.87742904254475518 aF0.85423388210462403 aa(lp50 F0.87850509138745136 aF0.85556597060527406 aa(lp51 F0.87630273594246855 aF0.85497960978295207 aa(lp52 F0.87150150099661761 aF0.85311404573235705 aa(lp53 F0.86545009288665653 aF0.85106287607175057 aa(lp54 F0.85992979305314632 aF0.85012490393643336 aa(lp55 F0.85674521421235983 aF0.85143575290904006 aa(lp56 F0.85728373640844002 aF0.85562255793392061 aa(lp57 F0.86217400786028231 aF0.86262532049112672 aa(lp58 F0.87111587741832541 aF0.87171156121510773 aa(lp59 F0.88289280153938476 aF0.88164599271906297 aa(lp60 F0.8955844862104293 aF0.89097415390987622 aa(lp61 F0.90697921755911604 aF0.89837067039468299 aa(lp62 F0.91511205984888377 aF0.90296650268907797 aa(lp63 F0.91878556790367516 aF0.90455491768022223 aa(lp64 F0.91790348314806225 aF0.90362824686242293 aa(lp65 F0.91350899371368455 aF0.90125349910367636 aa(lp66 F0.90751372165950039 aF0.89883192094677189 aa(lp67 F0.90218798098954744 aF0.8977600012428778 aa(lp68 F0.89954460759381794 aF0.89905187254609686 aa(lp69 F0.90079760756865823 aF0.90303905410314589 aa(lp70 F0.90604941489177282 aF0.9092731804737938 aa(lp71 F0.91428517366257789 aF0.91667348904280699 aa(lp72 F0.92368000702391295 aF0.9238715224098426 aa(lp73 F0.93215944257794936 aF0.92964299804367578 aa(lp74 F0.93806377527601947 aF0.93328695326285416 aa(lp75 F0.9406817084696969 aF0.93481786130496036 aa(lp76 F0.94042890565042692 aF0.93489868398625475 aa(lp77 F0.9385938735015199 aF0.9345473953875898 aa(lp78 F0.93677925130833251 aF0.93474317060275147 aa(lp79 F0.93629364758714695 aF0.93609181033033573 aa(lp80 F0.93773634782694903 aF0.93867138067330314 aa(lp81 F0.94090239970134493 aF0.94209437355146675 aa(lp82 F0.94499685612166329 aF0.94573355230440959 aa(lp83 F0.9490391863635832 aF0.94900261907025341 aa(lp84 F0.95227772324192483 aF0.95157299808501039 aa(lp85 F0.95444059429166717 aF0.95344596834733042 aa(lp86 F0.95573470723292842 aF0.95487165552554498 aa(lp87 F0.95663568545223943 aF0.95617897513200778 aa(lp88 F0.95760989938852725 aF0.95761410956913806 aa(lp89 F0.95891568884039102 aF0.95926085849232079 aa(lp90 F0.96055660597727233 aF0.96105791604791813 aa(lp91 F0.96236618589044121 aF0.9628770196687344 aa(lp92 F0.96414486294909241 aF0.96460616332391547 aa(lp93 F0.96576600352625719 aF0.96619582950816785 aa(lp94 F0.96720790708044568 aF0.96765776065695108 aa(lp95 F0.96852109881322346 aF0.96903379744784379 aa(lp96 F0.96977202856365474 aF0.97036239445330053 aa(lp97 F0.97100265377817985 aF0.97166153280634948 aa(lp98 F0.97222111236627706 aF0.97293018879007298 aa(lp99 F0.97341494050328292 aF0.97415921679769335 aa(lp100 F0.9745690218080163 aF0.97534118141662884 aa(lp101 F0.97567578788250808 aF0.97647411348540503 aa(lp102 F0.97673571644477242 aF0.97756016958391834 aa(lp103 F0.97775295640229265 aF0.97860270245723024 aa(lp104 F0.97873135436887349 aF0.97960435229473986 aa(lp105 F0.97967304178952486 aF0.9805667058918911 aa(lp106 F0.98057892156016835 aF0.98149078634088627 aa(lp107 F0.98144958316243125 aF0.98237755513489833 aa(lp108 F0.98228580083683203 aF0.98322810630100266 aa(lp109 F0.98308857271782557 aF0.98404363408179485 aa(lp110 F0.98385898594450216 aF0.98482534857365467 aa(lp111 F0.98459811485952031 aF0.98557442910859239 aa(lp112 F0.98530699382451647 aF0.98629201758661877 aa(lp113 F0.98598662669943693 aF0.98697922585762121 aa(lp114 F0.986637998025786 aF0.98763714109958856 aa(lp115 F0.98726207657980891 aF0.9882668267786463 aa(lp116 F0.98785981417867175 aF0.98886932168304364 aa(lp117 F0.98843214385456402 aF0.98944563895456783 aa(lp118 F0.98897997863676479 aF0.98999676558233685 aa(lp119 F0.98950421091316576 aF0.99052366215756038 aa(lp120 F0.99000571190457376 aF0.99102726275208808 aa(lp121 F0.99048533131094751 aF0.99150847488887306 aa(lp122 F0.99094389701677121 aF0.99196817956110295 aa(lp123 F0.99138221483661249 aF0.99240723133140074 aa(lp124 F0.99180106840893134 aF0.99282645848812912 aa(lp125 F0.99220121909006653 aF0.99322666327444742 aa(lp126 F0.99258340597226302 aF0.99360862214558243 aa(lp127 F0.9929483459807924 aF0.99397308610398694 aa(lp128 F0.99329673392729134 aF0.99432078104343768 aa(lp129 F0.99362924273461939 aF0.99465240815930944 aa(lp130 F0.99394652363795299 aF0.99496864437820454 aa(lp131 F0.99424920646234094 aF0.99527014281080262 aa(lp132 F0.99453789986066521 aF0.99555753322976781 aa(lp133 F0.99481319175320082 aF0.9958314226012821 aa(lp134 F0.99507564954850025 aF0.99609239555975215 aa(lp135 F0.995325820678162 aF0.99634101499166583 aa(lp136 F0.99556423282559381 aF0.99657782253247851 aa(lp137 F0.99579139456981025 aF0.99680333917426944 aa(lp138 F0.99600779555703201 aF0.9970180657592822 aa(lp139 F0.9962139072624614 aF0.99722248362294019 aa(lp140 F0.996410183106193 aF0.99741705507338907 aa(lp141 F0.99659705924827502 aF0.99760222403571175 aa(lp142 F0.99677495482333578 aF0.99777841654319877 aa(lp143 F0.9969442725483636 aF0.99794604136360254 aa(lp144 F0.99710539915210117 aF0.99810549049116259 aa(lp145 F0.99725870577931097 aF0.99825713974038244 aa(lp146 F0.99740454881281282 aF0.99840134929275681 aa(lp147 F0.99754326946522975 aF0.99853846409596181 aa(lp148 F0.99767519632764934 aF0.99866881483652192 aa(lp149 F0.9978006409293686 aF0.99879271721702267 aa(lp150 F0.99791990943982589 aF0.9989104757497218 aa(lp151 F0.99803327748381143 aF0.99902237545422412 aa(lp152 F0.99814105578286894 aF0.99912870897829686 aa. ././@PaxHeader0000000000000000000000000000022100000000000010210 xustar00123 path=galpy-1.8.1/galpy/df/data/dfcorrection_shudf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_151_5.0000_40.sav 22 mtime=1667233475.0 galpy-1.8.1/galpy/df/data/dfcorrection_shudf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_151_0000644000175100001710000001702514327773303030704 0ustar00runnerdocker(lp1 (lp2 F1.0247460826575852 aF1.0010805795621225 aa(lp3 F1.0334528053257717 aF1.0043404666138369 aa(lp4 F1.0391834548319367 aF1.0046805085604837 aa(lp5 F1.0417616164986208 aF1.0020251573633892 aa(lp6 F1.0410892421084852 aF0.99638130189697938 aa(lp7 F1.0370021548411172 aF0.98787025323850464 aa(lp8 F1.0297502479560714 aF0.97664818488212346 aa(lp9 F1.0194180746404204 aF0.96309829243823375 aa(lp10 F1.0061900815781444 aF0.9476637745362595 aa(lp11 F0.99070445203630764 aF0.93075250723219516 aa(lp12 F0.97367754317389243 aF0.91288251579328206 aa(lp13 F0.95556867029976877 aF0.89470897992191301 aa(lp14 F0.93682138272720317 aF0.8768326416343819 aa(lp15 F0.91821942978331983 aF0.859660718360159 aa(lp16 F0.90078825870807977 aF0.84347930932862547 aa(lp17 F0.88532002205986493 aF0.82857355662954302 aa(lp18 F0.87196383018647194 aF0.81517642424853298 aa(lp19 F0.8601399381934236 aF0.80322370602652537 aa(lp20 F0.84893047827048651 aF0.79234828332741958 aa(lp21 F0.83792506157348645 aF0.7825856206657068 aa(lp22 F0.82746432502054579 aF0.77445869912569643 aa(lp23 F0.81844033469798283 aF0.76865891244628537 aa(lp24 F0.81196891790756376 aF0.7657615865946924 aa(lp25 F0.80902421667046975 aF0.76604696353194246 aa(lp26 F0.81013674515004552 aF0.76940751067460644 aa(lp27 F0.81518274268821966 aF0.77532392720361287 aa(lp28 F0.82330200461383085 aF0.78290595597081403 aa(lp29 F0.83297652421526513 aF0.79099936697528328 aa(lp30 F0.84227182921639587 aF0.79836559819877817 aa(lp31 F0.84923347876534272 aF0.80392173082555907 aa(lp32 F0.85236811036355298 aF0.80699365600529582 aa(lp33 F0.85106444104219503 aF0.80748655826891069 aa(lp34 F0.84578634989701429 aF0.80588423114209684 aa(lp35 F0.83795231827290073 aF0.80309889908781573 aa(lp36 F0.82955094140414187 aF0.8002534571238159 aa(lp37 F0.82262143206406335 aF0.79846490230156442 aa(lp38 F0.81873135961885146 aF0.79857452857058997 aa(lp39 F0.81854076707301493 aF0.80080522858045911 aa(lp40 F0.82167859283977029 aF0.804668454987295 aa(lp41 F0.82708261770448077 aF0.80941982745822494 aa(lp42 F0.83346768995003884 aF0.81445146552803094 aa(lp43 F0.8397198811938541 aF0.81939902467363379 aa(lp44 F0.84517779980123442 aF0.8241219270377852 aa(lp45 F0.84974392520792019 aF0.82866018850927003 aa(lp46 F0.85375818837658501 aF0.83319349583622893 aa(lp47 F0.85765140186004218 aF0.83790472129250781 aa(lp48 F0.86152271469313202 aF0.84274099178042317 aa(lp49 F0.8649171699563466 aF0.84725784648737557 aa(lp50 F0.86703142947247858 aF0.85079334187655742 aa(lp51 F0.86730615482272477 aF0.8529665645747444 aa(lp52 F0.86602231590146395 aF0.8541053610236452 aa(lp53 F0.86443249945941625 aF0.85520531707688074 aa(lp54 F0.86429377328368173 aF0.85742274731695678 aa(lp55 F0.86703249573166108 aF0.8614461817177812 aa(lp56 F0.87296940330714379 aF0.86708040317252633 aa(lp57 F0.88105619440621108 aF0.8733518824608848 aa(lp58 F0.88927286384224846 aF0.8790354879475446 aa(lp59 F0.89546878538998242 aF0.88322395559527112 aa(lp60 F0.89831286238922314 aF0.88569050141846206 aa(lp61 F0.89792318713679109 aF0.88696713407198158 aa(lp62 F0.89580605261446378 aF0.88809541950298054 aa(lp63 F0.89410497513050347 aF0.89010358642877163 aa(lp64 F0.89460109988965641 aF0.89349599371465971 aa(lp65 F0.89804815682789163 aF0.89809871593091306 aa(lp66 F0.90406549367274891 aF0.90329941372700917 aa(lp67 F0.91139883562174995 aF0.90836330277406696 aa(lp68 F0.91833871066574269 aF0.91262695180924425 aa(lp69 F0.92325653960378373 aF0.91564618994364544 aa(lp70 F0.92521350021947757 aF0.91736974106171365 aa(lp71 F0.92442116714422562 aF0.9182353836443452 aa(lp72 F0.92227007171091435 aF0.91906248351564113 aa(lp73 F0.92081933446867914 aF0.92074302641819927 aa(lp74 F0.92189591144054228 aF0.92382905639150359 aa(lp75 F0.92617470460230789 aF0.9281898708216102 aa(lp76 F0.93273284847069982 aF0.93298893158800411 aa(lp77 F0.9394319191078222 aF0.93709808403984129 aa(lp78 F0.94403107424555233 aF0.93973293539883507 aa(lp79 F0.94543261257059552 aF0.94089393648212638 aa(lp80 F0.94429436215756557 aF0.94133686500573166 aa(lp81 F0.94260301325580176 aF0.94211760746003215 aa(lp82 F0.94247597225827373 aF0.94398939141312732 aa(lp83 F0.94493507311673486 aF0.94699260269208452 aa(lp84 F0.9493783736364273 aF0.95047326816036271 aa(lp85 F0.95405082181615619 aF0.9535256535335499 aa(lp86 F0.95722609332439534 aF0.95557981589342522 aa(lp87 F0.95831942082636035 aF0.95673299921932986 aa(lp88 F0.95814668559172533 aF0.95760324600682767 aa(lp89 F0.95818213219212833 aF0.95884278000518386 aa(lp90 F0.95944286241961918 aF0.9606899605699043 aa(lp91 F0.96185857193845525 aF0.96287724617310622 aa(lp92 F0.96452748504767694 aF0.96492122317847118 aa(lp93 F0.9665469661597933 aF0.9665222605443442 aa(lp94 F0.9676919560182683 aF0.96774573722701396 aa(lp95 F0.96842332274156662 aF0.96888042491438453 aa(lp96 F0.96934525087400303 aF0.97015384765857315 aa(lp97 F0.97068199209792438 aF0.97157089823090415 aa(lp98 F0.97221220901382954 aF0.97298067157376034 aa(lp99 F0.97359910488105328 aF0.97425445408315448 aa(lp100 F0.97471741205065576 aF0.97538755572485347 aa(lp101 F0.97568517372763408 aF0.97645777232246078 aa(lp102 F0.9766657385604286 aF0.97752550338913813 aa(lp103 F0.97770310824915596 aF0.97858819853161993 aa(lp104 F0.97873414191881636 aF0.97961154767170933 aa(lp105 F0.97969698817953776 aF0.98057707888053536 aa(lp106 F0.98059018491674632 aF0.98149311381731141 aa(lp107 F0.98144514242151748 aF0.98237447932045907 aa(lp108 F0.9822793771174021 aF0.98322584702357074 aa(lp109 F0.98308761476657014 aF0.98404385595874921 aa(lp110 F0.98386094372742849 aF0.98482623448523088 aa(lp111 F0.98459912183670995 aF0.98557465979769243 aa(lp112 F0.98530667259346294 aF0.98629180581112708 aa(lp113 F0.9859862242601154 aF0.98697909521869243 aa(lp114 F0.98663798819450366 aF0.98763716794580114 aa(lp115 F0.98726218411359823 aF0.9882668679061376 aa(lp116 F0.9878598381042476 aF0.98886932320521137 aa(lp117 F0.98843212232208533 aF0.98944562960165439 aa(lp118 F0.98897997019478456 aF0.98999676387870861 aa(lp119 F0.98950421434695268 aF0.99052366384374657 aa(lp120 F0.9900057138521493 aF0.99102726323778101 aa(lp121 F0.99048533084920753 aF0.99150847463396263 aa(lp122 F0.99094389667393146 aF0.99196817946878635 aa(lp123 F0.99138221489341061 aF0.99240723136528564 aa(lp124 F0.99180106845662752 aF0.9928264585011457 aa(lp125 F0.99220121908361081 aF0.99322666327048814 aa(lp126 F0.99258340596635941 aF0.99360862214408374 aa(lp127 F0.99294834598224102 aF0.99397308610450508 aa(lp128 F0.99329673392712903 aF0.99432078104346822 aa(lp129 F0.99362924273513675 aF0.99465240815934064 aa(lp130 F0.99394652363732072 aF0.99496864437813726 aa(lp131 F0.99424920646297055 aF0.99527014281086346 aa(lp132 F0.99453789986002183 aF0.99555753322970586 aa(lp133 F0.99481319175385174 aF0.99583142260135049 aa(lp134 F0.99507564954786931 aF0.99609239555967866 aa(lp135 F0.99532582067874464 aF0.99634101499174088 aa(lp136 F0.99556423282508111 aF0.99657782253240612 aa(lp137 F0.99579139457024135 aF0.99680333917433583 aa(lp138 F0.99600779555668595 aF0.99701806575922347 aa(lp139 F0.99621390726272574 aF0.99722248362298993 aa(lp140 F0.99641018310600371 aF0.9974170550733481 aa(lp141 F0.99659705924839781 aF0.99760222403574428 aa(lp142 F0.99677495482327061 aF0.9977784165431739 aa(lp143 F0.99694427254838069 aF0.99794604136362142 aa(lp144 F0.99710539915212193 aF0.99810549049114983 aa(lp145 F0.99725870577926334 aF0.99825713974038921 aa(lp146 F0.99740454881287754 aF0.99840134929275481 aa(lp147 F0.9975432694651577 aF0.99853846409596025 aa(lp148 F0.99767519632771851 aF0.99866881483652681 aa(lp149 F0.9978006409293102 aF0.9987927172170169 aa(lp150 F0.99791990943986575 aF0.99891047574972702 aa(lp151 F0.99803327748379578 aF0.99902237545422079 aa(lp152 F0.99814105578285084 aF0.99912870897829587 aa. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/df.py0000644000175100001710000000465214327773303014724 0ustar00runnerdockerfrom ..util import config, conversion from ..util.conversion import physical_compatible class df: """Top-level class for DF classes""" def __init__(self,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a DF object INPUT: ro= (None) distance scale vo= (None) velocity scale OUTPUT: HISTORY: 2016-02-28 - Written - Bovy (UofT) """ # Parse ro and vo if ro is None: self._ro= config.__config__.getfloat('normalization','ro') self._roSet= False else: self._ro= conversion.parse_length_kpc(ro) self._roSet= True if vo is None: self._vo= config.__config__.getfloat('normalization','vo') self._voSet= False else: self._vo= conversion.parse_velocity_kms(vo) self._voSet= True return None def _check_consistent_units(self): """Internal function to check that the set of units for this object is consistent with that for the potential""" assert physical_compatible(self,self._pot), 'Physical conversion for the DF object is not consistent with that of the Potential given to it' def turn_physical_off(self): """ NAME: turn_physical_off PURPOSE: turn off automatic returning of outputs in physical units INPUT: (none) OUTPUT: (none) HISTORY: 2017-06-05 - Written - Bovy (UofT) """ self._roSet= False self._voSet= False return None def turn_physical_on(self,ro=None,vo=None): """ NAME: turn_physical_on PURPOSE: turn on automatic returning of outputs in physical units INPUT: ro= reference distance (kpc; can be Quantity) vo= reference velocity (km/s; can be Quantity) OUTPUT: (none) HISTORY: 2016-06-05 - Written - Bovy (UofT) 2020-04-22 - Don't turn on a parameter when it is False - Bovy (UofT) """ if not ro is False: self._roSet= True if not vo is False: self._voSet= True if not ro is None and ro: self._ro= conversion.parse_length_kpc(ro) if not vo is None and vo: self._vo= conversion.parse_velocity_kms(vo) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/diskdf.py0000644000175100001710000027466114327773303015610 0ustar00runnerdocker############################################################################### # diskdf.py: module that interprets (E,Lz) pairs in terms of a # distribution function (following Dehnen 1999) # # This module contains the following classes: # # diskdf - top-level class that represents a distribution function # dehnendf - inherits from diskdf, implements Dehnen's 'new' DF # shudf - inherits from diskdf, implements Shu's DF # DFcorrection - class that represents corrections to the input Sigma(R) # and sigma_R(R) to get closer to the targets ############################################################################### _EPSREL=10.**-14. _NSIGMA= 4. _INTERPDEGREE= 3 _RMIN=10.**-10. _MAXD_REJECTLOS= 4. _PROFILE= False import copy import os import os.path import pickle import numpy import scipy numpylog= numpy.lib.scimath.log # somehow, this code produces log(negative), which scipy (now numpy.lib.scimath.log) implements as log(|negative|) + i pi while numpy gives NaN and we want the scipy behavior; not sure where the log(negative) comes from though! I think it's for sigma=0 DFs (this test fails with numpy.log) where the DF eval has a log(~zero) that can be slightly negative because of numerical precision issues from scipy import integrate, interpolate, optimize, stats from ..actionAngle import actionAngleAdiabatic, actionAngleAxi from ..orbit import Orbit from ..potential import PowerSphericalPotential from ..util import conversion, save_pickles from ..util._optional_deps import _APY_LOADED, _APY_UNITS from ..util.ars import ars from ..util.conversion import (physical_conversion, potential_physical_input, surfdens_in_msolpc2) from .df import df from .surfaceSigmaProfile import expSurfaceSigmaProfile, surfaceSigmaProfile if _APY_LOADED: from astropy import units #scipy version from packaging.version import parse as parse_version _SCIPY_VERSION= parse_version(scipy.__version__) _SCIPY_VERSION_BREAK= parse_version('0.9') _CORRECTIONSDIR=os.path.join(os.path.dirname(os.path.realpath(__file__)),'data') _DEGTORAD= numpy.pi/180. class diskdf(df): """Class that represents a disk DF""" def __init__(self,dftype='dehnen', surfaceSigma=expSurfaceSigmaProfile, profileParams=(1./3.,1.0,0.2), correct=False, beta=0.,ro=None,vo=None,**kwargs): """ NAME: __init__ PURPOSE: Initialize a DF INPUT: dftype= 'dehnen' or 'corrected-dehnen', 'shu' or 'corrected-shu' surfaceSigma - instance or class name of the target surface density and sigma_R profile (default: both exponential) profileParams - parameters of the surface and sigma_R profile: (xD,xS,Sro) where xD - disk surface mass scalelength / Ro xS - disk velocity dispersion scalelength / Ro Sro - disk velocity dispersion at Ro (/vo) Directly given to the 'surfaceSigmaProfile class, so could be anything that class takes beta - power-law index of the rotation curve correct - correct the DF (i.e., DFcorrection kwargs are also given) + DFcorrection kwargs (except for those already specified) OUTPUT: HISTORY: 2010-03-10 - Written - Bovy (NYU) """ df.__init__(self,ro=ro,vo=vo) self._dftype= dftype if isinstance(surfaceSigma,surfaceSigmaProfile): self._surfaceSigmaProfile= surfaceSigma else: if _APY_LOADED and isinstance(profileParams[0],units.Quantity): newprofileParams=\ (conversion.parse_length(profileParams[0],ro=self._ro), conversion.parse_length(profileParams[1],ro=self._ro), conversion.parse_velocity(profileParams[2],vo=self._vo)) self._roSet= True self._voSet= True profileParams= newprofileParams self._surfaceSigmaProfile= surfaceSigma(profileParams) self._beta= beta self._gamma= numpy.sqrt(2./(1.+self._beta)) if correct or 'corrections' in kwargs or 'rmax' in kwargs \ or 'niter' in kwargs or 'npoints' in kwargs: self._correct= True #Load corrections self._corr= DFcorrection(dftype=self.__class__, surfaceSigmaProfile=self._surfaceSigmaProfile, beta=beta,**kwargs) else: self._correct= False #Setup aA objects for frequency and rap,rperi calculation self._aA= actionAngleAdiabatic(pot=PowerSphericalPotential(normalize=1., alpha=2.-2.*self._beta),gamma=0.) return None @physical_conversion('phasespacedensity2d',pop=True) def __call__(self,*args,**kwargs): """ NAME: __call__ PURPOSE: evaluate the distribution function INPUT: either an orbit instance, a list of such instances, or E,Lz 1) Orbit instance or list: a) Orbit instance alone: use initial condition b) Orbit instance + t: call the Orbit instance (for list, each instance is called at t) 2) E - energy (/vo^2; or can be Quantity) L - angular momentun (/ro/vo; or can be Quantity) 3) array vxvv [3/4,nt] [must be in natural units /vo,/ro; use Orbit interface for physical-unit input) KWARGS: marginalizeVperp - marginalize over perpendicular velocity (only supported with 1a) for single orbits above) marginalizeVlos - marginalize over line-of-sight velocity (only supported with 1a) for single orbits above) nsigma= number of sigma to integrate over when marginalizing +scipy.integrate.quad keywords OUTPUT: DF(orbit/E,L) HISTORY: 2010-07-10 - Written - Bovy (NYU) """ if isinstance(args[0],Orbit): if len(args[0]) > 1: raise RuntimeError('Only single-object Orbit instances can be passed to DF instances at this point') #pragma: no cover if len(args) == 1: if kwargs.pop('marginalizeVperp',False): return self._call_marginalizevperp(args[0],**kwargs) elif kwargs.pop('marginalizeVlos',False): return self._call_marginalizevlos(args[0],**kwargs) else: return numpy.real(self.eval(*vRvTRToEL(\ args[0].vR(use_physical=False), args[0].vT(use_physical=False), args[0].R(use_physical=False), self._beta,self._dftype))) else: no= args[0](args[1]) return numpy.real(self.eval(*vRvTRToEL(no.vR(use_physical=False), no.vT(use_physical=False), no.R(use_physical=False), self._beta, self._dftype))) elif isinstance(args[0],list) \ and isinstance(args[0][0],Orbit): if numpy.any([len(no) > 1 for no in args[0]]): raise RuntimeError('Only single-object Orbit instances can be passed to DF instances at this point') #pragma: no cover #Grab all of the vR, vT, and R vR= numpy.array([o.vR(use_physical=False) for o in args[0]]) vT= numpy.array([o.vT(use_physical=False) for o in args[0]]) R= numpy.array([o.R(use_physical=False) for o in args[0]]) return numpy.real(self.eval(*vRvTRToEL(vR,vT,R,self._beta, self._dftype))) elif isinstance(args[0],numpy.ndarray) and \ not (hasattr(args[0],'isscalar') and args[0].isscalar): #Grab all of the vR, vT, and R vR= args[0][1] vT= args[0][2] R= args[0][0] return numpy.real(self.eval(*vRvTRToEL(vR,vT,R,self._beta, self._dftype))) else: return numpy.real(self.eval(*args)) def _call_marginalizevperp(self,o,**kwargs): """Call the DF, marginalizing over perpendicular velocity""" #Get l, vlos l= o.ll(obs=[1.,0.,0.],ro=1.)*_DEGTORAD vlos= o.vlos(ro=1.,vo=1.,obs=[1.,0.,0.,0.,0.,0.]) R= o.R(use_physical=False) phi= o.phi(use_physical=False) #Get local circular velocity, projected onto the los vcirc= R**self._beta vcirclos= vcirc*numpy.sin(phi+l) #Marginalize alphalos= phi+l if not 'nsigma' in kwargs or ('nsigma' in kwargs and \ kwargs['nsigma'] is None): nsigma= _NSIGMA else: nsigma= kwargs['nsigma'] kwargs.pop('nsigma',None) sigmaR2= self.targetSigma2(R,use_physical=False) sigmaR1= numpy.sqrt(sigmaR2) #Use the asymmetric drift equation to estimate va va= sigmaR2/2./R**self._beta*(1./self._gamma**2.-1. -R*self._surfaceSigmaProfile.surfacemassDerivative(R,log=True) -R*self._surfaceSigmaProfile.sigma2Derivative(R,log=True)) if numpy.fabs(va) > sigmaR1: va = 0. #To avoid craziness near the center if numpy.fabs(numpy.sin(alphalos)) < numpy.sqrt(1./2.): cosalphalos= numpy.cos(alphalos) tanalphalos= numpy.tan(alphalos) return integrate.quad(_marginalizeVperpIntegrandSinAlphaSmall, -self._gamma*va/sigmaR1-nsigma, -self._gamma*va/sigmaR1+nsigma, args=(self,R,cosalphalos,tanalphalos, vlos-vcirclos,vcirc, sigmaR1/self._gamma), **kwargs)[0]/numpy.fabs(cosalphalos)\ *sigmaR1/self._gamma else: sinalphalos= numpy.sin(alphalos) cotalphalos= 1./numpy.tan(alphalos) return integrate.quad(_marginalizeVperpIntegrandSinAlphaLarge, -nsigma,nsigma, args=(self,R,sinalphalos,cotalphalos, vlos-vcirclos,vcirc,sigmaR1), **kwargs)[0]/numpy.fabs(sinalphalos)*sigmaR1 def _call_marginalizevlos(self,o,**kwargs): """Call the DF, marginalizing over line-of-sight velocity""" #Get d, l, vperp l= o.ll(obs=[1.,0.,0.],ro=1.)*_DEGTORAD vperp= o.vll(ro=1.,vo=1.,obs=[1.,0.,0.,0.,0.,0.]) R= o.R(use_physical=False) phi= o.phi(use_physical=False) #Get local circular velocity, projected onto the perpendicular #direction vcirc= R**self._beta vcircperp= vcirc*numpy.cos(phi+l) #Marginalize alphaperp= numpy.pi/2.+phi+l if not 'nsigma' in kwargs or ('nsigma' in kwargs and \ kwargs['nsigma'] is None): nsigma= _NSIGMA else: nsigma= kwargs['nsigma'] kwargs.pop('nsigma',None) sigmaR2= self.targetSigma2(R,use_physical=False) sigmaR1= numpy.sqrt(sigmaR2) #Use the asymmetric drift equation to estimate va va= sigmaR2/2./R**self._beta*(1./self._gamma**2.-1. -R*self._surfaceSigmaProfile.surfacemassDerivative(R,log=True) -R*self._surfaceSigmaProfile.sigma2Derivative(R,log=True)) if numpy.fabs(va) > sigmaR1: va = 0. #To avoid craziness near the center if numpy.fabs(numpy.sin(alphaperp)) < numpy.sqrt(1./2.): cosalphaperp= numpy.cos(alphaperp) tanalphaperp= numpy.tan(alphaperp) #we can reuse the VperpIntegrand, since it is just another angle return integrate.quad(_marginalizeVperpIntegrandSinAlphaSmall, -self._gamma*va/sigmaR1-nsigma, -self._gamma*va/sigmaR1+nsigma, args=(self,R,cosalphaperp,tanalphaperp, vperp-vcircperp,vcirc, sigmaR1/self._gamma), **kwargs)[0]/numpy.fabs(cosalphaperp)\ *sigmaR1/self._gamma else: sinalphaperp= numpy.sin(alphaperp) cotalphaperp= 1./numpy.tan(alphaperp) #we can reuse the VperpIntegrand, since it is just another angle return integrate.quad(_marginalizeVperpIntegrandSinAlphaLarge, -nsigma,nsigma, args=(self,R,sinalphaperp,cotalphaperp, vperp-vcircperp,vcirc,sigmaR1), **kwargs)[0]/numpy.fabs(sinalphaperp)*sigmaR1 @potential_physical_input @physical_conversion('velocity2',pop=True) def targetSigma2(self,R,log=False): """ NAME: targetSigma2 PURPOSE: evaluate the target Sigma_R^2(R) INPUT: R - radius at which to evaluate (can be Quantity) OUTPUT: target Sigma_R^2(R) log - if True, return the log (default: False) HISTORY: 2010-03-28 - Written - Bovy (NYU) """ return self._surfaceSigmaProfile.sigma2(R,log=log) @potential_physical_input @physical_conversion('surfacedensity',pop=True) def targetSurfacemass(self,R,log=False): """ NAME: targetSurfacemass PURPOSE: evaluate the target surface mass at R INPUT: R - radius at which to evaluate (can be Quantity) log - if True, return the log (default: False) OUTPUT: Sigma(R) HISTORY: 2010-03-28 - Written - Bovy (NYU) """ return self._surfaceSigmaProfile.surfacemass(R,log=log) @physical_conversion('surfacedensitydistance',pop=True) def targetSurfacemassLOS(self,d,l,log=False,deg=True): """ NAME: targetSurfacemassLOS PURPOSE: evaluate the target surface mass along the LOS given l and d INPUT: d - distance along the line of sight (can be Quantity) l - Galactic longitude (in deg, unless deg=False; can be Quantity) deg= if False, l is in radians log - if True, return the log (default: False) OUTPUT: Sigma(d,l) x d HISTORY: 2011-03-23 - Written - Bovy (NYU) """ #Calculate R and phi if _APY_LOADED and isinstance(l,units.Quantity): lrad= conversion.parse_angle(l) elif deg: lrad= l*_DEGTORAD else: lrad= l d= conversion.parse_length(d,ro=self._ro) R, phi= _dlToRphi(d,lrad) if log: return self._surfaceSigmaProfile.surfacemass(R,log=log)\ +numpylog(d) else: return self._surfaceSigmaProfile.surfacemass(R,log=log)\ *d @physical_conversion('surfacedensitydistance',pop=True) def surfacemassLOS(self,d,l,deg=True,target=True, romberg=False,nsigma=None,relative=None): """ NAME: surfacemassLOS PURPOSE: evaluate the surface mass along the LOS given l and d INPUT: d - distance along the line of sight (can be Quantity) l - Galactic longitude (in deg, unless deg=False; can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: target= if True, use target surfacemass (default) romberg - if True, use a romberg integrator (default: False) deg= if False, l is in radians OUTPUT: Sigma(d,l) x d HISTORY: 2011-03-24 - Written - Bovy (NYU) """ #Calculate R and phi if _APY_LOADED and isinstance(l,units.Quantity): lrad= conversion.parse_angle(l) elif deg: lrad= l*_DEGTORAD else: lrad= l d= conversion.parse_length(d,ro=self._ro) R, phi= _dlToRphi(d,lrad) if target: if relative: return d else: return self.targetSurfacemass(R,use_physical=False)*d else: return self.surfacemass(R,romberg=romberg,nsigma=nsigma, relative=relative,use_physical=False)\ *d @physical_conversion('position',pop=True) def sampledSurfacemassLOS(self,l,n=1,maxd=None,target=True): """ NAME: sampledSurfacemassLOS PURPOSE: sample a distance along the line of sight INPUT: l - Galactic longitude (in rad; can be Quantity) n= number of distances to sample maxd= maximum distance to consider (for the rejection sampling) (can be Quantity) target= if True, sample from the 'target' surface mass density, rather than the actual surface mass density (default=True) OUTPUT: list of samples HISTORY: 2011-03-24 - Written - Bovy (NYU) """ #First calculate where the maximum is if target: minR= optimize.fmin_bfgs(lambda x: \ -self.targetSurfacemassLOS(x,l, use_physical=False, deg=False), 0.,disp=False)[0] maxSM= self.targetSurfacemassLOS(minR,l,deg=False, use_physical=False) else: minR= optimize.fmin_bfgs(lambda x: \ -self.surfacemassLOS(x,l, deg=False, use_physical=False), 0.,disp=False)[0] maxSM= self.surfacemassLOS(minR,l,deg=False,use_physical=False) #Now rejection-sample l= conversion.parse_angle(l) maxd= conversion.parse_length(maxd,ro=self._ro) if maxd is None: maxd= _MAXD_REJECTLOS out= [] while len(out) < n: #sample prop= numpy.random.random()*maxd if target: surfmassatprop= self.targetSurfacemassLOS(prop,l,deg=False, use_physical=False) else: surfmassatprop= self.surfacemassLOS(prop,l,deg=False, use_physical=False) if surfmassatprop/maxSM > numpy.random.random(): #accept out.append(prop) return numpy.array(out) @potential_physical_input @physical_conversion('velocity',pop=True) def sampleVRVT(self,R,n=1,nsigma=None,target=True): """ NAME: sampleVRVT PURPOSE: sample a radial and azimuthal velocity at R INPUT: R - Galactocentric distance (can be Quantity) n= number of distances to sample nsigma= number of sigma to rejection-sample on target= if True, sample using the 'target' sigma_R rather than the actual sigma_R (default=True) OUTPUT: list of samples BUGS: should use the fact that vR and vT separate HISTORY: 2011-03-24 - Written - Bovy (NYU) """ #Determine where the max of the v-distribution is using asymmetric drift maxVR= 0. maxVT= optimize.brentq(_vtmaxEq,0.,R**self._beta+0.2,(R,self)) maxVD= self(Orbit([R,maxVR,maxVT])) #Now rejection-sample if nsigma == None: nsigma= _NSIGMA out= [] if target: sigma= numpy.sqrt(self.targetSigma2(R,use_physical=False)) else: sigma= numpy.sqrt(self.sigma2(R,use_physical=False)) while len(out) < n: #sample vrg, vtg= numpy.random.normal(), numpy.random.normal() propvR= vrg*nsigma*sigma propvT= vtg*nsigma*sigma/self._gamma+maxVT VDatprop= self(Orbit([R,propvR,propvT])) if VDatprop/maxVD > numpy.random.uniform()*numpy.exp(-0.5*(vrg**2.+vtg**2.)): #accept out.append(numpy.array([propvR,propvT])) return numpy.array(out) def sampleLOS(self,los,n=1,deg=True,maxd=None,nsigma=None, targetSurfmass=True,targetSigma2=True): """ NAME: sampleLOS PURPOSE: sample along a given LOS INPUT: los - line of sight (in deg, unless deg=False; can be Quantity) n= number of desired samples deg= los in degrees? (default=True) targetSurfmass, targetSigma2= if True, use target surface mass and sigma2 profiles, respectively (there is not much point to doing the latter) (default=True) OUTPUT: returns list of Orbits BUGS: target=False uses target distribution for derivatives (this is a detail) HISTORY: 2011-03-24 - Started - Bovy (NYU) """ if _APY_LOADED and isinstance(los,units.Quantity): l= conversion.parse_angle(los) elif deg: l= los*_DEGTORAD else: l= los out= [] #sample distances ds= self.sampledSurfacemassLOS(l,n=n,maxd=maxd,target=targetSurfmass, use_physical=False) for ii in range(int(n)): #Calculate R and phi thisR,thisphi= _dlToRphi(ds[ii],l) #sample velocities vv= self.sampleVRVT(thisR,n=1,nsigma=nsigma,target=targetSigma2, use_physical=False)[0] if self._roSet and self._voSet: out.append(Orbit([thisR,vv[0],vv[1],thisphi],ro=self._ro, vo=self._vo)) else: out.append(Orbit([thisR,vv[0],vv[1],thisphi])) return out @potential_physical_input @physical_conversion('velocity',pop=True) def asymmetricdrift(self,R): """ NAME: asymmetricdrift PURPOSE: estimate the asymmetric drift (vc-mean-vphi) from an approximation to the Jeans equation INPUT: R - radius at which to calculate the asymmetric drift (can be Quantity) OUTPUT: asymmetric drift at R HISTORY: 2011-04-02 - Written - Bovy (NYU) """ sigmaR2= self.targetSigma2(R,use_physical=False) return sigmaR2/2./R**self._beta*(1./self._gamma**2.-1. -R*self._surfaceSigmaProfile.surfacemassDerivative(R,log=True) -R*self._surfaceSigmaProfile.sigma2Derivative(R,log=True)) @potential_physical_input @physical_conversion('surfacedensity',pop=True) def surfacemass(self,R,romberg=False,nsigma=None,relative=False): """ NAME: surfacemass PURPOSE: calculate the surface-mass at R by marginalizing over velocity INPUT: R - radius at which to calculate the surfacemass density (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: surface mass at R HISTORY: 2010-03-XX - Written - Bovy (NYU) """ if nsigma == None: nsigma= _NSIGMA logSigmaR= self.targetSurfacemass(R,log=True,use_physical=False) sigmaR2= self.targetSigma2(R,use_physical=False) sigmaR1= numpy.sqrt(sigmaR2) logsigmaR2= numpylog(sigmaR2) if relative: norm= 1. else: norm= numpy.exp(logSigmaR) #Use the asymmetric drift equation to estimate va va= sigmaR2/2./R**self._beta*(1./self._gamma**2.-1. -R*self._surfaceSigmaProfile.surfacemassDerivative(R,log=True) -R*self._surfaceSigmaProfile.sigma2Derivative(R,log=True)) if numpy.fabs(va) > sigmaR1: va = 0.#To avoid craziness near the center if romberg: return numpy.real(bovy_dblquad(_surfaceIntegrand, self._gamma*(R**self._beta-va)/sigmaR1-nsigma, self._gamma*(R**self._beta-va)/sigmaR1+nsigma, lambda x: 0., lambda x: nsigma, [R,self,logSigmaR,logsigmaR2,sigmaR1, self._gamma], tol=10.**-8)/numpy.pi*norm) else: return integrate.dblquad(_surfaceIntegrand, self._gamma*(R**self._beta-va)/sigmaR1-nsigma, self._gamma*(R**self._beta-va)/sigmaR1+nsigma, lambda x: 0., lambda x: nsigma, (R,self,logSigmaR,logsigmaR2,sigmaR1, self._gamma), epsrel=_EPSREL)[0]/numpy.pi*norm @potential_physical_input @physical_conversion('velocity2surfacedensity',pop=True) def sigma2surfacemass(self,R,romberg=False,nsigma=None, relative=False): """ NAME: sigma2surfacemass PURPOSE: calculate the product sigma_R^2 x surface-mass at R by marginalizing over velocity INPUT: R - radius at which to calculate the sigma_R^2 x surfacemass density (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: sigma_R^2 x surface-mass at R HISTORY: 2010-03-XX - Written - Bovy (NYU) """ if nsigma == None: nsigma= _NSIGMA logSigmaR= self.targetSurfacemass(R,log=True,use_physical=False) sigmaR2= self.targetSigma2(R,use_physical=False) sigmaR1= numpy.sqrt(sigmaR2) logsigmaR2= numpylog(sigmaR2) if relative: norm= 1. else: norm= numpy.exp(logSigmaR+logsigmaR2) #Use the asymmetric drift equation to estimate va va= sigmaR2/2./R**self._beta*(1./self._gamma**2.-1. -R*self._surfaceSigmaProfile.surfacemassDerivative(R,log=True) -R*self._surfaceSigmaProfile.sigma2Derivative(R,log=True)) if numpy.fabs(va) > sigmaR1: va = 0. #To avoid craziness near the center if romberg: return numpy.real(bovy_dblquad(_sigma2surfaceIntegrand, self._gamma*(R**self._beta-va)/sigmaR1-nsigma, self._gamma*(R**self._beta-va)/sigmaR1+nsigma, lambda x: 0., lambda x: nsigma, [R,self,logSigmaR,logsigmaR2,sigmaR1, self._gamma], tol=10.**-8)/numpy.pi*norm) else: return integrate.dblquad(_sigma2surfaceIntegrand, self._gamma*(R**self._beta-va)/sigmaR1-nsigma, self._gamma*(R**self._beta-va)/sigmaR1+nsigma, lambda x: 0., lambda x: nsigma, (R,self,logSigmaR,logsigmaR2,sigmaR1, self._gamma), epsrel=_EPSREL)[0]/numpy.pi*norm def vmomentsurfacemass(self,*args,**kwargs): """ NAME: vmomentsurfacemass PURPOSE: calculate the an arbitrary moment of the velocity distribution at R times the surfacmass INPUT: R - radius at which to calculate the moment (in natural units) n - vR^n m - vT^m OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) deriv= None, 'R', or 'phi': calculates derivative of the moment wrt R or phi OUTPUT: at R (no support for units) HISTORY: 2011-03-30 - Written - Bovy (NYU) """ 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= surfdens_in_msolpc2(vo,ro)*vo**(args[1]+args[2]) if _APY_UNITS: u= units.Msun/units.pc**2*(units.km/units.s)**(args[1]+args[2]) out= self._vmomentsurfacemass(*args,**kwargs) if _APY_UNITS: return units.Quantity(out*fac,unit=u) else: return out*fac else: return self._vmomentsurfacemass(*args,**kwargs) def _vmomentsurfacemass(self,R,n,m,romberg=False,nsigma=None, relative=False,phi=0.,deriv=None): """Non-physical version of vmomentsurfacemass, otherwise the same""" #odd moments of vR are zero if isinstance(n,int) and n%2 == 1: return 0. if nsigma == None: nsigma= _NSIGMA logSigmaR= self.targetSurfacemass(R,log=True,use_physical=False) sigmaR2= self.targetSigma2(R,use_physical=False) sigmaR1= numpy.sqrt(sigmaR2) logsigmaR2= numpylog(sigmaR2) if relative: norm= 1. else: norm= numpy.exp(logSigmaR+logsigmaR2*(n+m)/2.)/self._gamma**m #Use the asymmetric drift equation to estimate va va= sigmaR2/2./R**self._beta*(1./self._gamma**2.-1. -R*self._surfaceSigmaProfile.surfacemassDerivative(R,log=True) -R*self._surfaceSigmaProfile.sigma2Derivative(R,log=True)) if numpy.fabs(va) > sigmaR1: va = 0. #To avoid craziness near the center if deriv is None: if romberg: return numpy.real(bovy_dblquad(_vmomentsurfaceIntegrand, self._gamma*(R**self._beta-va)/sigmaR1-nsigma, self._gamma*(R**self._beta-va)/sigmaR1+nsigma, lambda x: -nsigma, lambda x: nsigma, [R,self,logSigmaR,logsigmaR2,sigmaR1, self._gamma,n,m], tol=10.**-8)/numpy.pi*norm/2.) else: return integrate.dblquad(_vmomentsurfaceIntegrand, self._gamma*(R**self._beta-va)/sigmaR1-nsigma, self._gamma*(R**self._beta-va)/sigmaR1+nsigma, lambda x: -nsigma, lambda x: nsigma, (R,self,logSigmaR,logsigmaR2,sigmaR1, self._gamma,n,m), epsrel=_EPSREL)[0]/numpy.pi*norm/2. else: if romberg: return numpy.real(bovy_dblquad(_vmomentderivsurfaceIntegrand, self._gamma*(R**self._beta-va)/sigmaR1-nsigma, self._gamma*(R**self._beta-va)/sigmaR1+nsigma, lambda x: -nsigma, lambda x: nsigma, [R,self,logSigmaR,logsigmaR2,sigmaR1, self._gamma,n,m,deriv], tol=10.**-8)/numpy.pi*norm/2.) else: return integrate.dblquad(_vmomentderivsurfaceIntegrand, self._gamma*(R**self._beta-va)/sigmaR1-nsigma, self._gamma*(R**self._beta-va)/sigmaR1+nsigma, lambda x: -nsigma, lambda x: nsigma, (R,self,logSigmaR,logsigmaR2,sigmaR1, self._gamma,n,m,deriv), epsrel=_EPSREL)[0]/numpy.pi*norm/2. @potential_physical_input @physical_conversion('frequency-kmskpc',pop=True) def oortA(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: oortA PURPOSE: calculate the Oort function A INPUT: R - radius at which to calculate A (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: Oort A at R HISTORY: 2011-04-19 - Written - Bovy (NYU) BUGS: could be made more efficient, e.g., surfacemass is calculated multiple times """ #2A= meanvphi/R-dmeanvR/R/dphi-dmeanvphi/dR meanvphi= self.meanvT(R,romberg=romberg,nsigma=nsigma,phi=phi, use_physical=False) dmeanvRRdphi= 0. #We know this, since the DF does not depend on phi surfmass= self._vmomentsurfacemass(R,0,0,phi=phi,romberg=romberg,nsigma=nsigma) dmeanvphidR= self._vmomentsurfacemass(R,0,1,deriv='R',phi=phi,romberg=romberg,nsigma=nsigma)/\ surfmass\ -self._vmomentsurfacemass(R,0,1,phi=phi,romberg=romberg,nsigma=nsigma)\ /surfmass**2.\ *self._vmomentsurfacemass(R,0,0,deriv='R',phi=phi,romberg=romberg,nsigma=nsigma) return 0.5*(meanvphi/R-dmeanvRRdphi/R-dmeanvphidR) @potential_physical_input @physical_conversion('frequency-kmskpc',pop=True) def oortB(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: oortB PURPOSE: calculate the Oort function B INPUT: R - radius at which to calculate B (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: Oort B at R HISTORY: 2011-04-19 - Written - Bovy (NYU) BUGS: could be made more efficient, e.g., surfacemass is calculated multiple times """ #2B= -meanvphi/R+dmeanvR/R/dphi-dmeanvphi/dR meanvphi= self.meanvT(R,romberg=romberg,nsigma=nsigma,phi=phi, use_physical=False) dmeanvRRdphi= 0. #We know this, since the DF does not depend on phi surfmass= self._vmomentsurfacemass(R,0,0,phi=phi,romberg=romberg,nsigma=nsigma) dmeanvphidR= self._vmomentsurfacemass(R,0,1,deriv='R',phi=phi,romberg=romberg,nsigma=nsigma)/\ surfmass\ -self._vmomentsurfacemass(R,0,1,phi=phi,romberg=romberg,nsigma=nsigma)\ /surfmass**2.\ *self._vmomentsurfacemass(R,0,0,deriv='R',phi=phi,romberg=romberg,nsigma=nsigma) return 0.5*(-meanvphi/R+dmeanvRRdphi/R-dmeanvphidR) @potential_physical_input @physical_conversion('frequency-kmskpc',pop=True) def oortC(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: oortC PURPOSE: calculate the Oort function C INPUT: R - radius at which to calculate C (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: Oort C at R HISTORY: 2011-04-19 - Written - Bovy (NYU) BUGS: could be made more efficient, e.g., surfacemass is calculated multiple times we know this is zero, but it is calculated anyway (bug or feature?) """ #2C= -meanvR/R-dmeanvphi/R/dphi+dmeanvR/dR meanvr= self.meanvR(R,romberg=romberg,nsigma=nsigma,phi=phi, use_physical=False) dmeanvphiRdphi= 0. #We know this, since the DF does not depend on phi surfmass= self._vmomentsurfacemass(R,0,0,phi=phi,romberg=romberg,nsigma=nsigma) dmeanvRdR= self._vmomentsurfacemass(R,1,0,deriv='R',phi=phi,romberg=romberg,nsigma=nsigma)/\ surfmass #other terms is zero because f is even in vR return 0.5*(-meanvr/R-dmeanvphiRdphi/R+dmeanvRdR) @potential_physical_input @physical_conversion('frequency-kmskpc',pop=True) def oortK(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: oortK PURPOSE: calculate the Oort function K INPUT: R - radius at which to calculate K (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: Oort K at R HISTORY: 2011-04-19 - Written - Bovy (NYU) BUGS: could be made more efficient, e.g., surfacemass is calculated multiple times we know this is zero, but it is calculated anyway (bug or feature?) """ #2K= meanvR/R+dmeanvphi/R/dphi+dmeanvR/dR meanvr= self.meanvR(R,romberg=romberg,nsigma=nsigma,phi=phi, use_physical=False) dmeanvphiRdphi= 0. #We know this, since the DF does not depend on phi surfmass= self._vmomentsurfacemass(R,0,0,phi=phi,romberg=romberg,nsigma=nsigma) dmeanvRdR= self._vmomentsurfacemass(R,1,0,deriv='R',phi=phi,romberg=romberg,nsigma=nsigma)/\ surfmass #other terms is zero because f is even in vR return 0.5*(+meanvr/R+dmeanvphiRdphi/R+dmeanvRdR) @potential_physical_input @physical_conversion('velocity2',pop=True) def sigma2(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: sigma2 PURPOSE: calculate sigma_R^2 at R by marginalizing over velocity INPUT: R - radius at which to calculate sigma_R^2 density (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: sigma_R^2 at R HISTORY: 2010-03-XX - Written - Bovy (NYU) """ return self.sigma2surfacemass(R,romberg,nsigma,use_physical=False)\ /self.surfacemass(R,romberg,nsigma,use_physical=False) @potential_physical_input @physical_conversion('velocity2',pop=True) def sigmaT2(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: sigmaT2 PURPOSE: calculate sigma_T^2 at R by marginalizing over velocity INPUT: R - radius at which to calculate sigma_T^2 (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: sigma_T^2 at R HISTORY: 2011-03-30 - Written - Bovy (NYU) """ surfmass= self.surfacemass(R,romberg=romberg,nsigma=nsigma, use_physical=False) return (self._vmomentsurfacemass(R,0,2,romberg=romberg,nsigma=nsigma) -self._vmomentsurfacemass(R,0,1,romberg=romberg,nsigma=nsigma)\ **2.\ /surfmass)/surfmass @potential_physical_input @physical_conversion('velocity2',pop=True) def sigmaR2(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: sigmaR2 (duplicate of sigma2 for consistency) PURPOSE: calculate sigma_R^2 at R by marginalizing over velocity INPUT: R - radius at which to calculate sigma_R^2 (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: sigma_R^2 at R HISTORY: 2011-03-30 - Written - Bovy (NYU) """ return self.sigma2(R,romberg=romberg,nsigma=nsigma,use_physical=False) @potential_physical_input @physical_conversion('velocity',pop=True) def meanvT(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: meanvT PURPOSE: calculate at R by marginalizing over velocity INPUT: R - radius at which to calculate (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: at R HISTORY: 2011-03-30 - Written - Bovy (NYU) """ return self._vmomentsurfacemass(R,0,1,romberg=romberg,nsigma=nsigma)\ /self.surfacemass(R,romberg=romberg,nsigma=nsigma, use_physical=False) @potential_physical_input @physical_conversion('velocity',pop=True) def meanvR(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: meanvR PURPOSE: calculate at R by marginalizing over velocity INPUT: R - radius at which to calculate (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: at R HISTORY: 2011-03-30 - Written - Bovy (NYU) """ return self._vmomentsurfacemass(R,1,0,romberg=romberg,nsigma=nsigma)\ /self.surfacemass(R,romberg=romberg,nsigma=nsigma, use_physical=False) @potential_physical_input def skewvT(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: skewvT PURPOSE: calculate skew in vT at R by marginalizing over velocity INPUT: R - radius at which to calculate (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: skewvT HISTORY: 2011-12-07 - Written - Bovy (NYU) """ surfmass= self.surfacemass(R,romberg=romberg,nsigma=nsigma, use_physical=False) vt= self._vmomentsurfacemass(R,0,1,romberg=romberg,nsigma=nsigma)\ /surfmass vt2= self._vmomentsurfacemass(R,0,2,romberg=romberg,nsigma=nsigma)\ /surfmass vt3= self._vmomentsurfacemass(R,0,3,romberg=romberg,nsigma=nsigma)\ /surfmass s2= vt2-vt**2. return (vt3-3.*vt*vt2+2.*vt**3.)*s2**(-1.5) @potential_physical_input def skewvR(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: skewvR PURPOSE: calculate skew in vR at R by marginalizing over velocity INPUT: R - radius at which to calculate (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: skewvR HISTORY: 2011-12-07 - Written - Bovy (NYU) """ surfmass= self.surfacemass(R,romberg=romberg,nsigma=nsigma, use_physical=False) vr= self._vmomentsurfacemass(R,1,0,romberg=romberg,nsigma=nsigma)\ /surfmass vr2= self._vmomentsurfacemass(R,2,0,romberg=romberg,nsigma=nsigma)\ /surfmass vr3= self._vmomentsurfacemass(R,3,0,romberg=romberg,nsigma=nsigma)\ /surfmass s2= vr2-vr**2. return (vr3-3.*vr*vr2+2.*vr**3.)*s2**(-1.5) @potential_physical_input def kurtosisvT(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: kurtosisvT PURPOSE: calculate excess kurtosis in vT at R by marginalizing over velocity INPUT: R - radius at which to calculate (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: kurtosisvT HISTORY: 2011-12-07 - Written - Bovy (NYU) """ surfmass= self.surfacemass(R,romberg=romberg,nsigma=nsigma, use_physical=False) vt= self._vmomentsurfacemass(R,0,1,romberg=romberg,nsigma=nsigma)\ /surfmass vt2= self._vmomentsurfacemass(R,0,2,romberg=romberg,nsigma=nsigma)\ /surfmass vt3= self._vmomentsurfacemass(R,0,3,romberg=romberg,nsigma=nsigma)\ /surfmass vt4= self._vmomentsurfacemass(R,0,4,romberg=romberg,nsigma=nsigma)\ /surfmass s2= vt2-vt**2. return (vt4-4.*vt*vt3+6.*vt**2.*vt2-3.*vt**4.)*s2**(-2.)-3. @potential_physical_input def kurtosisvR(self,R,romberg=False,nsigma=None,phi=0.): """ NAME: kurtosisvR PURPOSE: calculate excess kurtosis in vR at R by marginalizing over velocity INPUT: R - radius at which to calculate (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over KEYWORDS: romberg - if True, use a romberg integrator (default: False) OUTPUT: kurtosisvR HISTORY: 2011-12-07 - Written - Bovy (NYU) """ surfmass= self.surfacemass(R,romberg=romberg,nsigma=nsigma, use_physical=False) vr= self._vmomentsurfacemass(R,1,0,romberg=romberg,nsigma=nsigma)\ /surfmass vr2= self._vmomentsurfacemass(R,2,0,romberg=romberg,nsigma=nsigma)\ /surfmass vr3= self._vmomentsurfacemass(R,3,0,romberg=romberg,nsigma=nsigma)\ /surfmass vr4= self._vmomentsurfacemass(R,4,0,romberg=romberg,nsigma=nsigma)\ /surfmass s2= vr2-vr**2. return (vr4-4.*vr*vr3+6.*vr**2.*vr2-3.*vr**4.)*s2**(-2.)-3. def _ELtowRRapRperi(self,E,L): """ NAME: _ELtowRRapRperi PURPOSE: calculate the radial frequency based on E,L, also return rap and rperi INPUT: E - energy L - angular momentum OUTPUT: wR(E.L) HISTORY: 2010-07-11 - Written - Bovy (NYU) """ if self._beta == 0.: xE= numpy.exp(E-.5) else: #non-flat rotation curve xE= (2.*E/(1.+1./self._beta))**(1./2./self._beta) rperi,rap= self._aA.calcRapRperi(xE,0.,L/xE,0.,0.) #Replace the above w/ aA= actionAngleAxi(xE,0.,L/xE, pot=PowerSphericalPotential(normalize=1., alpha=2.-2.*self._beta).toPlanar()) TR= aA.TR() return (2.*numpy.pi/TR,rap,rperi) def sample(self,n=1,rrange=None,returnROrbit=True,returnOrbit=False, nphi=1.,los=None,losdeg=True,nsigma=None,maxd=None,target=True): r""" NAME: sample PURPOSE: sample n*nphi points from this DF INPUT: n - number of desired sample (specifying this rather than calling this routine n times is more efficient) rrange - if you only want samples in this rrange, set this keyword (only works when asking for an (RZ)Orbit) (can be Quantity) returnROrbit - if True, return a planarROrbit instance: [R,vR,vT] (default) returnOrbit - if True, return a planarOrbit instance (including phi) nphi - number of azimuths to sample for each E,L los= line of sight sampling along this line of sight (can be Quantity) losdeg= los in degrees? (default=True) target= if True, use target surface mass and sigma2 profiles (default=True) nsigma= number of sigma to rejection-sample on maxd= maximum distance to consider (for the rejection sampling) OUTPUT: n*nphi list of [[E,Lz],...] or list of planar(R)Orbits CAUTION: lists of EL need to be post-processed to account for the \kappa/\omega_R discrepancy HISTORY: 2010-07-10 - Started - Bovy (NYU) """ raise NotImplementedError("'sample' method for this disk df is not implemented") def _estimatemeanvR(self,R,phi=0.,log=False): """ NAME: _estimatemeanvR PURPOSE: quickly estimate meanvR (useful in evolveddiskdf where we need an estimate of this but we do not want to spend too much time on it) INPUT: R - radius at which to evaluate (/ro) phi= azimuth (not used) OUTPUT: target Sigma_R^2(R) log - if True, return the log (default: False) HISTORY: 2010-03-28 - Written - Bovy (NYU) """ return 0. def _estimatemeanvT(self,R,phi=0.,log=False): """ NAME: _estimatemeanvT PURPOSE: quickly estimate meanvR (useful in evolveddiskdf where we need an estimate of this but we do not want to spend too much time on it) INPUT: R - radius at which to evaluate (/ro) phi= azimuth (not used) OUTPUT: target Sigma_R^2(R) HISTORY: 2010-03-28 - Written - Bovy (NYU) """ return R**self._beta-self.asymmetricdrift(R,use_physical=False) def _estimateSigmaR2(self,R,phi=0.,log=False): """ NAME: _estimateSigmaR2 PURPOSE: quickly estimate SigmaR2 (useful in evolveddiskdf where we need an estimate of this but we do not want to spend too much time on it) INPUT: R - radius at which to evaluate (/ro) phi= azimuth (not used) OUTPUT: target Sigma_R^2(R) log - if True, return the log (default: False) HISTORY: 2010-03-28 - Written - Bovy (NYU) """ return self.targetSigma2(R,log=log,use_physical=False) def _estimateSigmaT2(self,R,phi=0.,log=False): """ NAME: _estimateSigmaT2 PURPOSE: quickly estimate SigmaT2 (useful in evolveddiskdf where we need an estimate of this but we do not want to spend too much time on it) INPUT: R - radius at which to evaluate (/ro) phi= azimuth (not used) OUTPUT: target Sigma_R^2(R) log - if True, return the log (default: False) HISTORY: 2010-03-28 - Written - Bovy (NYU) """ if log: return self.targetSigma2(R,log=log,use_physical=False)\ -2.*numpylog(self._gamma) else: return self.targetSigma2(R,log=log,use_physical=False)\ /self._gamma**2. class dehnendf(diskdf): """Dehnen's 'new' df""" def __init__(self,surfaceSigma=expSurfaceSigmaProfile, profileParams=(1./3.,1.0,0.2), correct=False, beta=0.,**kwargs): """ NAME: __init__ PURPOSE: Initialize a Dehnen 'new' DF INPUT: surfaceSigma - instance or class name of the target surface density and sigma_R profile (default: both exponential) profileParams - parameters of the surface and sigma_R profile: (xD,xS,Sro) where xD - disk surface mass scalelength (can be Quantity) xS - disk velocity dispersion scalelength (can be Quantity) Sro - disk velocity dispersion at Ro (can be Quantity) Directly given to the 'surfaceSigmaProfile class, so could be anything that class takes beta - power-law index of the rotation curve correct - if True, correct the DF ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) +DFcorrection kwargs (except for those already specified) OUTPUT: instance HISTORY: 2010-03-10 - Written - Bovy (NYU) """ return diskdf.__init__(self,surfaceSigma=surfaceSigma, profileParams=profileParams, correct=correct,dftype='dehnen', beta=beta,**kwargs) def eval(self,E,L,logSigmaR=0.,logsigmaR2=0.): """ NAME: eval PURPOSE: evaluate the distribution function INPUT: E - energy (can be Quantity) L - angular momentum (can be Quantity) OUTPUT: DF(E,L) HISTORY: 2010-03-10 - Written - Bovy (NYU) 2010-03-28 - Moved to dehnenDF - Bovy (NYU) """ if _PROFILE: #pragma: no cover import time start= time.time() E= conversion.parse_energy(E,vo=self._vo) L= conversion.parse_angmom(L,ro=self._ro,vo=self._vo) #Calculate Re,LE, OmegaE if self._beta == 0.: xE= numpy.exp(E-.5) logOLLE= numpylog(L/xE-1.) else: #non-flat rotation curve xE= (2.*E/(1.+1./self._beta))**(1./2./self._beta) logOLLE= self._beta*numpylog(xE)+numpylog(L/xE-xE**self._beta) if _PROFILE: #pragma: no cover one_time= (time.time()-start) start= time.time() if self._correct: correction= self._corr.correct(xE,log=True) else: correction= numpy.zeros(2) if _PROFILE: #pragma: no cover corr_time= (time.time()-start) start= time.time() SRE2= self.targetSigma2(xE,log=True,use_physical=False)+correction[1] if _PROFILE: #pragma: no cover targSigma_time= (time.time()-start) start= time.time() out= self._gamma*numpy.exp(logsigmaR2-SRE2+self.targetSurfacemass(xE,log=True,use_physical=False)-logSigmaR+numpy.exp(logOLLE-SRE2)+correction[0])/2./numpy.pi out_time= (time.time()-start) tot_time= one_time+corr_time+targSigma_time+out_time print(one_time/tot_time, corr_time/tot_time, targSigma_time/tot_time, out_time/tot_time, tot_time) return out else: return self._gamma*numpy.exp(logsigmaR2-SRE2+self.targetSurfacemass(xE,log=True,use_physical=False)-logSigmaR+numpy.exp(logOLLE-SRE2)+correction[0])/2./numpy.pi def sample(self,n=1,rrange=None,returnROrbit=True,returnOrbit=False, nphi=1.,los=None,losdeg=True,nsigma=None,targetSurfmass=True, targetSigma2=True, maxd=None,**kwargs): r""" NAME: sample PURPOSE: sample n*nphi points from this DF INPUT: n - number of desired sample (specifying this rather than calling this routine n times is more efficient) rrange - if you only want samples in this rrange, set this keyword (only works when asking for an (RZ)Orbit returnROrbit - if True, return a planarROrbit instance: [R,vR,vT] (default) returnOrbit - if True, return a planarOrbit instance (including phi) nphi - number of azimuths to sample for each E,L los= if set, sample along this line of sight (deg) (assumes that the Sun is located at R=1,phi=0) losdeg= if False, los is in radians (default=True) targetSurfmass, targetSigma2= if True, use target surface mass and sigma2 profiles, respectively (there is not much point to doing the latter) (default=True) nsigma= number of sigma to rejection-sample on maxd= maximum distance to consider (for the rejection sampling) OUTPUT: n*nphi list of [[E,Lz],...] or list of planar(R)Orbits CAUTION: lists of EL need to be post-processed to account for the \kappa/\omega_R discrepancy; EL not returned in physical units HISTORY: 2010-07-10 - Started - Bovy (NYU) """ if not los is None: return self.sampleLOS(los,deg=losdeg,n=n,maxd=maxd, nsigma=nsigma,targetSurfmass=targetSurfmass, targetSigma2=targetSigma2) #First sample xE if self._correct: xE= numpy.array(ars([0.,0.],[True,False],[0.05,2.],_ars_hx, _ars_hpx,nsamples=n, hxparams=(self._surfaceSigmaProfile, self._corr))) else: xE= numpy.array(ars([0.,0.],[True,False],[0.05,2.],_ars_hx, _ars_hpx,nsamples=n, hxparams=(self._surfaceSigmaProfile, None))) #Calculate E if self._beta == 0.: E= numpylog(xE)+0.5 else: #non-flat rotation curve E= .5*xE**(2.*self._beta)*(1.+1./self._beta) #Then sample Lz LCE= xE**(self._beta+1.) OR= xE**(self._beta-1.) Lz= self._surfaceSigmaProfile.sigma2(xE)*numpylog(stats.uniform.rvs(size=n))/OR if self._correct: Lz*= self._corr.correct(xE,log=False)[1,:] Lz+= LCE if not returnROrbit and not returnOrbit: out= [[e,l] for e,l in zip(E,Lz)] else: if not rrange is None: rrange[0]= conversion.parse_length(rrange[0],ro=self._ro) rrange[1]= conversion.parse_length(rrange[1],ro=self._ro) if not hasattr(self,'_psp'): self._psp= PowerSphericalPotential(alpha=2.-self._beta,normalize=True).toPlanar() out= [] for ii in range(int(n)): try: wR, rap, rperi= self._ELtowRRapRperi(E[ii],Lz[ii]) except ValueError: continue TR= 2.*numpy.pi/wR tr= stats.uniform.rvs()*TR if tr > TR/2.: tr-= TR/2. thisOrbit= Orbit([rperi,0.,Lz[ii]/rperi]) else: thisOrbit= Orbit([rap,0.,Lz[ii]/rap]) thisOrbit.integrate(numpy.array([0.,tr]),self._psp) if returnOrbit: vxvv= thisOrbit(tr).vxvv[0] thisOrbit= Orbit(vxvv=numpy.array([vxvv[0],vxvv[1],vxvv[2], stats.uniform.rvs()\ *numpy.pi*2.])\ .reshape(4)) else: thisOrbit= thisOrbit(tr) kappa= _kappa(thisOrbit.vxvv[0,0],self._beta) if not rrange == None: if thisOrbit.vxvv[0,0] < rrange[0] \ or thisOrbit.vxvv[0,0] > rrange[1]: continue mult= numpy.ceil(kappa/wR*nphi)-1. kappawR= kappa/wR*nphi-mult while mult > 0: if returnOrbit: out.append(Orbit(vxvv=numpy.array([vxvv[0],vxvv[1], vxvv[2], stats.uniform.rvs()*numpy.pi*2.]).reshape(4))) else: out.append(thisOrbit) mult-= 1 if stats.uniform.rvs() > kappawR: continue out.append(thisOrbit) #Recurse to get enough if len(out) < n*nphi: out.extend(self.sample(n=int(n-len(out)/nphi),rrange=rrange, returnROrbit=returnROrbit, returnOrbit=returnOrbit,nphi=int(nphi), los=los,losdeg=losdeg)) if len(out) > n*nphi: print(n, nphi, n*nphi) out= out[0:int(n*nphi)] if kwargs.get('use_physical',True) and \ self._roSet and self._voSet: if isinstance(out[0],Orbit): dumb= [o.turn_physical_on(ro=self._ro,vo=self._vo) for o in out] return out def _dlnfdR(self,R,vR,vT): #Calculate a bunch of stuff that we need if self._beta == 0.: E= vR**2./2.+vT**2./2.+numpylog(R) xE= numpy.exp(E-.5) OE= xE**-1. LCE= xE dRedR= xE/R else: #non-flat rotation curve E= vR**2./2.+vT**2./2.+1./2./self._beta*R**(2.*self._beta) xE= (2.*E/(1.+1./self._beta))**(1./2./self._beta) OE= xE**(self._beta-1.) LCE= xE**(self._beta+1.) dRedR= xE/2./self._beta/E*R**(2.*self._beta-1.) return self._dlnfdRe(R,vR,vT,E=E,xE=xE,OE=OE,LCE=LCE)*dRedR\ +self._dlnfdl(R,vR,vT,E=E,xE=xE,OE=OE)*vT def _dlnfdvR(self,R,vR,vT): #Calculate a bunch of stuff that we need if self._beta == 0.: E= vR**2./2.+vT**2./2.+numpylog(R) xE= numpy.exp(E-.5) OE= xE**-1. LCE= xE dRedvR= xE*vR else: #non-flat rotation curve E= vR**2./2.+vT**2./2.+1./2./self._beta*R**(2.*self._beta) xE= (2.*E/(1.+1./self._beta))**(1./2./self._beta) OE= xE**(self._beta-1.) LCE= xE**(self._beta+1.) dRedvR= xE/2./self._beta/E*vR return self._dlnfdRe(R,vR,vT,E=E,xE=xE,OE=OE,LCE=LCE)*dRedvR def _dlnfdvT(self,R,vR,vT): #Calculate a bunch of stuff that we need if self._beta == 0.: E= vR**2./2.+vT**2./2.+numpylog(R) xE= numpy.exp(E-.5) OE= xE**-1. LCE= xE dRedvT= xE*vT else: #non-flat rotation curve E= vR**2./2.+vT**2./2.+1./2./self._beta*R**(2.*self._beta) xE= (2.*E/(1.+1./self._beta))**(1./2./self._beta) OE= xE**(self._beta-1.) LCE= xE**(self._beta+1.) dRedvT= xE/2./self._beta/E*vT return self._dlnfdRe(R,vR,vT,E=E,xE=xE,OE=OE,LCE=LCE)*dRedvT\ +self._dlnfdl(R,vR,vT,E=E,xE=xE,OE=OE)*R def _dlnfdRe(self,R,vR,vT,E=None,xE=None,OE=None,LCE=None): """d ln f(x,v) / d R_e""" #Calculate a bunch of stuff that we need if E is None or xE is None or OE is None or LCE is None: if self._beta == 0.: E= vR**2./2.+vT**2./2.+numpylog(R) xE= numpy.exp(E-.5) OE= xE**-1. LCE= xE else: #non-flat rotation curve E= vR**2./2.+vT**2./2.+1./2./self._beta*R**(2.*self._beta) xE= (2.*E/(1.+1./self._beta))**(1./2./self._beta) OE= xE**(self._beta-1.) LCE= xE**(self._beta+1.) L= R*vT sigma2xE= self._surfaceSigmaProfile.sigma2(xE,log=False) return (self._surfaceSigmaProfile.surfacemassDerivative(xE,log=True)\ -(1.+OE*(L-LCE)/sigma2xE)*self._surfaceSigmaProfile.sigma2Derivative(xE,log=True)\ +(L-LCE)/sigma2xE*(self._beta-1.)*xE**(self._beta-2.)\ -OE*(self._beta+1.)/sigma2xE*xE**self._beta) def _dlnfdl(self,R,vR,vT,E=None,xE=None,OE=None): #Calculate a bunch of stuff that we need if E is None or xE is None or OE is None: if self._beta == 0.: E= vR**2./2.+vT**2./2.+numpylog(R) xE= numpy.exp(E-.5) OE= xE**-1. else: #non-flat rotation curve E= vR**2./2.+vT**2./2.+1./2./self._beta*R**(2.*self._beta) xE= (2.*E/(1.+1./self._beta))**(1./2./self._beta) OE= xE**(self._beta-1.) sigma2xE= self._surfaceSigmaProfile.sigma2(xE,log=False) return OE/sigma2xE class shudf(diskdf): """Shu's df (1969)""" def __init__(self,surfaceSigma=expSurfaceSigmaProfile, profileParams=(1./3.,1.0,0.2), correct=False, beta=0.,**kwargs): """ NAME: __init__ PURPOSE: Initialize a Shu DF INPUT: surfaceSigma - instance or class name of the target surface density and sigma_R profile (default: both exponential) profileParams - parameters of the surface and sigma_R profile: (xD,xS,Sro) where xD - disk surface mass scalelength (can be Quantity) xS - disk velocity dispersion scalelength (can be Quantity) Sro - disk velocity dispersion at Ro (can be Quantity) Directly given to the 'surfaceSigmaProfile class, so could be anything that class takes beta - power-law index of the rotation curve correct - if True, correct the DF ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) +DFcorrection kwargs (except for those already specified) OUTPUT: instance HISTORY: 2010-05-09 - Written - Bovy (NYU) """ return diskdf.__init__(self,surfaceSigma=surfaceSigma, profileParams=profileParams, correct=correct,dftype='shu', beta=beta,**kwargs) def eval(self,E,L,logSigmaR=0.,logsigmaR2=0.): """ NAME: eval PURPOSE: evaluate the distribution function INPUT: E - energy (/vo^2) L - angular momentun (/ro/vo) OUTPUT: DF(E,L) HISTORY: 2010-05-09 - Written - Bovy (NYU) """ E= conversion.parse_energy(E,vo=self._vo) L= conversion.parse_angmom(L,ro=self._ro,vo=self._vo) #Calculate RL,LL, OmegaL if self._beta == 0.: xL= L logECLE= numpylog(-numpylog(xL)-0.5+E) else: #non-flat rotation curve xL= L**(1./(self._beta+1.)) logECLE= numpylog(-0.5*(1./self._beta+1.)*xL**(2.*self._beta)+E) if xL < 0.: #We must remove counter-rotating mass return 0. if self._correct: correction= self._corr.correct(xL,log=True) else: correction= numpy.zeros(2) SRE2= self.targetSigma2(xL,log=True,use_physical=False)+correction[1] return self._gamma*numpy.exp(logsigmaR2-SRE2+self.targetSurfacemass(xL,log=True,use_physical=False)-logSigmaR-numpy.exp(logECLE-SRE2)+correction[0])/2./numpy.pi def sample(self,n=1,rrange=None,returnROrbit=True,returnOrbit=False, nphi=1.,los=None,losdeg=True,nsigma=None,maxd=None, targetSurfmass=True,targetSigma2=True,**kwargs): r""" NAME: sample PURPOSE: sample n*nphi points from this DF INPUT: n - number of desired sample (specifying this rather than calling this routine n times is more efficient) rrange - if you only want samples in this rrange, set this keyword (only works when asking for an (RZ)Orbit returnROrbit - if True, return a planarROrbit instance: [R,vR,vT] (default) returnOrbit - if True, return a planarOrbit instance (including phi) nphi - number of azimuths to sample for each E,L los= if set, sample along this line of sight (deg) (assumes that the Sun is located at R=1,phi=0) losdeg= if False, los is in radians (default=True) targetSurfmass, targetSigma2= if True, use target surface mass and sigma2 profiles, respectively (there is not much point to doing the latter) (default=True) nsigma= number of sigma to rejection-sample on maxd= maximum distance to consider (for the rejection sampling) OUTPUT: n*nphi list of [[E,Lz],...] or list of planar(R)Orbits CAUTION: lists of EL need to be post-processed to account for the \kappa/\omega_R discrepancy HISTORY: 2010-07-10 - Started - Bovy (NYU) """ if not los is None: return self.sampleLOS(los,n=n,maxd=maxd, nsigma=nsigma,targetSurfmass=targetSurfmass, targetSigma2=targetSigma2) #First sample xL if self._correct: xL= numpy.array(ars([0.,0.],[True,False],[0.05,2.],_ars_hx, _ars_hpx,nsamples=n, hxparams=(self._surfaceSigmaProfile, self._corr))) else: xL= numpy.array(ars([0.,0.],[True,False],[0.05,2.],_ars_hx, _ars_hpx,nsamples=n, hxparams=(self._surfaceSigmaProfile, None))) #Calculate Lz Lz= xL**(self._beta+1.) #Then sample E if self._beta == 0.: ECL= numpylog(xL)+0.5 else: ECL= 0.5*(1./self._beta+1.)*xL**(2.*self._beta) E= -self._surfaceSigmaProfile.sigma2(xL)*numpylog(stats.uniform.rvs(size=n)) if self._correct: E*= self._corr.correct(xL,log=False)[1,:] E+= ECL if not returnROrbit and not returnOrbit: out= [[e,l] for e,l in zip(E,Lz)] else: if not rrange is None: rrange[0]= conversion.parse_length(rrange[0],ro=self._ro) rrange[1]= conversion.parse_length(rrange[1],ro=self._ro) if not hasattr(self,'_psp'): self._psp= PowerSphericalPotential(alpha=2.-self._beta,normalize=True).toPlanar() out= [] for ii in range(n): try: wR, rap, rperi= self._ELtowRRapRperi(E[ii],Lz[ii]) except ValueError: #pragma: no cover continue TR= 2.*numpy.pi/wR tr= stats.uniform.rvs()*TR if tr > TR/2.: tr-= TR/2. thisOrbit= Orbit([rperi,0.,Lz[ii]/rperi]) else: thisOrbit= Orbit([rap,0.,Lz[ii]/rap]) thisOrbit.integrate(numpy.array([0.,tr]),self._psp) if returnOrbit: vxvv= thisOrbit(tr).vxvv[0] thisOrbit= Orbit(vxvv=numpy.array([vxvv[0],vxvv[1],vxvv[2], stats.uniform.rvs()*numpy.pi*2.]).reshape(4)) else: thisOrbit= thisOrbit(tr) kappa= _kappa(thisOrbit.vxvv[0,0],self._beta) if not rrange == None: if thisOrbit.vxvv[0,0] < rrange[0] \ or thisOrbit.vxvv[0,0] > rrange[1]: continue mult= numpy.ceil(kappa/wR*nphi)-1. kappawR= kappa/wR*nphi-mult while mult > 0: if returnOrbit: out.append(Orbit(vxvv=numpy.array([vxvv[0],vxvv[1], vxvv[2], stats.uniform.rvs()*numpy.pi*2.]).reshape(4))) else: out.append(thisOrbit) mult-= 1 if stats.uniform.rvs() > kappawR: continue out.append(thisOrbit) #Recurse to get enough if len(out) < n*nphi: out.extend(self.sample(n=int(n-len(out)/nphi),rrange=rrange, returnROrbit=returnROrbit, returnOrbit=returnOrbit,nphi=nphi)) if len(out) > n*nphi: out= out[0:int(n*nphi)] if kwargs.get('use_physical',True) and \ self._roSet and self._voSet: if isinstance(out[0],Orbit): dumb= [o.turn_physical_on(ro=self._ro,vo=self._vo) for o in out] return out def _dlnfdR(self,R,vR,vT): #Calculate a bunch of stuff that we need E, L= vRvTRToEL(vR,vT,R,self._beta,self._dftype) if self._beta == 0.: xL= L dRldR= vT ECL= numpylog(xL)+0.5 dECLEdR= 0. else: #non-flat rotation curve xL= L**(1./(self._beta+1.)) dRldR= L**(1./(self._beta+1.))/R/(self._beta+1.) ECL= 0.5*(1./self._beta+1.)*xL**(2.*self._beta) dECLdRl= (1.+self._beta)*xL**(2.*self._beta-1) dEdR= R**(2.*self._beta-1.) dECLEdR= dECLdRl*dRldR-dEdR sigma2xL= self._surfaceSigmaProfile.sigma2(xL,log=False) return (self._surfaceSigmaProfile.surfacemassDerivative(xL,log=True)\ -(1.+(ECL-E)/sigma2xL)*self._surfaceSigmaProfile.sigma2Derivative(xL,log=True))*dRldR\ +dECLEdR/sigma2xL def _dlnfdvR(self,R,vR,vT): #Calculate a bunch of stuff that we need E, L= vRvTRToEL(vR,vT,R,self._beta,self._dftype) if self._beta == 0.: xL= L else: #non-flat rotation curve xL= L**(1./(self._beta+1.)) sigma2xL= self._surfaceSigmaProfile.sigma2(xL,log=False) return -vR/sigma2xL def _dlnfdvT(self,R,vR,vT): #Calculate a bunch of stuff that we need E, L= vRvTRToEL(vR,vT,R,self._beta,self._dftype) if self._beta == 0.: xL= L dRldvT= R ECL= numpylog(xL)+0.5 dECLEdvT= 1./vT-vT else: #non-flat rotation curve xL= L**(1./(self._beta+1.)) dRldvT= L**(1./(self._beta+1.))/vT/(self._beta+1.) ECL= 0.5*(1./self._beta+1.)*xL**(2.*self._beta) dECLdRl= (1.+self._beta)*xL**(2.*self._beta-1) dEdvT= vT dECLEdvT= dECLdRl*dRldvT-dEdvT sigma2xL= self._surfaceSigmaProfile.sigma2(xL,log=False) return (self._surfaceSigmaProfile.surfacemassDerivative(xL,log=True)\ -(1.+(ECL-E)/sigma2xL)*self._surfaceSigmaProfile.sigma2Derivative(xL,log=True))*dRldvT\ +dECLEdvT/sigma2xL class schwarzschilddf(shudf): """Schwarzschild's df""" def __init__(self,surfaceSigma=expSurfaceSigmaProfile, profileParams=(1./3.,1.0,0.2), correct=False, beta=0.,**kwargs): """ NAME: __init__ PURPOSE: Initialize a Schwarzschild DF INPUT: surfaceSigma - instance or class name of the target surface density and sigma_R profile (default: both exponential) profileParams - parameters of the surface and sigma_R profile: (xD,xS,Sro) where xD - disk surface mass scalelength (can be Quantity) xS - disk velocity dispersion scalelength (can be Quantity) Sro - disk velocity dispersion at Ro (can be Quantity) Directly given to the 'surfaceSigmaProfile class, so could be anything that class takes beta - power-law index of the rotation curve correct - if True, correct the DF ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) +DFcorrection kwargs (except for those already specified) OUTPUT: instance HISTORY: 2017-09-17 - Written - Bovy (UofT) """ # Schwarzschild == Shu w/ energy computed in epicycle approx. # so all functions are the same as in Shu, only thing different is # how E is computed return diskdf.__init__(self,surfaceSigma=surfaceSigma, profileParams=profileParams, correct=correct,dftype='schwarzschild', beta=beta,**kwargs) def _surfaceIntegrand(vR,vT,R,df,logSigmaR,logsigmaR2,sigmaR1,gamma): """Internal function that is the integrand for the surface mass integration""" E,L= _vRpvTpRToEL(vR,vT,R,df._beta,sigmaR1,gamma,df._dftype) return df.eval(E,L,logSigmaR,logsigmaR2)*2.*numpy.pi/df._gamma #correct def _sigma2surfaceIntegrand(vR,vT,R,df,logSigmaR,logsigmaR2,sigmaR1,gamma): """Internal function that is the integrand for the sigma-squared times surface mass integration""" E,L= _vRpvTpRToEL(vR,vT,R,df._beta,sigmaR1,gamma,df._dftype) return vR**2.*df.eval(E,L,logSigmaR,logsigmaR2)*2.*numpy.pi/df._gamma #correct def _vmomentsurfaceIntegrand(vR,vT,R,df,logSigmaR,logsigmaR2,sigmaR1,gamma, n,m): """Internal function that is the integrand for the velocity moment times surface mass integration""" E,L= _vRpvTpRToEL(vR,vT,R,df._beta,sigmaR1,gamma,df._dftype) return vR**n*vT**m*df.eval(E,L,logSigmaR,logsigmaR2)*2.*numpy.pi/df._gamma #correct def _vmomentderivsurfaceIntegrand(vR,vT,R,df,logSigmaR,logsigmaR2,sigmaR1, gamma,n,m,deriv): """Internal function that is the integrand for the derivative of velocity moment times surface mass integration""" E,L= _vRpvTpRToEL(vR,vT,R,df._beta,sigmaR1,gamma,df._dftype) if deriv.lower() == 'r': return vR**n*vT**m*df.eval(E,L,logSigmaR,logsigmaR2)*2.*numpy.pi/df._gamma*df._dlnfdR(R,vR*sigmaR1,vT*sigmaR1/gamma) #correct else: return 0. def _vRpvTpRToEL(vR,vT,R,beta,sigmaR1,gamma,dftype='dehnen'): """Internal function that calculates E and L given velocities normalized by the velocity dispersion""" vR*= sigmaR1 vT*= sigmaR1/gamma return vRvTRToEL(vR,vT,R,beta,dftype) def _oned_intFunc(x,twodfunc,gfun,hfun,tol,args): """Internal function for bovy_dblquad""" thisargs= copy.deepcopy(args) thisargs.insert(0,x) return integrate.romberg(twodfunc,gfun(x),hfun(x),args=thisargs,tol=tol) def bovy_dblquad(func, a, b, gfun, hfun, args=(), tol=1.48e-08): """ NAME: bovy_dblquad PURPOSE: like scipy.integrate's dblquad, but using Romberg integration for the one-d integrals and using tol INPUT: same as scipy.integrate.dblquad except for tol and epsrel,epsabs OUTPUT: value HISTORY: 2010-03-11 - Written - Bpvy (NYU) """ return integrate.romberg(_oned_intFunc,a,b,args=(func,gfun,hfun,tol,args),tol=tol) class DFcorrection: """Class that contains the corrections necessary to reach exponential profiles""" def __init__(self,**kwargs): """ NAME: __init__ PURPOSE: initialize the corrections: set them, load them, or calculate and save them OPTIONAL INPUTS: corrections - if Set, these are the corrections and they should be used as such npoints - number of points from 0 to Rmax rmax - correct up to this radius (/ro) (default: 5) savedir - save the corrections in this directory surfaceSigmaProfile - target surfacemass and sigma_R^2 instance beta - power-law index of the rotation curve (when calculating) dftype - classname of the DF niter - number of iterations to perform to calculate the corrections interp_k - 'k' keyword to give to InterpolatedUnivariateSpline OUTPUT: HISTORY: 2010-03-10 - Written - Bovy (NYU) """ if not 'surfaceSigmaProfile' in kwargs: raise DFcorrectionError("surfaceSigmaProfile not given") else: self._surfaceSigmaProfile= kwargs['surfaceSigmaProfile'] self._rmax= kwargs.get('rmax',5.) self._niter= kwargs.get('niter',20) if not 'npoints' in kwargs: if 'corrections' in kwargs: self._npoints= kwargs['corrections'].shape[0] else: #pragma: no cover self._npoints= 151 #would take too long to cover else: self._npoints= kwargs['npoints'] self._dftype= kwargs.get('dftype',dehnendf) self._beta= kwargs.get('beta',0.) self._rs= numpy.linspace(_RMIN,self._rmax,self._npoints) self._interp_k= kwargs.get('interp_k',_INTERPDEGREE) if 'corrections' in kwargs: self._corrections= kwargs['corrections'] if not len(self._corrections) == self._npoints: raise DFcorrectionError("Number of corrections has to be equal to the number of points npoints") else: self._savedir= kwargs.get('savedir',_CORRECTIONSDIR) self._savefilename= self._createSavefilename(self._niter) if os.path.exists(self._savefilename): savefile= open(self._savefilename,'rb') self._corrections= numpy.array(pickle.load(savefile)) savefile.close() else: #Calculate the corrections self._corrections= self._calc_corrections() #Interpolation; smoothly go to zero interpRs= numpy.append(self._rs,2.*self._rmax) self._surfaceInterpolate= interpolate.InterpolatedUnivariateSpline(interpRs, numpylog(numpy.append(self._corrections[:,0],1.)), k=self._interp_k) self._sigma2Interpolate= interpolate.InterpolatedUnivariateSpline(interpRs, numpylog(numpy.append(self._corrections[:,1],1.)), k=self._interp_k) #Interpolation for R < _RMIN surfaceInterpolateSmallR= interpolate.UnivariateSpline(interpRs[0:_INTERPDEGREE+2],numpylog(self._corrections[0:_INTERPDEGREE+2,0]),k=_INTERPDEGREE) self._surfaceDerivSmallR= surfaceInterpolateSmallR.derivatives(interpRs[0])[1] sigma2InterpolateSmallR= interpolate.UnivariateSpline(interpRs[0:_INTERPDEGREE+2],numpylog(self._corrections[0:_INTERPDEGREE+2,1]),k=_INTERPDEGREE) self._sigma2DerivSmallR= sigma2InterpolateSmallR.derivatives(interpRs[0])[1] return None def _createSavefilename(self,niter): #Form surfaceSigmaProfile string sspFormat= self._surfaceSigmaProfile.formatStringParams() sspString= '' for format in sspFormat: sspString+= format+'_' return os.path.join(self._savedir,'dfcorrection_'+ self._dftype.__name__+'_'+ self._surfaceSigmaProfile.__class__.__name__+'_'+ sspString % self._surfaceSigmaProfile.outputParams()+ '%6.4f_%i_%6.4f_%i.sav' % (self._beta,self._npoints,self._rmax,niter)) def correct(self,R,log=False): """ NAME: correct PURPOSE: calculate the correction in Sigma and sigma2 at R INPUT: R - Galactocentric radius(/ro) log - if True, return the log of the correction OUTPUT: [Sigma correction, sigma2 correction] HISTORY: 2010-03-10 - Written - Bovy (NYU) """ if isinstance(R,numpy.ndarray): out= numpy.empty((2,len(R))) #R < _RMIN rmin_indx= (R < _RMIN) if numpy.sum(rmin_indx) > 0: out[0,rmin_indx]= numpylog(self._corrections[0,0])\ +self._surfaceDerivSmallR*(R[rmin_indx]-_RMIN) out[1,rmin_indx]= numpylog(self._corrections[0,1])\ +self._sigma2DerivSmallR*(R[rmin_indx]-_RMIN) #R > 2rmax rmax_indx= (R > (2.*self._rmax)) if numpy.sum(rmax_indx) > 0: out[:,rmax_indx]= 0. #'normal' R r_indx= (R >= _RMIN)*(R <= (2.*self._rmax)) if numpy.sum(r_indx) > 0: out[0,r_indx]= self._surfaceInterpolate(R[r_indx]) out[1,r_indx]= self._sigma2Interpolate(R[r_indx]) if log: return out else: return numpy.exp(out) if R < _RMIN: out= numpy.array([numpylog(self._corrections[0,0])+self._surfaceDerivSmallR*(R-_RMIN), numpylog(self._corrections[0,1])+self._sigma2DerivSmallR*(R-_RMIN)]) elif R > (2.*self._rmax): out= numpy.array([0.,0.]) else: if _SCIPY_VERSION >= _SCIPY_VERSION_BREAK: out= numpy.array([self._surfaceInterpolate(R), self._sigma2Interpolate(R)]) else: #pragma: no cover out= numpy.array([self._surfaceInterpolate(R)[0], self._sigma2Interpolate(R)[0]]) if log: return out else: return numpy.exp(out) def derivLogcorrect(self,R): """ NAME: derivLogcorrect PURPOSE: calculate the derivative of the log of the correction in Sigma and sigma2 at R INPUT: R - Galactocentric radius(/ro) OUTPUT: [d log(Sigma correction)/dR, d log(sigma2 correction)/dR] HISTORY: 2010-03-10 - Written - Bovy (NYU) """ if R < _RMIN: out= numpy.array([self._surfaceDerivSmallR, self._sigma2DerivSmallR]) elif R > (2.*self._rmax): out= numpy.array([0.,0.]) else: if _SCIPY_VERSION >= _SCIPY_VERSION_BREAK: out= numpy.array([self._surfaceInterpolate(R,nu=1), self._sigma2Interpolate(R,nu=1)]) else: #pragma: no cover out= numpy.array([self._surfaceInterpolate(R,nu=1)[0], self._sigma2Interpolate(R,nu=1)[0]]) return out def _calc_corrections(self): """Internal function that calculates the corrections""" searchIter= self._niter-1 while searchIter > 0: trySavefilename= self._createSavefilename(searchIter) if os.path.exists(trySavefilename): trySavefile= open(trySavefilename,'rb') corrections= numpy.array(pickle.load(trySavefile)) trySavefile.close() break else: searchIter-= 1 if searchIter == 0: corrections= numpy.ones((self._npoints,2)) for ii in range(searchIter,self._niter): if ii == 0: currentDF= self._dftype(surfaceSigma=self._surfaceSigmaProfile, beta=self._beta) else: currentDF= self._dftype(surfaceSigma=self._surfaceSigmaProfile, beta=self._beta, corrections=corrections, npoints=self._npoints, rmax=self._rmax, savedir=self._savedir, interp_k=self._interp_k) newcorrections= numpy.zeros((self._npoints,2)) for jj in range(self._npoints): thisSurface= currentDF.surfacemass(self._rs[jj], use_physical=False) newcorrections[jj,0]= currentDF.targetSurfacemass(self._rs[jj],use_physical=False)/thisSurface newcorrections[jj,1]= currentDF.targetSigma2(self._rs[jj],use_physical=False)*thisSurface\ /currentDF.sigma2surfacemass(self._rs[jj], use_physical=False) #print(jj, newcorrections[jj,:]) corrections*= newcorrections #Save picklethis= [] for arr in list(corrections): picklethis.append([float(a) for a in arr]) save_pickles(self._savefilename,picklethis) #We pickle a list for platform-independence) return corrections class DFcorrectionError(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value) def vRvTRToEL(vR,vT,R,beta,dftype='dehnen'): """ NAME: vRvTRToEL PURPOSE: calculate the energy and angular momentum INPUT: vR - radial velocity vT - rotational velocity R - Galactocentric radius OUTPUT: HISTORY: 2010-03-10 - Written - Bovy (NYU) """ if dftype == 'schwarzschild': # Compute E in the epicycle approximation gamma= numpy.sqrt(2./(1.+beta)) L= R*vT if beta == 0.: xL= L else: #non-flat rotation curve xL= L**(1./(beta+1.)) return (0.5*vR**2.+0.5*gamma**2.*(vT-R**beta)**2. +xL**(2.*beta)/2.+axipotential(xL,beta=beta), L) else: return (axipotential(R,beta)+0.5*vR**2.+0.5*vT**2.,vT*R) def axipotential(R,beta=0.): """ NAME: axipotential PURPOSE: return the axisymmetric potential at R/Ro INPUT: R - Galactocentric radius beta - rotation curve power-law OUTPUT: Pot(R)/vo**2. HISTORY: 2010-03-01 - Written - Bovy (NYU) """ if beta == 0.: if numpy.any(R == 0.): out= numpy.empty(R.shape) out[R == 0.]= numpylog(_RMIN) out[R != 0.]= numpylog(R[R != 0.]) return out else: return numpylog(R) else: #non-flat rotation curve return R**(2.*beta)/2./beta def _ars_hx(x,args): """ NAME: _ars_hx PURPOSE: h(x) for ARS sampling of the input surfacemass profile INPUT: x - R(/ro) args= (surfaceSigma, dfcorr) surfaceSigma - surfaceSigmaProfile instance dfcorr - DFcorrection instance OUTPUT: log(x)+log surface(x) + log(correction) HISTORY: 2010-07-11 - Written - Bovy (NYU) """ surfaceSigma, dfcorr= args if dfcorr is None: return numpylog(x)+surfaceSigma.surfacemass(x,log=True) else: return numpylog(x)+surfaceSigma.surfacemass(x,log=True)+dfcorr.correct(x)[0] def _ars_hpx(x,args): """ NAME: _ars_hpx PURPOSE: h'(x) for ARS sampling of the input surfacemass profile INPUT: x - R(/ro) args= (surfaceSigma, dfcorr) surfaceSigma - surfaceSigmaProfile instance dfcorr - DFcorrection instance OUTPUT: derivative of log(x)+log surface(x) + log(correction) wrt x HISTORY: 2010-07-11 - Written - Bovy (NYU) """ surfaceSigma, dfcorr= args if dfcorr is None: return 1./x+surfaceSigma.surfacemassDerivative(x,log=True) else: return 1./x+surfaceSigma.surfacemassDerivative(x,log=True)+dfcorr.derivLogcorrect(x)[0] def _kappa(R,beta): """Internal function to give kappa(r)""" return numpy.sqrt(2.*(1.+beta))*R**(beta-1) def _dlToRphi(d,l): """Convert d and l to R and phi, l is in radians""" R= numpy.sqrt(1.+d**2.-2.*d*numpy.cos(l)) if R == 0.: R+= 0.0001 d+= 0.0001 if 1./numpy.cos(l) < d and numpy.cos(l) > 0.: theta= numpy.pi-numpy.arcsin(d/R*numpy.sin(l)) else: theta= numpy.arcsin(d/R*numpy.sin(l)) return (R,theta) def _vtmaxEq(vT,R,diskdf): """Equation to solve to find the max vT at R""" #Calculate a bunch of stuff that we need if diskdf._beta == 0.: E= vT**2./2.+numpylog(R) xE= numpy.exp(E-.5) OE= xE**-1. LCE= xE dxEdvT= xE*vT else: #non-flat rotation curve E= vT**2./2.+1./2./diskdf._beta*R**(2.*diskdf._beta) xE= (2.*E/(1.+1./diskdf._beta))**(1./2./diskdf._beta) OE= xE**(diskdf._beta-1.) LCE= xE**(diskdf._beta+1.) dxEdvT= xE/2./diskdf._beta/E*vT L= R*vT sigma2xE= diskdf._surfaceSigmaProfile.sigma2(xE,log=False) return OE*R/sigma2xE+\ (diskdf._surfaceSigmaProfile.surfacemassDerivative(xE,log=True)\ -(1.+OE*(L-LCE)/sigma2xE)*diskdf._surfaceSigmaProfile.sigma2Derivative(xE,log=True)\ +(L-LCE)/sigma2xE*(diskdf._beta-1.)*xE**(diskdf._beta-2.)\ -OE*(diskdf._beta+1.)/sigma2xE*xE**diskdf._beta)\ *dxEdvT def _marginalizeVperpIntegrandSinAlphaLarge(vR,df,R,sinalpha,cotalpha, vlos,vcirc,sigma): return df(*vRvTRToEL(vR*sigma,cotalpha*vR*sigma+vlos/sinalpha+vcirc, R,df._beta,df._dftype)) def _marginalizeVperpIntegrandSinAlphaSmall(vT,df,R,cosalpha,tanalpha, vlos,vcirc,sigma): return df(*vRvTRToEL(tanalpha*vT*sigma-vlos/cosalpha,vT*sigma+vcirc, R,df._beta,df._dftype)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/eddingtondf.py0000644000175100001710000001371114327773303016614 0ustar00runnerdocker# Class that implements isotropic spherical DFs computed using the Eddington # formula import numpy from scipy import integrate, interpolate from ..potential import evaluateR2derivs from ..potential.Potential import _evaluatePotentials, _evaluateRforces from ..util import conversion from .sphericaldf import isotropicsphericaldf, sphericaldf 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.""" 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/evolveddiskdf.py0000644000175100001710000034675314327773303017177 0ustar00runnerdocker############################################################################### # evolveddiskdf.py: module that builds a distribution function as a # steady-state DF + subsequent evolution # # This module contains the following classes: # # evolveddiskdf - top-level class that represents a distribution function ############################################################################### _NSIGMA= 4. _NTS= 1000 _PROFILE= False import copy import sys import time as time_module import warnings import numpy from scipy import integrate from ..orbit import Orbit from ..potential import calcRotcurve from ..potential.Potential import _check_c from ..util import galpyWarning, plot from ..util.conversion import (parse_time, physical_conversion, potential_physical_input) from ..util.quadpack import dblquad from .df import df _DEGTORAD= numpy.pi/180. _RADTODEG= 180./numpy.pi _NAN= numpy.nan class evolveddiskdf(df): """Class that represents a diskdf as initial DF + subsequent secular evolution""" def __init__(self,initdf,pot,to=0.): """ NAME: __init__ PURPOSE: initialize INPUT: initdf - the df at the start of the evolution (at to) (units are transferred) pot - potential to integrate orbits in to= initial time (time at which initdf is evaluated; orbits are integrated from current t back to to) (can be Quantity) OUTPUT: instance HISTORY: 2011-03-30 - Written - Bovy (NYU) """ if initdf._roSet: ro= initdf._ro else: ro= None if initdf._voSet: vo= initdf._vo else: vo= None df.__init__(self,ro=ro,vo=vo) self._initdf= initdf self._pot= pot self._to= parse_time(to,ro=self._ro,vo=self._vo) @physical_conversion('phasespacedensity2d',pop=True) def __call__(self,*args,**kwargs): """ NAME: __call__ PURPOSE: evaluate the distribution function INPUT: Orbit instance: a) Orbit instance alone: use initial state and t=0 b) Orbit instance + t: Orbit instance *NOT* called (i.e., Orbit's initial condition is used, call Orbit yourself), t can be Quantity If t is a list of t, DF is returned for each t, times must be in descending order and equally spaced (does not work with marginalize...) marginalizeVperp - marginalize over perpendicular velocity (only supported with 1a) above) + nsigma, +scipy.integrate.quad keywords marginalizeVlos - marginalize over line-of-sight velocity (only supported with 1a) above) + nsigma, +scipy.integrate.quad keywords log= if True, return the log (not for deriv, bc that can be negative) integrate_method= method argument of orbit.integrate deriv= None, 'R', or 'phi': calculates derivative of the moment wrt R or phi **not with the marginalize options** OUTPUT: DF(orbit,t) HISTORY: 2011-03-30 - Written - Bovy (NYU) 2011-04-15 - Added list of times option - Bovy (NYU) """ integrate_method= kwargs.pop('integrate_method','dopr54_c') # Must match Python fallback for non-C potentials here, bc odeint needs # custom t list to avoid numerically instabilities if '_c' in integrate_method and not _check_c(self._pot): if ('leapfrog' in integrate_method \ or 'symplec' in integrate_method): integrate_method= 'leapfrog' else: integrate_method= 'odeint' deriv= kwargs.get('deriv',None) if isinstance(args[0],Orbit): if len(args[0]) > 1: raise RuntimeError('Only single-object Orbit instances can be passed to DF instances at this point') #pragma: no cover if len(args) == 1: t= 0. else: t= args[1] else: raise OSError("Input to __call__ not understood; this has to be an Orbit instance with optional time") if isinstance(t,list): t= numpy.array(t) tlist= True elif isinstance(t,numpy.ndarray) and \ not (hasattr(t,'isscalar') and t.isscalar): tlist= True else: tlist= False t= parse_time(t,ro=self._ro,vo=self._vo) if kwargs.pop('marginalizeVperp',False): if tlist: raise OSError("Input times to __call__ is a list; this is not supported in conjunction with marginalizeVperp") if kwargs.pop('log',False): return numpy.log(self._call_marginalizevperp(args[0],integrate_method=integrate_method,**kwargs)) else: return self._call_marginalizevperp(args[0],integrate_method=integrate_method,**kwargs) elif kwargs.pop('marginalizeVlos',False): if tlist: raise OSError("Input times to __call__ is a list; this is not supported in conjunction with marginalizeVlos") if kwargs.pop('log',False): return numpy.log(self._call_marginalizevlos(args[0],integrate_method=integrate_method,**kwargs)) else: return self._call_marginalizevlos(args[0],integrate_method=integrate_method,**kwargs) #Integrate back if tlist: if self._to == t[0]: if kwargs.get('log',False): return numpy.log([self._initdf(args[0],use_physical=False)]) else: return [self._initdf(args[0],use_physical=False)] ts= self._create_ts_tlist(t,integrate_method) o= args[0] #integrate orbit if _PROFILE: #pragma: no cover start= time_module.time() if not deriv is None: #Also calculate the derivative of the initial df with respect to R, phi, vR, and vT, and the derivative of Ro wrt R/phi etc., to calculate the derivative; in this case we also integrate a small area of phase space if deriv.lower() == 'r': dderiv= 10.**-10. tmp= o.R(use_physical=False)+dderiv dderiv= tmp-o.R(use_physical=False) msg= o.integrate_dxdv([dderiv,0.,0.,0.],ts,self._pot,method=integrate_method) elif deriv.lower() == 'phi': dderiv= 10.**-10. tmp= o.phi(use_physical=False)+dderiv dderiv= tmp-o.phi(use_physical=False) msg= o.integrate_dxdv([0.,0.,0.,dderiv],ts,self._pot,method=integrate_method) if not msg is None and msg > 0.: # pragma: no cover print("Warning: dxdv integration inaccurate, returning zero everywhere ... result might not be correct ...") if kwargs.get('log',False) and deriv is None: return numpy.zeros(len(t))-numpy.finfo(numpy.dtype(numpy.float64)).max else: return numpy.zeros(len(t)) else: o.integrate(ts,self._pot,method=integrate_method) if _PROFILE: #pragma: no cover int_time= (time_module.time()-start) #Now evaluate the DF if _PROFILE: #pragma: no cover start= time_module.time() if integrate_method == 'odeint': retval= [] os= [o(self._to+t[0]-ti,use_physical=False) for ti in t] retval= numpy.array(self._initdf(os,use_physical=False)) else: if len(t) == 1: orb_array= o.getOrbit().T orb_array= orb_array[:,1] else: orb_array= o.getOrbit().T retval= self._initdf(orb_array,use_physical=False) if (isinstance(retval,float) or len(retval.shape) == 0) \ and numpy.isnan(retval): retval= 0. elif not isinstance(retval,float) and len(retval.shape) > 0: retval[(numpy.isnan(retval))]= 0. if len(t) > 1: retval= retval[::-1] if _PROFILE: #pragma: no cover df_time= (time_module.time()-start) tot_time= int_time+df_time print(int_time/tot_time, df_time/tot_time, tot_time) if not deriv is None: if integrate_method == 'odeint': dlnfdRo= numpy.array([self._initdf._dlnfdR(o.R(self._to+t[0]-ti,use_physical=False), o.vR(self._to+t[0]-ti,use_physical=False), o.vT(self._to+t[0]-ti,use_physical=False)) for ti in t]) dlnfdvRo= numpy.array([self._initdf._dlnfdvR(o.R(self._to+t[0]-ti,use_physical=False), o.vR(self._to+t[0]-ti,use_physical=False), o.vT(self._to+t[0]-ti,use_physical=False)) for ti in t]) dlnfdvTo= numpy.array([self._initdf._dlnfdvT(o.R(self._to+t[0]-ti,use_physical=False), o.vR(self._to+t[0]-ti,use_physical=False), o.vT(self._to+t[0]-ti,use_physical=False)) for ti in t]) dRo= numpy.array([o.getOrbit_dxdv()[list(ts).index(self._to+t[0]-ti),0] for ti in t])/dderiv dvRo= numpy.array([o.getOrbit_dxdv()[list(ts).index(self._to+t[0]-ti),1] for ti in t])/dderiv dvTo= numpy.array([o.getOrbit_dxdv()[list(ts).index(self._to+t[0]-ti),2] for ti in t])/dderiv #print(dRo, dvRo, dvTo) dlnfderiv= dlnfdRo*dRo+dlnfdvRo*dvRo+dlnfdvTo*dvTo retval*= dlnfderiv else: if len(t) == 1: dlnfdRo= self._initdf._dlnfdR(orb_array[0], orb_array[1], orb_array[2]) dlnfdvRo= self._initdf._dlnfdvR(orb_array[0], orb_array[1], orb_array[2]) dlnfdvTo= self._initdf._dlnfdvT(orb_array[0], orb_array[1], orb_array[2]) else: dlnfdRo= numpy.array([self._initdf._dlnfdR(orb_array[0,ii], orb_array[1,ii], orb_array[2,ii]) for ii in range(len(t))]) dlnfdvRo= numpy.array([self._initdf._dlnfdvR(orb_array[0,ii], orb_array[1,ii], orb_array[2,ii]) for ii in range(len(t))]) dlnfdvTo= numpy.array([self._initdf._dlnfdvT(orb_array[0,ii], orb_array[1,ii], orb_array[2,ii]) for ii in range(len(t))]) dorb_array= o.getOrbit_dxdv().T if len(t) == 1: dorb_array= dorb_array[:,1] dRo= dorb_array[0]/dderiv dvRo= dorb_array[1]/dderiv dvTo= dorb_array[2]/dderiv #print(dRo, dvRo, dvTo) dlnfderiv= dlnfdRo*dRo+dlnfdvRo*dvRo+dlnfdvTo*dvTo if len(t) > 1: dlnfderiv= dlnfderiv[::-1] retval*= dlnfderiv else: if self._to == t and deriv is None: if kwargs.get('log',False): return numpy.log(self._initdf(args[0],use_physical=False)) else: return self._initdf(args[0],use_physical=False) elif self._to == t and not deriv is None: if deriv.lower() == 'r': return self._initdf(args[0])\ *self._initdf._dlnfdR(args[0].vxvv[...,0], args[0].vxvv[...,1], args[0].vxvv[...,2]) elif deriv.lower() == 'phi': return 0. if integrate_method == 'odeint': ts= numpy.linspace(t,self._to,_NTS) else: ts= numpy.linspace(t,self._to,2) o= args[0] #integrate orbit if not deriv is None: ts= numpy.linspace(t,self._to,_NTS) #Also calculate the derivative of the initial df with respect to R, phi, vR, and vT, and the derivative of Ro wrt R/phi etc., to calculate the derivative; in this case we also integrate a small area of phase space if deriv.lower() == 'r': dderiv= 10.**-10. tmp= o.R(use_physical=False)+dderiv dderiv= tmp-o.R(use_physical=False) o.integrate_dxdv([dderiv,0.,0.,0.],ts,self._pot,method=integrate_method) elif deriv.lower() == 'phi': dderiv= 10.**-10. tmp= o.phi(use_physical=False)+dderiv dderiv= tmp-o.phi(use_physical=False) o.integrate_dxdv([0.,0.,0.,dderiv],ts,self._pot,method=integrate_method) else: o.integrate(ts,self._pot,method=integrate_method) #int_time= (time.time()-start) #Now evaluate the DF if o.R(self._to-t,use_physical=False) <= 0.: if kwargs.get('log',False): return -numpy.finfo(numpy.dtype(numpy.float64)).max else: return numpy.finfo(numpy.dtype(numpy.float64)).eps #start= time.time() retval= self._initdf(o(self._to-t,use_physical=False), use_physical=False) #print( int_time/(time.time()-start)) if numpy.isnan(retval): print(retval, o.vxvv, o(self._to-t).vxvv) if not deriv is None: thisorbit= o(self._to-t).vxvv[0] dlnfdRo= self._initdf._dlnfdR(thisorbit[0], thisorbit[1], thisorbit[2]) dlnfdvRo= self._initdf._dlnfdvR(thisorbit[0], thisorbit[1], thisorbit[2]) dlnfdvTo= self._initdf._dlnfdvT(thisorbit[0], thisorbit[1], thisorbit[2]) indx= list(ts).index(self._to-t) dRo= o.getOrbit_dxdv()[indx,0]/dderiv dvRo= o.getOrbit_dxdv()[indx,1]/dderiv dvTo= o.getOrbit_dxdv()[indx,2]/dderiv dlnfderiv= dlnfdRo*dRo+dlnfdvRo*dvRo+dlnfdvTo*dvTo retval*= dlnfderiv if kwargs.get('log',False) and deriv is None: if tlist: out= numpy.log(retval) out[retval == 0.]= -numpy.finfo(numpy.dtype(numpy.float64)).max else: if retval == 0.: out= -numpy.finfo(numpy.dtype(numpy.float64)).max else: out= numpy.log(retval) return out else: return retval def vmomentsurfacemass(self,R,n,m,t=0.,nsigma=None,deg=False, epsrel=1.e-02,epsabs=1.e-05,phi=0., grid=None,gridpoints=101,returnGrid=False, hierarchgrid=False,nlevels=2, print_progress=False, integrate_method='dopr54_c', deriv=None): """ NAME: vmomentsurfacemass PURPOSE: calculate the an arbitrary moment of the velocity distribution at (R,phi) times the surfacmass INPUT: R - radius at which to calculate the moment (in natural units) phi= azimuth (rad unless deg=True) n - vR^n m - vT^m t= time at which to evaluate the DF (can be a list or ndarray; if this is the case, list needs to be in descending order and equally spaced) nsigma - number of sigma to integrate the velocities over (based on an estimate, so be generous, but not too generous) deg= azimuth is in degree (default=False) epsrel, epsabs - scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF) grid= if set to True, build a grid and use that to evaluate integrals; if set to a grid-objects (such as returned by this procedure), use this grid; if this was created for a list of times, moments are calculated for each time gridpoints= number of points to use for the grid in 1D (default=101) returnGrid= if True, return the grid object (default=False) hierarchgrid= if True, use a hierarchical grid (default=False) nlevels= number of hierarchical levels for the hierarchical grid print_progress= if True, print progress updates integrate_method= orbit.integrate method argument deriv= None, 'R', or 'phi': calculates derivative of the moment wrt R or phi **onnly with grid options** OUTPUT: at R,phi (no support for units) COMMENT: grid-based calculation is the only one that is heavily tested (although the test suite also tests the direct calculation) HISTORY: 2011-03-30 - Written - Bovy (NYU) """ #if we have already precalculated a grid, use that if not grid is None and isinstance(grid,evolveddiskdfGrid): if returnGrid: return (self._vmomentsurfacemassGrid(n,m,grid),grid) else: return self._vmomentsurfacemassGrid(n,m,grid) elif not grid is None \ and isinstance(grid,evolveddiskdfHierarchicalGrid): if returnGrid: return (self._vmomentsurfacemassHierarchicalGrid(n,m,grid), grid) else: return self._vmomentsurfacemassHierarchicalGrid(n,m,grid) #Otherwise we need to do some more work if deg: az= phi*_DEGTORAD else: az= phi if nsigma is None: nsigma= _NSIGMA if _PROFILE: #pragma: no cover start= time_module.time() if hasattr(self._initdf,'_estimatemeanvR') \ and hasattr(self._initdf,'_estimatemeanvT') \ and hasattr(self._initdf,'_estimateSigmaR2') \ and hasattr(self._initdf,'_estimateSigmaT2'): sigmaR1= numpy.sqrt(self._initdf._estimateSigmaR2(R,phi=az)) sigmaT1= numpy.sqrt(self._initdf._estimateSigmaT2(R,phi=az)) meanvR= self._initdf._estimatemeanvR(R,phi=az) meanvT= self._initdf._estimatemeanvT(R,phi=az) else: warnings.warn("No '_estimateSigmaR2' etc. functions found for initdf in evolveddf; thus using potentially slow sigmaR2 etc functions", galpyWarning) sigmaR1= numpy.sqrt(self._initdf.sigmaR2(R,phi=az,use_physical=False)) sigmaT1= numpy.sqrt(self._initdf.sigmaT2(R,phi=az,use_physical=False)) meanvR= self._initdf.meanvR(R,phi=az,use_physical=False) meanvT= self._initdf.meanvT(R,phi=az,use_physical=False) if _PROFILE: #pragma: no cover setup_time= (time_module.time()-start) if not grid is None and isinstance(grid,bool) and grid: if not hierarchgrid: if _PROFILE: #pragma: no cover start= time_module.time() grido= self._buildvgrid(R,az,nsigma,t, sigmaR1,sigmaT1,meanvR,meanvT, gridpoints,print_progress, integrate_method,deriv) if _PROFILE: #pragma: no cover grid_time= (time_module.time()-start) print(setup_time/(setup_time+grid_time), \ grid_time/(setup_time+grid_time), \ setup_time+grid_time) if returnGrid: return (self._vmomentsurfacemassGrid(n,m,grido),grido) else: return self._vmomentsurfacemassGrid(n,m,grido) else: #hierarchical grid grido= evolveddiskdfHierarchicalGrid(self,R,az,nsigma,t, sigmaR1,sigmaT1,meanvR, meanvT, gridpoints,nlevels,deriv, print_progress=print_progress) if returnGrid: return (self._vmomentsurfacemassHierarchicalGrid(n,m, grido), grido) else: return self._vmomentsurfacemassHierarchicalGrid(n,m,grido) #Calculate the initdf moment and then calculate the ratio initvmoment= self._initdf.vmomentsurfacemass(R,n,m,nsigma=nsigma, phi=phi) if initvmoment == 0.: initvmoment= 1. norm= sigmaR1**(n+1)*sigmaT1**(m+1)*initvmoment if isinstance(t,(list,numpy.ndarray)): raise OSError("list of times is only supported with grid-based calculation") return dblquad(_vmomentsurfaceIntegrand, meanvT/sigmaT1-nsigma, meanvT/sigmaT1+nsigma, lambda x: meanvR/sigmaR1 -numpy.sqrt(nsigma**2.-(x-meanvT/sigmaT1)**2.), lambda x: meanvR/sigmaR1 +numpy.sqrt(nsigma**2.-(x-meanvT/sigmaT1)**2.), (R,az,self,n,m,sigmaR1,sigmaT1,t,initvmoment), epsrel=epsrel,epsabs=epsabs)[0]*norm @potential_physical_input @physical_conversion('angle',pop=True) def vertexdev(self,R,t=0.,nsigma=None,deg=False, epsrel=1.e-02,epsabs=1.e-05,phi=0., grid=None,gridpoints=101,returnGrid=False, sigmaR2=None,sigmaT2=None,sigmaRT=None,surfacemass=None, hierarchgrid=False,nlevels=2, integrate_method='dopr54_c'): """ NAME: vertexdev PURPOSE: calculate the vertex deviation of the velocity distribution at (R,phi) INPUT: R - radius at which to calculate the moment (can be Quantity) phi= azimuth (rad unless deg=True; can be Quantity) t= time at which to evaluate the DF (can be a list or ndarray; if this is the case, list needs to be in descending order and equally spaced) (can be Quantity) sigmaR2, sigmaT2, sigmaRT= if set the vertex deviation is simply calculated using these nsigma - number of sigma to integrate the velocities over (based on an estimate, so be generous) deg= azimuth is in degree (default=False); do not set this when giving phi as a Quantity epsrel, epsabs - scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF) grid= if set to True, build a grid and use that to evaluate integrals; if set to a grid-objects (such as returned by this procedure), use this grid gridpoints= number of points to use for the grid in 1D (default=101) returnGrid= if True, return the grid object (default=False) hierarchgrid= if True, use a hierarchical grid (default=False) nlevels= number of hierarchical levels for the hierarchical grid integrate_method= orbit.integrate method argument OUTPUT: vertex deviation in rad HISTORY: 2011-03-31 - Written - Bovy (NYU) """ #The following aren't actually the moments, but they are the moments #times the surface-mass density; that drops out if isinstance(grid,evolveddiskdfGrid) or \ isinstance(grid,evolveddiskdfHierarchicalGrid): grido= grid elif (sigmaR2 is None or sigmaT2 is None or sigmaRT is None) \ and isinstance(grid,bool) and grid: #Precalculate the grid (sigmaR2_tmp,grido)= self.vmomentsurfacemass(R,2,0,deg=deg,t=t, nsigma=nsigma,phi=phi, epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=True, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) else: grido= False if sigmaR2 is None: sigmaR2= self.sigmaR2(R,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method, use_physical=False) if sigmaT2 is None: sigmaT2= self.sigmaT2(R,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method, use_physical=False) if sigmaRT is None: sigmaRT= self.sigmaRT(R,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method, use_physical=False) warnings.warn("In versions >1.3, the output unit of evolveddiskdf.vertexdev has been changed to radian (from degree before)",galpyWarning) if returnGrid and ((isinstance(grid,bool) and grid) or isinstance(grid,evolveddiskdfGrid) or isinstance(grid,evolveddiskdfHierarchicalGrid)): return (-numpy.arctan(2.*sigmaRT/(sigmaR2-sigmaT2))/2.,grido) else: return -numpy.arctan(2.*sigmaRT/(sigmaR2-sigmaT2))/2. @potential_physical_input @physical_conversion('velocity',pop=True) def meanvR(self,R,t=0.,nsigma=None,deg=False,phi=0., epsrel=1.e-02,epsabs=1.e-05, grid=None,gridpoints=101,returnGrid=False, surfacemass=None, hierarchgrid=False,nlevels=2,integrate_method='dopr54_c'): """ NAME: meanvR PURPOSE: calculate the mean vR of the velocity distribution at (R,phi) INPUT: R - radius at which to calculate the moment(/ro) (can be Quantity) phi= azimuth (rad unless deg=True; can be Quantity) t= time at which to evaluate the DF (can be a list or ndarray; if this is the case, list needs to be in descending order and equally spaced) (can be Quantity) surfacemass= if set use this pre-calculated surfacemass nsigma - number of sigma to integrate the velocities over (based on an estimate, so be generous) deg= azimuth is in degree (default=False); do not set this when giving phi as a Quantity epsrel, epsabs - scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF) grid= if set to True, build a grid and use that to evaluate integrals; if set to a grid-objects (such as returned by this procedure), use this grid gridpoints= number of points to use for the grid in 1D (default=101) returnGrid= if True, return the grid object (default=False) hierarchgrid= if True, use a hierarchical grid (default=False) nlevels= number of hierarchical levels for the hierarchical grid integrate_method= orbit.integrate method argument OUTPUT: mean vR HISTORY: 2011-03-31 - Written - Bovy (NYU) """ if isinstance(grid,evolveddiskdfGrid) or \ isinstance(grid,evolveddiskdfHierarchicalGrid): grido= grid vmomentR= self.vmomentsurfacemass(R,1,0,deg=deg,t=t,phi=phi, nsigma=nsigma, epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) elif isinstance(grid,bool) and grid: #Precalculate the grid (vmomentR,grido)= self.vmomentsurfacemass(R,1,0,deg=deg,t=t, nsigma=nsigma,phi=phi, epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=True, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) else: grido= False vmomentR= self.vmomentsurfacemass(R,1,0,deg=deg,t=t,phi=phi, nsigma=nsigma, epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels,integrate_method=integrate_method) if surfacemass is None: surfacemass= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels,integrate_method=integrate_method) out= vmomentR/surfacemass if returnGrid and ((isinstance(grid,bool) and grid) or isinstance(grid,evolveddiskdfGrid) or isinstance(grid,evolveddiskdfHierarchicalGrid)): return (out,grido) else: return out @potential_physical_input @physical_conversion('velocity',pop=True) def meanvT(self,R,t=0.,nsigma=None,deg=False,phi=0., epsrel=1.e-02,epsabs=1.e-05, grid=None,gridpoints=101,returnGrid=False, surfacemass=None, hierarchgrid=False,nlevels=2,integrate_method='dopr54_c'): """ NAME: meanvT PURPOSE: calculate the mean vT of the velocity distribution at (R,phi) INPUT: R - radius at which to calculate the moment (can be Quantity) phi= azimuth (rad unless deg=True; can be Quantity) t= time at which to evaluate the DF (can be a list or ndarray; if this is the case, list needs to be in descending order and equally spaced) (can be Quantity) surfacemass= if set use this pre-calculated surfacemass nsigma - number of sigma to integrate the velocities over (based on an estimate, so be generous) deg= azimuth is in degree (default=False); do not set this when giving phi as a Quantity epsrel, epsabs - scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF) grid= if set to True, build a grid and use that to evaluate integrals; if set to a grid-objects (such as returned by this procedure), use this grid gridpoints= number of points to use for the grid in 1D (default=101) returnGrid= if True, return the grid object (default=False) hierarchgrid= if True, use a hierarchical grid (default=False) nlevels= number of hierarchical levels for the hierarchical grid integrate_method= orbit.integrate method argument OUTPUT: mean vT HISTORY: 2011-03-31 - Written - Bovy (NYU) """ if isinstance(grid,evolveddiskdfGrid) or \ isinstance(grid,evolveddiskdfHierarchicalGrid): grido= grid vmomentT= self.vmomentsurfacemass(R,0,1,deg=deg,t=t, nsigma=nsigma,phi=phi, epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) elif isinstance(grid,bool) and grid: #Precalculate the grid (vmomentT,grido)= self.vmomentsurfacemass(R,0,1,deg=deg,t=t, nsigma=nsigma,phi=phi, epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=True, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) else: grido= False vmomentT= self.vmomentsurfacemass(R,0,1,deg=deg,t=t, nsigma=nsigma,phi=phi, epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels,integrate_method=integrate_method) if surfacemass is None: surfacemass= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) out= vmomentT/surfacemass if returnGrid and ((isinstance(grid,bool) and grid) or isinstance(grid,evolveddiskdfGrid) or isinstance(grid,evolveddiskdfHierarchicalGrid)): return (out,grido) else: return out @potential_physical_input @physical_conversion('velocity2',pop=True) def sigmaR2(self,R,t=0.,nsigma=None,deg=False,phi=0., epsrel=1.e-02,epsabs=1.e-05, grid=None,gridpoints=101,returnGrid=False, surfacemass=None,meanvR=None, hierarchgrid=False,nlevels=2, integrate_method='dopr54_c'): """ NAME: sigmaR2 PURPOSE: calculate the radial variance of the velocity distribution at (R,phi) INPUT: R - radius at which to calculate the moment (can be Quantity) phi= azimuth (rad unless deg=True; can be Quantity) t= time at which to evaluate the DF (can be a list or ndarray; if this is the case, list needs to be in descending order and equally spaced) (can be Quantity) surfacemass, meanvR= if set use this pre-calculated surfacemass and mean vR nsigma - number of sigma to integrate the velocities over (based on an estimate, so be generous) deg= azimuth is in degree (default=False); do not set this when giving phi as a Quantity epsrel, epsabs - scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF) grid= if set to True, build a grid and use that to evaluate integrals; if set to a grid-objects (such as returned by this procedure), use this grid gridpoints= number of points to use for the grid in 1D (default=101) returnGrid= if True, return the grid object (default=False) hierarchgrid= if True, use a hierarchical grid (default=False) nlevels= number of hierarchical levels for the hierarchical grid integrate_method= orbit.integrate method argument OUTPUT: variance of vR HISTORY: 2011-03-31 - Written - Bovy (NYU) """ #The following aren't actually the moments, but they are the moments #times the surface-mass density if isinstance(grid,evolveddiskdfGrid) or \ isinstance(grid,evolveddiskdfHierarchicalGrid): grido= grid sigmaR2= self.vmomentsurfacemass(R,2,0,deg=deg,t=t,phi=phi, nsigma=nsigma, epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) elif (meanvR is None or surfacemass is None ) \ and isinstance(grid,bool) and grid: #Precalculate the grid (sigmaR2,grido)= self.vmomentsurfacemass(R,2,0,deg=deg,t=t, nsigma=nsigma,phi=phi, epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=True, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) else: grido= False sigmaR2= self.vmomentsurfacemass(R,2,0,deg=deg,t=t, nsigma=nsigma,phi=phi, epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) if surfacemass is None: surfacemass= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) if meanvR is None: meanvR= self.vmomentsurfacemass(R,1,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method)/surfacemass out= sigmaR2/surfacemass-meanvR**2. if returnGrid and ((isinstance(grid,bool) and grid) or isinstance(grid,evolveddiskdfGrid) or isinstance(grid,evolveddiskdfHierarchicalGrid)): return (out,grido) else: return out @potential_physical_input @physical_conversion('velocity2',pop=True) def sigmaT2(self,R,t=0.,nsigma=None,deg=False,phi=0., epsrel=1.e-02,epsabs=1.e-05, grid=None,gridpoints=101,returnGrid=False, surfacemass=None,meanvT=None, hierarchgrid=False,nlevels=2, integrate_method='dopr54_c'): """ NAME: sigmaT2 PURPOSE: calculate the rotational-velocity variance of the velocity distribution at (R,phi) INPUT: R - radius at which to calculate the moment (can be Quantity) phi= azimuth (rad unless deg=True; can be Quantity) t= time at which to evaluate the DF (can be a list or ndarray; if this is the case, list needs to be in descending order and equally spaced) (can be Quantity) surfacemass, meanvT= if set use this pre-calculated surfacemass and mean rotational velocity nsigma - number of sigma to integrate the velocities over (based on an estimate, so be generous) deg= azimuth is in degree (default=False); do not set this when giving phi as a Quantity epsrel, epsabs - scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF) grid= if set to True, build a grid and use that to evaluate integrals; if set to a grid-objects (such as returned by this procedure), use this grid gridpoints= number of points to use for the grid in 1D (default=101) returnGrid= if True, return the grid object (default=False) hierarchgrid= if True, use a hierarchical grid (default=False) nlevels= number of hierarchical levels for the hierarchical grid integrate_method= orbit.integrate method argument OUTPUT: variance of vT HISTORY: 2011-03-31 - Written - Bovy (NYU) """ if isinstance(grid,evolveddiskdfGrid) or \ isinstance(grid,evolveddiskdfHierarchicalGrid): grido= grid sigmaT2= self.vmomentsurfacemass(R,0,2,deg=deg,t=t,phi=phi, nsigma=nsigma, epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) elif (meanvT is None or surfacemass is None ) \ and isinstance(grid,bool) and grid: #Precalculate the grid (sigmaT2,grido)= self.vmomentsurfacemass(R,0,2,deg=deg,t=t, nsigma=nsigma,phi=phi, epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=True, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) else: grido= False sigmaT2= self.vmomentsurfacemass(R,0,2,deg=deg,t=t, nsigma=nsigma,phi=phi, epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) if surfacemass is None: surfacemass= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) if meanvT is None: meanvT= self.vmomentsurfacemass(R,0,1,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method)/surfacemass out= sigmaT2/surfacemass-meanvT**2. if returnGrid and ((isinstance(grid,bool) and grid) or isinstance(grid,evolveddiskdfGrid) or isinstance(grid,evolveddiskdfHierarchicalGrid)): return (out,grido) else: return out @potential_physical_input @physical_conversion('velocity2',pop=True) def sigmaRT(self,R,t=0.,nsigma=None,deg=False, epsrel=1.e-02,epsabs=1.e-05,phi=0., grid=None,gridpoints=101,returnGrid=False, surfacemass=None,meanvR=None,meanvT=None, hierarchgrid=False,nlevels=2, integrate_method='dopr54_c'): """ NAME: sigmaRT PURPOSE: calculate the radial-rotational co-variance of the velocity distribution at (R,phi) INPUT: R - radius at which to calculate the moment (can be Quantity) phi= azimuth (rad unless deg=True; can be Quantity) t= time at which to evaluate the DF (can be a list or ndarray; if this is the case, list needs to be in descending order and equally spaced) (can be Quantity) surfacemass, meanvR, meavT= if set use this pre-calculated surfacemass and mean vR and vT nsigma - number of sigma to integrate the velocities over (based on an estimate, so be generous) deg= azimuth is in degree (default=False); do not set this when giving phi as a Quantity epsrel, epsabs - scipy.integrate keywords (the integration calculates the ration of this vmoment to that of the initial DF) grid= if set to True, build a grid and use that to evaluate integrals; if set to a grid-objects (such as returned by this procedure), use this grid gridpoints= number of points to use for the grid in 1D (default=101) returnGrid= if True, return the grid object (default=False) hierarchgrid= if True, use a hierarchical grid (default=False) nlevels= number of hierarchical levels for the hierarchical grid integrate_method= orbit.integrate method argument OUTPUT: covariance of vR and vT HISTORY: 2011-03-31 - Written - Bovy (NYU) """ #The following aren't actually the moments, but they are the moments #times the surface-mass density if isinstance(grid,evolveddiskdfGrid) or \ isinstance(grid,evolveddiskdfHierarchicalGrid): grido= grid sigmaRT= self.vmomentsurfacemass(R,1,1,deg=deg,t=t,phi=phi, nsigma=nsigma, epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) elif (meanvR is None or surfacemass is None ) \ and isinstance(grid,bool) and grid: #Precalculate the grid (sigmaRT,grido)= self.vmomentsurfacemass(R,1,1,deg=deg,t=t, nsigma=nsigma,phi=phi, epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=True, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) else: grido= False sigmaRT= self.vmomentsurfacemass(R,1,1,deg=deg,t=t, nsigma=nsigma,phi=phi, epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) if surfacemass is None: surfacemass= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) if meanvR is None: meanvR= self.vmomentsurfacemass(R,1,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method)/surfacemass if meanvT is None: meanvT= self.vmomentsurfacemass(R,0,1,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grido, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method)/surfacemass out= sigmaRT/surfacemass-meanvR*meanvT if returnGrid and ((isinstance(grid,bool) and grid) or isinstance(grid,evolveddiskdfGrid) or isinstance(grid,evolveddiskdfHierarchicalGrid)): return (out,grido) else: return out @potential_physical_input @physical_conversion('frequency-kmskpc',pop=True) def oortA(self,R,t=0.,nsigma=None,deg=False,phi=0., epsrel=1.e-02,epsabs=1.e-05, grid=None,gridpoints=101,returnGrids=False, derivRGrid=None,derivphiGrid=None,derivGridpoints=101, derivHierarchgrid=False, hierarchgrid=False,nlevels=2,integrate_method='dopr54_c'): """ NAME: oortA PURPOSE: calculate the Oort function A at (R,phi,t) INPUT: R - radius at which to calculate A (can be Quantity) phi= azimuth (rad unless deg=True; can be Quantity t= time at which to evaluate the DF (can be a list or ndarray; if this is the case, list needs to be in descending order and equally spaced) (can be Quantity) nsigma - number of sigma to integrate the velocities over (based on an estimate, so be generous) deg= azimuth is in degree (default=False); do not set this when giving phi as a Quantity epsrel, epsabs - scipy.integrate keywords grid= if set to True, build a grid and use that to evaluate integrals; if set to a grid-objects (such as returned by this procedure), use this grid derivRGrid, derivphiGrid= if set to True, build a grid and use that to evaluate integrals of the derivatives of the DF;if set to a grid-objects (such as returned by this procedure), use this grid gridpoints= number of points to use for the grid in 1D (default=101) derivGridpoints= number of points to use for the grid in 1D (default=101) returnGrid= if True, return the grid objects (default=False) hierarchgrid= if True, use a hierarchical grid (default=False) derivHierarchgrid= if True, use a hierarchical grid (default=False) nlevels= number of hierarchical levels for the hierarchical grid integrate_method= orbit.integrate method argument OUTPUT: Oort A at R,phi,t HISTORY: 2011-10-16 - Written - Bovy (NYU) """ #First calculate the grids if they are not given if isinstance(grid,bool) and grid: (surfacemass,grid)= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=True, gridpoints=gridpoints, returnGrid=True, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) elif isinstance(grid,evolveddiskdfGrid) or \ isinstance(grid,evolveddiskdfHierarchicalGrid): surfacemass= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) if isinstance(derivRGrid,bool) and derivRGrid: (dsurfacemassdR,derivRGrid)= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=True, gridpoints=derivGridpoints, returnGrid=True, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='R') elif isinstance(derivRGrid,evolveddiskdfGrid) or \ isinstance(derivRGrid,evolveddiskdfHierarchicalGrid): dsurfacemassdR= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivRGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='R') if isinstance(derivphiGrid,bool) and derivphiGrid: (dsurfacemassdphi,derivphiGrid)= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=True, gridpoints=derivGridpoints, returnGrid=True, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='phi') elif isinstance(derivphiGrid,evolveddiskdfGrid) or \ isinstance(derivphiGrid,evolveddiskdfHierarchicalGrid): dsurfacemassdphi= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivphiGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='phi') #2A= meanvT/R-dmeanvR/R/dphi-dmeanvphi/dR #meanvT meanvT= self.meanvT(R,t=t,nsigma=nsigma,deg=deg,phi=phi, epsrel=epsrel,epsabs=epsabs, grid=grid,gridpoints=gridpoints,returnGrid=False, surfacemass=surfacemass, hierarchgrid=hierarchgrid, nlevels=nlevels,integrate_method=integrate_method, use_physical=False) dmeanvRdphi= (self.vmomentsurfacemass(R,1,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivphiGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='phi') /surfacemass -self.vmomentsurfacemass(R,1,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) /surfacemass**2.*dsurfacemassdphi) dmeanvTdR= (self.vmomentsurfacemass(R,0,1,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivRGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='R') /surfacemass -self.vmomentsurfacemass(R,0,1,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) /surfacemass**2.*dsurfacemassdR) if returnGrids: return (0.5*(meanvT/R-dmeanvRdphi/R-dmeanvTdR),grid, derivRGrid,derivphiGrid) else: return 0.5*(meanvT/R-dmeanvRdphi/R-dmeanvTdR) @potential_physical_input @physical_conversion('frequency-kmskpc',pop=True) def oortB(self,R,t=0.,nsigma=None,deg=False,phi=0., epsrel=1.e-02,epsabs=1.e-05, grid=None,gridpoints=101,returnGrids=False, derivRGrid=None,derivphiGrid=None,derivGridpoints=101, derivHierarchgrid=False, hierarchgrid=False,nlevels=2,integrate_method='dopr54_c'): """ NAME: oortB PURPOSE: calculate the Oort function B at (R,phi,t) INPUT: R - radius at which to calculate B (can be Quantity) phi= azimuth (rad unless deg=True; can be Quantity) t= time at which to evaluate the DF (can be a list or ndarray; if this is the case, list needs to be in descending order and equally spaced) (can be Quantity) nsigma - number of sigma to integrate the velocities over (based on an estimate, so be generous) deg= azimuth is in degree (default=False); do not set this when giving phi as a Quantity epsrel, epsabs - scipy.integrate keywords grid= if set to True, build a grid and use that to evaluate integrals; if set to a grid-objects (such as returned by this procedure), use this grid derivRGrid, derivphiGrid= if set to True, build a grid and use that to evaluat integrals of the derivatives of the DF: if set to a grid-objects (such as returned by this procedure), use this grid gridpoints= number of points to use for the grid in 1D (default=101) derivGridpoints= number of points to use for the grid in 1D (default=101) returnGrid= if True, return the grid objects (default=False) hierarchgrid= if True, use a hierarchical grid (default=False) derivHierarchgrid= if True, use a hierarchical grid (default=False) nlevels= number of hierarchical levels for the hierarchical grid integrate_method= orbit.integrate method argument OUTPUT: Oort B at R,phi,t HISTORY: 2011-10-16 - Written - Bovy (NYU) """ #First calculate the grids if they are not given if isinstance(grid,bool) and grid: (surfacemass,grid)= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=True, gridpoints=gridpoints, returnGrid=True, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) elif isinstance(grid,evolveddiskdfGrid) or \ isinstance(grid,evolveddiskdfHierarchicalGrid): surfacemass= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) if isinstance(derivRGrid,bool) and derivRGrid: (dsurfacemassdR,derivRGrid)= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=True, gridpoints=derivGridpoints, returnGrid=True, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='R') elif isinstance(derivRGrid,evolveddiskdfGrid) or \ isinstance(derivRGrid,evolveddiskdfHierarchicalGrid): dsurfacemassdR= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivRGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='R') if isinstance(derivphiGrid,bool) and derivphiGrid: (dsurfacemassdphi,derivphiGrid)= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=True, gridpoints=derivGridpoints, returnGrid=True, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='phi') elif isinstance(derivphiGrid,evolveddiskdfGrid) or \ isinstance(derivphiGrid,evolveddiskdfHierarchicalGrid): dsurfacemassdphi= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivphiGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='phi') #2B= -meanvT/R+dmeanvR/R/dphi-dmeanvphi/dR #meanvT meanvT= self.meanvT(R,t=t,nsigma=nsigma,deg=deg,phi=phi, epsrel=epsrel,epsabs=epsabs, grid=grid,gridpoints=gridpoints,returnGrid=False, surfacemass=surfacemass, hierarchgrid=hierarchgrid, nlevels=nlevels,integrate_method=integrate_method, use_physical=False) dmeanvRdphi= (self.vmomentsurfacemass(R,1,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivphiGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='phi') /surfacemass -self.vmomentsurfacemass(R,1,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) /surfacemass**2.*dsurfacemassdphi) dmeanvTdR= (self.vmomentsurfacemass(R,0,1,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivRGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='R') /surfacemass -self.vmomentsurfacemass(R,0,1,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) /surfacemass**2.*dsurfacemassdR) if returnGrids: return (0.5*(-meanvT/R+dmeanvRdphi/R-dmeanvTdR),grid, derivRGrid,derivphiGrid) else: return 0.5*(-meanvT/R+dmeanvRdphi/R-dmeanvTdR) @potential_physical_input @physical_conversion('frequency-kmskpc',pop=True) def oortC(self,R,t=0.,nsigma=None,deg=False,phi=0., epsrel=1.e-02,epsabs=1.e-05, grid=None,gridpoints=101,returnGrids=False, derivRGrid=None,derivphiGrid=None,derivGridpoints=101, derivHierarchgrid=False, hierarchgrid=False,nlevels=2,integrate_method='dopr54_c'): """ NAME: oortC PURPOSE: calculate the Oort function C at (R,phi,t) INPUT: R - radius at which to calculate C (can be Quantity) phi= azimuth (rad unless deg=True; can be Quantity) t= time at which to evaluate the DF (can be a list or ndarray; if this is the case, list needs to be in descending order and equally spaced) (can be Quantity) nsigma - number of sigma to integrate the velocities over (based on an estimate, so be generous) deg= azimuth is in degree (default=False); do not set this when giving phi as a Quantity epsrel, epsabs - scipy.integrate keywords grid= if set to True, build a grid and use that to evaluate integrals; if set to a grid-objects (such as returned by this procedure), use this grid derivRGrid, derivphiGrid= if set to True, build a grid and use that to evaluate integrals of the derivatives of the DF; if set to a grid-objects (such as returned by this procedure), use this grid gridpoints= number of points to use for the grid in 1D (default=101) derivGridpoints= number of points to use for the grid in 1D (default=101) returnGrid= if True, return the grid objects (default=False) hierarchgrid= if True, use a hierarchical grid (default=False) derivHierarchgrid= if True, use a hierarchical grid (default=False) nlevels= number of hierarchical levels for the hierarchical grid integrate_method= orbit.integrate method argument OUTPUT: Oort C at R,phi,t HISTORY: 2011-10-16 - Written - Bovy (NYU) """ #First calculate the grids if they are not given if isinstance(grid,bool) and grid: (surfacemass,grid)= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=True, gridpoints=gridpoints, returnGrid=True, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) elif isinstance(grid,evolveddiskdfGrid) or \ isinstance(grid,evolveddiskdfHierarchicalGrid): surfacemass= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) if isinstance(derivRGrid,bool) and derivRGrid: (dsurfacemassdR,derivRGrid)= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=True, gridpoints=derivGridpoints, returnGrid=True, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='R') elif isinstance(derivRGrid,evolveddiskdfGrid) or \ isinstance(derivRGrid,evolveddiskdfHierarchicalGrid): dsurfacemassdR= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivRGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='R') if isinstance(derivphiGrid,bool) and derivphiGrid: (dsurfacemassdphi,derivphiGrid)= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=True, gridpoints=derivGridpoints, returnGrid=True, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='phi') elif isinstance(derivphiGrid,evolveddiskdfGrid) or \ isinstance(derivphiGrid,evolveddiskdfHierarchicalGrid): dsurfacemassdphi= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivphiGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='phi') #2C= -meanvR/R-dmeanvT/R/dphi+dmeanvR/dR #meanvR meanvR= self.meanvR(R,t=t,nsigma=nsigma,deg=deg,phi=phi, epsrel=epsrel,epsabs=epsabs, grid=grid,gridpoints=gridpoints,returnGrid=False, surfacemass=surfacemass, hierarchgrid=hierarchgrid, nlevels=nlevels,integrate_method=integrate_method, use_physical=False) dmeanvTdphi= (self.vmomentsurfacemass(R,0,1,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivphiGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='phi') /surfacemass -self.vmomentsurfacemass(R,0,1,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) /surfacemass**2.*dsurfacemassdphi) dmeanvRdR= (self.vmomentsurfacemass(R,1,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivRGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='R') /surfacemass -self.vmomentsurfacemass(R,1,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) /surfacemass**2.*dsurfacemassdR) if returnGrids: return (0.5*(-meanvR/R-dmeanvTdphi/R+dmeanvRdR),grid, derivRGrid,derivphiGrid) else: return 0.5*(-meanvR/R-dmeanvTdphi/R+dmeanvRdR) @potential_physical_input @physical_conversion('frequency-kmskpc',pop=True) def oortK(self,R,t=0.,nsigma=None,deg=False,phi=0., epsrel=1.e-02,epsabs=1.e-05, grid=None,gridpoints=101,returnGrids=False, derivRGrid=None,derivphiGrid=None,derivGridpoints=101, derivHierarchgrid=False, hierarchgrid=False,nlevels=2,integrate_method='dopr54_c'): """ NAME: oortK PURPOSE: calculate the Oort function K at (R,phi,t) INPUT: R - radius at which to calculate K (can be Quantity) phi= azimuth (rad unless deg=True; can be Quantity) t= time at which to evaluate the DF (can be a list or ndarray; if this is the case, list needs to be in descending order and equally spaced) (can be Quantity) nsigma - number of sigma to integrate the velocities over (based on an estimate, so be generous) deg= azimuth is in degree (default=False); do not set this when giving phi as a Quantity epsrel, epsabs - scipy.integrate keywords grid= if set to True, build a grid and use that to evaluate integrals; if set to a grid-objects (such as returned by this procedure), use this grid derivRGrid, derivphiGrid= if set to True, build a grid and use that to evaluate integrals of the derivatives of the DF; if set to a grid-objects (such as returned by this procedure), use this grid gridpoints= number of points to use for the grid in 1D (default=101) derivGridpoints= number of points to use for the grid in 1D (default=101) returnGrid= if True, return the grid objects (default=False) hierarchgrid= if True, use a hierarchical grid (default=False) derivHierarchgrid= if True, use a hierarchical grid (default=False) nlevels= number of hierarchical levels for the hierarchical grid integrate_method= orbit.integrate method argument OUTPUT: Oort K at R,phi,t HISTORY: 2011-10-16 - Written - Bovy (NYU) """ #First calculate the grids if they are not given if isinstance(grid,bool) and grid: (surfacemass,grid)= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=True, gridpoints=gridpoints, returnGrid=True, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) elif isinstance(grid,evolveddiskdfGrid) or \ isinstance(grid,evolveddiskdfHierarchicalGrid): surfacemass= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) if isinstance(derivRGrid,bool) and derivRGrid: (dsurfacemassdR,derivRGrid)= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=True, gridpoints=derivGridpoints, returnGrid=True, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='R') elif isinstance(derivRGrid,evolveddiskdfGrid) or \ isinstance(derivRGrid,evolveddiskdfHierarchicalGrid): dsurfacemassdR= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivRGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='R') if isinstance(derivphiGrid,bool) and derivphiGrid: (dsurfacemassdphi,derivphiGrid)= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=True, gridpoints=derivGridpoints, returnGrid=True, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='phi') elif isinstance(derivphiGrid,evolveddiskdfGrid) or \ isinstance(derivphiGrid,evolveddiskdfHierarchicalGrid): dsurfacemassdphi= self.vmomentsurfacemass(R,0,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivphiGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='phi') #2K= meanvR/R+dmeanvT/R/dphi+dmeanvR/dR #meanvR meanvR= self.meanvR(R,t=t,nsigma=nsigma,deg=deg,phi=phi, epsrel=epsrel,epsabs=epsabs, grid=grid,gridpoints=gridpoints,returnGrid=False, surfacemass=surfacemass, hierarchgrid=hierarchgrid, nlevels=nlevels,integrate_method=integrate_method, use_physical=False) dmeanvTdphi= (self.vmomentsurfacemass(R,0,1,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivphiGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='phi') /surfacemass -self.vmomentsurfacemass(R,0,1,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) /surfacemass**2.*dsurfacemassdphi) dmeanvRdR= (self.vmomentsurfacemass(R,1,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=derivRGrid, gridpoints=derivGridpoints, returnGrid=False, hierarchgrid=derivHierarchgrid, nlevels=nlevels, integrate_method=integrate_method,deriv='R') /surfacemass -self.vmomentsurfacemass(R,1,0,deg=deg,t=t,phi=phi, nsigma=nsigma,epsrel=epsrel, epsabs=epsabs,grid=grid, gridpoints=gridpoints, returnGrid=False, hierarchgrid=hierarchgrid, nlevels=nlevels, integrate_method=integrate_method) /surfacemass**2.*dsurfacemassdR) if returnGrids: return (0.5*(meanvR/R+dmeanvTdphi/R+dmeanvRdR),grid, derivRGrid,derivphiGrid) else: return 0.5*(meanvR/R+dmeanvTdphi/R+dmeanvRdR) def _vmomentsurfacemassGrid(self,n,m,grid): """Internal function to evaluate vmomentsurfacemass using a grid rather than direct integration""" if len(grid.df.shape) == 3: tlist= True else: tlist= False if tlist: nt= grid.df.shape[2] out= [] for ii in range(nt): out.append(numpy.dot(grid.vRgrid**n,numpy.dot(grid.df[:,:,ii],grid.vTgrid**m))*\ (grid.vRgrid[1]-grid.vRgrid[0])*(grid.vTgrid[1]-grid.vTgrid[0])) return numpy.array(out) else: return numpy.dot(grid.vRgrid**n,numpy.dot(grid.df,grid.vTgrid**m))*\ (grid.vRgrid[1]-grid.vRgrid[0])*(grid.vTgrid[1]-grid.vTgrid[0]) def _buildvgrid(self,R,phi,nsigma,t,sigmaR1,sigmaT1,meanvR,meanvT, gridpoints,print_progress,integrate_method,deriv): """Internal function to grid the vDF at a given location""" out= evolveddiskdfGrid() out.sigmaR1= sigmaR1 out.sigmaT1= sigmaT1 out.meanvR= meanvR out.meanvT= meanvT out.vRgrid= numpy.linspace(meanvR-nsigma*sigmaR1,meanvR+nsigma*sigmaR1, gridpoints) out.vTgrid= numpy.linspace(meanvT-nsigma*sigmaT1,meanvT+nsigma*sigmaT1, gridpoints) if isinstance(t,(list,numpy.ndarray)): nt= len(t) out.df= numpy.zeros((gridpoints,gridpoints,nt)) for ii in range(gridpoints): for jj in range(gridpoints-1,-1,-1):#Reverse, so we get the peak before we get to the extreme lags NOT NECESSARY if print_progress: #pragma: no cover sys.stdout.write('\r'+"Velocity gridpoint %i out of %i" % \ (jj+ii*gridpoints+1,gridpoints*gridpoints)) sys.stdout.flush() thiso= Orbit([R,out.vRgrid[ii],out.vTgrid[jj],phi]) out.df[ii,jj,:]= self(thiso,numpy.array(t).flatten(), integrate_method=integrate_method, deriv=deriv,use_physical=False) out.df[ii,jj,numpy.isnan(out.df[ii,jj,:])]= 0. #BOVY: for now if print_progress: sys.stdout.write('\n') #pragma: no cover else: out.df= numpy.zeros((gridpoints,gridpoints)) for ii in range(gridpoints): for jj in range(gridpoints): if print_progress: #pragma: no cover sys.stdout.write('\r'+"Velocity gridpoint %i out of %i" % \ (jj+ii*gridpoints+1,gridpoints*gridpoints)) sys.stdout.flush() thiso= Orbit([R,out.vRgrid[ii],out.vTgrid[jj],phi]) out.df[ii,jj]= self(thiso,t, integrate_method=integrate_method, deriv=deriv,use_physical=False) if numpy.isnan(out.df[ii,jj]): out.df[ii,jj]= 0. #BOVY: for now if print_progress: sys.stdout.write('\n') #pragma: no cover return out def _create_ts_tlist(self,t,integrate_method): #Check input if not all(t == sorted(t,reverse=True)): raise OSError("List of times has to be sorted in descending order") #Initialize if integrate_method == 'odeint': _NTS= 1000 tmax= numpy.amax(t) ts= numpy.linspace(tmax,self._to,_NTS) #Add other t ts= list(ts) ts.extend([self._to+tmax-ti for ti in t if ti != tmax]) else: if len(t) == 1: #Special case this because it is confusing ts= numpy.array([t[0],self._to]) else: ts= -t+self._to+numpy.amax(t) #sort ts= list(ts) ts.sort(reverse=True) return numpy.array(ts) def _call_marginalizevperp(self,o,integrate_method='dopr54_c',**kwargs): """Call the DF, marginalizing over perpendicular velocity""" #Get d, l, vlos l= o.ll(obs=[1.,0.,0.],ro=1.)*_DEGTORAD vlos= o.vlos(ro=1.,vo=1.,obs=[1.,0.,0.,0.,0.,0.]) R= o.R(use_physical=False) phi= o.phi(use_physical=False) #Get local circular velocity, projected onto the los if isinstance(self._pot,list): vcirc= calcRotcurve([p for p in self._pot if not p.isNonAxi],R)[0] else: vcirc= calcRotcurve(self._pot,R)[0] vcirclos= vcirc*numpy.sin(phi+l) #Marginalize alphalos= phi+l if not 'nsigma' in kwargs or ('nsigma' in kwargs and \ kwargs['nsigma'] is None): nsigma= _NSIGMA else: nsigma= kwargs['nsigma'] kwargs.pop('nsigma',None) #BOVY: add asymmetric drift here? if numpy.fabs(numpy.sin(alphalos)) < numpy.sqrt(1./2.): sigmaR1= numpy.sqrt(self._initdf.sigmaT2(R,phi=phi, use_physical=False)) #Slight abuse cosalphalos= numpy.cos(alphalos) tanalphalos= numpy.tan(alphalos) return integrate.quad(_marginalizeVperpIntegrandSinAlphaSmall, -nsigma,nsigma, args=(self,R,cosalphalos,tanalphalos, vlos-vcirclos,vcirc, sigmaR1,phi), **kwargs)[0]/numpy.fabs(cosalphalos)*sigmaR1 else: sigmaR1= numpy.sqrt(self._initdf.sigmaR2(R,phi=phi, use_physical=False)) sinalphalos= numpy.sin(alphalos) cotalphalos= 1./numpy.tan(alphalos) return integrate.quad(_marginalizeVperpIntegrandSinAlphaLarge, -nsigma,nsigma, args=(self,R,sinalphalos,cotalphalos, vlos-vcirclos,vcirc,sigmaR1,phi), **kwargs)[0]/numpy.fabs(sinalphalos)*sigmaR1 def _call_marginalizevlos(self,o,integrate_method='dopr54_c',**kwargs): """Call the DF, marginalizing over line-of-sight velocity""" #Get d, l, vperp l= o.ll(obs=[1.,0.,0.],ro=1.)*_DEGTORAD vperp= o.vll(ro=1.,vo=1.,obs=[1.,0.,0.,0.,0.,0.]) R= o.R(use_physical=False) phi= o.phi(use_physical=False) #Get local circular velocity, projected onto the perpendicular #direction if isinstance(self._pot,list): vcirc= calcRotcurve([p for p in self._pot if not p.isNonAxi],R)[0] else: vcirc= calcRotcurve(self._pot,R)[0] vcircperp= vcirc*numpy.cos(phi+l) #Marginalize alphaperp= numpy.pi/2.+phi+l if not 'nsigma' in kwargs or ('nsigma' in kwargs and \ kwargs['nsigma'] is None): nsigma= _NSIGMA else: nsigma= kwargs['nsigma'] kwargs.pop('nsigma',None) if numpy.fabs(numpy.sin(alphaperp)) < numpy.sqrt(1./2.): sigmaR1= numpy.sqrt(self._initdf.sigmaT2(R,phi=phi, use_physical=False)) #slight abuse va= vcirc-self._initdf.meanvT(R,phi=phi,use_physical=False) cosalphaperp= numpy.cos(alphaperp) tanalphaperp= numpy.tan(alphaperp) #we can reuse the VperpIntegrand, since it is just another angle return integrate.quad(_marginalizeVperpIntegrandSinAlphaSmall, -va/sigmaR1-nsigma, -va/sigmaR1+nsigma, args=(self,R,cosalphaperp,tanalphaperp, vperp-vcircperp,vcirc, sigmaR1,phi), **kwargs)[0]/numpy.fabs(cosalphaperp)*sigmaR1 else: sigmaR1= numpy.sqrt(self._initdf.sigmaR2(R,phi=phi, use_physical=False)) sinalphaperp= numpy.sin(alphaperp) cotalphaperp= 1./numpy.tan(alphaperp) #we can reuse the VperpIntegrand, since it is just another angle return integrate.quad(_marginalizeVperpIntegrandSinAlphaLarge, -nsigma,nsigma, args=(self,R,sinalphaperp,cotalphaperp, vperp-vcircperp,vcirc,sigmaR1,phi), **kwargs)[0]/numpy.fabs(sinalphaperp)*sigmaR1 def _vmomentsurfacemassHierarchicalGrid(self,n,m,grid): """Internal function to evaluate vmomentsurfacemass using a hierarchical grid rather than direct integration, rather unnecessary""" return grid(n,m) class evolveddiskdfGrid: """(not quite) Empty class since it is only used to store some stuff""" def __init__(self): return None def plot(self,tt=0): """ NAME: plot PURPOSE: plot the velocity distribution INPUT: t= optional time index OUTPUT: plot of velocity distribution to output device HISTORY: 2011-06-27 - Written - Bovy (NYU) """ xrange= [self.vRgrid[0],self.vRgrid[len(self.vRgrid)-1]] yrange= [self.vTgrid[0],self.vTgrid[len(self.vTgrid)-1]] if len(self.df.shape) == 3: plotthis= self.df[:,:,tt] else: plotthis= self.df plot.dens2d(plotthis.T,cmap='gist_yarg',origin='lower', aspect=(xrange[1]-xrange[0])/\ (yrange[1]-yrange[0]), extent=[xrange[0],xrange[1], yrange[0],yrange[1]], xlabel=r'$v_R / v_0$', ylabel=r'$v_T / v_0$') class evolveddiskdfHierarchicalGrid: """Class that holds a hierarchical velocity grid""" def __init__(self,edf,R,phi,nsigma,t,sigmaR1,sigmaT1,meanvR,meanvT, gridpoints,nlevels,deriv,upperdxdy=None,print_progress=False, nlevelsTotal=None): """ NAME: __init__ PURPOSE: Initialize a hierarchical grid INPUT: edf - evolveddiskdf instance R - Radius phi- azimuth nsigma - number of sigma to integrate over t- time sigmaR1 - radial dispersion sigmaT1 - rotational-velocity dispersion meanvR - mean of radial velocity meanvT - mean of rotational velocity gridpoints- number of gridpoints nlevels- number of levels to build deriv- None, 'R', or 'phi': calculates derivative of the moment wrt R or phi upperdxdy= area element of previous hierarchical level print_progress= if True, print progress on building the grid OUTPUT: object HISTORY: 2011-04-21 - Written - Bovy (NYU) """ self.sigmaR1= sigmaR1 self.sigmaT1= sigmaT1 self.meanvR= meanvR self.meanvT= meanvT self.gridpoints= gridpoints self.vRgrid= numpy.linspace(self.meanvR-nsigma*self.sigmaR1, self.meanvR+nsigma*self.sigmaR1, self.gridpoints) self.vTgrid= numpy.linspace(self.meanvT-nsigma*self.sigmaT1, self.meanvT+nsigma*self.sigmaT1, self.gridpoints) self.t= t if nlevelsTotal is None: nlevelsTotal= nlevels self.nlevels= nlevels self.nlevelsTotal= nlevelsTotal if isinstance(t,(list,numpy.ndarray)): nt= len(t) self.df= numpy.zeros((gridpoints,gridpoints,nt)) dxdy= (self.vRgrid[1]-self.vRgrid[0])\ *(self.vTgrid[1]-self.vTgrid[0]) if nlevels > 0: xsubmin= int(gridpoints)//4 xsubmax= gridpoints-int(gridpoints)//4 else: xsubmin= gridpoints xsubmax= 0 ysubmin, ysubmax= xsubmin, xsubmax for ii in range(gridpoints): for jj in range(gridpoints): if print_progress: #pragma: no cover sys.stdout.write('\r'+"Velocity gridpoint %i out of %i" % \ (jj+ii*gridpoints+1,gridpoints*gridpoints)) sys.stdout.flush() #If this is part of a subgrid, ignore if nlevels > 1 and ii >= xsubmin and ii < xsubmax \ and jj >= ysubmin and jj < ysubmax: continue thiso= Orbit([R,self.vRgrid[ii],self.vTgrid[jj],phi]) self.df[ii,jj,:]= edf(thiso,numpy.array(t).flatten(), deriv=deriv) self.df[ii,jj,numpy.isnan(self.df[ii,jj,:])]= 0.#BOVY: for now #Multiply in area, somewhat tricky for edge objects if upperdxdy is None or (ii != 0 and ii != gridpoints-1\ and jj != 0 and jj != gridpoints-1): self.df[ii,jj,:]*= dxdy elif ((ii == 0 or ii == gridpoints-1) and \ (jj != 0 and jj != gridpoints-1))\ or \ ((jj == 0 or jj == gridpoints-1) and \ (ii != 0 and ii != gridpoints-1)): #edge self.df[ii,jj,:]*= 1.5*dxdy/1.5 #turn this off for now else: #corner self.df[ii,jj,:]*= 2.25*dxdy/2.25 #turn this off for now if print_progress: sys.stdout.write('\n') #pragma: no cover else: self.df= numpy.zeros((gridpoints,gridpoints)) dxdy= (self.vRgrid[1]-self.vRgrid[0])\ *(self.vTgrid[1]-self.vTgrid[0]) if nlevels > 0: xsubmin= int(gridpoints)//4 xsubmax= gridpoints-int(gridpoints)//4 else: xsubmin= gridpoints xsubmax= 0 ysubmin, ysubmax= xsubmin, xsubmax for ii in range(gridpoints): for jj in range(gridpoints): if print_progress: #pragma: no cover sys.stdout.write('\r'+"Velocity gridpoint %i out of %i" % \ (jj+ii*gridpoints+1,gridpoints*gridpoints)) sys.stdout.flush() #If this is part of a subgrid, ignore if nlevels > 1 and ii >= xsubmin and ii < xsubmax \ and jj >= ysubmin and jj < ysubmax: continue thiso= Orbit([R,self.vRgrid[ii],self.vTgrid[jj],phi]) self.df[ii,jj]= edf(thiso,t,deriv=deriv) if numpy.isnan(self.df[ii,jj]): self.df[ii,jj]= 0. #BOVY: for now #Multiply in area, somewhat tricky for edge objects if upperdxdy is None or (ii != 0 and ii != gridpoints-1\ and jj != 0 and jj != gridpoints-1): self.df[ii,jj]*= dxdy elif ((ii == 0 or ii == gridpoints-1) and \ (jj != 0 and jj != gridpoints-1))\ or \ ((jj == 0 or jj == gridpoints-1) and \ (ii != 0 and ii != gridpoints-1)): #edge self.df[ii,jj]*= 1.5*dxdy/1.5#turn this off for now else: #corner self.df[ii,jj]*= 2.25*dxdy/2.25#turn this off for now if print_progress: sys.stdout.write('\n') #pragma: no cover if nlevels > 1: #Set up subgrid subnsigma= (self.meanvR-self.vRgrid[xsubmin])/self.sigmaR1 self.subgrid= evolveddiskdfHierarchicalGrid(edf,R,phi, subnsigma,t, sigmaR1, sigmaT1, meanvR, meanvT, gridpoints, nlevels-1, deriv, upperdxdy=dxdy, print_progress=print_progress, nlevelsTotal=nlevelsTotal) else: self.subgrid= None return None def __call__(self,n,m): """Call""" if isinstance(self.t,(list,numpy.ndarray)): tlist= True else: tlist= False if tlist: nt= self.df.shape[2] out= [] for ii in range(nt): #We already multiplied in the area out.append(numpy.dot(self.vRgrid**n,numpy.dot(self.df[:,:,ii], self.vTgrid**m))) if self.subgrid is None: return numpy.array(out) else: return numpy.array(out)+ self.subgrid(n,m) else: #We already multiplied in the area thislevel= numpy.dot(self.vRgrid**n,numpy.dot(self.df,self.vTgrid**m)) if self.subgrid is None: return thislevel else: return thislevel+self.subgrid(n,m) def plot(self,tt=0,vmax=None,aspect=None,extent=None): """ NAME: plot PURPOSE: plot the velocity distribution INPUT: t= optional time index OUTPUT: plot of velocity distribution to output device HISTORY: 2011-06-27 - Written - Bovy (NYU) """ if vmax is None: vmax= self.max(tt=tt)*2. #Figure out how big of a grid we need dvR= (self.vRgrid[1]-self.vRgrid[0]) dvT= (self.vTgrid[1]-self.vTgrid[0]) nvR= len(self.vRgrid) nvT= len(self.vTgrid) nUpperLevels= self.nlevelsTotal-self.nlevels nvRTot= nvR*2**nUpperLevels nvTTot= nvT*2**nUpperLevels plotthis= numpy.zeros((nvRTot,nvTTot)) if len(self.df.shape) == 3: plotdf= copy.copy(self.df[:,:,tt]) else: plotdf= copy.copy(self.df) plotdf[(plotdf == 0.)]= _NAN #Fill up the grid if nUpperLevels > 0: xsubmin= nvRTot//2-nvR//2-1 xsubmax= nvRTot//2+nvR//2 ysubmin= nvTTot//2-nvT//2-1 ysubmax= nvTTot//2+nvT//2 #Set outside this subgrid to NaN plotthis[:,:]= _NAN #within the grid gets filled in below else: xsubmin= 0 xsubmax= nvR ysubmin= 0 ysubmax= nvT #Fill in this level plotthis[xsubmin:xsubmax,ysubmin:ysubmax]= plotdf/dvR/dvT/nvR/nvT #Plot if nUpperLevels == 0: xrange= [self.vRgrid[0]+dvR/2.,self.vRgrid[len(self.vRgrid)-1]-dvR/2.] yrange= [self.vTgrid[0]+dvT/2.,self.vTgrid[len(self.vTgrid)-1]-dvT/2.] aspect=(xrange[1]-xrange[0])/\ (yrange[1]-yrange[0]) extent=[xrange[0],xrange[1], yrange[0],yrange[1]] plot.dens2d(plotthis.T,cmap='gist_yarg',origin='lower', interpolation='nearest', aspect=aspect, extent=extent, xlabel=r'$v_R / v_0$', ylabel=r'$v_T / v_0$', vmin=0.,vmax=vmax) else: plot.dens2d(plotthis.T,cmap='gist_yarg',origin='lower', aspect=aspect,extent=extent, interpolation='nearest', overplot=True,vmin=0.,vmax=vmax) if not self.subgrid is None: self.subgrid.plot(tt=tt,vmax=vmax,aspect=aspect,extent=extent) def max(self,tt=0): if not self.subgrid is None: if len(self.df.shape) == 3: return numpy.amax([numpy.amax(self.df[:,:,tt]), self.subgrid.max(tt)]) else: return numpy.amax([numpy.amax(self.df[:,:]), self.subgrid.max()]) else: if len(self.df.shape) == 3: return numpy.amax(self.df[:,:,tt]) else: return numpy.amax(self.df[:,:]) def _vmomentsurfaceIntegrand(vR,vT,R,az,df,n,m,sigmaR1,sigmaT1,t,initvmoment): """Internal function that is the integrand for the velocity moment times surface mass integration""" o= Orbit([R,vR*sigmaR1,vT*sigmaT1,az]) return vR**n*vT**m*df(o,t)/initvmoment def _marginalizeVperpIntegrandSinAlphaLarge(vR,df,R,sinalpha,cotalpha, vlos,vcirc,sigma,phi): return df(Orbit([R,vR*sigma,cotalpha*vR*sigma+vlos/sinalpha+vcirc,phi])) def _marginalizeVperpIntegrandSinAlphaSmall(vT,df,R,cosalpha,tanalpha, vlos,vcirc,sigma,phi): return df(Orbit([R,tanalpha*vT*sigma-vlos/cosalpha,vT*sigma+vcirc,phi])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/isotropicHernquistdf.py0000644000175100001710000000500514327773303020554 0ustar00runnerdocker# Class that implements isotropic spherical Hernquist DF # computed using the Eddington formula import numpy from ..potential import HernquistPotential, evaluatePotentials from ..util import conversion from .sphericaldf import isotropicsphericaldf class isotropicHernquistdf(isotropicsphericaldf): """Class that implements isotropic spherical Hernquist DF computed using the Eddington formula""" def __init__(self,pot=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize an isotropic Hernquist distribution function INPUT: pot= (None) Hernquist Potential instance ro=, vo= galpy unit parameters OUTPUT: None HISTORY: 2020-08-09 - Written - Lane (UofT) """ assert isinstance(pot,HernquistPotential),'pot= must be potential.HernquistPotential' isotropicsphericaldf.__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. # first factor = mass to make the DF that of mass density self._fEnorm= self._psi0*self._pot.a\ /numpy.sqrt(2.)/(2*numpy.pi)**3/self._GMa**1.5 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) """ 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: # Set to dummy and 0 later, prevents functions throwing errors? Etilde[Etilde_out]=0.5 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 return fE 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)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/isotropicNFWdf.py0000644000175100001710000000575414327773303017237 0ustar00runnerdocker# Class that implements isotropic spherical NFW DF import numpy from ..potential import NFWPotential from ..util import conversion from .sphericaldf import isotropicsphericaldf # Coefficients of the improved analytical approximation that JB made _COEFFS= numpy.array([7.8480631889123114, -41.0268009529575863, 92.5144063082258157,-117.6477872907975382, 92.6397009471828170, -46.6587221550257851, 14.9776586391246376, -2.9784827749197880, 0.2583468299241013,0.0232272797489981, 0.0926081086527954]) class isotropicNFWdf(isotropicsphericaldf): """Class that implements the approximate isotropic spherical NFW DF (either `Widrow 2000 `__ or an improved fit by Lane et al. 2021). """ def __init__(self,pot=None,widrow=False,rmax=1e4,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize an isotropic NFW distribution function INPUT: pot= (None) NFW Potential instance widrow= (False) if True, use the approximate form from Widrow (2000), otherwise use improved fit that has <~1e-5 relative density errors rmax= (1e4) maximum radius to consider (can be Quantity); set to numpy.inf to evaluate NFW w/o cut-off ro=, vo= galpy unit parameters OUTPUT: None HISTORY: 2021-02-01 - Written - Bovy (UofT) """ assert isinstance(pot,NFWPotential),'pot= must be potential.NFWPotential' isotropicsphericaldf.__init__(self,pot=pot,rmax=rmax,ro=ro,vo=vo) self._Etildemax= pot._amp/pot.a self._fEnorm= (9.1968e-2)**widrow/\ (4.*numpy.pi)/pot.a**1.5/pot._amp**0.5 self._widrow= widrow self._Etildemin= -pot(self._rmax,0,use_physical=False)/self._Etildemax def fE(self,E): """ NAME: fE PURPOSE Calculate the energy portion of an isotropic NFW distribution function INPUT: E - The energy (can be Quantity) OUTPUT: fE - The value of the energy portion of the DF HISTORY: 2021-02-01 - Written - Bovy (UofT) """ Etilde= -conversion.parse_energy(E,vo=self._vo)/self._Etildemax out= numpy.zeros_like(Etilde) indx= (Etilde > self._Etildemin)*(Etilde <= 1.) if self._widrow: out[indx]= self._fEnorm*Etilde[indx]**1.5*(1-Etilde[indx])**-2.5\ *(-numpy.log(Etilde[indx])/(1.-Etilde[indx]))**-2.7419\ *numpy.exp(0.3620*Etilde[indx]-0.5639*Etilde[indx]**2. -0.0859*Etilde[indx]**3.-0.4912*Etilde[indx]**4.) else: out[indx]= self._fEnorm*Etilde[indx]**1.5*(1-Etilde[indx])**-2.5\ *(-numpy.log(Etilde[indx])/(1.-Etilde[indx]))**-2.75\ *numpy.polyval(_COEFFS,Etilde[indx]) return out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/isotropicPlummerdf.py0000644000175100001710000000413114327773303020212 0ustar00runnerdocker# Class that implements isotropic spherical Plummer DF import numpy from ..potential import PlummerPotential from ..util import conversion from .sphericaldf import isotropicsphericaldf class isotropicPlummerdf(isotropicsphericaldf): """Class that implements isotropic spherical Plummer DF: .. math:: f(E) = {24\\sqrt{2} \\over 7\\pi^3}\\,{b^2\\over (GM)^5}\\,(-E)^{7/2} for :math:`-GM/b \\leq E \\leq 0` and zero otherwise. The parameter :math:`GM` is the total mass and :math:`b` the Plummer profile's scale parameter. """ def __init__(self,pot=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize an isotropic Plummer distribution function INPUT: pot= (None) Plummer Potential instance ro=, vo= galpy unit parameters OUTPUT: None HISTORY: 2020-10-01 - Written - Bovy (UofT) """ assert isinstance(pot,PlummerPotential),'pot= must be potential.PlummerPotential' isotropicsphericaldf.__init__(self,pot=pot,ro=ro,vo=vo) self._Etildemax= pot._amp/pot._b # /amp^4 instead of /amp^5 to make the DF that of mass density self._fEnorm=24.*numpy.sqrt(2.)/7./numpy.pi**3.*pot._b**2./pot._amp**4. def fE(self,E): """ NAME: fE PURPOSE Calculate the energy portion of an isotropic Plummer distribution function INPUT: E - The energy (can be Quantity) OUTPUT: fE - The value of the energy portion of the DF HISTORY: 2020-10-01 - Written - Bovy (UofT) """ Etilde= -conversion.parse_energy(E,vo=self._vo) out= numpy.zeros_like(Etilde) indx= (Etilde > 0)*(Etilde <= self._Etildemax) out[indx]= self._fEnorm*(Etilde[indx])**3.5 return out 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._b/numpy.sqrt(ms**(-2./3.)-1.) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/jeans.py0000644000175100001710000001052714327773303015431 0ustar00runnerdocker# jeans.py: utilities related to the Jeans equations import numpy from scipy import integrate from ..potential.Potential import (evaluateDensities, evaluaterforces, evaluateSurfaceDensities) from ..potential.Potential import flatten as flatten_pot from ..util.conversion import physical_conversion, potential_physical_input _INVSQRTTWO= 1./numpy.sqrt(2.) @potential_physical_input @physical_conversion('velocity',pop=True) def sigmar(Pot,r,dens=None,beta=0.): """ NAME: sigmar PURPOSE: Compute the radial velocity dispersion using the spherical Jeans equation INPUT: Pot - potential or list of potentials (evaluated at R=r/sqrt(2),z=r/sqrt(2), sphericity not checked) r - Galactocentric radius (can be Quantity) dens= (None) tracer density profile (function of r); if None, the density is assumed to be that corresponding to the potential beta= (0.) anisotropy; can be a constant or a function of r OUTPUT: sigma_r(r) HISTORY: 2018-07-05 - Written - Bovy (UofT) """ Pot= flatten_pot(Pot) if dens is None: dens= lambda r: evaluateDensities(Pot,r*_INVSQRTTWO,r*_INVSQRTTWO, phi=numpy.pi/4., use_physical=False) if callable(beta): intFactor= lambda x: numpy.exp(2.*integrate.quad(lambda y: beta(y)/y, 1.,x)[0]) else: # assume to be number intFactor= lambda x: x**(2.*beta) return numpy.sqrt(integrate.quad(lambda x: -intFactor(x)*dens(x) *evaluaterforces(Pot, x*_INVSQRTTWO, x*_INVSQRTTWO, phi=numpy.pi/4., use_physical=False), r,numpy.inf)[0]/ dens(r)/intFactor(r)) @potential_physical_input @physical_conversion('velocity',pop=True) def sigmalos(Pot,R,dens=None,surfdens=None,beta=0.,sigma_r=None): """ NAME: sigmalos PURPOSE: Compute the line-of-sight velocity dispersion using the spherical Jeans equation INPUT: Pot - potential or list of potentials (evaluated at R=r/sqrt(2),z=r/sqrt(2), sphericity not checked) R - Galactocentric projected radius (can be Quantity) dens= (None) tracer density profile (function of r); if None, the density is assumed to be that corresponding to the potential surfdens= (None) tracer surface density profile (value at R or function of R); if None, the surface density is assumed to be that corresponding to the density beta= (0.) anisotropy; can be a constant or a function of r sigma_r= (None) if given, the solution of the spherical Jeans equation sigma_r(r) (used instead of solving the Jeans equation as part of this routine) OUTPUT: sigma_los(R) HISTORY: 2018-08-27 - Written - Bovy (UofT) """ Pot= flatten_pot(Pot) if dens is None: densPot= True dens= lambda r: evaluateDensities(Pot,r*_INVSQRTTWO,r*_INVSQRTTWO, use_physical=False) else: densPot= False if callable(surfdens): called_surfdens= surfdens(R) elif surfdens is None: if densPot: called_surfdens= evaluateSurfaceDensities(Pot,R,numpy.inf, use_physical=False) if not densPot or numpy.isnan(called_surfdens): called_surfdens=\ 2.*integrate.quad(lambda x: dens(numpy.sqrt(R**2.+x**2.)), 0.,numpy.inf)[0] else: called_surfdens= surfdens if callable(beta): call_beta= beta else: call_beta= lambda x: beta if sigma_r is None: call_sigma_r= lambda r: sigmar(Pot,r,dens=dens,beta=beta) elif not callable(sigma_r): call_sigma_r= lambda x: sigma_r else: call_sigma_r= sigma_r return numpy.sqrt(2.*integrate.quad(\ lambda x: (1.-call_beta(x)*R**2./x**2.)*x*dens(x)\ *call_sigma_r(x)**2./numpy.sqrt(x**2.-R**2.),R,numpy.inf)[0]\ /called_surfdens) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/kingdf.py0000644000175100001710000001601214327773303015566 0ustar00runnerdocker# Class that represents a King DF import numpy from scipy import integrate, interpolate, special from ..util import conversion from .df import df from .sphericaldf import isotropicsphericaldf _FOURPI= 4.*numpy.pi _TWOOVERSQRTPI= 2./numpy.sqrt(numpy.pi) class kingdf(isotropicsphericaldf): """Class that represents a King DF: .. 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. See also :ref:`King potential `. """ 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) """ # 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= 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.sigma= self._velocity_scale self._sigma2= self.sigma**2. self.rho1= self._density_scale # 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, ro=ro,vo=vo) # Now initialize the isotropic DF isotropicsphericaldf.__init__(self,pot=pot,scale=self.r0, rmax=self.rt,ro=ro,vo=vo) 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, self._radius_scale*self._scalefree_kdf._r, k=3) # Setup velocity DF interpolator for velocity sampling here self._rmin_sampling= 0. 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 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# mass density, not /self.M as for number density class _scalefreekingdf: """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 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) dWdr= 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] 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 # 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] 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] 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.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 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)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/osipkovmerrittHernquistdf.py0000644000175100001710000000567414327773303021656 0ustar00runnerdocker# Class that implements the anisotropic spherical Hernquist DF with radially # varying anisotropy of the Osipkov-Merritt type import numpy from ..potential import HernquistPotential, evaluatePotentials from ..util import conversion from .osipkovmerrittdf import _osipkovmerrittdf class osipkovmerrittHernquistdf(_osipkovmerrittdf): """Class that implements the anisotropic spherical Hernquist DF with radially varying anisotropy of the Osipkov-Merritt type .. math:: \\beta(r) = \\frac{1}{1+r_a^2/r^2} with :math:`r_a` the anistropy radius. """ def __init__(self,pot=None,ra=1.4,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a Hernquist DF with Osipkov-Merritt anisotropy INPUT: pot - Hernquist potential which determines the DF ra - anisotropy radius (can be a Quantity) ro=, vo= galpy unit parameters OUTPUT: None HISTORY: 2020-11-12 - Written - Bovy (UofT) """ assert isinstance(pot,HernquistPotential), \ 'pot= must be potential.HernquistPotential' _osipkovmerrittdf.__init__(self,pot=pot,ra=ra,ro=ro,vo=vo) self._psi0= -evaluatePotentials(self._pot,0,0,use_physical=False) self._GMa = self._psi0*self._pot.a**2. self._a2overra2= self._pot.a**2./self._ra2 # First factor is the mass to make the DF that of the mass density self._fQnorm= self._psi0*self._pot.a\ /numpy.sqrt(2.)/(2*numpy.pi)**3/self._GMa**1.5 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: 2020-11-12 - Written - Bovy (UofT) """ Qtilde= conversion.parse_energy(Q,vo=self._vo)/self._psi0 # Handle potential Q outside of bounds Qtilde_out = numpy.where(numpy.logical_or(Qtilde<0,Qtilde>1))[0] if len(Qtilde_out)>0: # Dummy variable now and 0 later, prevents numerical issues Qtilde[Qtilde_out]=0.5 sqrtQtilde= numpy.sqrt(Qtilde) # The 'ergodic' part fQ= sqrtQtilde/(1-Qtilde)**2.\ *((1.-2.*Qtilde)*(8.*Qtilde**2.-8.*Qtilde-3.)\ +((3.*numpy.arcsin(sqrtQtilde))\ /numpy.sqrt(Qtilde*(1.-Qtilde)))) # The other part fQ+= 8.*self._a2overra2*sqrtQtilde*(1.-2.*Qtilde) if len(Qtilde_out) > 0: fQ[Qtilde_out]= 0. return self._fQnorm*fQ 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)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/osipkovmerrittNFWdf.py0000644000175100001710000000547614327773303020326 0ustar00runnerdocker# Class that implements the anisotropic spherical NFW DF with radially # varying anisotropy of the Osipkov-Merritt type import numpy from ..potential import NFWPotential from ..util import conversion from .isotropicNFWdf import isotropicNFWdf from .osipkovmerrittdf import _osipkovmerrittdf _COEFFS= numpy.array([-0.9958957901383353, 4.2905266124525259, -7.6069046709185919,7.0313234865878806, -3.6920719890718035, 0.8313023634615980, -0.2179687331774083, -0.0408426627412238, 0.0802975743915827]) class osipkovmerrittNFWdf(_osipkovmerrittdf): """Class that implements the anisotropic spherical NFW DF with radially varying anisotropy of the Osipkov-Merritt type .. math:: \\beta(r) = \\frac{1}{1+r_a^2/r^2} with :math:`r_a` the anistropy radius. """ def __init__(self,pot=None,ra=1.4,rmax=1e4,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a NFW DF with Osipkov-Merritt anisotropy INPUT: pot - NFW potential which determines the DF ra - anisotropy radius (can be a Quantity) rmax= (1e4) maximum radius to consider (can be Quantity); set to numpy.inf to evaluate NFW w/o cut-off ro=, vo= galpy unit parameters OUTPUT: None HISTORY: 2020-11-12 - Written - Bovy (UofT) """ assert isinstance(pot,NFWPotential), \ 'pot= must be potential.NFWPotential' _osipkovmerrittdf.__init__(self,pot=pot,ra=ra,rmax=rmax,ro=ro,vo=vo) self._Qtildemax= pot._amp/pot.a self._Qtildemin= -pot(self._rmax,0,use_physical=False)/self._Qtildemax self._a2overra2= self._pot.a**2./self._ra2 self._fQnorm= self._a2overra2/(4.*numpy.pi)/pot.a**1.5/pot._amp**0.5 # Initialize isotropic version to use as part of fQ self._idf= isotropicNFWdf(pot=pot,rmax=rmax,ro=ro,vo=vo) def fQ(self,Q): """ NAME: fQ PURPOSE Calculate the f(Q) portion of an Osipkov-Merritt NFW 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-09 - Written - Bovy (UofT) """ Qtilde= conversion.parse_energy(Q,vo=self._vo)/self._Qtildemax out= numpy.zeros_like(Qtilde) indx= (Qtilde > self._Qtildemin)*(Qtilde <= 1.) # The 'ergodic' part out[indx]= self._idf.fE(-Q[indx]) # The other part out[indx]+= self._fQnorm*numpy.polyval(_COEFFS,Qtilde[indx])\ /(Qtilde[indx]**(2./3.)\ *(numpy.log(Qtilde[indx])/(1-Qtilde[indx]))**2.) return out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/osipkovmerrittdf.py0000644000175100001710000002025314327773303017741 0ustar00runnerdocker# Class that implements anisotropic DFs of the Osipkov-Merritt type import numpy from scipy import integrate, interpolate, special from ..potential import evaluateDensities from ..potential.Potential import _evaluatePotentials from ..util import conversion from .eddingtondf import eddingtondf from .sphericaldf import anisotropicsphericaldf, sphericaldf # 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()._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) 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.""" 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/quasiisothermaldf.py0000644000175100001710000027417114327773303020064 0ustar00runnerdocker#A 'Binney' quasi-isothermal DF import hashlib import warnings import numpy from scipy import integrate, interpolate, optimize from .. import actionAngle, potential from ..actionAngle import actionAngleIsochrone from ..orbit import Orbit from ..potential import IsochronePotential from ..potential import flatten as flatten_potential from ..util import conversion, galpyWarning from ..util._optional_deps import _APY_LOADED, _APY_UNITS from ..util.conversion import (actionAngle_physical_input, parse_angmom, parse_length, parse_length_kpc, parse_velocity, parse_velocity_kms, physical_compatible, physical_conversion, potential_physical_input) from .df import df if _APY_LOADED: from astropy import units _NSIGMA=4 _DEFAULTNGL=10 _DEFAULTNGL2=20 class quasiisothermaldf(df): """Class that represents a 'Binney' quasi-isothermal DF""" def __init__(self,hr,sr,sz,hsr,hsz,pot=None,aA=None, cutcounter=False, _precomputerg=True,_precomputergrmax=None, _precomputergnLz=51, refr=1.,lo=10./220./8., ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a quasi-isothermal DF INPUT: hr - radial scale length (can be Quantity) sr - radial velocity dispersion at the solar radius (can be Quantity) sz - vertical velocity dispersion at the solar radius (can be Quantity) hsr - radial-velocity-dispersion scale length (can be Quantity) hsz - vertial-velocity-dispersion scale length (can be Quantity) pot= Potential instance or list thereof aA= actionAngle instance used to convert (x,v) to actions [must be an instance of an actionAngle class that computes (J,Omega,angle) for a given (x,v)] cutcounter= if True, set counter-rotating stars' DF to zero refr= reference radius for dispersions (can be different from ro) (can be Quantity) lo= reference angular momentum below where there are significant numbers of retrograde stars (can be Quantity) ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) OTHER INPUTS: _precomputerg= if True (default), pre-compute the rL(L) _precomputergrmax= if set, this is the maximum R for which to pre-compute rg (default: 5*hr) _precomputergnLz if set, number of Lz to pre-compute rg for (default: 51) OUTPUT: object HISTORY: 2012-07-25 - Started - Bovy (IAS@MPIA) """ df.__init__(self,ro=ro,vo=vo) self._hr= parse_length(hr,ro=self._ro) self._sr= parse_velocity(sr,vo=self._vo) self._sz= parse_velocity(sz,vo=self._vo) self._hsr= parse_length(hsr,ro=self._ro) self._hsz= parse_length(hsz,ro=self._ro) self._refr= parse_length(refr,ro=self._ro) self._lo= parse_angmom(lo,ro=self._ro,vo=self._vo) self._lnsr= numpy.log(self._sr) self._lnsz= numpy.log(self._sz) self._maxVT_hash= None self._maxVT_ip= None if pot is None: raise OSError("pot= must be set") self._pot= flatten_potential(pot) if aA is None: raise OSError("aA= must be set") self._aA= aA if not self._aA._pot == self._pot: if not isinstance(self._aA,actionAngleIsochrone): raise OSError("Potential in aA does not appear to be the same as given potential pot") elif isinstance(self._pot,IsochronePotential) and \ not self._aA.b == self._pot.b and \ not self._aA.amp == self._pot._amp: raise OSError("Potential in aA does not appear to be the same as given potential pot") self._check_consistent_units() self._cutcounter= cutcounter if _precomputerg: if _precomputergrmax is None: _precomputergrmax= 5*self._hr self._precomputergrmax= _precomputergrmax self._precomputergnLz= _precomputergnLz self._precomputergLzmin= 0.01 self._precomputergLzmax= self._precomputergrmax\ *potential.vcirc(self._pot,self._precomputergrmax) self._precomputergLzgrid= numpy.linspace(self._precomputergLzmin,self._precomputergLzmax,self._precomputergnLz) self._rls= numpy.array([potential.rl(self._pot,l) for l in self._precomputergLzgrid]) #Spline interpolate self._rgInterp= interpolate.InterpolatedUnivariateSpline(self._precomputergLzgrid,self._rls,k=3) else: self._precomputergrmax= 0. self._rgInterp= None self._rls= None self._precomputergnr= None self._precomputergLzgrid= None self._precomputergLzmin= \ numpy.finfo(numpy.dtype(numpy.float64)).max self._precomputergLzmax= \ numpy.finfo(numpy.dtype(numpy.float64)).min self._precomputerg= _precomputerg self._glxdef, self._glwdef= \ numpy.polynomial.legendre.leggauss(_DEFAULTNGL) self._glxdef2, self._glwdef2= \ numpy.polynomial.legendre.leggauss(_DEFAULTNGL2) self._glxdef12, self._glwdef12= \ numpy.polynomial.legendre.leggauss(_DEFAULTNGL//2) return None @physical_conversion('phasespacedensity',pop=True) def __call__(self,*args,**kwargs): """ NAME: __call__ PURPOSE: return the DF INPUT: Either: a)(jr,lz,jz) tuple; each can be a Quantity where: jr - radial action lz - z-component of angular momentum jz - vertical action b) R,vR,vT,z,vz c) Orbit instance: initial condition used if that's it, orbit(t) if there is a time given as well log= if True, return the natural log +scipy.integrate.quadrature kwargs func= function of (jr,lz,jz) to multiply f with (useful for moments) OUTPUT: value of DF HISTORY: 2012-07-25 - Written - Bovy (IAS@MPIA) NOTE: For Miyamoto-Nagai/adiabatic approximation this seems to take about 30 ms / evaluation in the extended Solar neighborhood For a MWPotential/adiabatic approximation this takes about 50 ms / evaluation in the extended Solar neighborhood For adiabatic-approximation grid this seems to take about 0.67 to 0.75 ms / evaluation in the extended Solar neighborhood (includes some out of the grid) up to 200x faster when called with vector R,vR,vT,z,vz """ #First parse log log= kwargs.pop('log',False) _return_actions= kwargs.pop('_return_actions',False) _return_freqs= kwargs.pop('_return_freqs',False) _func= kwargs.pop('func',None) if 'rg' in kwargs: thisrg= kwargs.pop('rg') kappa= kwargs.pop('kappa') nu= kwargs.pop('nu') Omega= kwargs.pop('Omega') else: thisrg= None kappa= None nu= None Omega= None #First parse args if len(args) == 1 and not isinstance(args[0],Orbit): #(jr,lz,jz) jr,lz,jz= args[0] jr= parse_angmom(jr,ro=self._ro,vo=self._vo) lz= parse_angmom(lz,ro=self._ro,vo=self._vo) jz= parse_angmom(jz,ro=self._ro,vo=self._vo) else: #Use self._aA to calculate the actions if isinstance(args[0],Orbit) and len(args[0].shape) > 1: raise RuntimeError("Evaluating quasiisothermaldf with Orbit instances with multi-dimensional shapes is not supported") #pragma: no cover try: jr,lz,jz= self._aA(*args,use_physical=False,**kwargs) except actionAngle.UnboundError: if log: return -numpy.finfo(numpy.dtype(numpy.float64)).max else: return 0. #if isinstance(jr,(list,numpy.ndarray)) and len(jr) > 1: jr= jr[0] #if isinstance(jz,(list,numpy.ndarray)) and len(jz) > 1: jz= jz[0] if not isinstance(lz,numpy.ndarray) and self._cutcounter and lz < 0.: if log: return -numpy.finfo(numpy.dtype(numpy.float64)).max else: return 0. #First calculate rg if thisrg is None: thisrg= self._rg(lz) #Then calculate the epicycle and vertical frequencies kappa, nu= self._calc_epifreq(thisrg), self._calc_verticalfreq(thisrg) Omega= numpy.fabs(lz)/thisrg/thisrg #calculate surface-densities and sigmas lnsurfmass= (self._refr-thisrg)/self._hr lnsr= self._lnsr+(self._refr-thisrg)/self._hsr lnsz= self._lnsz+(self._refr-thisrg)/self._hsz #Calculate func if not _func is None: if log: funcTerm= numpy.log(_func(jr,lz,jz)) else: funcFactor= _func(jr,lz,jz) #Calculate fsr else: if log: funcTerm= 0. else: funcFactor= 1. if log: lnfsr= numpy.log(Omega)+lnsurfmass-2.*lnsr-numpy.log(numpy.pi)\ -numpy.log(kappa)\ +numpy.log(1.+numpy.tanh(lz/self._lo))\ -kappa*jr*numpy.exp(-2.*lnsr) lnfsz= numpy.log(nu)-numpy.log(2.*numpy.pi)\ -2.*lnsz-nu*jz*numpy.exp(-2.*lnsz) out= lnfsr+lnfsz+funcTerm if isinstance(lz,numpy.ndarray): out[numpy.isnan(out)]= -numpy.finfo(numpy.dtype(numpy.float64)).max if self._cutcounter: out[(lz < 0.)]= -numpy.finfo(numpy.dtype(numpy.float64)).max elif numpy.isnan(out): out= -numpy.finfo(numpy.dtype(numpy.float64)).max else: srm2= numpy.exp(-2.*lnsr) fsr= Omega*numpy.exp(lnsurfmass)*srm2/numpy.pi/kappa\ *(1.+numpy.tanh(lz/self._lo))\ *numpy.exp(-kappa*jr*srm2) szm2= numpy.exp(-2.*lnsz) fsz= nu/2./numpy.pi*szm2*numpy.exp(-nu*jz*szm2) out= fsr*fsz*funcFactor if isinstance(lz,numpy.ndarray): out[numpy.isnan(out)]= 0. if self._cutcounter: out[(lz < 0.)]= 0. elif numpy.isnan(out): out= 0. if _return_actions and _return_freqs: return (out,jr,lz,jz,thisrg,kappa,nu,Omega) elif _return_actions: return (out,jr,lz,jz) elif _return_freqs: return (out,thisrg,kappa,nu,Omega) else: return out @potential_physical_input @physical_conversion('position',pop=True) def estimate_hr(self,R,z=0.,dR=10.**-8.,**kwargs): """ NAME: estimate_hr PURPOSE: estimate the exponential scale length at R INPUT: R - Galactocentric radius (can be Quantity) z= height (default: 0 pc) (can be Quantity) dR- range in R to use (can be Quantity) density kwargs OUTPUT: estimated hR HISTORY: 2012-09-11 - Written - Bovy (IAS) 2013-01-28 - Re-written - Bovy """ Rs= [R-dR/2.,R+dR/2.] if z is None: sf= numpy.array([self.surfacemass_z(r,use_physical=False, **kwargs) for r in Rs]) else: sf= numpy.array([self.density(r,z,use_physical=False, **kwargs) for r in Rs]) lsf= numpy.log(sf) return -dR/(lsf[1]-lsf[0]) @potential_physical_input @physical_conversion('position',pop=True) def estimate_hz(self,R,z,dz=10.**-8.,**kwargs): """ NAME: estimate_hz PURPOSE: estimate the exponential scale height at R INPUT: R - Galactocentric radius (can be Quantity) dz - z range to use (can be Quantity) density kwargs OUTPUT: estimated hz HISTORY: 2012-08-30 - Written - Bovy (IAS) 2013-01-28 - Re-written - Bovy """ if z == 0.: zs= [z,z+dz] else: zs= [z-dz/2.,z+dz/2.] sf= numpy.array([self.density(R,zz,use_physical=False, **kwargs) for zz in zs]) lsf= numpy.log(sf) return -dz/(lsf[1]-lsf[0]) @potential_physical_input @physical_conversion('position',pop=True) def estimate_hsr(self,R,z=0.,dR=10.**-8.,**kwargs): """ NAME: estimate_hsr PURPOSE: estimate the exponential scale length of the radial dispersion at R INPUT: R - Galactocentric radius (can be Quantity) z= height (default: 0 pc) (can be Quantity) dR- range in R to use (can be Quantity) density kwargs OUTPUT: estimated hsR HISTORY: 2013-03-08 - Written - Bovy (IAS) """ Rs= [R-dR/2.,R+dR/2.] sf= numpy.array([self.sigmaR2(r,z,use_physical=False, **kwargs) for r in Rs]) lsf= numpy.log(sf)/2. return -dR/(lsf[1]-lsf[0]) @potential_physical_input @physical_conversion('position',pop=True) def estimate_hsz(self,R,z=0.,dR=10.**-8.,**kwargs): """ NAME: estimate_hsz PURPOSE: estimate the exponential scale length of the vertical dispersion at R INPUT: R - Galactocentric radius (can be Quantity) z= height (default: 0 pc) (can be Quantity) dR- range in R to use (can be Quantity) density kwargs OUTPUT: estimated hsz HISTORY: 2013-03-08 - Written - Bovy (IAS) """ Rs= [R-dR/2.,R+dR/2.] sf= numpy.array([self.sigmaz2(r,z,use_physical=False, **kwargs) for r in Rs]) lsf= numpy.log(sf)/2. return -dR/(lsf[1]-lsf[0]) @potential_physical_input @physical_conversion('numbersurfacedensity',pop=True) def surfacemass_z(self,R,nz=7,zmax=1.,fixed_quad=True,fixed_order=8, **kwargs): r""" NAME: surfacemass_z PURPOSE: calculate the vertically-integrated surface density INPUT: R - Galactocentric radius (can be Quantity) fixed_quad= if True (default), use Gauss-Legendre integration fixed_order= (20), order of GL integration to use nz= number of zs to use to estimate zmax= maximum z to use (can be Quantity) density kwargs OUTPUT: \Sigma(R) HISTORY: 2012-08-30 - Written - Bovy (IAS) """ if fixed_quad: return 2.*integrate.fixed_quad(lambda x: self.density(R*numpy.ones(fixed_order),x,use_physical=False), 0.,.5,n=fixed_order)[0] zs= numpy.linspace(0.,zmax,nz) sf= numpy.array([self.density(R,z,use_physical=False, **kwargs) for z in zs]) lsf= numpy.log(sf) #Interpolate lsfInterp= interpolate.UnivariateSpline(zs, lsf, k=3) #Integrate return 2.*integrate.quad((lambda x: numpy.exp(lsfInterp(x))), 0.,1.)[0] def vmomentdensity(self,*args,**kwargs): """ NAME: vmomentdensity PURPOSE: calculate the an arbitrary moment of the velocity distribution at R times the density INPUT: R - radius at which to calculate the moment(/ro) n - vR^n m - vT^m o - vz^o OPTIONAL INPUT: nsigma - number of sigma to integrate the vR and vz velocities over (when doing explicit numerical integral; default: 4) vTmax - upper limit for integration over vT (default: 1.5) mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples gl= use Gauss-Legendre _returngl= if True, return the evaluated DF _return_actions= if True, return the evaluated actions (does not work with _returngl currently) _return_freqs= if True, return the evaluated frequencies and rg (does not work with _returngl currently) OUTPUT: at R,z (no support for units) HISTORY: 2012-08-06 - Written - Bovy (IAS@MPIA) """ 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= parse_length_kpc(ro) vo= kwargs.pop('vo',None) if vo is None and hasattr(self,'_voSet') and self._voSet: vo= self._vo vo= parse_velocity_kms(vo) if use_physical and not vo is None and not ro is None: fac= vo**(args[2]+args[3]+args[4])/ro**3 if _APY_UNITS: u= 1/units.kpc**3*(units.km/units.s)**(args[2]+args[3]+args[4]) out= self._vmomentdensity(*args,**kwargs) if _APY_UNITS: return units.Quantity(out*fac,unit=u) else: return out*fac else: return self._vmomentdensity(*args,**kwargs) def _vmomentdensity(self,R,z,n,m,o,nsigma=None,mc=False,nmc=10000, _returnmc=False,_vrs=None,_vts=None,_vzs=None, _rawgausssamples=False, gl=False,ngl=_DEFAULTNGL,_returngl=False,_glqeval=None, _return_actions=False,_jr=None,_lz=None,_jz=None, _return_freqs=False, _rg=None,_kappa=None,_nu=None,_Omega=None, _sigmaR1=None,_sigmaz1=None, **kwargs): """Non-physical version of vmomentdensity, otherwise the same""" if isinstance(R,numpy.ndarray): return numpy.array([self._vmomentdensity(r,zz,n,m,o,nsigma=nsigma, mc=mc,nmc=nmc, gl=gl,ngl=ngl,**kwargs) for r,zz in zip(R,z)]) if isinstance(self._aA,(actionAngle.actionAngleAdiabatic, actionAngle.actionAngleAdiabaticGrid)): if n % 2 == 1. or o % 2 == 1.: return 0. #we know this must be the case if nsigma == None: nsigma= _NSIGMA if _sigmaR1 is None: sigmaR1= self._sr*numpy.exp((self._refr-R)/self._hsr) else: sigmaR1= _sigmaR1 if _sigmaz1 is None: sigmaz1= self._sz*numpy.exp((self._refr-R)/self._hsz) else: sigmaz1= _sigmaz1 thisvc= potential.vcirc(self._pot,R,use_physical=False) #Use the asymmetric drift equation to estimate va gamma= numpy.sqrt(0.5) va= sigmaR1**2./2./thisvc\ *(gamma**2.-1. #Assume close to flat rotation curve, sigphi2/sigR2 =~ 0.5 +R*(1./self._hr+2./self._hsr)) if numpy.fabs(va) > sigmaR1: va = 0.#To avoid craziness near the center if gl: if ngl % 2 == 1: raise ValueError("ngl must be even") if not _glqeval is None and ngl != _glqeval.shape[0]: _glqeval= None #Use Gauss-Legendre integration for all if ngl == _DEFAULTNGL: glx, glw= self._glxdef, self._glwdef glx12, glw12= self._glxdef12, self._glwdef12 elif ngl == _DEFAULTNGL2: glx, glw= self._glxdef2, self._glwdef2 glx12, glw12= self._glxdef, self._glwdef else: glx, glw= numpy.polynomial.legendre.leggauss(ngl) glx12, glw12= numpy.polynomial.legendre.leggauss(ngl//2) #Evaluate everywhere if isinstance(self._aA,(actionAngle.actionAngleAdiabatic, actionAngle.actionAngleAdiabaticGrid)): vRgl= nsigma*sigmaR1/2.*(glx+1.) vzgl= nsigma*sigmaz1/2.*(glx+1.) vRglw= glw vzglw= glw else: vRgl= nsigma*sigmaR1/2.*(glx12+1.) #vRgl= 1.5/2.*(glx12+1.) vRgl= list(vRgl) vRgl.extend(-nsigma*sigmaR1/2.*(glx12+1.)) #vRgl.extend(-1.5/2.*(glx12+1.)) vRgl= numpy.array(vRgl) vzgl= nsigma*sigmaz1/2.*(glx12+1.) #vzgl= 1.5/2.*(glx12+1.) vzgl= list(vzgl) vzgl.extend(-nsigma*sigmaz1/2.*(glx12+1.)) #vzgl.extend(-1.5/2.*(glx12+1.)) vzgl= numpy.array(vzgl) vRglw= glw12 vRglw= list(vRglw) vRglw.extend(glw12) vRglw= numpy.array(vRglw) vzglw= glw12 vzglw= list(vzglw) vzglw.extend(glw12) vzglw= numpy.array(vzglw) if 'vTmax' in kwargs: vTmax = kwargs['vTmax'] else: vTmax = 1.5 vTgl= vTmax/2.*(glx+1.) #Tile everything vTgl= numpy.tile(vTgl,(ngl,ngl,1)).T vRgl= numpy.tile(numpy.reshape(vRgl,(1,ngl)).T,(ngl,1,ngl)) vzgl= numpy.tile(vzgl,(ngl,ngl,1)) vTglw= numpy.tile(glw,(ngl,ngl,1)).T #also tile weights vRglw= numpy.tile(numpy.reshape(vRglw,(1,ngl)).T,(ngl,1,ngl)) vzglw= numpy.tile(vzglw,(ngl,ngl,1)) #evaluate if _glqeval is None and _jr is None: logqeval, jr, lz, jz, rg, kappa, nu, Omega= self(R+numpy.zeros(ngl*ngl*ngl), vRgl.flatten(), vTgl.flatten(), z+numpy.zeros(ngl*ngl*ngl), vzgl.flatten(), log=True, _return_actions=True, _return_freqs=True, use_physical=False) logqeval= numpy.reshape(logqeval,(ngl,ngl,ngl)) elif not _jr is None and _rg is None: logqeval, jr, lz, jz, rg, kappa, nu, Omega= self((_jr,_lz,_jz), log=True, _return_actions=True, _return_freqs=True, use_physical=False) logqeval= numpy.reshape(logqeval,(ngl,ngl,ngl)) elif not _jr is None and not _rg is None: logqeval, jr, lz, jz, rg, kappa, nu, Omega= self((_jr,_lz,_jz), rg=_rg,kappa=_kappa,nu=_nu, Omega=_Omega, log=True, _return_actions=True, _return_freqs=True, use_physical=False) logqeval= numpy.reshape(logqeval,(ngl,ngl,ngl)) else: logqeval= _glqeval if _returngl: return (numpy.sum(numpy.exp(logqeval)*vRgl**n*vTgl**m*vzgl**o *vTglw*vRglw*vzglw)*sigmaR1*sigmaz1*0.125*vTmax*nsigma**2, logqeval) elif _return_actions and _return_freqs: return (numpy.sum(numpy.exp(logqeval)*vRgl**n*vTgl**m*vzgl**o *vTglw*vRglw*vzglw)*sigmaR1*sigmaz1*0.125*vTmax*nsigma**2, jr,lz,jz, rg,kappa,nu,Omega) elif _return_actions: return (numpy.sum(numpy.exp(logqeval)*vRgl**n*vTgl**m*vzgl**o *vTglw*vRglw*vzglw)*sigmaR1*sigmaz1*0.125*vTmax*nsigma**2, jr,lz,jz) else: return numpy.sum(numpy.exp(logqeval)*vRgl**n*vTgl**m*vzgl**o *vTglw*vRglw*vzglw*sigmaR1*sigmaz1*0.125*vTmax*nsigma**2) elif mc: mvT= (thisvc-va)/gamma/sigmaR1 if _vrs is None: vrs= numpy.random.normal(size=nmc) else: vrs= _vrs if _vts is None: vts= numpy.random.normal(size=nmc)+mvT else: if _rawgausssamples: vts= _vts+mvT else: vts= _vts if _vzs is None: vzs= numpy.random.normal(size=nmc) else: vzs= _vzs Is= _vmomentsurfaceMCIntegrand(vzs,vrs,vts,numpy.ones(nmc)*R, numpy.ones(nmc)*z, self,sigmaR1,gamma,sigmaz1,mvT, n,m,o) if _returnmc: if _rawgausssamples: return (numpy.mean(Is)*sigmaR1**(2.+n+m)*gamma**(1.+m)*sigmaz1**(1.+o), vrs,vts-mvT,vzs) else: return (numpy.mean(Is)*sigmaR1**(2.+n+m)*gamma**(1.+m)*sigmaz1**(1.+o), vrs,vts,vzs) else: return numpy.mean(Is)*sigmaR1**(2.+n+m)*gamma**(1.+m)*sigmaz1**(1.+o) else: #pragma: no cover because this is too slow; a warning is shown warnings.warn("Calculations using direct numerical integration using tplquad is not recommended and extremely slow; it has also not been carefully tested",galpyWarning) return integrate.tplquad(_vmomentsurfaceIntegrand, 1./gamma*(thisvc-va)/sigmaR1-nsigma, 1./gamma*(thisvc-va)/sigmaR1+nsigma, lambda x: 0., lambda x: nsigma, lambda x,y: 0., lambda x,y: nsigma, (R,z,self,sigmaR1,gamma,sigmaz1,n,m,o), **kwargs)[0]*sigmaR1**(2.+n+m)*gamma**(1.+m)*sigmaz1**(1.+o) def jmomentdensity(self,*args,**kwargs): """ NAME: jmomentdensity PURPOSE: calculate the an arbitrary moment of an action of the velocity distribution at R times the surfacmass INPUT: R - radius at which to calculate the moment(/ro) n - jr^n m - lz^m o - jz^o OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over (when doing explicit numerical integral) mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples OUTPUT: at R (no support for units) HISTORY: 2012-08-09 - Written - Bovy (IAS@MPIA) """ 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= parse_length_kpc(ro) vo= kwargs.pop('vo',None) if vo is None and hasattr(self,'_voSet') and self._voSet: vo= self._vo vo= parse_velocity_kms(vo) if use_physical and not vo is None and not ro is None: fac= (ro*vo)**(args[2]+args[3]+args[4])/ro**3 if _APY_UNITS: u= 1/units.kpc**3*(units.kpc*units.km/units.s)**(args[2]+args[3]+args[4]) out= self._jmomentdensity(*args,**kwargs) if _APY_UNITS: return units.Quantity(out*fac,unit=u) else: return out*fac else: return self._jmomentdensity(*args,**kwargs) def _jmomentdensity(self,R,z,n,m,o,nsigma=None,mc=True,nmc=10000, _returnmc=False,_vrs=None,_vts=None,_vzs=None, **kwargs): """Non-physical version of jmomentdensity, otherwise the same""" if nsigma == None: nsigma= _NSIGMA sigmaR1= self._sr*numpy.exp((self._refr-R)/self._hsr) sigmaz1= self._sz*numpy.exp((self._refr-R)/self._hsz) thisvc= potential.vcirc(self._pot,R,use_physical=False) #Use the asymmetric drift equation to estimate va gamma= numpy.sqrt(0.5) va= sigmaR1**2./2./thisvc\ *(gamma**2.-1. #Assume close to flat rotation curve, sigphi2/sigR2 =~ 0.5 +R*(1./self._hr+2./self._hsr)) if numpy.fabs(va) > sigmaR1: va = 0.#To avoid craziness near the center if mc: mvT= (thisvc-va)/gamma/sigmaR1 if _vrs is None: vrs= numpy.random.normal(size=nmc) else: vrs= _vrs if _vts is None: vts= numpy.random.normal(size=nmc)+mvT else: vts= _vts if _vzs is None: vzs= numpy.random.normal(size=nmc) else: vzs= _vzs Is= _jmomentsurfaceMCIntegrand(vzs,vrs,vts,numpy.ones(nmc)*R,numpy.ones(nmc)*z,self,sigmaR1,gamma,sigmaz1,mvT,n,m,o) if _returnmc: return (numpy.mean(Is)*sigmaR1**2.*gamma*sigmaz1, vrs,vts,vzs) else: return numpy.mean(Is)*sigmaR1**2.*gamma*sigmaz1 else: #pragma: no cover because this is too slow; a warning is shown warnings.warn("Calculations using direct numerical integration using tplquad is not recommended and extremely slow; it has also not been carefully tested",galpyWarning) return integrate.tplquad(_jmomentsurfaceIntegrand, 1./gamma*(thisvc-va)/sigmaR1-nsigma, 1./gamma*(thisvc-va)/sigmaR1+nsigma, lambda x: 0., lambda x: nsigma, lambda x,y: 0., lambda x,y: nsigma, (R,z,self,sigmaR1,gamma,sigmaz1,n,m,o), **kwargs)[0]*sigmaR1**2.*gamma*sigmaz1 @potential_physical_input @physical_conversion('numberdensity',pop=True) def density(self,R,z,nsigma=None,mc=False,nmc=10000, gl=True,ngl=_DEFAULTNGL,**kwargs): """ NAME: density PURPOSE: calculate the density at R,z by marginalizing over velocity INPUT: R - radius at which to calculate the density (can be Quantity) z - height at which to calculate the density (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over scipy.integrate.tplquad kwargs epsabs and epsrel mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples gl= if True, calculate using Gauss-Legendre integration ngl= if gl, use ngl-th order Gauss-Legendre integration for each dimension OUTPUT: density at (R,z) HISTORY: 2012-07-26 - Written - Bovy (IAS@MPIA) """ return self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, gl=gl,ngl=ngl, **kwargs) @potential_physical_input @physical_conversion('velocity2',pop=True) def sigmaR2(self,R,z,nsigma=None,mc=False,nmc=10000, gl=True,ngl=_DEFAULTNGL,**kwargs): """ NAME: sigmaR2 PURPOSE: calculate sigma_R^2 by marginalizing over velocity INPUT: R - radius at which to calculate this (can be Quantity) z - height at which to calculate this (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over scipy.integrate.tplquad kwargs epsabs and epsrel mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples gl= if True, calculate using Gauss-Legendre integration ngl= if gl, use ngl-th order Gauss-Legendre integration for each dimension OUTPUT: sigma_R^2 HISTORY: 2012-07-30 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs= self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=True, **kwargs) return self._vmomentdensity(R,z,2.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass elif gl: surfmass, glqeval= self._vmomentdensity(R,z,0.,0.,0., gl=gl,ngl=ngl, _returngl=True, **kwargs) return self._vmomentdensity(R,z,2.,0.,0., ngl=ngl,gl=gl, _glqeval=glqeval, **kwargs)/surfmass else: #pragma: no cover because this is too slow; a warning is shown return (self._vmomentdensity(R,z,2.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)/ self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)) @potential_physical_input @physical_conversion('velocity2',pop=True) def sigmaRz(self,R,z,nsigma=None,mc=False,nmc=10000, gl=True,ngl=_DEFAULTNGL,**kwargs): """ NAME: sigmaRz PURPOSE: calculate sigma_RZ^2 by marginalizing over velocity INPUT: R - radius at which to calculate this (can be Quantity) z - height at which to calculate this (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over scipy.integrate.tplquad kwargs epsabs and epsrel mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples gl= if True, calculate using Gauss-Legendre integration ngl= if gl, use ngl-th order Gauss-Legendre integration for each dimension OUTPUT: sigma_Rz^2 HISTORY: 2012-07-30 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs= self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=True, **kwargs) return self._vmomentdensity(R,z,1.,0.,1., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass elif gl: surfmass, glqeval= self._vmomentdensity(R,z,0.,0.,0., gl=gl,ngl=ngl, _returngl=True, **kwargs) return self._vmomentdensity(R,z,1.,0.,1., ngl=ngl,gl=gl, _glqeval=glqeval, **kwargs)/surfmass else: #pragma: no cover because this is too slow; a warning is shown return (self._vmomentdensity(R,z,1.,0.,1., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)/ self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)) @potential_physical_input @physical_conversion('angle',pop=True) def tilt(self,R,z,nsigma=None,mc=False,nmc=10000, gl=True,ngl=_DEFAULTNGL,**kwargs): """ NAME: tilt PURPOSE: calculate the tilt of the velocity ellipsoid by marginalizing over velocity INPUT: R - radius at which to calculate this (can be Quantity) z - height at which to calculate this (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over scipy.integrate.tplquad kwargs epsabs and epsrel mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples gl= if True, calculate using Gauss-Legendre integration ngl= if gl, use ngl-th order Gauss-Legendre integration for each dimension OUTPUT: tilt in rad HISTORY: 2012-12-23 - Written - Bovy (IAS) 2017-10-28 - Changed return unit to rad - Bovy (UofT) """ warnings.warn("In versions >1.3, the output unit of quasiisothermaldf.tilt has been changed to radian (from degree before)",galpyWarning) if mc: surfmass, vrs, vts, vzs= self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=True, **kwargs) tsigmar2= self._vmomentdensity(R,z,2.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass tsigmaz2= self._vmomentdensity(R,z,0.,0.,2., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass tsigmarz= self._vmomentdensity(R,z,1.,0.,1., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass return 0.5*numpy.arctan(2.*tsigmarz/(tsigmar2-tsigmaz2)) elif gl: surfmass, glqeval= self._vmomentdensity(R,z,0.,0.,0., gl=gl,ngl=ngl, _returngl=True, **kwargs) tsigmar2= self._vmomentdensity(R,z,2.,0.,0., ngl=ngl,gl=gl, _glqeval=glqeval, **kwargs)/surfmass tsigmaz2= self._vmomentdensity(R,z,0.,0.,2., ngl=ngl,gl=gl, _glqeval=glqeval, **kwargs)/surfmass tsigmarz= self._vmomentdensity(R,z,1.,0.,1., ngl=ngl,gl=gl, _glqeval=glqeval, **kwargs)/surfmass return 0.5*numpy.arctan(2.*tsigmarz/(tsigmar2-tsigmaz2)) else: raise NotImplementedError("Use either mc=True or gl=True") @potential_physical_input @physical_conversion('velocity2',pop=True) def sigmaz2(self,R,z,nsigma=None,mc=False,nmc=10000, gl=True,ngl=_DEFAULTNGL,**kwargs): """ NAME: sigmaz2 PURPOSE: calculate sigma_z^2 by marginalizing over velocity INPUT: R - radius at which to calculate this (can be Quantity) z - height at which to calculate this (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over scipy.integrate.tplquad kwargs epsabs and epsrel mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples gl= if True, calculate using Gauss-Legendre integration ngl= if gl, use ngl-th order Gauss-Legendre integration for each dimension OUTPUT: sigma_z^2 HISTORY: 2012-07-30 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs= self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=True, **kwargs) return self._vmomentdensity(R,z,0.,0.,2., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass elif gl: surfmass, glqeval= self._vmomentdensity(R,z,0.,0.,0., gl=gl,ngl=ngl, _returngl=True, **kwargs) return self._vmomentdensity(R,z,0.,0.,2., ngl=ngl,gl=gl, _glqeval=glqeval, **kwargs)/surfmass else: #pragma: no cover because this is too slow; a warning is shown return (self._vmomentdensity(R,z,0.,0.,2., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)/ self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)) @potential_physical_input @physical_conversion('velocity',pop=True) def meanvT(self,R,z,nsigma=None,mc=False,nmc=10000, gl=True,ngl=_DEFAULTNGL,**kwargs): """ NAME: meanvT PURPOSE: calculate the mean rotational velocity by marginalizing over velocity INPUT: R - radius at which to calculate this (can be Quantity) z - height at which to calculate this (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over scipy.integrate.tplquad kwargs epsabs and epsrel mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples gl= if True, calculate using Gauss-Legendre integration ngl= if gl, use ngl-th order Gauss-Legendre integration for each dimension OUTPUT: meanvT HISTORY: 2012-07-30 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs= self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=True, **kwargs) return self._vmomentdensity(R,z,0.,1.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass elif gl: surfmass, glqeval= self._vmomentdensity(R,z,0.,0.,0., gl=gl,ngl=ngl, _returngl=True, **kwargs) return self._vmomentdensity(R,z,0.,1.,0., ngl=ngl,gl=gl, _glqeval=glqeval, **kwargs)/surfmass else: #pragma: no cover because this is too slow; a warning is shown return (self._vmomentdensity(R,z,0.,1.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)/ self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)) @potential_physical_input @physical_conversion('velocity',pop=True) def meanvR(self,R,z,nsigma=None,mc=False,nmc=10000, gl=True,ngl=_DEFAULTNGL,**kwargs): """ NAME: meanvR PURPOSE: calculate the mean radial velocity by marginalizing over velocity INPUT: R - radius at which to calculate this (can be Quantity) z - height at which to calculate this (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over scipy.integrate.tplquad kwargs epsabs and epsrel mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples gl= if True, calculate using Gauss-Legendre integration ngl= if gl, use ngl-th order Gauss-Legendre integration for each dimension OUTPUT: meanvR HISTORY: 2012-12-23 - Written - Bovy (IAS) """ if mc: surfmass, vrs, vts, vzs= self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=True, **kwargs) return self._vmomentdensity(R,z,1.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass elif gl: surfmass, glqeval= self._vmomentdensity(R,z,0.,0.,0., gl=gl,ngl=ngl, _returngl=True, **kwargs) return self._vmomentdensity(R,z,1.,0.,0., ngl=ngl,gl=gl, _glqeval=glqeval, **kwargs)/surfmass else: #pragma: no cover because this is too slow; a warning is shown return (self._vmomentdensity(R,z,1.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)/ self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)) @potential_physical_input @physical_conversion('velocity',pop=True) def meanvz(self,R,z,nsigma=None,mc=False,nmc=10000, gl=True,ngl=_DEFAULTNGL,**kwargs): """ NAME: meanvz PURPOSE: calculate the mean vertical velocity by marginalizing over velocity INPUT: R - radius at which to calculate this (can be Quantity) z - height at which to calculate this (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over scipy.integrate.tplquad kwargs epsabs and epsrel mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples gl= if True, calculate using Gauss-Legendre integration ngl= if gl, use ngl-th order Gauss-Legendre integration for each dimension OUTPUT: meanvz HISTORY: 2012-12-23 - Written - Bovy (IAS) """ if mc: surfmass, vrs, vts, vzs= self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=True, **kwargs) return self._vmomentdensity(R,z,0.,0.,1., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass elif gl: surfmass, glqeval= self._vmomentdensity(R,z,0.,0.,0., gl=gl,ngl=ngl, _returngl=True, **kwargs) return self._vmomentdensity(R,z,0.,0.,1., ngl=ngl,gl=gl, _glqeval=glqeval, **kwargs)/surfmass else: #pragma: no cover because this is too slow; a warning is shown return (self._vmomentdensity(R,z,0.,0.,1., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)/ self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)) @potential_physical_input @physical_conversion('velocity2',pop=True) def sigmaT2(self,R,z,nsigma=None,mc=False,nmc=10000, gl=True,ngl=_DEFAULTNGL,**kwargs): """ NAME: sigmaT2 PURPOSE: calculate sigma_T^2 by marginalizing over velocity INPUT: R - radius at which to calculate this (can be Quantity) z - height at which to calculate this (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over scipy.integrate.tplquad kwargs epsabs and epsrel mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples gl= if True, calculate using Gauss-Legendre integration ngl= if gl, use ngl-th order Gauss-Legendre integration for each dimension OUTPUT: sigma_T^2 HISTORY: 2012-07-30 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs= self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=True, **kwargs) mvt= self._vmomentdensity(R,z,0.,1.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass return self._vmomentdensity(R,z,0.,2.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass\ -mvt**2. elif gl: surfmass, glqeval= self._vmomentdensity(R,z,0.,0.,0., gl=gl,ngl=ngl, _returngl=True, **kwargs) mvt= self._vmomentdensity(R,z,0.,1.,0., ngl=ngl,gl=gl, _glqeval=glqeval, **kwargs)/surfmass return self._vmomentdensity(R,z,0.,2.,0., ngl=ngl,gl=gl, _glqeval=glqeval, **kwargs)/surfmass-mvt**2. else: #pragma: no cover because this is too slow; a warning is shown surfmass= self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs) return (self._vmomentdensity(R,z,0.,2.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)/surfmass\ -(self._vmomentdensity(R,z,0.,2.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)/surfmass)**2.) @potential_physical_input @physical_conversion('action',pop=True) def meanjr(self,R,z,nsigma=None,mc=True,nmc=10000,**kwargs): """ NAME: meanjr PURPOSE: calculate the mean radial action by marginalizing over velocity INPUT: R - radius at which to calculate this (can be Quantity) z - height at which to calculate this (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over scipy.integrate.tplquad kwargs epsabs and epsrel mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples OUTPUT: meanjr HISTORY: 2012-08-09 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs= self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=True, **kwargs) return self._jmomentdensity(R,z,1.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass else: #pragma: no cover because this is too slow; a warning is shown return (self._jmomentdensity(R,z,1.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)/ self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)) @potential_physical_input @physical_conversion('action',pop=True) def meanlz(self,R,z,nsigma=None,mc=True,nmc=10000,**kwargs): """ NAME: meanlz PURPOSE: calculate the mean angular momemtum by marginalizing over velocity INPUT: R - radius at which to calculate this (can be Quantity) z - height at which to calculate this (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over scipy.integrate.tplquad kwargs epsabs and epsrel mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples OUTPUT: meanlz HISTORY: 2012-08-09 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs= self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=True, **kwargs) return self._jmomentdensity(R,z,0.,1.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass else: #pragma: no cover because this is too slow; a warning is shown return (self._jmomentdensity(R,z,0.,1.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)/ self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)) @potential_physical_input @physical_conversion('action',pop=True) def meanjz(self,R,z,nsigma=None,mc=True,nmc=10000,**kwargs): """ NAME: meanjz PURPOSE: calculate the mean vertical action by marginalizing over velocity INPUT: R - radius at which to calculate this (can be Quantity) z - height at which to calculate this (can be Quantity) OPTIONAL INPUT: nsigma - number of sigma to integrate the velocities over scipy.integrate.tplquad kwargs epsabs and epsrel mc= if True, calculate using Monte Carlo integration nmc= if mc, use nmc samples OUTPUT: meanjz HISTORY: 2012-08-09 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs= self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=True, **kwargs) return self._jmomentdensity(R,z,0.,0.,1., nsigma=nsigma,mc=mc,nmc=nmc,_returnmc=False, _vrs=vrs,_vts=vts,_vzs=vzs, **kwargs)/surfmass else: #pragma: no cover because this is too slow; a warning is shown return (self._jmomentdensity(R,z,0.,0.,1., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)/ self._vmomentdensity(R,z,0.,0.,0., nsigma=nsigma,mc=mc,nmc=nmc, **kwargs)) @potential_physical_input def sampleV(self,R,z,n=1,**kwargs): """ NAME: sampleV PURPOSE: sample a radial, azimuthal, and vertical velocity at R,z INPUT: R - Galactocentric distance (can be Quantity) z - height (can be Quantity) n= number of distances to sample OUTPUT: list of samples HISTORY: 2012-12-17 - Written - Bovy (IAS) """ use_physical= kwargs.pop('use_physical',True) vo= kwargs.pop('vo',None) if vo is None and hasattr(self,'_voSet') and self._voSet: vo= self._vo vo= parse_velocity_kms(vo) #Determine the maximum of the velocity distribution maxVR= 0. maxVz= 0. # scipy 1.5.0: issue scipy#12298: fmin_powell now returns multiD array, # so squeeze out single dimensions by hand maxVT= numpy.squeeze(\ optimize.fmin_powell((lambda x: -self(R,0.,x,z,0.,log=True, use_physical=False)), 1.)) logmaxVD= self(R,maxVR,maxVT,z,maxVz,log=True,use_physical=False) #Now rejection-sample vRs= [] vTs= [] vzs= [] while len(vRs) < n: nmore= n-len(vRs)+1 #sample propvR= numpy.random.normal(size=nmore)*2.*self._sr propvT= numpy.random.normal(size=nmore)*2.*self._sr+maxVT propvz= numpy.random.normal(size=nmore)*2.*self._sz VDatprop= self(R+numpy.zeros(nmore), propvR,propvT,z+numpy.zeros(nmore), propvz,log=True,use_physical=False)-logmaxVD VDatprop-= -0.5*(propvR**2./4./self._sr**2.+propvz**2./4./self._sz**2.\ +(propvT-maxVT)**2./4./self._sr**2.) VDatprop= numpy.reshape(VDatprop,(nmore)) indx= (VDatprop > numpy.log(numpy.random.random(size=nmore))) #accept vRs.extend(list(propvR[indx])) vTs.extend(list(propvT[indx])) vzs.extend(list(propvz[indx])) out= numpy.empty((n,3)) out[:,0]= vRs[0:n] out[:,1]= vTs[0:n] out[:,2]= vzs[0:n] if use_physical and not vo is None: if _APY_UNITS: return units.Quantity(out*vo,unit=units.km/units.s) else: return out*vo else: return out @potential_physical_input def sampleV_interpolate(self,R,z,R_pixel,z_pixel,num_std=3,R_min=None, R_max=None,z_max=None,**kwargs): """ NAME: sampleV_interpolate PURPOSE: Given an array of R and z coordinates of stars, return the positions and their radial, azimuthal, and vertical velocity. INPUT: R - array of Galactocentric distance (can be Quantity) z - array of height (can be Quantity) R_pixel, z_pixel= the pixel size for creating the grid for interpolation (in natural unit) num_std= number of standard deviation to be considered outliers sampled separately from interpolation R_min, R_max, z_max= optional edges of the grid OUTPUT: coord_v= a numpy array containing the sampled velocity, (vR, vT, vz), where each row correspond to the row of (R,z) HISTORY: 2018-08-10 - Written - Samuel Wong (University of Toronto) """ use_physical= kwargs.pop('use_physical',True) vo= kwargs.pop('vo',None) if vo is None and hasattr(self,'_voSet') and self._voSet: vo= self._vo vo= parse_velocity_kms(vo) #Initialize output array coord_v= numpy.empty((numpy.size(R), 3)) #Since the sign of z doesn't matter, work with absolute value of z z= numpy.abs(z) # Grid edges if R_min is None: R_min= numpy.amax([numpy.mean(R)-num_std*numpy.std(R), numpy.amin(R)]) if R_max is None: R_max= numpy.amin([numpy.mean(R)+num_std*numpy.std(R), numpy.amax(R)]) if z_max is None: z_max= numpy.amin([numpy.mean(z)+num_std*numpy.std(z), numpy.amax(z)]) z_min= 0. #Always start grid at z=0 for stars close to plane #Separate the coordinates into outliers and normal points #Define outliers as points outside of grid mask= numpy.any([R < R_min, R > R_max, z > z_max],axis = 0) outliers_R= R[mask] outliers_z= z[mask] normal_R= R[~mask] normal_z= z[~mask] #Sample the velocity of outliers directly (without interpolation) outlier_coord_v= numpy.empty((outliers_R.size, 3)) for i in range(outliers_R.size): outlier_coord_v[i]= self.sampleV(outliers_R[i], outliers_z[i], use_physical=False)[0] #Prepare for optimizing maxVT on a grid #Get the new hash of the parameters of grid new_hash= hashlib.md5(numpy.array([R_min,R_max,z_max,R_pixel,z_pixel])).hexdigest() #Reuse old interpolated object if new hash matches the old one if new_hash == self._maxVT_hash: ip_max_vT= self._maxVT_ip #Generate a new interpolation object if different from before else: R_number= int((R_max - R_min)/R_pixel) z_number= int((z_max - z_min)/z_pixel) R_linspace= numpy.linspace(R_min, R_max, R_number) z_linspace= numpy.linspace(z_min, z_max, z_number) Rv, zv= numpy.meshgrid(R_linspace, z_linspace) grid= numpy.dstack((Rv, zv)) #This grid stores (R,z) coordinate #Grid is a 3 dimensional array since it stores pairs of values, but #grid max vT is a 2 dimensional array grid_max_vT= numpy.empty((grid.shape[0], grid.shape[1])) #Optimize max_vT on the grid for i in range(z_number): for j in range(R_number): R, z= grid[i][j] grid_max_vT[i][j]= numpy.squeeze(\ optimize.fmin_powell((lambda x: -self( R,0.,x,z,0.,log=True, use_physical=False)),1.)) #Determine degree of interpolation ky= numpy.min([R_number-1,3]) kx= numpy.min([z_number-1,3]) #Generate interpolation object ip_max_vT= interpolate.RectBivariateSpline(z_linspace,R_linspace, grid_max_vT,kx=kx,ky=ky) #Store interpolation object self._maxVT_ip= ip_max_vT #Update hash of parameters self._maxVT_hash= new_hash #Evaluate interpolation object to get maxVT at the normal coordinates normal_max_vT= ip_max_vT.ev(normal_z, normal_R) #Sample all 3 velocities at a normal point and use interpolated vT normal_coord_v= \ self._sampleV_preoptimized(normal_R,normal_z,normal_max_vT) #Combine normal and outlier result, preserving original order coord_v[mask]= outlier_coord_v coord_v[~mask]= normal_coord_v if use_physical and not vo is None: if _APY_UNITS: return units.Quantity(coord_v*vo,unit=units.km/units.s) else: return coord_v*vo else: return coord_v def _sampleV_preoptimized(self,R,z,maxVT): """ NAME: _sampleV_preoptimized PURPOSE: sample a radial, azimuthal, and vertical velocity at R,z; R,z can be an array of positions maxVT is already optimized INPUT: R - Galactocentric distance (can be Quantity) z - height (can be Quantity) maxVT - an array of pre-optimized maximum vT at corresponding R,z OUTPUT: a numpy array containing the sampled velocity, (vR, vT, vz), where each row correspond to the row of (R,z) HISTORY: 2018-08-09 - Written - Samuel Wong (University of Toronto) """ length = numpy.size(R) out= numpy.empty((length,3)) #Initialize output #Determine the maximum of the velocity distribution maxVR= numpy.zeros(length) maxVz= numpy.zeros(length) logmaxVD= self(R,maxVR,maxVT,z,maxVz,log=True,use_physical=False) #Now rejection-sample #Initialize boolean index of position remaining to be sampled remain_indx = numpy.full(length,True) while numpy.any(remain_indx): nmore= numpy.sum(remain_indx) propvR= numpy.random.normal(size=nmore)*2.*self._sr propvT= numpy.random.normal(size=nmore)*2.*self._sr+maxVT[remain_indx] propvz= numpy.random.normal(size=nmore)*2.*self._sz VDatprop= self(R[remain_indx],propvR,propvT,z[remain_indx],propvz, log=True, use_physical=False)-logmaxVD[remain_indx] VDatprop-= -0.5*(propvR**2./4./self._sr**2.+ propvz**2./4./self._sz**2.+ (propvT-maxVT[remain_indx])**2./4./self._sr**2.) accept_indx= (VDatprop > numpy.log(numpy.random.random(size=nmore))) vR_accept= propvR[accept_indx] vT_accept= propvT[accept_indx] vz_accept= propvz[accept_indx] #Get the indexing of rows of output array that need to be updated #with newly accepted velocity to_change= numpy.copy(remain_indx) to_change[remain_indx]= accept_indx out[to_change]= numpy.stack((vR_accept,vT_accept,vz_accept), axis = 1) #Removing accepted sampled from remain index remain_indx[remain_indx]= ~accept_indx return out @actionAngle_physical_input @physical_conversion('phasespacedensityvelocity2',pop=True) def pvR(self,vR,R,z,gl=True,ngl=_DEFAULTNGL2,nsigma=4.,vTmax=1.5): """ NAME: pvR PURPOSE: calculate the marginalized vR probability at this location (NOT normalized by the density) INPUT: vR - radial velocity (can be Quantity) R - radius (can be Quantity) z - height (can be Quantity) gl - use Gauss-Legendre integration (True, currently the only option) ngl - order of Gauss-Legendre integration nsigma - sets integration limits to [-1,+1]*nsigma*sigma_z(R) for integration over vz (default: 4) vTmax - sets integration limits to [0,vTmax] for integration over vT (default: 1.5) OUTPUT: p(vR,R,z) HISTORY: 2012-12-22 - Written - Bovy (IAS) """ sigmaz1= self._sz*numpy.exp((self._refr-R)/self._hsz) if gl: if ngl % 2 == 1: raise ValueError("ngl must be even") #Use Gauss-Legendre integration for all if ngl == _DEFAULTNGL: glx, glw= self._glxdef, self._glwdef glx12, glw12= self._glxdef12, self._glwdef12 elif ngl == _DEFAULTNGL2: glx, glw= self._glxdef2, self._glwdef2 glx12, glw12= self._glxdef, self._glwdef else: glx, glw= numpy.polynomial.legendre.leggauss(ngl) glx12, glw12= numpy.polynomial.legendre.leggauss(ngl//2) #Evaluate everywhere if isinstance(self._aA,(actionAngle.actionAngleAdiabatic, actionAngle.actionAngleAdiabaticGrid)): vzgl= nsigma*sigmaz1/2.*(glx+1.) vzglw= glw vzfac= nsigma*sigmaz1 #2 x integration over [0,nsigma*sigmaz1] else: vzgl= nsigma*sigmaz1/2.*(glx12+1.) vzgl= list(vzgl) vzgl.extend(-nsigma*sigmaz1/2.*(glx12+1.)) vzgl= numpy.array(vzgl) vzglw= glw12 vzglw= list(vzglw) vzglw.extend(glw12) vzglw= numpy.array(vzglw) vzfac = 0.5*nsigma*sigmaz1 #integration over [-nsigma*sigmaz1,0] and [0,nsigma*sigmaz1] vTgl= vTmax/2.*(glx+1.) vTfac= 0.5 * vTmax #integration over [0.,vTmax] #Tile everything vTgl= numpy.tile(vTgl,(ngl,1)).T vzgl= numpy.tile(vzgl,(ngl,1)) vTglw= numpy.tile(glw,(ngl,1)).T #also tile weights vzglw= numpy.tile(vzglw,(ngl,1)) #evaluate logqeval= numpy.reshape(self(R+numpy.zeros(ngl*ngl), vR+numpy.zeros(ngl*ngl), vTgl.flatten(), z+numpy.zeros(ngl*ngl), vzgl.flatten(), log=True, use_physical=False), (ngl,ngl)) return numpy.sum(numpy.exp(logqeval)*vTglw*vzglw*vzfac)*vTfac @actionAngle_physical_input @physical_conversion('phasespacedensityvelocity2',pop=True) def pvT(self,vT,R,z,gl=True,ngl=_DEFAULTNGL2,nsigma=4.): """ NAME: pvT PURPOSE: calculate the marginalized vT probability at this location (NOT normalized by the density) INPUT: vT - rotational velocity (can be Quantity) R - radius (can be Quantity) z - height (can be Quantity) gl - use Gauss-Legendre integration (True, currently the only option) ngl - order of Gauss-Legendre integration nsigma - sets integration limits to [-1,+1]*nsigma*sigma(R) for integration over vz and vR (default: 4) OUTPUT: p(vT,R,z) HISTORY: 2012-12-22 - Written - Bovy (IAS) 2018-01-12 - Added Gauss-Legendre integration prefactor nsigma^2/4 - Trick (MPA) """ sigmaR1= self._sr*numpy.exp((self._refr-R)/self._hsr) sigmaz1= self._sz*numpy.exp((self._refr-R)/self._hsz) if gl: if ngl % 2 == 1: raise ValueError("ngl must be even") #Use Gauss-Legendre integration for all if ngl == _DEFAULTNGL: glx, glw= self._glxdef, self._glwdef glx12, glw12= self._glxdef12, self._glwdef12 elif ngl == _DEFAULTNGL2: glx, glw= self._glxdef2, self._glwdef2 glx12, glw12= self._glxdef, self._glwdef else: glx, glw= numpy.polynomial.legendre.leggauss(ngl) glx12, glw12= numpy.polynomial.legendre.leggauss(ngl//2) #Evaluate everywhere if isinstance(self._aA,(actionAngle.actionAngleAdiabatic, actionAngle.actionAngleAdiabaticGrid)): vRgl= nsigma*sigmaR1/2.*(glx+1.) vzgl= nsigma*sigmaz1/2.*(glx+1.) vRglw= glw vzglw= glw vRfac= nsigma*sigmaR1 #2 x integration over [0,nsigma*sigmaR1] vzfac= nsigma*sigmaz1 #2 x integration over [0,nsigma*sigmaz1] else: vRgl= nsigma*sigmaR1/2.*(glx12+1.) vRgl= list(vRgl) vRgl.extend(-nsigma*sigmaR1/2.*(glx12+1.)) vRgl= numpy.array(vRgl) vzgl= nsigma*sigmaz1/2.*(glx12+1.) vzgl= list(vzgl) vzgl.extend(-nsigma*sigmaz1/2.*(glx12+1.)) vzgl= numpy.array(vzgl) vRglw= glw12 vRglw= list(vRglw) vRglw.extend(glw12) vRglw= numpy.array(vRglw) vzglw= glw12 vzglw= list(vzglw) vzglw.extend(glw12) vzglw= numpy.array(vzglw) vRfac = 0.5*nsigma*sigmaR1 #integration over [-nsigma*sigmaR1,0] and [0,nsigma*sigmaR1] vzfac = 0.5*nsigma*sigmaz1 #integration over [-nsigma*sigmaz1,0] and [0,nsigma*sigmaz1] #Tile everything vRgl= numpy.tile(vRgl,(ngl,1)).T vzgl= numpy.tile(vzgl,(ngl,1)) vRglw= numpy.tile(vRglw,(ngl,1)).T #also tile weights vzglw= numpy.tile(vzglw,(ngl,1)) #evaluate logqeval= numpy.reshape(self(R+numpy.zeros(ngl*ngl), vRgl.flatten(), vT+numpy.zeros(ngl*ngl), z+numpy.zeros(ngl*ngl), vzgl.flatten(), log=True, use_physical=False), (ngl,ngl)) return numpy.sum(numpy.exp(logqeval)*vRglw*vzglw*vRfac*vzfac) @actionAngle_physical_input @physical_conversion('phasespacedensityvelocity2',pop=True) def pvz(self,vz,R,z,gl=True,ngl=_DEFAULTNGL2, nsigma=4.,vTmax=1.5, _return_actions=False,_jr=None,_lz=None,_jz=None, _return_freqs=False, _rg=None,_kappa=None,_nu=None,_Omega=None, _sigmaR1=None): """ NAME: pvz PURPOSE: calculate the marginalized vz probability at this location (NOT normalized by the density) INPUT: vz - vertical velocity (can be Quantity) R - radius (can be Quantity) z - height (can be Quantity) gl - use Gauss-Legendre integration (True, currently the only option) ngl - order of Gauss-Legendre integration nsigma - sets integration limits to [-1,+1]*nsigma*sigma_R(R) for integration over vR (default: 4) vTmax - sets integration limits to [0,vTmax] for integration over vT (default: 1.5) OUTPUT: p(vz,R,z) HISTORY: 2012-12-22 - Written - Bovy (IAS) """ if _sigmaR1 is None: sigmaR1= self._sr*numpy.exp((self._refr-R)/self._hsr) else: sigmaR1= _sigmaR1 if gl: if ngl % 2 == 1: raise ValueError("ngl must be even") #Use Gauss-Legendre integration for all if ngl == _DEFAULTNGL: glx, glw= self._glxdef, self._glwdef glx12, glw12= self._glxdef12, self._glwdef12 elif ngl == _DEFAULTNGL2: glx, glw= self._glxdef2, self._glwdef2 glx12, glw12= self._glxdef, self._glwdef else: glx, glw= numpy.polynomial.legendre.leggauss(ngl) glx12, glw12= numpy.polynomial.legendre.leggauss(ngl//2) #Evaluate everywhere if isinstance(self._aA,(actionAngle.actionAngleAdiabatic, actionAngle.actionAngleAdiabaticGrid)): vRgl= (glx+1.) vRglw= glw vRfac= nsigma*sigmaR1 #2 x integration over [0,nsigma*sigmaR1] else: vRgl= (glx12+1.) vRgl= list(vRgl) vRgl.extend(-(glx12+1.)) vRgl= numpy.array(vRgl) vRglw= glw12 vRglw= list(vRglw) vRglw.extend(glw12) vRglw= numpy.array(vRglw) vRfac = 0.5*nsigma*sigmaR1 #integration over [-nsigma*sigmaR1,0] and [0,nsigma*sigmaR1] vTgl= vTmax/2.*(glx+1.) vTfac= 0.5 * vTmax #integration over [0.,vTmax] #Tile everything vTgl= numpy.tile(vTgl,(ngl,1)).T vRgl= numpy.tile(vRgl,(ngl,1)) vTglw= numpy.tile(glw,(ngl,1)).T #also tile weights vRglw= numpy.tile(vRglw,(ngl,1)) #If inputs are arrays, tile if isinstance(R,numpy.ndarray): nR= len(R) R= numpy.tile(R,(ngl,ngl,1)).T.flatten() z= numpy.tile(z,(ngl,ngl,1)).T.flatten() vz= numpy.tile(vz,(ngl,ngl,1)).T.flatten() vTgl= numpy.tile(vTgl,(nR,1,1)).flatten() vRgl= numpy.tile(vRgl,(nR,1,1)).flatten() vTglw= numpy.tile(vTglw,(nR,1,1)) vRglw= numpy.tile(vRglw,(nR,1,1)) scalarOut= False else: R= R+numpy.zeros(ngl*ngl) z= z+numpy.zeros(ngl*ngl) vz= vz+numpy.zeros(ngl*ngl) nR= 1 scalarOut= True vRgl= vRgl.flatten() vRgl*= numpy.tile(nsigma*sigmaR1/2.,(ngl,ngl,1)).T.flatten() #evaluate if _jr is None and _rg is None: logqeval, jr, lz, jz, rg, kappa, nu, Omega= self(R, vRgl.flatten(), vTgl.flatten(), z, vz, log=True, _return_actions=True, _return_freqs=True, use_physical=False) logqeval= numpy.reshape(logqeval,(nR,ngl*ngl)) elif not _jr is None and not _rg is None: logqeval, jr, lz, jz, rg, kappa, nu, Omega= self((_jr,_lz,_jz), rg=_rg,kappa=_kappa,nu=_nu, Omega=_Omega, log=True, _return_actions=True, _return_freqs=True, use_physical=False) logqeval= numpy.reshape(logqeval,(nR,ngl*ngl)) elif not _jr is None and _rg is None: logqeval, jr, lz, jz, rg, kappa, nu, Omega= self((_jr,_lz,_jz), log=True, _return_actions=True, _return_freqs=True, use_physical=False) logqeval= numpy.reshape(logqeval,(nR,ngl*ngl)) elif _jr is None and not _rg is None: logqeval, jr, lz, jz, rg, kappa, nu, Omega= self(R, vRgl.flatten(), vTgl.flatten(), z, vz, rg=_rg,kappa=_kappa,nu=_nu, Omega=_Omega, log=True, _return_actions=True, _return_freqs=True, use_physical=False) logqeval= numpy.reshape(logqeval,(nR,ngl*ngl)) vRglw= numpy.reshape(vRglw,(nR,ngl*ngl)) vTglw= numpy.reshape(vTglw,(nR,ngl*ngl)) if scalarOut: result= numpy.sum(numpy.exp(logqeval)*vTglw*vRglw,axis=1)[0]*vRfac*vTfac else: result= numpy.sum(numpy.exp(logqeval)*vTglw*vRglw,axis=1)*vRfac*vTfac if _return_actions and _return_freqs: return (result, jr,lz,jz, rg, kappa, nu, Omega) elif _return_freqs: return (result, rg, kappa, nu, Omega) elif _return_actions: return (result, jr,lz,jz) else: return result @actionAngle_physical_input @physical_conversion('phasespacedensityvelocity',pop=True) def pvRvT(self,vR,vT,R,z,gl=True,ngl=_DEFAULTNGL2,nsigma=4.): """ NAME: pvRvT PURPOSE: calculate the marginalized (vR,vT) probability at this location (NOT normalized by the density) INPUT: vR - radial velocity (can be Quantity) vT - rotational velocity (can be Quantity) R - radius (can be Quantity) z - height (can be Quantity) gl - use Gauss-Legendre integration (True, currently the only option) ngl - order of Gauss-Legendre integration nsigma - sets integration limits to [-1,+1]*nsigma*sigma_z(R) for integration over vz (default: 4) OUTPUT: p(vR,vT,R,z) HISTORY: 2013-01-02 - Written - Bovy (IAS) 2018-01-12 - Added Gauss-Legendre integration prefactor nsigma/2 - Trick (MPA) """ sigmaz1= self._sz*numpy.exp((self._refr-R)/self._hsz) if gl: if ngl % 2 == 1: raise ValueError("ngl must be even") #Use Gauss-Legendre integration for all if ngl == _DEFAULTNGL: glx, glw= self._glxdef, self._glwdef glx12, glw12= self._glxdef12, self._glwdef12 elif ngl == _DEFAULTNGL2: glx, glw= self._glxdef2, self._glwdef2 glx12, glw12= self._glxdef, self._glwdef else: glx, glw= numpy.polynomial.legendre.leggauss(ngl) glx12, glw12= numpy.polynomial.legendre.leggauss(ngl//2) #Evaluate everywhere if isinstance(self._aA,(actionAngle.actionAngleAdiabatic, actionAngle.actionAngleAdiabaticGrid)): vzgl= nsigma*sigmaz1/2.*(glx+1.) vzglw= glw vzfac= nsigma*sigmaz1 #2 x integration over [0,nsigma*sigmaz1] else: vzgl= nsigma*sigmaz1/2.*(glx12+1.) vzgl= list(vzgl) vzgl.extend(-nsigma*sigmaz1/2.*(glx12+1.)) vzgl= numpy.array(vzgl) vzglw= glw12 vzglw= list(vzglw) vzglw.extend(glw12) vzglw= numpy.array(vzglw) vzfac = 0.5*nsigma*sigmaz1 #integration over [-nsigma*sigmaz1,0] and [0,nsigma*sigmaz1] #evaluate logqeval= self(R+numpy.zeros(ngl), vR+numpy.zeros(ngl), vT+numpy.zeros(ngl), z+numpy.zeros(ngl), vzgl, log=True,use_physical=False) return numpy.sum(numpy.exp(logqeval)*vzglw*vzfac) @actionAngle_physical_input @physical_conversion('phasespacedensityvelocity',pop=True) def pvTvz(self,vT,vz,R,z,gl=True,ngl=_DEFAULTNGL2,nsigma=4.): """ NAME: pvTvz PURPOSE: calculate the marginalized (vT,vz) probability at this location (NOT normalized by the density) INPUT: vT - rotational velocity (can be Quantity) vz - vertical velocity (can be Quantity) R - radius (can be Quantity) z - height (can be Quantity) gl - use Gauss-Legendre integration (True, currently the only option) ngl - order of Gauss-Legendre integration nsigma - sets integration limits to [-1,+1]*nsigma*sigma_R(R) for integration over vR (default: 4) OUTPUT: p(vT,vz,R,z) HISTORY: 2012-12-22 - Written - Bovy (IAS) 2018-01-12 - Added Gauss-Legendre integration prefactor nsigma/2 - Trick (MPA) """ sigmaR1= self._sr*numpy.exp((self._refr-R)/self._hsr) if gl: if ngl % 2 == 1: raise ValueError("ngl must be even") #Use Gauss-Legendre integration for all if ngl == _DEFAULTNGL: glx, glw= self._glxdef, self._glwdef glx12, glw12= self._glxdef12, self._glwdef12 elif ngl == _DEFAULTNGL2: glx, glw= self._glxdef2, self._glwdef2 glx12, glw12= self._glxdef, self._glwdef else: glx, glw= numpy.polynomial.legendre.leggauss(ngl) glx12, glw12= numpy.polynomial.legendre.leggauss(ngl//2) #Evaluate everywhere if isinstance(self._aA,(actionAngle.actionAngleAdiabatic, actionAngle.actionAngleAdiabaticGrid)): vRgl= nsigma*sigmaR1/2.*(glx+1.) vRglw= glw vRfac= nsigma*sigmaR1 #2 x integration over [0,nsigma*sigmaR1] else: vRgl= nsigma*sigmaR1/2.*(glx12+1.) vRgl= list(vRgl) vRgl.extend(-nsigma*sigmaR1/2.*(glx12+1.)) vRgl= numpy.array(vRgl) vRglw= glw12 vRglw= list(vRglw) vRglw.extend(glw12) vRglw= numpy.array(vRglw) vRfac = 0.5*nsigma*sigmaR1 #integration over [-nsigma*sigmaR1,0] and [0,nsigma*sigmaR1] #evaluate logqeval= self(R+numpy.zeros(ngl), vRgl, vT+numpy.zeros(ngl), z+numpy.zeros(ngl), vz+numpy.zeros(ngl), log=True,use_physical=False) return numpy.sum(numpy.exp(logqeval)*vRglw*vRfac) @actionAngle_physical_input @physical_conversion('phasespacedensityvelocity',pop=True) def pvRvz(self,vR,vz,R,z,gl=True,ngl=_DEFAULTNGL2,vTmax=1.5): """ NAME: pvR PURPOSE: calculate the marginalized (vR,vz) probability at this location (NOT normalized by the density) INPUT: vR - radial velocity (can be Quantity) vz - vertical velocity (can be Quantity) R - radius (can be Quantity) z - height (can be Quantity) gl - use Gauss-Legendre integration (True, currently the only option) ngl - order of Gauss-Legendre integration vTmax - sets integration limits to [0,vTmax] for integration over vT (default: 1.5) OUTPUT: p(vR,vz,R,z) HISTORY: 2013-01-02 - Written - Bovy (IAS) 2018-01-12 - Added Gauss-Legendre integration prefactor vTmax/2 - Trick (MPA) """ if gl: if ngl % 2 == 1: raise ValueError("ngl must be even") #Use Gauss-Legendre integration for all if ngl == _DEFAULTNGL: glx, glw= self._glxdef, self._glwdef glx12, glw12= self._glxdef12, self._glwdef12 elif ngl == _DEFAULTNGL2: glx, glw= self._glxdef2, self._glwdef2 glx12, glw12= self._glxdef, self._glwdef else: glx, glw= numpy.polynomial.legendre.leggauss(ngl) glx12, glw12= numpy.polynomial.legendre.leggauss(ngl//2) #Evaluate everywhere vTgl= vTmax/2.*(glx+1.) vTglw= glw vTfac= 0.5 * vTmax #integration over [0.,vTmax] #If inputs are arrays, tile if isinstance(R,numpy.ndarray): nR= len(R) R= numpy.tile(R,(ngl,1)).T.flatten() z= numpy.tile(z,(ngl,1)).T.flatten() vR= numpy.tile(vR,(ngl,1)).T.flatten() vz= numpy.tile(vz,(ngl,1)).T.flatten() vTgl= numpy.tile(vTgl,(nR,1)).flatten() vTglw= numpy.tile(vTglw,(nR,1)) scalarOut= False else: R= R+numpy.zeros(ngl) vR= vR+numpy.zeros(ngl) z= z+numpy.zeros(ngl) vz= vz+numpy.zeros(ngl) nR= 1 scalarOut= True #evaluate logqeval= numpy.reshape(self(R, vR, vTgl, z, vz, log=True, use_physical=False), (nR,ngl)) out= numpy.sum(numpy.exp(logqeval)*vTglw*vTfac,axis=1) if scalarOut: return out[0] else: return out def _calc_epifreq(self,r): """ NAME: _calc_epifreq PURPOSE: calculate the epicycle frequency at r INPUT: r - radius OUTPUT: kappa HISTORY: 2012-07-25 - Written - Bovy (IAS@MPIA) NOTE: takes about 0.1 ms for a Miyamoto-Nagai potential """ return potential.epifreq(self._pot,r) def _calc_verticalfreq(self,r): """ NAME: _calc_verticalfreq PURPOSE: calculate the vertical frequency at r INPUT: r - radius OUTPUT: nu HISTORY: 2012-07-25 - Written - Bovy (IAS@MPIA) NOTE: takes about 0.05 ms for a Miyamoto-Nagai potential """ return potential.verticalfreq(self._pot,r) def _rg(self,lz): """ NAME: _rg PURPOSE: calculate the radius of a circular orbit of Lz INPUT: lz - Angular momentum OUTPUT: radius HISTORY: 2012-07-25 - Written - Bovy (IAS@MPIA) NOTE: seems to take about ~0.5 ms for a Miyamoto-Nagai potential; ~0.75 ms for a MWPotential about the same with or without interpolation of the rotation curve Not sure what to do about negative lz... """ if isinstance(lz,numpy.ndarray): indx= (lz > self._precomputergLzmax)*(lz < self._precomputergLzmin) indxc= True^indx out= numpy.empty(lz.shape) out[indxc]= self._rgInterp(lz[indxc]) out[indx]= numpy.array([potential.rl(self._pot,lz[indx][ii]) for ii in range(numpy.sum(indx))]) return out else: if lz > self._precomputergLzmax or lz < self._precomputergLzmin: return potential.rl(self._pot,lz) return numpy.atleast_1d(self._rgInterp(lz)) def _vmomentsurfaceIntegrand(vz,vR,vT,R,z,df,sigmaR1,gamma,sigmaz1,n,m,o): #pragma: no cover because this is too slow; a warning is shown """Internal function that is the integrand for the vmomentsurface mass integration""" return vR**n*vT**m*vz**o*df(R,vR*sigmaR1,vT*sigmaR1*gamma,z,vz*sigmaz1, use_physical=False) def _vmomentsurfaceMCIntegrand(vz,vR,vT,R,z,df,sigmaR1,gamma,sigmaz1,mvT,n,m,o): """Internal function that is the integrand for the vmomentsurface mass integration""" return vR**n*vT**m*vz**o*df(R,vR*sigmaR1,vT*sigmaR1*gamma,z,vz*sigmaz1,use_physical=False)*numpy.exp(vR**2./2.+(vT-mvT)**2./2.+vz**2./2.) def _jmomentsurfaceIntegrand(vz,vR,vT,R,z,df,sigmaR1,gamma,sigmaz1,n,m,o): #pragma: no cover because this is too slow; a warning is shown """Internal function that is the integrand for the vmomentsurface mass integration""" return df(R,vR*sigmaR1,vT*sigmaR1*gamma,z,vz*sigmaz1,use_physical=False, func= (lambda x,y,z: x**n*y**m*z**o)) def _jmomentsurfaceMCIntegrand(vz,vR,vT,R,z,df,sigmaR1,gamma,sigmaz1,mvT,n,m,o): """Internal function that is the integrand for the vmomentsurface mass integration""" return df(R,vR*sigmaR1,vT*sigmaR1*gamma,z,vz*sigmaz1,use_physical=False, func=(lambda x,y,z: x**n*y**m*z**o))\ *numpy.exp(vR**2./2.+(vT-mvT)**2./2.+vz**2./2.) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/sphericaldf.py0000644000175100001710000005623214327773303016620 0ustar00runnerdocker# 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 # # 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,r,n=1): to sample the velocity angle at r # * _p_v_at_r(self,v,r): which returns p(v|r) # constantbetadf is an example of this # import warnings import numpy import scipy.interpolate from scipy import integrate, special from ..orbit import Orbit from ..potential import mass from ..potential.Potential import _evaluatePotentials from ..potential.SCFPotential import _RToxi, _xiToR from ..util import _optional_deps, conversion, galpyWarning from ..util.conversion import physical_conversion from .df import df # Use _APY_LOADED/_APY_UNITS like this to be able to change them in tests if _optional_deps._APY_LOADED: from astropy import units class sphericaldf(df): """Superclass for spherical distribution functions""" def __init__(self,pot=None,denspot=None,rmax=None, scale=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initializes a spherical DF 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) rmax= (None) maximum radius to consider (can be Quantity); DF is cut off at E = Phi(rmax) scale= (None) length-scale parameter to be used internally ro= ,vo= galpy unit parameters OUTPUT: None HISTORY: 2020-07-22 - Written - Lane (UofT) """ 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: # pragma: no cover raise OSError("pot= must be set") self._pot= pot self._denspot= self._pot if denspot is None else denspot if not conversion.physical_compatible(self._pot,self._denspot): raise RuntimeError("Unit-conversion parameters of input potential incompatible with those of the density potential") self._rmax= numpy.inf if rmax is None \ else conversion.parse_length(rmax,ro=self._ro) try: self._scale= pot._scale except AttributeError: try: self._scale= pot[0]._scale except (TypeError,AttributeError): self._scale= conversion.parse_length(scale,ro=self._ro) \ if scale is not None else 1. ############################## 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) OUTPUT: Value of DF HISTORY: 2020-07-22 - Written - Lane (UofT) """ # Get E,L,Lz if len(args) == 1: if not isinstance(args[0],Orbit): # Assume tuple (E,L,Lz) 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.)) Lz= args[0].Lz(use_physical=False) 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) 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) vtotSq= vR**2.+vT**2.+vz**2. E= numpy.atleast_1d(0.5*vtotSq+_evaluatePotentials(self._pot,R,z)) Lz= numpy.atleast_1d(R*vT) r= numpy.sqrt(R**2.+z**2.) vrad= (R*vR+z*vz)/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,**kwargs): """ NAME: vmomentdensity PURPOSE: calculate 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 HISTORY: 2020-09-04 - Written - Bovy (UofT) """ 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 _optional_deps._APY_UNITS: u= 1/units.kpc**3*(units.km/units.s)**(n+m) out= self._vmomentdensity(r,n,m) if _optional_deps._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): """ NAME: sigmar PURPOSE: calculate the radial velocity dispersion at radius r INPUT: r - spherical radius at which to calculate the radial velocity dispersion OUTPUT: sigma_r(r) HISTORY: 2020-09-04 - Written - Bovy (UofT) """ 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): """ NAME: sigmar PURPOSE: calculate the tangential velocity dispersion at radius r INPUT: r - spherical radius at which to calculate the tangential velocity dispersion OUTPUT: sigma_t(r) HISTORY: 2020-09-04 - Written - Bovy (UofT) """ 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): """ NAME: sigmar PURPOSE: calculate the anisotropy at radius r INPUT: r - spherical radius at which to calculate the anisotropy OUTPUT: beta(r) HISTORY: 2020-09-04 - Written - Bovy (UofT) """ r= conversion.parse_length(r,ro=self._ro) return 1.-self._vmomentdensity(r,0,2)/2./self._vmomentdensity(r,2,0) ############################### SAMPLING THE DF################################ def sample(self,R=None,z=None,phi=None,n=1,return_orbit=True,rmin=0.): """ NAME: sample PURPOSE: Return full 6D samples of the DF INPUT: R= cylindrical 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 rmin= (0.) only sample r > rmin (can be Quantity) OPTIONAL INPUT: return_orbit= (True) If True output is an 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; the (R,vR,vT,z,vz,phi) is either in internal units or is a set of Quantities 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 - Lane (UofT) """ if hasattr(self,'_rmin_sampling') and rmin != self._rmin_sampling: # Build new grids, easiest if hasattr(self,'_xi_cmf_interpolator'): delattr(self,'_xi_cmf_interpolator') if hasattr(self,'_v_vesc_pvr_interpolator'): delattr(self,'_v_vesc_pvr_interpolator') self._rmin_sampling= conversion.parse_length(rmin,ro=self._ro) if R is None or z is None: # Full 6D samples r= self._sample_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 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 """\ """an equal-length array""" n= len(R) 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) else: phi= conversion.parse_angle(phi) phi= phi*numpy.ones(n) \ if not hasattr(phi,'__len__') or len(phi) < n \ else phi eta,psi= self._sample_velocity_angles(r,n=n) v= self._sample_v(r,eta,n=n) vr= v*numpy.cos(eta) vtheta= v*numpy.sin(eta)*numpy.cos(psi) 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: o.turn_physical_on(ro=self._ro,vo=self._vo) return o else: if _optional_deps._APY_UNITS and self._voSet and self._roSet: R= units.Quantity(R)*self._ro*units.kpc vR= units.Quantity(vR)*self._vo*units.km/units.s vT= units.Quantity(vT)*self._vo*units.km/units.s z= units.Quantity(z)*self._ro*units.kpc vz= units.Quantity(vz)*self._vo*units.km/units.s phi= units.Quantity(phi)*units.rad return (R,vR,vT,z,vz,phi) def _sample_r(self,n=1): """Generate radial position samples from potential Note - the function interpolates the normalized CMF onto the variable xi defined as: .. 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.uniform(size=n) 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 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)""" ximin= _RToxi(self._rmin_sampling,a=self._scale) ximax= _RToxi(self._rmax,a=self._scale) xis= numpy.arange(ximin,ximax,1e-4) rs= _xiToR(xis,a=self._scale) # try/except necessary when mass doesn't take arrays, also need to # switch to a more general mass method at some point... #try: ms= mass(self._denspot,rs,use_physical=False) #except ValueError: # ms= numpy.array([mass(self._denspot,r,use_physical=False) for r in rs]) mnorm= mass(self._denspot,self._rmax,use_physical=False) if self._rmin_sampling > 0: ms-= mass(self._denspot,self._rmin_sampling) mnorm-= mass(self._denspot,self._rmin_sampling) ms/= mnorm # Add total mass point if numpy.isinf(self._rmax): xis= numpy.append(xis,1) ms= numpy.append(ms,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(1.-2*numpy.random.uniform(size=n)) return phi_samples,theta_samples def _sample_v(self,r,eta,n=1): """Generate velocity samples: typically the total velocity, but not for OM""" 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)*self._vmax_at_r(self._pot,r) def _sample_velocity_angles(self,r,n=1): """Generate samples of angles that set radial vs tangential velocities""" eta_samples= self._sample_eta(r,n) 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 numpy.sqrt(2.*(\ _evaluatePotentials(self._pot,self._rmax+1e-10,0) -_evaluatePotentials(self._pot,r,0.))) def _make_pvr_interpolator(self,r_a_start=-3,r_a_end=3, n_r_a=120,n_v_vesc=100): """ NAME: _make_pvr_interpolator 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 log10(r/a) r_a_end= radius grid end location in units of log10(r/a) n_r_a= number of radius grid points to use n_v_vesc= number of velocity grid points to use OUTPUT: None (But sets self._v_vesc_pvr_interpolator) HISTORY: Written 2020-07-24 - James Lane (UofT) """ # Make an array of r/a by v/vesc and then calculate p(v|r) r_a_start= numpy.amax([\ numpy.log10((self._rmin_sampling+1e-8)/self._scale), r_a_start]) r_a_end= numpy.amin([numpy.log10((self._rmax-1e-8)/self._scale), r_a_end]) r_a_values= 10.**numpy.linspace(r_a_start,r_a_end,n_r_a) v_vesc_values= numpy.linspace(0,1,n_v_vesc) r_a_grid, v_vesc_grid= numpy.meshgrid(r_a_values,v_vesc_values) 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 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 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))) for i in range(pvr_grid_cml_norm.shape[1]): cml_pvr= pvr_grid_cml_norm[:,i] if numpy.any(cml_pvr < 0): warnings.warn("The DF appears to have negative regions; we'll try to ignore these for sampling the DF, but this may adversely affect the generated samples. Proceed with care!",galpyWarning) cml_pvr[cml_pvr < 0]= 0. start_indx= numpy.amax(numpy.arange(len(cml_pvr))[cml_pvr == numpy.amin(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[start_indx:end_indx], v_vesc_values[start_indx:end_indx],k=1) 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 # Create the interpolator return scipy.interpolate.RectBivariateSpline( numpy.log10(r_a_grid[0,:]), icdf_pvr_grid_reg[:,0], icdf_v_vesc_grid_reg.T,kx=1,ky=1) class isotropicsphericaldf(sphericaldf): """Superclass for isotropic spherical distribution functions""" def __init__(self,pot=None,denspot=None,rmax=None, scale=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize an isotropic distribution function 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) rmax= (None) maximum radius to consider (can be Quantity); DF is cut off at E = Phi(rmax) scale= scale parameter to be used internally ro=, vo= galpy unit parameters OUTPUT: None HISTORY: 2020-09-02 - Written - Bovy (UofT) """ sphericaldf.__init__(self,pot=pot,denspot=denspot,rmax=rmax, 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. return 2.*numpy.pi\ *integrate.quad(lambda v: v**(2.+m+n)* self.fE(_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//2+0.5)\ /special.gamma(m//2+n//2+1.5) def _sample_eta(self,r,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): if hasattr(self,'_fE_interp'): return self._fE_interp(_evaluatePotentials(self._pot,r,0)\ +0.5*v**2.)*v**2. else: return self.fE(_evaluatePotentials(self._pot,r,0)\ +0.5*v**2.)*v**2. class anisotropicsphericaldf(sphericaldf): """Superclass for anisotropic spherical distribution functions""" def __init__(self,pot=None,denspot=None,rmax=None, scale=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize an anisotropic distribution function 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) rmax= (None) maximum radius to consider (can be Quantity); DF is cut off at E = Phi(rmax) scale= (None) length-scale parameter to be used internally ro= ,vo= galpy unit parameters OUTPUT: None HISTORY: 2020-07-22 - Written - Lane (UofT) """ sphericaldf.__init__(self,pot=pot,denspot=denspot,rmax=rmax, scale=scale,ro=ro,vo=vo) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/streamdf.py0000644000175100001710000044247514327773303016151 0ustar00runnerdocker#The DF of a tidal stream import copy import multiprocessing import warnings import numpy import scipy from packaging.version import parse as parse_version from scipy import integrate, interpolate, optimize, special _SCIPY_VERSION= parse_version(scipy.__version__) if _SCIPY_VERSION < parse_version('0.10'): #pragma: no cover from scipy.maxentropy import logsumexp elif _SCIPY_VERSION < parse_version('0.19'): #pragma: no cover from scipy.misc import logsumexp else: from scipy.special import logsumexp from ..actionAngle.actionAngleIsochroneApprox import dePeriod from ..orbit import Orbit from ..potential import flatten as flatten_potential from ..util import (ars, conversion, coords, fast_cholesky_invert, galpyWarning, multi, plot, stable_cho_factor) from ..util._optional_deps import _APY_LOADED, _APY_UNITS from ..util.conversion import physical_conversion from .df import df if _APY_LOADED: from astropy import units _INTERPDURINGSETUP= True _USEINTERP= True _USESIMPLE= True # cast a wide net _TWOPIWRAPS= numpy.arange(-4,5)*2.*numpy.pi _labelDict= {'x': r'$X$', 'y': r'$Y$', 'z': r'$Z$', 'r': r'$R$', 'phi': r'$\phi$', 'vx':r'$V_X$', 'vy':r'$V_Y$', 'vz':r'$V_Z$', 'vr':r'$V_R$', 'vt':r'$V_T$', 'll':r'$\mathrm{Galactic\ longitude\, (deg)}$', 'bb':r'$\mathrm{Galactic\ latitude\, (deg)}$', 'dist':r'$\mathrm{distance\, (kpc)}$', 'pmll':r'$\mu_l\,(\mathrm{mas\,yr}^{-1})$', 'pmbb':r'$\mu_b\,(\mathrm{mas\,yr}^{-1})$', 'vlos':r'$V_{\mathrm{los}}\,(\mathrm{km\,s}^{-1})$'} class streamdf(df): """The DF of a tidal stream""" def __init__(self,sigv,progenitor=None,pot=None,aA=None,useTM=False, tdisrupt=None,sigMeanOffset=6.,leading=True, sigangle=None, deltaAngleTrack=None,nTrackChunks=None,nTrackIterations=None, progIsTrack=False, ro=None,vo=None, Vnorm=None,Rnorm=None, R0=8.,Zsun=0.0208,vsun=[-11.1,8.*30.24,7.25], multi=None,interpTrack=_INTERPDURINGSETUP, useInterp=_USEINTERP,nosetup=False,nospreadsetup=False, approxConstTrackFreq=False,useTMHessian=False, custom_transform=None): """ NAME: __init__ PURPOSE: Initialize a quasi-isothermal DF INPUT: sigv - radial velocity dispersion of the progenitor (can be Quantity) tdisrupt= (5 Gyr) time since start of disruption (can be Quantity) leading= (True) if True, model the leading part of the stream if False, model the trailing part progenitor= progenitor orbit as Orbit instance (will be re-integrated, so don't bother integrating the orbit before) progIsTrack= (False) if True, then the progenitor (x,v) is actually the (x,v) of the stream track at zero angle separation; useful when initializing with an orbit fit; the progenitor's position will be calculated pot= Potential instance or list thereof aA= actionAngle instance used to convert (x,v) to actions useTM= (False) if set to an actionAngleTorus instance, use this to speed up calculations sigMeanOffset= (6.) offset between the mean of the frequencies and the progenitor, in units of the largest eigenvalue of the frequency covariance matrix (along the largest eigenvector), should be positive; to model the trailing part, set leading=False sigangle= (sigv/122/[1km/s]=1.8sigv in natural coordinates) estimate of the angle spread of the debris initially (can be Quantity) deltaAngleTrack= (None) angle to estimate the stream track over (rad; or can be Quantity) nTrackChunks= (floor(deltaAngleTrack/0.15)+1) number of chunks to divide the progenitor track in nTrackIterations= Number of iterations to perform when establishing the track; each iteration starts from a previous approximation to the track in (x,v) and calculates a new track based on the deviation between the previous track and the desired track in action-angle coordinates; if not set, an appropriate value is determined based on the magnitude of the misalignment between stream and orbit, with larger numbers of iterations for larger misalignments interpTrack= (might change), interpolate the stream track while setting up the instance (can be done by hand by calling self._interpolate_stream_track() and self._interpolate_stream_track_aA()) useInterp= (might change), use interpolation by default when calculating approximated frequencies and angles nosetup= (False) if True, don't setup the stream track and anything else that is expensive nospreadsetup= (False) if True, don't setup the spread around the stream track (only for nosetup is False) multi= (None) if set, use multi-processing Coordinate transformation inputs: vo= (220) circular velocity to normalize velocities with [used to be Vnorm; can be Quantity] ro= (8) Galactocentric radius to normalize positions with [used to be Rnorm; can be Quantity] R0= (8) Galactocentric radius of the Sun (kpc) [can be different from ro; can be Quantity] Zsun= (0.0208) Sun's height above the plane (kpc; can be Quantity) vsun= ([-11.1,241.92,7.25]) Sun's motion in cylindrical coordinates (vR positive away from center) (can be Quantity) custom_transform= (None) matrix implementing the rotation from (ra,dec) to a custom set of sky coordinates approxConstTrackFreq= (False) if True, approximate the stream assuming that the frequency is constant along the stream (only works with useTM, for which this leads to a significant speed-up) useTMHessian= (False) if True, compute the basic Hessian dO/dJ_prog using TM; otherwise use aA OUTPUT: object HISTORY: 2013-09-16 - Started - Bovy (IAS) 2013-11-25 - Started over - Bovy (IAS) """ if ro is None and not Rnorm is None: warnings.warn("WARNING: Rnorm keyword input to streamdf is deprecated in favor of the standard ro keyword", galpyWarning) ro= Rnorm if vo is None and not Vnorm is None: warnings.warn("WARNING: Vnorm keyword input to streamdf is deprecated in favor of the standard vo keyword", galpyWarning) vo= Vnorm df.__init__(self,ro=ro,vo=vo) sigv= conversion.parse_velocity(sigv,vo=self._vo) self._sigv= sigv if tdisrupt is None: self._tdisrupt= 5./conversion.time_in_Gyr(self._vo,self._ro) else: self._tdisrupt= conversion.parse_time(tdisrupt,ro=self._ro,vo=self._vo) self._sigMeanOffset= sigMeanOffset if pot is None: #pragma: no cover raise OSError("pot= must be set") self._pot= flatten_potential(pot) self._aA= aA if not self._aA._pot == self._pot: raise OSError("Potential in aA does not appear to be the same as given potential pot") self._check_consistent_units() if useTM: self._useTM= True self._aAT= useTM # confusing, no? self._approxConstTrackFreq= approxConstTrackFreq if not self._aAT._pot == self._pot: raise OSError("Potential in useTM=actionAngleTorus instance does not appear to be the same as given potential pot") else: self._useTM= False if (multi is True): #if set to boolean, enable cpu_count processes self._multi= multiprocessing.cpu_count() else: self._multi= multi self._progenitor_setup(progenitor,leading,useTMHessian) sigangle= conversion.parse_angle(sigangle) deltaAngleTrack= conversion.parse_angle(deltaAngleTrack) self._offset_setup(sigangle,leading,deltaAngleTrack) # if progIsTrack, calculate the progenitor that gives a track that is approximately the given orbit if progIsTrack: self._setup_progIsTrack() R0= conversion.parse_length_kpc(R0) Zsun= conversion.parse_length_kpc(Zsun) vsun= conversion.parse_velocity_kms(vsun) vsun[0]= conversion.parse_velocity_kms(vsun[0]) vsun[1]= conversion.parse_velocity_kms(vsun[1]) vsun[2]= conversion.parse_velocity_kms(vsun[2]) self._setup_coord_transform(R0,Zsun,vsun,progenitor,custom_transform) #Determine the stream track if not nosetup: self._determine_nTrackIterations(nTrackIterations) self._determine_stream_track(nTrackChunks) self._useInterp= useInterp if interpTrack or self._useInterp: self._interpolate_stream_track() self._interpolate_stream_track_aA() self.calc_stream_lb() if not nospreadsetup: self._determine_stream_spread() return None def _progenitor_setup(self,progenitor,leading,useTMHessian): """The part of the setup relating to the progenitor's orbit""" #Progenitor orbit: Calculate actions, frequencies, and angles for the progenitor self._progenitor= progenitor() #call to get new Orbit # Make sure we do not use physical coordinates self._progenitor.turn_physical_off() acfs= self._aA.actionsFreqsAngles(self._progenitor, _firstFlip=(not leading), use_physical=False) self._progenitor_jr= acfs[0][0] self._progenitor_lz= acfs[1][0] self._progenitor_jz= acfs[2][0] self._progenitor_Omegar= acfs[3] self._progenitor_Omegaphi= acfs[4] self._progenitor_Omegaz= acfs[5] self._progenitor_Omega= numpy.array([acfs[3],acfs[4],acfs[5]]).reshape(3) self._progenitor_angler= acfs[6] self._progenitor_anglephi= acfs[7] self._progenitor_anglez= acfs[8] self._progenitor_angle= numpy.array([acfs[6],acfs[7],acfs[8]]).reshape(3) #Calculate dO/dJ Jacobian at the progenitor if useTMHessian: h, fr,fp,fz,e= self._aAT.hessianFreqs(self._progenitor_jr, self._progenitor_lz, self._progenitor_jz) self._dOdJp= h # Replace frequencies with TM frequencies self._progenitor_Omegar= fr self._progenitor_Omegaphi= fp self._progenitor_Omegaz= fz self._progenitor_Omega= numpy.array([self._progenitor_Omegar, self._progenitor_Omegaphi, self._progenitor_Omegaz]).reshape(3) else: self._dOdJp= calcaAJac(self._progenitor.vxvv[0], self._aA,dxv=None,dOdJ=True, _initacfs=acfs) self._dOdJpInv= numpy.linalg.inv(self._dOdJp) self._dOdJpEig= numpy.linalg.eig(self._dOdJp) return None def _offset_setup(self,sigangle,leading,deltaAngleTrack): """The part of the setup related to calculating the stream/progenitor offset""" #From the progenitor orbit, determine the sigmas in J and angle self._sigjr= (self._progenitor.rap()-self._progenitor.rperi())/numpy.pi*self._sigv self._siglz= self._progenitor.rperi()*self._sigv self._sigjz= 2.*self._progenitor.zmax()/numpy.pi*self._sigv #Estimate the frequency covariance matrix from a diagonal J matrix x dOdJ self._sigjmatrix= numpy.diag([self._sigjr**2., self._siglz**2., self._sigjz**2.]) self._sigomatrix= numpy.dot(self._dOdJp, numpy.dot(self._sigjmatrix,self._dOdJp.T)) #Estimate angle spread as the ratio of the largest to the middle eigenvalue self._sigomatrixEig= numpy.linalg.eig(self._sigomatrix) self._sigomatrixEigsortIndx= numpy.argsort(self._sigomatrixEig[0]) self._sortedSigOEig= sorted(self._sigomatrixEig[0]) if sigangle is None: self._sigangle= self._sigv*1.8 else: self._sigangle= sigangle self._sigangle2= self._sigangle**2. self._lnsigangle= numpy.log(self._sigangle) #Estimate the frequency mean as lying along the direction of the largest eigenvalue self._dsigomeanProgDirection= self._sigomatrixEig[1][:,numpy.argmax(self._sigomatrixEig[0])] self._progenitor_Omega_along_dOmega= \ numpy.dot(self._progenitor_Omega,self._dsigomeanProgDirection) #Make sure we are modeling the correct part of the stream self._leading= leading self._sigMeanSign= 1. if self._leading and self._progenitor_Omega_along_dOmega < 0.: self._sigMeanSign= -1. elif not self._leading and self._progenitor_Omega_along_dOmega > 0.: self._sigMeanSign= -1. self._progenitor_Omega_along_dOmega*= self._sigMeanSign self._sigomean= self._progenitor_Omega\ +self._sigMeanOffset*self._sigMeanSign\ *numpy.sqrt(numpy.amax(self._sigomatrixEig[0]))\ *self._dsigomeanProgDirection #numpy.dot(self._dOdJp, # numpy.array([self._sigjr,self._siglz,self._sigjz])) self._dsigomeanProg= self._sigomean-self._progenitor_Omega self._meandO= self._sigMeanOffset\ *numpy.sqrt(numpy.amax(self._sigomatrixEig[0])) #Store cholesky of sigomatrix for fast evaluation self._sigomatrixNorm=\ numpy.sqrt(numpy.sum(self._sigomatrix**2.)) self._sigomatrixinv, self._sigomatrixLogdet= \ fast_cholesky_invert(self._sigomatrix/self._sigomatrixNorm, tiny=10.**-15.,logdet=True) self._sigomatrixinv/= self._sigomatrixNorm deltaAngleTrackLim = (self._sigMeanOffset+4.) * numpy.sqrt( self._sortedSigOEig[2]) * self._tdisrupt if (deltaAngleTrack is None): deltaAngleTrack = deltaAngleTrackLim else: if (deltaAngleTrack > deltaAngleTrackLim): warnings.warn("WARNING: angle range large compared to plausible value.", galpyWarning) self._deltaAngleTrack= deltaAngleTrack return None def _setup_coord_transform(self,R0,Zsun,vsun,progenitor,custom_transform): #Set the coordinate-transformation parameters; check that these do not conflict with those in the progenitor orbit object; need to use the original, since this objects _progenitor has physical turned off if progenitor._roSet \ and (numpy.fabs(self._ro-progenitor._ro) > 10.**-.8 \ or numpy.fabs(R0-progenitor._ro) > 10.**-8.): warnings.warn("Warning: progenitor's ro does not agree with streamdf's ro and R0; this may have unexpected consequences when projecting into observables", galpyWarning) if progenitor._voSet \ and numpy.fabs(self._vo-progenitor._vo) > 10.**-8.: warnings.warn("Warning: progenitor's vo does not agree with streamdf's vo; this may have unexpected consequences when projecting into observables", galpyWarning) if (progenitor._roSet or progenitor._voSet) \ and numpy.fabs(Zsun-progenitor._zo) > 10.**-8.: warnings.warn("Warning: progenitor's zo does not agree with streamdf's Zsun; this may have unexpected consequences when projecting into observables", galpyWarning) if (progenitor._roSet or progenitor._voSet) \ and numpy.any(numpy.fabs(vsun-numpy.array([0.,self._vo,0.])\ -progenitor._solarmotion) > 10.**-8.): warnings.warn("Warning: progenitor's solarmotion does not agree with streamdf's vsun (after accounting for vo); this may have unexpected consequences when projecting into observables", galpyWarning) self._R0= R0 self._Zsun= Zsun self._vsun= vsun self._custom_transform= custom_transform return None def _setup_progIsTrack(self): """If progIsTrack, the progenitor orbit that was passed to the streamdf initialization is the track at zero angle separation; this routine computes an actual progenitor position that gives the desired track given the parameters of the streamdf""" # We need to flip the sign of the offset, to go to the progenitor self._sigMeanSign*= -1. # Use _determine_stream_track_single to calculate the track-progenitor # offset at zero angle separation prog_stream_offset=\ _determine_stream_track_single(self._aA, self._progenitor, 0., #time = 0 self._progenitor_angle, self._sigMeanSign, self._dsigomeanProgDirection, lambda x: self.meanOmega(x,use_physical=False), 0.) #angle = 0 # Setup the new progenitor orbit progenitor= Orbit(prog_stream_offset[3]) # Flip the offset sign again self._sigMeanSign*= -1. # Now re-do the previous setup self._progenitor_setup(progenitor,self._leading,False) self._offset_setup(self._sigangle,self._leading, self._deltaAngleTrack) return None @physical_conversion('angle',pop=True) def misalignment(self,isotropic=False,**kwargs): """ NAME: misalignment PURPOSE: calculate the misalignment between the progenitor's frequency and the direction along which the stream disrupts INPUT: isotropic= (False), if True, return the misalignment assuming an isotropic action distribution OUTPUT: misalignment in rad HISTORY: 2013-12-05 - Written - Bovy (IAS) 2017-10-28 - Changed output unit to rad - Bovy (UofT) """ warnings.warn("In versions >1.3, the output unit of streamdf.misalignment has been changed to radian (from degree before)",galpyWarning) if isotropic: dODir= self._dOdJpEig[1][:,numpy.argmax(numpy.fabs(self._dOdJpEig[0]))] else: dODir= self._dsigomeanProgDirection out= numpy.arccos(numpy.sum(self._progenitor_Omega*dODir)/numpy.sqrt(numpy.sum(self._progenitor_Omega**2.))) if out > numpy.pi/2.: return out-numpy.pi else: return out def freqEigvalRatio(self,isotropic=False): """ NAME: freqEigvalRatio PURPOSE: calculate the ratio between the largest and 2nd-to-largest (in abs) eigenvalue of sqrt(dO/dJ^T V_J dO/dJ) (if this is big, a 1D stream will form) INPUT: isotropic= (False), if True, return the ratio assuming an isotropic action distribution (i.e., just of dO/dJ) OUTPUT: ratio between eigenvalues of fabs(dO / dJ) HISTORY: 2013-12-05 - Written - Bovy (IAS) """ if isotropic: sortedEig= sorted(numpy.fabs(self._dOdJpEig[0])) return sortedEig[2]/sortedEig[1] else: return numpy.sqrt(self._sortedSigOEig)[2]\ /numpy.sqrt(self._sortedSigOEig)[1] @physical_conversion('time',pop=True) def estimateTdisrupt(self,deltaAngle): """ NAME: estimateTdisrupt PURPOSE: estimate the time of disruption INPUT: deltaAngle- spread in angle since disruption OUTPUT: time in natural units HISTORY: 2013-11-27 - Written - Bovy (IAS) """ return deltaAngle\ /numpy.sqrt(numpy.sum(self._dsigomeanProg**2.)) def subhalo_encounters(self,venc=numpy.inf,sigma=150./220., nsubhalo=0.3,bmax=0.025,yoon=False): """ NAME: subhalo_encounters PURPOSE: estimate the number of encounters with subhalos over the lifetime of this stream, using a formalism similar to that of Yoon et al. (2011) INPUT: venc= (numpy.inf) count encounters with (relative) speeds less than this (relative radial velocity in cylindrical stream frame, unless yoon is True) (can be Quantity) sigma= (150/220) velocity dispersion of the DM subhalo population (can be Quantity) nsubhalo= (0.3) spatial number density of subhalos (can be Quantity) bmax= (0.025) maximum impact parameter (if larger than width of stream) (can be Quantity) yoon= (False) if True, use erroneous Yoon et al. formula OUTPUT: number of encounters HISTORY: 2016-01-19 - Written - Bovy (UofT) """ venc= conversion.parse_velocity(venc,vo=self._vo) sigma= conversion.parse_velocity(sigma,vo=self._vo) nsubhalo= conversion.parse_numdens(nsubhalo,ro=self._ro) bmax= conversion.parse_length(bmax,ro=self._ro) Ravg= numpy.mean(numpy.sqrt(self._progenitor.orbit[0,:,0]**2. +self._progenitor.orbit[0,:,3]**2.)) if numpy.isinf(venc): vencFac= 1. elif yoon: vencFac= (1.-(1.+venc**2./4./sigma**2.)\ *numpy.exp(-venc**2./4./sigma**2.)) else: vencFac= (1.-numpy.exp(-venc**2./2./sigma**2.)) if yoon: yoonFac= 2*numpy.sqrt(2.) else: yoonFac= 1. # Figure out width of stream w= self.sigangledAngle(self._meandO*self._tdisrupt,simple=True, use_physical=False) if bmax < w*Ravg/2.: bmax= w*Ravg/2. return yoonFac/numpy.sqrt(2.)*numpy.sqrt(numpy.pi)*Ravg*sigma\ *self._tdisrupt**2.*self._meandO\ *bmax*nsubhalo*vencFac ############################STREAM TRACK FUNCTIONS############################# def plotTrack(self,d1='x',d2='z',interp=True,spread=0,simple=_USESIMPLE, *args,**kwargs): """ NAME: plotTrack PURPOSE: plot the stream track INPUT: d1= plot this on the X axis ('x','y','z','R','phi','vx','vy','vz','vR','vt','ll','bb','dist','pmll','pmbb','vlos') d2= plot this on the Y axis (same list as for d1) interp= (True) if True, use the interpolated stream track spread= (0) if int > 0, also plot the spread around the track as spread x sigma scaleToPhysical= (False), if True, plot positions in kpc and velocities in km/s simple= (False), if True, use a simple estimate for the spread in perpendicular angle galpy.util.plot.plotplot args and kwargs OUTPUT: plot to output device HISTORY: 2013-12-09 - Written - Bovy (IAS) """ if not hasattr(self,'_ObsTrackLB') and \ (d1.lower() == 'll' or d1.lower() == 'bb' or d1.lower() == 'dist' or d1.lower() == 'pmll' or d1.lower() == 'pmbb' or d1.lower() == 'vlos' or d2.lower() == 'll' or d2.lower() == 'bb' or d2.lower() == 'dist' or d2.lower() == 'pmll' or d2.lower() == 'pmbb' or d2.lower() == 'vlos'): self.calc_stream_lb() phys= kwargs.pop('scaleToPhysical',False) tx= self._parse_track_dim(d1,interp=interp,phys=phys) ty= self._parse_track_dim(d2,interp=interp,phys=phys) plot.plot(tx,ty,*args, xlabel=_labelDict[d1.lower()], ylabel=_labelDict[d2.lower()], **kwargs) if spread: addx, addy= self._parse_track_spread(d1,d2,interp=interp,phys=phys, simple=simple) if ('ls' in kwargs and kwargs['ls'] == 'none') \ or ('linestyle' in kwargs \ and kwargs['linestyle'] == 'none'): kwargs.pop('ls',None) kwargs.pop('linestyle',None) spreadls= 'none' else: spreadls= '-.' spreadmarker= kwargs.pop('marker',None) spreadcolor= kwargs.pop('color',None) spreadlw= kwargs.pop('lw',1.) plot.plot(tx+spread*addx,ty+spread*addy,ls=spreadls, marker=spreadmarker,color=spreadcolor, lw=spreadlw, overplot=True) plot.plot(tx-spread*addx,ty-spread*addy,ls=spreadls, marker=spreadmarker,color=spreadcolor, lw=spreadlw, overplot=True) return None def plotProgenitor(self,d1='x',d2='z',*args,**kwargs): """ NAME: plotProgenitor PURPOSE: plot the progenitor orbit INPUT: d1= plot this on the X axis ('x','y','z','R','phi','vx','vy','vz','vR','vt','ll','bb','dist','pmll','pmbb','vlos') d2= plot this on the Y axis (same list as for d1) scaleToPhysical= (False), if True, plot positions in kpc and velocities in km/s galpy.util.plot.plot args and kwargs OUTPUT: plot to output device HISTORY: 2013-12-09 - Written - Bovy (IAS) """ tts= self._progenitor.t[self._progenitor.t \ < self._trackts[self._nTrackChunks-1]] obs= [self._R0,0.,self._Zsun] obs.extend(self._vsun) phys= kwargs.pop('scaleToPhysical',False) tx= self._parse_progenitor_dim(d1,tts,ro=self._ro,vo=self._vo, obs=obs,phys=phys) ty= self._parse_progenitor_dim(d2,tts,ro=self._ro,vo=self._vo, obs=obs,phys=phys) plot.plot(tx,ty,*args, xlabel=_labelDict[d1.lower()], ylabel=_labelDict[d2.lower()], **kwargs) return None def _parse_track_dim(self,d1,interp=True,phys=False): """Parse the dimension to plot the stream track for""" if interp: interpStr= 'interpolated' else: interpStr= '' if d1.lower() == 'x': tx= self.__dict__['_%sObsTrackXY' % interpStr][:,0] elif d1.lower() == 'y': tx= self.__dict__['_%sObsTrackXY' % interpStr][:,1] elif d1.lower() == 'z': tx= self.__dict__['_%sObsTrackXY' % interpStr][:,2] elif d1.lower() == 'r': tx= self.__dict__['_%sObsTrack' % interpStr][:,0] elif d1.lower() == 'phi': tx= self.__dict__['_%sObsTrack' % interpStr][:,5] elif d1.lower() == 'vx': tx= self.__dict__['_%sObsTrackXY' % interpStr][:,3] elif d1.lower() == 'vy': tx= self.__dict__['_%sObsTrackXY' % interpStr][:,4] elif d1.lower() == 'vz': tx= self.__dict__['_%sObsTrackXY' % interpStr][:,5] elif d1.lower() == 'vr': tx= self.__dict__['_%sObsTrack' % interpStr][:,1] elif d1.lower() == 'vt': tx= self.__dict__['_%sObsTrack' % interpStr][:,2] elif d1.lower() == 'll': tx= self.__dict__['_%sObsTrackLB' % interpStr][:,0] elif d1.lower() == 'bb': tx= self.__dict__['_%sObsTrackLB' % interpStr][:,1] elif d1.lower() == 'dist': tx= self.__dict__['_%sObsTrackLB' % interpStr][:,2] elif d1.lower() == 'pmll': tx= self.__dict__['_%sObsTrackLB' % interpStr][:,4] elif d1.lower() == 'pmbb': tx= self.__dict__['_%sObsTrackLB' % interpStr][:,5] elif d1.lower() == 'vlos': tx= self.__dict__['_%sObsTrackLB' % interpStr][:,3] if phys and (d1.lower() == 'x' or d1.lower() == 'y' \ or d1.lower() == 'z' or d1.lower() == 'r'): tx= copy.copy(tx) tx*= self._ro if phys and (d1.lower() == 'vx' or d1.lower() == 'vy' \ or d1.lower() == 'vz' or d1.lower() == 'vr' \ or d1.lower() == 'vt'): tx= copy.copy(tx) tx*= self._vo return tx def _parse_progenitor_dim(self,d1,ts,ro=None,vo=None,obs=None, phys=False): """Parse the dimension to plot the progenitor orbit for""" if d1.lower() == 'x': tx= self._progenitor.x(ts,ro=ro,vo=vo,obs=obs,use_physical=False) elif d1.lower() == 'y': tx= self._progenitor.y(ts,ro=ro,vo=vo,obs=obs,use_physical=False) elif d1.lower() == 'z': tx= self._progenitor.z(ts,ro=ro,vo=vo,obs=obs,use_physical=False) elif d1.lower() == 'r': tx= self._progenitor.R(ts,ro=ro,vo=vo,obs=obs,use_physical=False) elif d1.lower() == 'phi': tx= self._progenitor.phi(ts,ro=ro,vo=vo,obs=obs) elif d1.lower() == 'vx': tx= self._progenitor.vx(ts,ro=ro,vo=vo,obs=obs,use_physical=False) elif d1.lower() == 'vy': tx= self._progenitor.vy(ts,ro=ro,vo=vo,obs=obs,use_physical=False) elif d1.lower() == 'vz': tx= self._progenitor.vz(ts,ro=ro,vo=vo,obs=obs,use_physical=False) elif d1.lower() == 'vr': tx= self._progenitor.vR(ts,ro=ro,vo=vo,obs=obs,use_physical=False) elif d1.lower() == 'vt': tx= self._progenitor.vT(ts,ro=ro,vo=vo,obs=obs,use_physical=False) elif d1.lower() == 'll': tx= self._progenitor.ll(ts,ro=ro,vo=vo,obs=obs) elif d1.lower() == 'bb': tx= self._progenitor.bb(ts,ro=ro,vo=vo,obs=obs) elif d1.lower() == 'dist': tx= self._progenitor.dist(ts,ro=ro,vo=vo,obs=obs) elif d1.lower() == 'pmll': tx= self._progenitor.pmll(ts,ro=ro,vo=vo,obs=obs) elif d1.lower() == 'pmbb': tx= self._progenitor.pmbb(ts,ro=ro,vo=vo,obs=obs) elif d1.lower() == 'vlos': tx= self._progenitor.vlos(ts,ro=ro,vo=vo,obs=obs) if phys and (d1.lower() == 'x' or d1.lower() == 'y' \ or d1.lower() == 'z' or d1.lower() == 'r'): tx= copy.copy(tx) tx*= self._ro if phys and (d1.lower() == 'vx' or d1.lower() == 'vy' \ or d1.lower() == 'vz' or d1.lower() == 'vr' \ or d1.lower() == 'vt'): tx= copy.copy(tx) tx*= self._vo return tx def _parse_track_spread(self,d1,d2,interp=True,phys=False, simple=_USESIMPLE): """Determine the spread around the track""" if not hasattr(self,'_allErrCovs'): self._determine_stream_spread(simple=simple) okaySpreadR= ['r','vr','vt','z','vz','phi'] okaySpreadXY= ['x','y','z','vx','vy','vz'] okaySpreadLB= ['ll','bb','dist','vlos','pmll','pmbb'] #Determine which coordinate system we're in coord= [False,False,False] #R, XY, LB if d1.lower() in okaySpreadR and d2.lower() in okaySpreadR: coord[0]= True elif d1.lower() in okaySpreadXY and d2.lower() in okaySpreadXY: coord[1]= True elif d1.lower() in okaySpreadLB and d2.lower() in okaySpreadLB: coord[2]= True else: raise NotImplementedError("plotting the spread for coordinates from different systems not implemented yet ...") #Get the right 2D Jacobian indxDict= {} indxDict['r']= 0 indxDict['vr']= 1 indxDict['vt']= 2 indxDict['z']= 3 indxDict['vz']= 4 indxDict['phi']= 5 indxDictXY= {} indxDictXY['x']= 0 indxDictXY['y']= 1 indxDictXY['z']= 2 indxDictXY['vx']= 3 indxDictXY['vy']= 4 indxDictXY['vz']= 5 indxDictLB= {} indxDictLB['ll']= 0 indxDictLB['bb']= 1 indxDictLB['dist']= 2 indxDictLB['vlos']= 3 indxDictLB['pmll']= 4 indxDictLB['pmbb']= 5 if coord[0]: relevantCov= self._allErrCovs relevantDict= indxDict if phys:#apply scale factors tcov= copy.copy(relevantCov) scaleFac= numpy.array([self._ro,self._vo,self._vo, self._ro,self._vo,1.]) tcov*= numpy.tile(scaleFac,(6,1)) tcov*= numpy.tile(scaleFac,(6,1)).T relevantCov= tcov elif coord[1]: relevantCov= self._allErrCovsXY relevantDict= indxDictXY if phys:#apply scale factors tcov= copy.copy(relevantCov) scaleFac= numpy.array([self._ro,self._ro,self._ro, self._vo,self._vo,self._vo]) tcov*= numpy.tile(scaleFac,(6,1)) tcov*= numpy.tile(scaleFac,(6,1)).T relevantCov= tcov elif coord[2]: relevantCov= self._allErrCovsLBUnscaled relevantDict= indxDictLB indx0= numpy.array([[relevantDict[d1.lower()],relevantDict[d1.lower()]], [relevantDict[d2.lower()],relevantDict[d2.lower()]]]) indx1= numpy.array([[relevantDict[d1.lower()],relevantDict[d2.lower()]], [relevantDict[d1.lower()],relevantDict[d2.lower()]]]) cov= relevantCov[:,indx0,indx1] #cov contains all nTrackChunks covs if not interp: out= numpy.empty((self._nTrackChunks,2)) eigDir= numpy.array([1.,0.]) for ii in range(self._nTrackChunks): covEig= numpy.linalg.eig(cov[ii]) minIndx= numpy.argmin(covEig[0]) minEigvec= covEig[1][:,minIndx] #this is the direction of the transverse spread if numpy.sum(minEigvec*eigDir) < 0.: minEigvec*= -1. #Keep them pointing in the same direction out[ii]= minEigvec*numpy.sqrt(covEig[0][minIndx]) eigDir= minEigvec else: #We slerp the minor eigenvector and interpolate the eigenvalue #First store all of the eigenvectors on the track allEigval= numpy.empty(self._nTrackChunks) allEigvec= numpy.empty((self._nTrackChunks,2)) eigDir= numpy.array([1.,0.]) for ii in range(self._nTrackChunks): covEig= numpy.linalg.eig(cov[ii]) minIndx= numpy.argmin(covEig[0]) minEigvec= covEig[1][:,minIndx] #this is the direction of the transverse spread if numpy.sum(minEigvec*eigDir) < 0.: minEigvec*= -1. #Keep them pointing in the same direction allEigval[ii]= numpy.sqrt(covEig[0][minIndx]) allEigvec[ii]= minEigvec eigDir= minEigvec #Now interpolate where needed interpEigval=\ interpolate.InterpolatedUnivariateSpline(self._thetasTrack, allEigval,k=3) interpolatedEigval= interpEigval(self._interpolatedThetasTrack) #Interpolate in chunks interpolatedEigvec= numpy.empty((len(self._interpolatedThetasTrack), 2)) for ii in range(self._nTrackChunks-1): slerpOmega= numpy.arccos(numpy.sum(allEigvec[ii]*allEigvec[ii+1])) slerpts= (self._interpolatedThetasTrack-self._thetasTrack[ii])/\ (self._thetasTrack[ii+1]-self._thetasTrack[ii]) slerpIndx= (slerpts >= 0.)*(slerpts <= 1.) for jj in range(2): interpolatedEigvec[slerpIndx,jj]=\ (numpy.sin((1-slerpts[slerpIndx])*slerpOmega)*allEigvec[ii,jj] +numpy.sin(slerpts[slerpIndx]*slerpOmega)*allEigvec[ii+1,jj])/numpy.sin(slerpOmega) out= numpy.tile(interpolatedEigval.T,(2,1)).T*interpolatedEigvec if coord[2]: #if LB, undo rescalings that were applied before out[:,0]*= self._ErrCovsLBScale[relevantDict[d1.lower()]] out[:,1]*= self._ErrCovsLBScale[relevantDict[d2.lower()]] return (out[:,0],out[:,1]) def plotCompareTrackAAModel(self,**kwargs): """ NAME: plotCompareTrackAAModel PURPOSE: plot the comparison between the underlying model's dOmega_perp vs. dangle_r (line) and the track in (x,v)'s dOmega_perp vs. dangle_r (dots; explicitly calculating the track's action-angle coordinates) INPUT: galpy.util.plot.plot kwargs OUTPUT: plot HISTORY: 2014-08-27 - Written - Bovy (IAS) """ #First calculate the model model_adiff= (self._ObsTrackAA[:,3:]-self._progenitor_angle)[:,0]\ *self._sigMeanSign model_operp= numpy.dot(self._ObsTrackAA[:,:3]-self._progenitor_Omega, self._dsigomeanProgDirection)\ *self._sigMeanSign #Then calculate the track's frequency-angle coordinates if self._multi is None: aatrack= numpy.empty((self._nTrackChunks,6)) for ii in range(self._nTrackChunks): aatrack[ii]= self._aA.actionsFreqsAngles(Orbit(self._ObsTrack[ii,:]), use_physical=False)[3:] else: aatrack= numpy.reshape(\ multi.parallel_map( (lambda x: self._aA.actionsFreqsAngles(Orbit(self._ObsTrack[x,:]),use_physical=False)[3:]), range(self._nTrackChunks), numcores=numpy.amin([self._nTrackChunks, multiprocessing.cpu_count(), self._multi])),(self._nTrackChunks,6)) track_adiff= (aatrack[:,3:]-self._progenitor_angle)[:,0]\ *self._sigMeanSign track_operp= numpy.dot(aatrack[:,:3]-self._progenitor_Omega, self._dsigomeanProgDirection)\ *self._sigMeanSign overplot= kwargs.pop('overplot',False) yrange= kwargs.pop('yrange', [0.,numpy.amax(numpy.hstack((model_operp,track_operp)))*1.1]) xlabel= kwargs.pop('xlabel',r'$\Delta \theta_R$') ylabel= kwargs.pop('ylabel',r'$\Delta \Omega_\parallel$') plot.plot(model_adiff,model_operp,'k-',overplot=overplot, xlabel=xlabel,ylabel=ylabel,yrange=yrange,**kwargs) plot.plot(track_adiff,track_operp,'ko',overplot=True, **kwargs) return None def _determine_nTrackIterations(self,nTrackIterations): """Determine a good value for nTrackIterations based on the misalignment between stream and orbit; just based on some rough experience for now""" if not nTrackIterations is None: self.nTrackIterations= nTrackIterations return None if numpy.fabs(self.misalignment(quantity=False)) < 1./180.*numpy.pi: self.nTrackIterations= 0 elif numpy.fabs(self.misalignment(quantity=False)) >= 1./180.*numpy.pi \ and numpy.fabs(self.misalignment(quantity=False)) < 3./180.*numpy.pi: self.nTrackIterations= 1 elif numpy.fabs(self.misalignment(quantity=False)) >= 3./180.*numpy.pi: self.nTrackIterations= 2 return None def _determine_stream_track(self,nTrackChunks): """Determine the track of the stream in real space""" #Determine how much orbital time is necessary for the progenitor's orbit to cover the stream if nTrackChunks is None: #default is floor(self._deltaAngleTrack/0.15)+1 self._nTrackChunks= int(numpy.floor(self._deltaAngleTrack/0.15))+1 else: self._nTrackChunks= nTrackChunks if self._nTrackChunks < 4: self._nTrackChunks= 4 if not hasattr(self,'nInterpolatedTrackChunks'): self.nInterpolatedTrackChunks= 1001 dt= self._deltaAngleTrack\ /self._progenitor_Omega_along_dOmega self._trackts= numpy.linspace(0.,2*dt,2*self._nTrackChunks-1) #to be sure that we cover it if self._useTM: return self._determine_stream_track_TM() #Instantiate an auxiliaryTrack, which is an Orbit instance at the mean frequency of the stream, and zero angle separation wrt the progenitor; prog_stream_offset is the offset between this track and the progenitor at zero angle prog_stream_offset=\ _determine_stream_track_single(self._aA, self._progenitor, 0., #time = 0 self._progenitor_angle, self._sigMeanSign, self._dsigomeanProgDirection, lambda x: self.meanOmega(x,use_physical=False), 0.) #angle = 0 auxiliaryTrack= Orbit(prog_stream_offset[3]) if dt < 0.: self._trackts= numpy.linspace(0.,-2.*dt,2*self._nTrackChunks-1) #Flip velocities before integrating auxiliaryTrack= auxiliaryTrack.flip() auxiliaryTrack.integrate(self._trackts,self._pot) if dt < 0.: #Flip velocities again auxiliaryTrack.orbit[...,1]= -auxiliaryTrack.orbit[...,1] auxiliaryTrack.orbit[...,2]= -auxiliaryTrack.orbit[...,2] auxiliaryTrack.orbit[...,4]= -auxiliaryTrack.orbit[...,4] #Calculate the actions, frequencies, and angle for this auxiliary orbit acfs= self._aA.actionsFreqs(auxiliaryTrack(0.), use_physical=False) auxiliary_Omega= numpy.array([acfs[3],acfs[4],acfs[5]]).reshape(3\ ) auxiliary_Omega_along_dOmega= \ numpy.dot(auxiliary_Omega,self._dsigomeanProgDirection) #Now calculate the actions, frequencies, and angles + Jacobian for each chunk allAcfsTrack= numpy.empty((self._nTrackChunks,9)) alljacsTrack= numpy.empty((self._nTrackChunks,6,6)) allinvjacsTrack= numpy.empty((self._nTrackChunks,6,6)) thetasTrack= numpy.linspace(0.,self._deltaAngleTrack, self._nTrackChunks) ObsTrack= numpy.empty((self._nTrackChunks,6)) ObsTrackAA= numpy.empty((self._nTrackChunks,6)) detdOdJps= numpy.empty(self._nTrackChunks) if self._multi is None: for ii in range(self._nTrackChunks): multiOut= _determine_stream_track_single(self._aA, auxiliaryTrack, self._trackts[ii]*numpy.fabs(self._progenitor_Omega_along_dOmega/auxiliary_Omega_along_dOmega), #this factor accounts for the difference in frequency between the progenitor and the auxiliary track self._progenitor_angle, self._sigMeanSign, self._dsigomeanProgDirection, lambda x: self.meanOmega(x,use_physical=False), thetasTrack[ii]) allAcfsTrack[ii,:]= multiOut[0] alljacsTrack[ii,:,:]= multiOut[1] allinvjacsTrack[ii,:,:]= multiOut[2] ObsTrack[ii,:]= multiOut[3] ObsTrackAA[ii,:]= multiOut[4] detdOdJps[ii]= multiOut[5] else: multiOut= multi.parallel_map(\ (lambda x: _determine_stream_track_single(self._aA,auxiliaryTrack, self._trackts[x]*numpy.fabs(self._progenitor_Omega_along_dOmega/auxiliary_Omega_along_dOmega), self._progenitor_angle, self._sigMeanSign, self._dsigomeanProgDirection, lambda x: self.meanOmega(x,use_physical=False), thetasTrack[x])), range(self._nTrackChunks), numcores=numpy.amin([self._nTrackChunks, multiprocessing.cpu_count(), self._multi])) for ii in range(self._nTrackChunks): allAcfsTrack[ii,:]= multiOut[ii][0] alljacsTrack[ii,:,:]= multiOut[ii][1] allinvjacsTrack[ii,:,:]= multiOut[ii][2] ObsTrack[ii,:]= multiOut[ii][3] ObsTrackAA[ii,:]= multiOut[ii][4] detdOdJps[ii]= multiOut[ii][5] #Repeat the track calculation using the previous track, to get closer to it for nn in range(self.nTrackIterations): if self._multi is None: for ii in range(self._nTrackChunks): multiOut= _determine_stream_track_single(self._aA, Orbit(ObsTrack[ii,:]), 0., self._progenitor_angle, self._sigMeanSign, self._dsigomeanProgDirection, lambda x:self.meanOmega(x,use_physical=False), thetasTrack[ii]) allAcfsTrack[ii,:]= multiOut[0] alljacsTrack[ii,:,:]= multiOut[1] allinvjacsTrack[ii,:,:]= multiOut[2] ObsTrack[ii,:]= multiOut[3] ObsTrackAA[ii,:]= multiOut[4] detdOdJps[ii]= multiOut[5] else: multiOut= multi.parallel_map(\ (lambda x: _determine_stream_track_single(self._aA,Orbit(ObsTrack[x,:]),0., self._progenitor_angle, self._sigMeanSign, self._dsigomeanProgDirection, lambda x: self.meanOmega(x,use_physical=False), thetasTrack[x])), range(self._nTrackChunks), numcores=numpy.amin([self._nTrackChunks, multiprocessing.cpu_count(), self._multi])) for ii in range(self._nTrackChunks): allAcfsTrack[ii,:]= multiOut[ii][0] alljacsTrack[ii,:,:]= multiOut[ii][1] allinvjacsTrack[ii,:,:]= multiOut[ii][2] ObsTrack[ii,:]= multiOut[ii][3] ObsTrackAA[ii,:]= multiOut[ii][4] detdOdJps[ii]= multiOut[ii][5] #Store the track self._thetasTrack= thetasTrack self._ObsTrack= ObsTrack self._ObsTrackAA= ObsTrackAA self._allAcfsTrack= allAcfsTrack self._alljacsTrack= alljacsTrack self._allinvjacsTrack= allinvjacsTrack self._detdOdJps= detdOdJps self._meandetdOdJp= numpy.mean(self._detdOdJps) self._logmeandetdOdJp= numpy.log(self._meandetdOdJp) self._calc_ObsTrackXY() return None def _calc_ObsTrackXY(self): #Also calculate _ObsTrackXY in XYZ,vXYZ coordinates self._ObsTrackXY= numpy.empty_like(self._ObsTrack) TrackX= self._ObsTrack[:,0]*numpy.cos(self._ObsTrack[:,5]) TrackY= self._ObsTrack[:,0]*numpy.sin(self._ObsTrack[:,5]) TrackZ= self._ObsTrack[:,3] TrackvX, TrackvY, TrackvZ=\ coords.cyl_to_rect_vec(self._ObsTrack[:,1], self._ObsTrack[:,2], self._ObsTrack[:,4], self._ObsTrack[:,5]) self._ObsTrackXY[:,0]= TrackX self._ObsTrackXY[:,1]= TrackY self._ObsTrackXY[:,2]= TrackZ self._ObsTrackXY[:,3]= TrackvX self._ObsTrackXY[:,4]= TrackvY self._ObsTrackXY[:,5]= TrackvZ return None def _determine_stream_track_TM(self): # With TM, can get the track in a single shot #Now calculate the actions, frequencies, and angles + Jacobian for each chunk thetasTrack= numpy.linspace(0.,self._deltaAngleTrack, self._nTrackChunks) if self._approxConstTrackFreq: alljacsTrack, allinvjacsTrack, ObsTrack, ObsTrackAA, detdOdJps= \ _determine_stream_track_TM_approxConstantTrackFreq(\ self._aAT, numpy.array([self._progenitor_jr,self._progenitor_lz, self._progenitor_jz]), self._progenitor_Omega, self._progenitor_angle, self._dOdJp, self._dOdJpInv, self._sigMeanSign, self._dsigomeanProgDirection, lambda x: self.meanOmega(x,use_physical=False), thetasTrack) #Store the track, didn't compute _allAcfsTrack self._thetasTrack= thetasTrack self._ObsTrack= ObsTrack self._ObsTrackAA= ObsTrackAA self._alljacsTrack= alljacsTrack self._allinvjacsTrack= allinvjacsTrack self._detdOdJps= detdOdJps self._meandetdOdJp= numpy.mean(self._detdOdJps) self._logmeandetdOdJp= numpy.log(self._meandetdOdJp) self._calc_ObsTrackXY() return None alljacsTrack= numpy.empty((self._nTrackChunks,6,6)) allinvjacsTrack= numpy.empty((self._nTrackChunks,6,6)) ObsTrack= numpy.empty((self._nTrackChunks,6)) ObsTrackAA= numpy.empty((self._nTrackChunks,6)) detdOdJps= numpy.empty(self._nTrackChunks) if self._multi is None: for ii in range(self._nTrackChunks): multiOut= _determine_stream_track_TM_single(\ self._aAT, numpy.array([self._progenitor_jr,self._progenitor_lz, self._progenitor_jz]), self._progenitor_Omega, self._progenitor_angle, self._dOdJp, self._dOdJpInv, self._sigMeanSign, self._dsigomeanProgDirection, lambda x: self.meanOmega(x,use_physical=False), thetasTrack[ii]) alljacsTrack[ii,:,:]= multiOut[0] allinvjacsTrack[ii,:,:]= multiOut[1] ObsTrack[ii,:]= multiOut[2] ObsTrackAA[ii,:]= multiOut[3] detdOdJps[ii]= multiOut[4] else: multiOut= multi.parallel_map(\ (lambda x: _determine_stream_track_TM_single(\ self._aAT, numpy.array([self._progenitor_jr,self._progenitor_lz, self._progenitor_jz]), self._progenitor_Omega, self._progenitor_angle, self._dOdJp, self._dOdJpInv, self._sigMeanSign, self._dsigomeanProgDirection, lambda x: self.meanOmega(x,use_physical=False), thetasTrack[x])), range(self._nTrackChunks), numcores=numpy.amin([self._nTrackChunks, multiprocessing.cpu_count(), self._multi])) for ii in range(self._nTrackChunks): alljacsTrack[ii,:,:]= multiOut[ii][0] allinvjacsTrack[ii,:,:]= multiOut[ii][1] ObsTrack[ii,:]= multiOut[ii][2] ObsTrackAA[ii,:]= multiOut[ii][3] detdOdJps[ii]= multiOut[ii][4] #Store the track, didn't compute _allAcfsTrack self._thetasTrack= thetasTrack self._ObsTrack= ObsTrack self._ObsTrackAA= ObsTrackAA self._alljacsTrack= alljacsTrack self._allinvjacsTrack= allinvjacsTrack self._detdOdJps= detdOdJps self._meandetdOdJp= numpy.mean(self._detdOdJps) self._logmeandetdOdJp= numpy.log(self._meandetdOdJp) #Also calculate _ObsTrackXY in XYZ,vXYZ coordinates self._calc_ObsTrackXY() return None def _determine_stream_spread(self,simple=_USESIMPLE): """Determine the spread around the stream track, just sets matrices that describe the covariances""" allErrCovs= numpy.empty((self._nTrackChunks,6,6)) if self._multi is None: for ii in range(self._nTrackChunks): allErrCovs[ii]= _determine_stream_spread_single(self._sigomatrixEig, self._thetasTrack[ii], lambda x: self.sigOmega(x,use_physical=False), lambda y: self.sigangledAngle(y,simple=simple,use_physical=False), self._allinvjacsTrack[ii]) else: multiOut= multi.parallel_map(\ (lambda x: _determine_stream_spread_single(self._sigomatrixEig, self._thetasTrack[x], lambda x: self.sigOmega(x,use_physical=False), lambda y: self.sigangledAngle(y,simple=simple,use_physical=False), self._allinvjacsTrack[x])), range(self._nTrackChunks), numcores=numpy.amin([self._nTrackChunks, multiprocessing.cpu_count(), self._multi])) for ii in range(self._nTrackChunks): allErrCovs[ii]= multiOut[ii] self._allErrCovs= allErrCovs #Also propagate to XYZ coordinates allErrCovsXY= numpy.empty_like(self._allErrCovs) allErrCovsEigvalXY= numpy.empty((len(self._thetasTrack),6)) allErrCovsEigvecXY= numpy.empty_like(self._allErrCovs) eigDir= numpy.array([numpy.array([1.,0.,0.,0.,0.,0.]) for ii in range(6)]) for ii in range(self._nTrackChunks): tjac= coords.cyl_to_rect_jac(*self._ObsTrack[ii]) allErrCovsXY[ii]=\ numpy.dot(tjac,numpy.dot(self._allErrCovs[ii],tjac.T)) #Eigen decomposition for interpolation teig= numpy.linalg.eig(allErrCovsXY[ii]) #Sort them to match them up later sortIndx= numpy.argsort(teig[0]) allErrCovsEigvalXY[ii]= teig[0][sortIndx] #Make sure the eigenvectors point in the same direction for jj in range(6): if numpy.sum(eigDir[jj]*teig[1][:,sortIndx[jj]]) < 0.: teig[1][:,sortIndx[jj]]*= -1. eigDir[jj]= teig[1][:,sortIndx[jj]] allErrCovsEigvecXY[ii]= teig[1][:,sortIndx] self._allErrCovsXY= allErrCovsXY #Interpolate the allErrCovsXY covariance matrices along the interpolated track #Interpolate the eigenvalues interpAllErrCovsEigvalXY=\ [interpolate.InterpolatedUnivariateSpline(self._thetasTrack, allErrCovsEigvalXY[:,ii], k=3) for ii in range(6)] #Now build the interpolated allErrCovsXY using slerp interpolatedAllErrCovsXY= numpy.empty((len(self._interpolatedThetasTrack), 6,6)) interpolatedEigval=\ numpy.array([interpAllErrCovsEigvalXY[ii](self._interpolatedThetasTrack) for ii in range(6)]) #6,ninterp #Interpolate in chunks interpolatedEigvec= numpy.empty((len(self._interpolatedThetasTrack), 6,6)) for ii in range(self._nTrackChunks-1): slerpOmegas=\ [numpy.arccos(numpy.sum(allErrCovsEigvecXY[ii,:,jj]*allErrCovsEigvecXY[ii+1,:,jj])) for jj in range(6)] slerpts= (self._interpolatedThetasTrack-self._thetasTrack[ii])/\ (self._thetasTrack[ii+1]-self._thetasTrack[ii]) slerpIndx= (slerpts >= 0.)*(slerpts <= 1.) for jj in range(6): for kk in range(6): interpolatedEigvec[slerpIndx,kk,jj]=\ (numpy.sin((1-slerpts[slerpIndx])*slerpOmegas[jj])*allErrCovsEigvecXY[ii,kk,jj] +numpy.sin(slerpts[slerpIndx]*slerpOmegas[jj])*allErrCovsEigvecXY[ii+1,kk,jj])/numpy.sin(slerpOmegas[jj]) for ii in range(len(self._interpolatedThetasTrack)): interpolatedAllErrCovsXY[ii]=\ numpy.dot(interpolatedEigvec[ii], numpy.dot(numpy.diag(interpolatedEigval[:,ii]), interpolatedEigvec[ii].T)) self._interpolatedAllErrCovsXY= interpolatedAllErrCovsXY #Also interpolate in l and b coordinates self._determine_stream_spreadLB(simple=simple) return None def _determine_stream_spreadLB(self,simple=_USESIMPLE, ro=None,vo=None, R0=None,Zsun=None,vsun=None): """Determine the spread in the stream in observable coordinates""" if not hasattr(self,'_allErrCovs'): self._determine_stream_spread(simple=simple) if ro is None: ro= self._ro if vo is None: vo= self._vo if R0 is None: R0= self._R0 if Zsun is None: Zsun= self._Zsun if vsun is None: vsun= self._vsun allErrCovsLB= numpy.empty_like(self._allErrCovs) obs= [R0,0.,Zsun] obs.extend(vsun) obskwargs= {} obskwargs['ro']= ro obskwargs['vo']= vo obskwargs['obs']= obs obskwargs['quantity']= False self._ErrCovsLBScale= [180.,90., self._progenitor.dist(**obskwargs), numpy.fabs(self._progenitor.vlos(**obskwargs)), numpy.sqrt(self._progenitor.pmll(**obskwargs)**2. +self._progenitor.pmbb(**obskwargs)**2.), numpy.sqrt(self._progenitor.pmll(**obskwargs)**2. +self._progenitor.pmbb(**obskwargs)**2.)] allErrCovsEigvalLB= numpy.empty((len(self._thetasTrack),6)) allErrCovsEigvecLB= numpy.empty_like(self._allErrCovs) eigDir= numpy.array([numpy.array([1.,0.,0.,0.,0.,0.]) for ii in range(6)]) for ii in range(self._nTrackChunks): tjacXY= coords.galcenrect_to_XYZ_jac(*self._ObsTrackXY[ii]) tjacLB= coords.lbd_to_XYZ_jac(*self._ObsTrackLB[ii], degree=True) tjacLB[:3,:]/= ro tjacLB[3:,:]/= vo for jj in range(6): tjacLB[:,jj]*= self._ErrCovsLBScale[jj] tjac= numpy.dot(numpy.linalg.inv(tjacLB),tjacXY) allErrCovsLB[ii]=\ numpy.dot(tjac,numpy.dot(self._allErrCovsXY[ii],tjac.T)) #Eigen decomposition for interpolation teig= numpy.linalg.eig(allErrCovsLB[ii]) #Sort them to match them up later sortIndx= numpy.argsort(teig[0]) allErrCovsEigvalLB[ii]= teig[0][sortIndx] #Make sure the eigenvectors point in the same direction for jj in range(6): if numpy.sum(eigDir[jj]*teig[1][:,sortIndx[jj]]) < 0.: teig[1][:,sortIndx[jj]]*= -1. eigDir[jj]= teig[1][:,sortIndx[jj]] allErrCovsEigvecLB[ii]= teig[1][:,sortIndx] self._allErrCovsLBUnscaled= allErrCovsLB #Interpolate the allErrCovsLB covariance matrices along the interpolated track #Interpolate the eigenvalues interpAllErrCovsEigvalLB=\ [interpolate.InterpolatedUnivariateSpline(self._thetasTrack, allErrCovsEigvalLB[:,ii], k=3) for ii in range(6)] #Now build the interpolated allErrCovsXY using slerp interpolatedAllErrCovsLB= numpy.empty((len(self._interpolatedThetasTrack), 6,6)) interpolatedEigval=\ numpy.array([interpAllErrCovsEigvalLB[ii](self._interpolatedThetasTrack) for ii in range(6)]) #6,ninterp #Interpolate in chunks interpolatedEigvec= numpy.empty((len(self._interpolatedThetasTrack), 6,6)) for ii in range(self._nTrackChunks-1): slerpOmegas=\ [numpy.arccos(numpy.sum(allErrCovsEigvecLB[ii,:,jj]*allErrCovsEigvecLB[ii+1,:,jj])) for jj in range(6)] slerpts= (self._interpolatedThetasTrack-self._thetasTrack[ii])/\ (self._thetasTrack[ii+1]-self._thetasTrack[ii]) slerpIndx= (slerpts >= 0.)*(slerpts <= 1.) for jj in range(6): for kk in range(6): interpolatedEigvec[slerpIndx,kk,jj]=\ (numpy.sin((1-slerpts[slerpIndx])*slerpOmegas[jj])*allErrCovsEigvecLB[ii,kk,jj] +numpy.sin(slerpts[slerpIndx]*slerpOmegas[jj])*allErrCovsEigvecLB[ii+1,kk,jj])/numpy.sin(slerpOmegas[jj]) for ii in range(len(self._interpolatedThetasTrack)): interpolatedAllErrCovsLB[ii]=\ numpy.dot(interpolatedEigvec[ii], numpy.dot(numpy.diag(interpolatedEigval[:,ii]), interpolatedEigvec[ii].T)) self._interpolatedAllErrCovsLBUnscaled= interpolatedAllErrCovsLB #Also calculate the (l,b,..) -> (X,Y,..) Jacobian at all of the interpolated and not interpolated points trackLogDetJacLB= numpy.empty_like(self._thetasTrack) interpolatedTrackLogDetJacLB=\ numpy.empty_like(self._interpolatedThetasTrack) for ii in range(self._nTrackChunks): tjacLB= coords.lbd_to_XYZ_jac(*self._ObsTrackLB[ii], degree=True) trackLogDetJacLB[ii]= numpy.log(numpy.linalg.det(tjacLB)) self._trackLogDetJacLB= trackLogDetJacLB for ii in range(len(self._interpolatedThetasTrack)): tjacLB=\ coords.lbd_to_XYZ_jac(*self._interpolatedObsTrackLB[ii], degree=True) interpolatedTrackLogDetJacLB[ii]=\ numpy.log(numpy.linalg.det(tjacLB)) self._interpolatedTrackLogDetJacLB= interpolatedTrackLogDetJacLB return None def _interpolate_stream_track(self): """Build interpolations of the stream track""" if hasattr(self,'_interpolatedThetasTrack'): return None #Already did this TrackX= self._ObsTrack[:,0]*numpy.cos(self._ObsTrack[:,5]) TrackY= self._ObsTrack[:,0]*numpy.sin(self._ObsTrack[:,5]) TrackZ= self._ObsTrack[:,3] TrackvX, TrackvY, TrackvZ=\ coords.cyl_to_rect_vec(self._ObsTrack[:,1], self._ObsTrack[:,2], self._ObsTrack[:,4], self._ObsTrack[:,5]) #Interpolate self._interpTrackX=\ interpolate.InterpolatedUnivariateSpline(self._thetasTrack, TrackX,k=3) self._interpTrackY=\ interpolate.InterpolatedUnivariateSpline(self._thetasTrack, TrackY,k=3) self._interpTrackZ=\ interpolate.InterpolatedUnivariateSpline(self._thetasTrack, TrackZ,k=3) self._interpTrackvX=\ interpolate.InterpolatedUnivariateSpline(self._thetasTrack, TrackvX,k=3) self._interpTrackvY=\ interpolate.InterpolatedUnivariateSpline(self._thetasTrack, TrackvY,k=3) self._interpTrackvZ=\ interpolate.InterpolatedUnivariateSpline(self._thetasTrack, TrackvZ,k=3) #Now store an interpolated version of the stream track self._interpolatedThetasTrack=\ numpy.linspace(0.,self._deltaAngleTrack, self.nInterpolatedTrackChunks) self._interpolatedObsTrackXY= numpy.empty((len(self._interpolatedThetasTrack),6)) self._interpolatedObsTrackXY[:,0]=\ self._interpTrackX(self._interpolatedThetasTrack) self._interpolatedObsTrackXY[:,1]=\ self._interpTrackY(self._interpolatedThetasTrack) self._interpolatedObsTrackXY[:,2]=\ self._interpTrackZ(self._interpolatedThetasTrack) self._interpolatedObsTrackXY[:,3]=\ self._interpTrackvX(self._interpolatedThetasTrack) self._interpolatedObsTrackXY[:,4]=\ self._interpTrackvY(self._interpolatedThetasTrack) self._interpolatedObsTrackXY[:,5]=\ self._interpTrackvZ(self._interpolatedThetasTrack) #Also in cylindrical coordinates self._interpolatedObsTrack= \ numpy.empty((len(self._interpolatedThetasTrack),6)) tR,tphi,tZ= coords.rect_to_cyl(self._interpolatedObsTrackXY[:,0], self._interpolatedObsTrackXY[:,1], self._interpolatedObsTrackXY[:,2]) tvR,tvT,tvZ=\ coords.rect_to_cyl_vec(self._interpolatedObsTrackXY[:,3], self._interpolatedObsTrackXY[:,4], self._interpolatedObsTrackXY[:,5], tR,tphi,tZ,cyl=True) self._interpolatedObsTrack[:,0]= tR self._interpolatedObsTrack[:,1]= tvR self._interpolatedObsTrack[:,2]= tvT self._interpolatedObsTrack[:,3]= tZ self._interpolatedObsTrack[:,4]= tvZ self._interpolatedObsTrack[:,5]= tphi return None def _interpolate_stream_track_aA(self): """Build interpolations of the stream track in action-angle coordinates""" if hasattr(self,'_interpolatedObsTrackAA'): return None #Already did this #Calculate 1D meanOmega on a fine grid in angle and interpolate if not hasattr(self,'_interpolatedThetasTrack'): self._interpolate_stream_track() dmOs= numpy.array([self.meanOmega(da,oned=True,use_physical=False) for da in self._interpolatedThetasTrack]) self._interpTrackAAdmeanOmegaOneD=\ interpolate.InterpolatedUnivariateSpline(\ self._interpolatedThetasTrack,dmOs,k=3) #Build the interpolated AA self._interpolatedObsTrackAA=\ numpy.empty((len(self._interpolatedThetasTrack),6)) for ii in range(len(self._interpolatedThetasTrack)): self._interpolatedObsTrackAA[ii,:3]=\ self._progenitor_Omega+dmOs[ii]*self._dsigomeanProgDirection\ *self._sigMeanSign self._interpolatedObsTrackAA[ii,3:]=\ self._progenitor_angle+self._interpolatedThetasTrack[ii]\ *self._dsigomeanProgDirection*self._sigMeanSign self._interpolatedObsTrackAA[ii,3:]=\ numpy.mod(self._interpolatedObsTrackAA[ii,3:],2.*numpy.pi) return None def calc_stream_lb(self, vo=None,ro=None, R0=None,Zsun=None,vsun=None): """ NAME: calc_stream_lb PURPOSE: convert the stream track to observational coordinates and store INPUT: Coordinate transformation inputs (all default to the instance-wide values): vo= circular velocity to normalize velocities with ro= Galactocentric radius to normalize positions with R0= Galactocentric radius of the Sun (kpc) Zsun= Sun's height above the plane (kpc) vsun= Sun's motion in cylindrical coordinates (vR positive away from center) OUTPUT: (none) HISTORY: 2013-12-02 - Written - Bovy (IAS) """ if vo is None: vo= self._vo if ro is None: ro= self._ro if R0 is None: R0= self._R0 if Zsun is None: Zsun= self._Zsun if vsun is None: vsun= self._vsun self._ObsTrackLB= numpy.empty_like(self._ObsTrack) XYZ= coords.galcencyl_to_XYZ(self._ObsTrack[:,0]*ro, self._ObsTrack[:,5], self._ObsTrack[:,3]*ro, Xsun=R0,Zsun=Zsun).T vXYZ= coords.galcencyl_to_vxvyvz(self._ObsTrack[:,1]*vo, self._ObsTrack[:,2]*vo, self._ObsTrack[:,4]*vo, self._ObsTrack[:,5], vsun=vsun,Xsun=R0,Zsun=Zsun).T slbd=coords.XYZ_to_lbd(XYZ[0],XYZ[1],XYZ[2], degree=True) svlbd= coords.vxvyvz_to_vrpmllpmbb(vXYZ[0],vXYZ[1],vXYZ[2], slbd[:,0],slbd[:,1],slbd[:,2], degree=True) self._ObsTrackLB[:,0]= slbd[:,0] self._ObsTrackLB[:,1]= slbd[:,1] self._ObsTrackLB[:,2]= slbd[:,2] self._ObsTrackLB[:,3]= svlbd[:,0] self._ObsTrackLB[:,4]= svlbd[:,1] self._ObsTrackLB[:,5]= svlbd[:,2] if hasattr(self,'_interpolatedObsTrackXY'): #Do the same for the interpolated track self._interpolatedObsTrackLB=\ numpy.empty_like(self._interpolatedObsTrackXY) XYZ=\ coords.galcenrect_to_XYZ(\ self._interpolatedObsTrackXY[:,0]*ro, self._interpolatedObsTrackXY[:,1]*ro, self._interpolatedObsTrackXY[:,2]*ro, Xsun=R0,Zsun=Zsun).T vXYZ=\ coords.galcenrect_to_vxvyvz(\ self._interpolatedObsTrackXY[:,3]*vo, self._interpolatedObsTrackXY[:,4]*vo, self._interpolatedObsTrackXY[:,5]*vo, vsun=vsun,Xsun=R0,Zsun=Zsun).T slbd=coords.XYZ_to_lbd(XYZ[0],XYZ[1],XYZ[2], degree=True) svlbd= coords.vxvyvz_to_vrpmllpmbb(vXYZ[0],vXYZ[1],vXYZ[2], slbd[:,0],slbd[:,1], slbd[:,2], degree=True) self._interpolatedObsTrackLB[:,0]= slbd[:,0] self._interpolatedObsTrackLB[:,1]= slbd[:,1] self._interpolatedObsTrackLB[:,2]= slbd[:,2] self._interpolatedObsTrackLB[:,3]= svlbd[:,0] self._interpolatedObsTrackLB[:,4]= svlbd[:,1] self._interpolatedObsTrackLB[:,5]= svlbd[:,2] if hasattr(self,'_allErrCovsLBUnscaled'): #Re-calculate this self._determine_stream_spreadLB(simple=_USESIMPLE, vo=vo,ro=ro, R0=R0,Zsun=Zsun,vsun=vsun) return None def _find_closest_trackpoint(self,R,vR,vT,z,vz,phi,interp=True,xy=False, usev=False): """For backward compatibility""" return self.find_closest_trackpoint(R,vR,vT,z,vz,phi, interp=interp,xy=xy, usev=usev) def find_closest_trackpoint(self,R,vR,vT,z,vz,phi,interp=True,xy=False, usev=False): """ NAME: find_closest_trackpoint PURPOSE: find the closest point on the stream track to a given point INPUT: R,vR,vT,z,vz,phi - phase-space coordinates of the given point interp= (True), if True, return the index of the interpolated track xy= (False) if True, input is X,Y,Z,vX,vY,vZ in Galactocentric rectangular coordinates; if xy, some coordinates may be missing (given as None) and they will not be used usev= (False) if True, also use velocities to find the closest point OUTPUT: index into the track of the closest track point HISTORY: 2013-12-04 - Written - Bovy (IAS) """ if xy: X= R Y= vR Z= vT else: X= R*numpy.cos(phi) Y= R*numpy.sin(phi) Z= z if xy and usev: vX= z vY= vz vZ= phi elif usev: vX= vR*numpy.cos(phi)-vT*numpy.sin(phi) vY= vR*numpy.sin(phi)+vT*numpy.cos(phi) vZ= vz present= [not X is None,not Y is None,not Z is None] if usev: present.extend([not vX is None,not vY is None,not vZ is None]) present= numpy.array(present,dtype='float') if X is None: X= 0. if Y is None: Y= 0. if Z is None: Z= 0. if usev and vX is None: vX= 0. if usev and vY is None: vY= 0. if usev and vZ is None: vZ= 0. if interp: dist2= present[0]*(X-self._interpolatedObsTrackXY[:,0])**2.\ +present[1]*(Y-self._interpolatedObsTrackXY[:,1])**2.\ +present[2]*(Z-self._interpolatedObsTrackXY[:,2])**2. if usev: dist2+= present[3]*(vX-self._interpolatedObsTrackXY[:,3])**2.\ +present[4]*(vY-self._interpolatedObsTrackXY[:,4])**2.\ +present[5]*(vZ-self._interpolatedObsTrackXY[:,5])**2. else: dist2= present[0]*(X-self._ObsTrackXY[:,0])**2.\ +present[1]*(Y-self._ObsTrackXY[:,1])**2.\ +present[2]*(Z-self._ObsTrackXY[:,2])**2. if usev: dist2+= present[3]*(vX-self._ObsTrackXY[:,3])**2.\ +present[4]*(vY-self._ObsTrackXY[:,4])**2.\ +present[5]*(vZ-self._ObsTrackXY[:,5])**2. return numpy.argmin(dist2) def _find_closest_trackpointLB(self,l,b,D,vlos,pmll,pmbb,interp=True, usev=False): return self.find_closest_trackpointLB(l,b,D,vlos,pmll,pmbb, interp=interp, usev=usev) def find_closest_trackpointLB(self,l,b,D,vlos,pmll,pmbb,interp=True, usev=False): """ NAME: find_closest_trackpointLB PURPOSE: find the closest point on the stream track to a given point in (l,b,...) coordinates INPUT: l,b,D,vlos,pmll,pmbb- coordinates in (deg,deg,kpc,km/s,mas/yr,mas/yr) interp= (True) if True, return the closest index on the interpolated track usev= (False) if True, also use the velocity components (default is to only use the positions) OUTPUT: index of closest track point on the interpolated or not-interpolated track HISTORY: 2013-12-17- Written - Bovy (IAS) """ if interp: nTrackPoints= len(self._interpolatedThetasTrack) else: nTrackPoints= len(self._thetasTrack) if l is None: l= 0. trackL= numpy.zeros(nTrackPoints) elif interp: trackL= self._interpolatedObsTrackLB[:,0] else: trackL= self._ObsTrackLB[:,0] if b is None: b= 0. trackB= numpy.zeros(nTrackPoints) elif interp: trackB= self._interpolatedObsTrackLB[:,1] else: trackB= self._ObsTrackLB[:,1] if D is None: D= 1. trackD= numpy.ones(nTrackPoints) elif interp: trackD= self._interpolatedObsTrackLB[:,2] else: trackD= self._ObsTrackLB[:,2] if usev: if vlos is None: vlos= 0. trackVlos= numpy.zeros(nTrackPoints) elif interp: trackVlos= self._interpolatedObsTrackLB[:,3] else: trackVlos= self._ObsTrackLB[:,3] if pmll is None: pmll= 0. trackPmll= numpy.zeros(nTrackPoints) elif interp: trackPmll= self._interpolatedObsTrackLB[:,4] else: trackPmll= self._ObsTrackLB[:,4] if pmbb is None: pmbb= 0. trackPmbb= numpy.zeros(nTrackPoints) elif interp: trackPmbb= self._interpolatedObsTrackLB[:,5] else: trackPmbb= self._ObsTrackLB[:,5] #Calculate rectangular coordinates XYZ= coords.lbd_to_XYZ(l,b,D,degree=True) trackXYZ= coords.lbd_to_XYZ(trackL,trackB,trackD,degree=True) if usev: vxvyvz= coords.vrpmllpmbb_to_vxvyvz(vlos,pmll,pmbb, XYZ[0],XYZ[1],XYZ[2], XYZ=True) trackvxvyvz= coords.vrpmllpmbb_to_vxvyvz(trackVlos,trackPmll, trackPmbb, trackXYZ[:,0], trackXYZ[:,1], trackXYZ[:,2], XYZ=True) #Calculate distance dist2= (XYZ[0]-trackXYZ[:,0])**2.\ +(XYZ[1]-trackXYZ[:,1])**2.\ +(XYZ[2]-trackXYZ[:,2])**2. if usev: dist2+= (vxvyvz[0]-trackvxvyvz[:,0])**2.\ +(vxvyvz[1]-trackvxvyvz[:,1])**2.\ +(vxvyvz[2]-trackvxvyvz[:,2])**2. return numpy.argmin(dist2) def _find_closest_trackpointaA(self,Or,Op,Oz,ar,ap,az,interp=True): """ NAME: _find_closest_trackpointaA PURPOSE: find the closest point on the stream track to a given point in frequency-angle coordinates INPUT: Or,Op,Oz,ar,ap,az - phase-space coordinates of the given point interp= (True), if True, return the index of the interpolated track OUTPUT: index into the track of the closest track point HISTORY: 2013-12-22 - Written - Bovy (IAS) """ #Calculate angle offset along the stream parallel to the stream track, # finding first the angle among a few wraps where the point is # closest to the parallel track and then the closest trackpoint to that # point da= numpy.stack(\ numpy.meshgrid(_TWOPIWRAPS+ar-self._progenitor_angle[0], _TWOPIWRAPS+ap-self._progenitor_angle[1], _TWOPIWRAPS+az-self._progenitor_angle[2], indexing='xy')).T.reshape((len(_TWOPIWRAPS)**3,3)) dapar= self._sigMeanSign*numpy.dot(da[numpy.argmin(numpy.linalg.norm(\ numpy.cross(da,self._dsigomeanProgDirection),axis=1))], self._dsigomeanProgDirection) if interp: dist= numpy.fabs(dapar-self._interpolatedThetasTrack) else: dist= numpy.fabs(dapar-self._thetasTrack) return numpy.argmin(dist) #########DISTRIBUTION AS A FUNCTION OF ANGLE ALONG THE STREAM################## def pOparapar(self,Opar,apar,tdisrupt=None): """ NAME: pOparapar PURPOSE: return the probability of a given parallel (frequency,angle) offset pair INPUT: Opar - parallel frequency offset (array) (can be Quantity) apar - parallel angle offset along the stream (scalar) (can be Quantity) OUTPUT: p(Opar,apar) HISTORY: 2015-12-07 - Written - Bovy (UofT) """ Opar= conversion.parse_frequency(Opar,ro=self._ro,vo=self._vo) apar= conversion.parse_angle(apar) if tdisrupt is None: tdisrupt= self._tdisrupt if isinstance(Opar,(int,float,numpy.float32,numpy.float64)): Opar= numpy.array([Opar]) out= numpy.zeros(len(Opar)) # Compute ts ts= apar/Opar # Evaluate out[(ts < tdisrupt)*(ts >= 0.)]=\ numpy.exp(-0.5*(Opar[(ts < tdisrupt)*(ts >= 0.)]-self._meandO)**2.\ /self._sortedSigOEig[2])/\ numpy.sqrt(self._sortedSigOEig[2]) return out def density_par(self,dangle,coord='apar',tdisrupt=None, **kwargs): """ NAME: density_par PURPOSE: calculate the density as a function of a parallel coordinate INPUT: dangle - parallel angle offset for this coordinate value coord - coordinate to return the density in ('apar' [default], 'll','ra','customra','phi') OUTPUT: density(angle) HISTORY: 2015-11-17 - Written - Bovy (UofT) """ if coord.lower() != 'apar': # Need to compute the Jacobian for this coordinate value ddangle= dangle+10.**-7. ddangle-= dangle if coord.lower() == 'phi': phi_h= coords.rect_to_cyl(\ self._interpTrackX(dangle+ddangle), self._interpTrackY(dangle+ddangle), self._interpTrackZ(dangle+ddangle)) phi= coords.rect_to_cyl(\ self._interpTrackX(dangle), self._interpTrackY(dangle), self._interpTrackZ(dangle)) jac= numpy.fabs(phi_h[1]-phi[1])/ddangle elif coord.lower() == 'll' or coord.lower() == 'ra' \ or coord.lower() == 'customra': XYZ_h= coords.galcenrect_to_XYZ(\ self._interpTrackX(dangle+ddangle)*self._ro, self._interpTrackY(dangle+ddangle)*self._ro, self._interpTrackZ(dangle+ddangle)*self._ro, Xsun=self._R0,Zsun=self._Zsun) lbd_h= coords.XYZ_to_lbd(XYZ_h[0],XYZ_h[1],XYZ_h[2], degree=True) XYZ= coords.galcenrect_to_XYZ(\ self._interpTrackX(dangle)*self._ro, self._interpTrackY(dangle)*self._ro, self._interpTrackZ(dangle)*self._ro, Xsun=self._R0,Zsun=self._Zsun) lbd= coords.XYZ_to_lbd(XYZ[0],XYZ[1],XYZ[2], degree=True) if coord.lower() == 'll': jac= numpy.fabs(lbd_h[0]-lbd[0])/ddangle else: radec_h= coords.lb_to_radec(lbd_h[0], lbd_h[1], degree=True) radec= coords.lb_to_radec(lbd[0], lbd[1], degree=True) if coord.lower() == 'ra': jac= numpy.fabs(radec_h[0]-radec[0])/ddangle else: xieta_h= coords.radec_to_custom(\ radec_h[0],radec_h[1],T=self._custom_transform, degree=True) xieta= coords.radec_to_custom(\ radec[0],radec[1],T=self._custom_transform, degree=True) jac= numpy.fabs(xieta_h[0]-xieta[0])/ddangle else: raise ValueError('Coordinate input %s not supported by density_par' % coord) else: jac= 1. return self._density_par(dangle,tdisrupt=tdisrupt,**kwargs)/jac def _density_par(self,dangle,tdisrupt=None): """The raw density as a function of parallel angle""" if tdisrupt is None: tdisrupt= self._tdisrupt dOmin= dangle/tdisrupt # Normalize to 1 close to progenitor return 0.5\ *(1.+special.erf((self._meandO-dOmin)\ /numpy.sqrt(2.*self._sortedSigOEig[2]))) def length(self,threshold=0.2,phys=False,ang=False,tdisrupt=None, **kwargs): """ NAME: length PURPOSE: calculate the length of the stream INPUT: threshold - threshold down from the density near the progenitor at which to define the 'end' of the stream phys= (False) if True, return the length in physical kpc ang= (False) if True, return the length in sky angular arc length in degree coord - coordinate to return the density in ('apar' [default], 'll','ra','customra','phi') OUTPUT: length (rad for parallel angle; kpc for physical length; deg for sky arc length) HISTORY: 2015-12-22 - Written - Bovy (UofT) """ peak_dens= self.density_par(0.1,tdisrupt=tdisrupt,**kwargs) # assume that this is the peak try: result=\ optimize.brentq(lambda x: self.density_par(x, tdisrupt=tdisrupt, **kwargs)\ -peak_dens*threshold, 0.1,self._deltaAngleTrack) except RuntimeError: #pragma: no cover raise RuntimeError('Length could not be returned, because length method failed to find the threshold value') except ValueError: raise ValueError('Length could not be returned, because length method failed to initialize') if phys: # Need to now integrate length dXda= self._interpTrackX.derivative() dYda= self._interpTrackY.derivative() dZda= self._interpTrackZ.derivative() result= integrate.quad(lambda da: numpy.sqrt(dXda(da)**2.\ +dYda(da)**2.\ +dZda(da)**2.), 0.,result)[0]*self._ro elif ang: # Need to now integrate length if numpy.median(numpy.roll(self._interpolatedObsTrackLB[:,0],-1) -self._interpolatedObsTrackLB[:,0]) > 0.: ll= dePeriod(self._interpolatedObsTrackLB[:,0][:,numpy.newaxis].T*numpy.pi/180.).T*180./numpy.pi else: ll= dePeriod(self._interpolatedObsTrackLB[::-1,0][:,numpy.newaxis].T*numpy.pi/180.).T[::-1]*180./numpy.pi if numpy.median(numpy.roll(self._interpolatedObsTrackLB[:,1],-1) -self._interpolatedObsTrackLB[:,1]) > 0.: bb= dePeriod(self._interpolatedObsTrackLB[:,1][:,numpy.newaxis].T*numpy.pi/180.).T*180./numpy.pi else: bb= dePeriod(self._interpolatedObsTrackLB[::-1,1][:,numpy.newaxis].T*numpy.pi/180.).T[::-1]*180./numpy.pi dlda= interpolate.InterpolatedUnivariateSpline(\ self._interpolatedThetasTrack,ll,k=3).derivative() dbda= interpolate.InterpolatedUnivariateSpline(\ self._interpolatedThetasTrack,bb,k=3).derivative() result= integrate.quad(lambda da: numpy.sqrt(dlda(da)**2.\ +dbda(da)**2.), 0.,result)[0] return result @physical_conversion('frequency',pop=True) def meanOmega(self,dangle,oned=False,offset_sign=None, tdisrupt=None): """ NAME: meanOmega PURPOSE: calculate the mean frequency as a function of angle, assuming a uniform time distribution up to a maximum time INPUT: dangle - angle offset oned= (False) if True, return the 1D offset from the progenitor (along the direction of disruption) offset_sign= sign of the frequency offset (shouldn't be set) OUTPUT: mean Omega HISTORY: 2013-12-01 - Written - Bovy (IAS) """ if offset_sign is None: offset_sign= self._sigMeanSign if tdisrupt is None: tdisrupt= self._tdisrupt dOmin= dangle/tdisrupt meandO= self._meandO dO1D= ((numpy.sqrt(2./numpy.pi)*numpy.sqrt(self._sortedSigOEig[2])\ *numpy.exp(-0.5*(meandO-dOmin)**2.\ /self._sortedSigOEig[2])/ (1.+special.erf((meandO-dOmin)\ /numpy.sqrt(2.*self._sortedSigOEig[2]))))\ +meandO) if oned: return dO1D else: return self._progenitor_Omega+dO1D*self._dsigomeanProgDirection\ *offset_sign @physical_conversion('frequency',pop=True) def sigOmega(self,dangle): """ NAME: sigmaOmega PURPOSE: calculate the 1D sigma in frequency as a function of angle, assuming a uniform time distribution up to a maximum time INPUT: dangle - angle offset OUTPUT: sigma Omega HISTORY: 2013-12-05 - Written - Bovy (IAS) """ dOmin= dangle/self._tdisrupt meandO= self._meandO sO1D2= ((numpy.sqrt(2./numpy.pi)*numpy.sqrt(self._sortedSigOEig[2])\ *(meandO+dOmin)\ *numpy.exp(-0.5*(meandO-dOmin)**2.\ /self._sortedSigOEig[2])/ (1.+special.erf((meandO-dOmin)\ /numpy.sqrt(2.*self._sortedSigOEig[2]))))\ +meandO**2.+self._sortedSigOEig[2]) mO= self.meanOmega(dangle,oned=True,use_physical=False) return numpy.sqrt(sO1D2-mO**2.) def ptdAngle(self,t,dangle): """ NAME: ptdangle PURPOSE: return the probability of a given stripping time at a given angle along the stream INPUT: t - stripping time dangle - angle offset along the stream OUTPUT: p(td|dangle) HISTORY: 2013-12-05 - Written - Bovy (IAS) """ if isinstance(t,(int,float,numpy.float32,numpy.float64)): t= numpy.array([t]) out= numpy.zeros(len(t)) if t > 0.: dO= dangle/t[t < self._tdisrupt] else: return 0. #p(t|a) = \int dO p(O,t|a) = \int dO p(t|O,a) p(O|a) = \int dO delta (t-a/O)p(O|a) = O*2/a p(O|a); p(O|a) = \int dt p(a|O,t) p(O)p(t) = 1/O p(O) out[t < self._tdisrupt]=\ dO**2./dangle*numpy.exp(-0.5*(dO-self._meandO)**2.\ /self._sortedSigOEig[2])/\ numpy.sqrt(self._sortedSigOEig[2]) return out @physical_conversion('time',pop=True) def meantdAngle(self,dangle): """ NAME: meantdAngle PURPOSE: calculate the mean stripping time at a given angle INPUT: dangle - angle offset along the stream OUTPUT: mean stripping time at this dangle HISTORY: 2013-12-05 - Written - Bovy (IAS) """ Tlow= dangle/(self._meandO+3.*numpy.sqrt(self._sortedSigOEig[2])) Thigh= dangle/(self._meandO-3.*numpy.sqrt(self._sortedSigOEig[2])) num= integrate.quad(lambda x: x*self.ptdAngle(x,dangle), Tlow,Thigh)[0] denom= integrate.quad(self.ptdAngle,Tlow,Thigh,(dangle,))[0] if denom == 0.: return self._tdisrupt elif numpy.isnan(denom): return 0. else: return num/denom @physical_conversion('time',pop=True) def sigtdAngle(self,dangle): """ NAME: sigtdAngle PURPOSE: calculate the dispersion in the stripping times at a given angle INPUT: dangle - angle offset along the stream OUTPUT: dispersion in the stripping times at this angle HISTORY: 2013-12-05 - Written - Bovy (IAS) """ Tlow= dangle/(self._meandO+3.*numpy.sqrt(self._sortedSigOEig[2])) Thigh= dangle/(self._meandO-3.*numpy.sqrt(self._sortedSigOEig[2])) numsig2= integrate.quad(lambda x: x**2.*self.ptdAngle(x,dangle), Tlow,Thigh)[0] nummean= integrate.quad(lambda x: x*self.ptdAngle(x,dangle), Tlow,Thigh)[0] denom= integrate.quad(self.ptdAngle,Tlow,Thigh,(dangle,))[0] if denom == 0.: return numpy.nan else: return numpy.sqrt(numsig2/denom-(nummean/denom)**2.) def pangledAngle(self,angleperp,dangle,smallest=False): """ NAME: pangledAngle PURPOSE: return the probability of a given perpendicular angle at a given angle along the stream INPUT: angleperp - perpendicular angle dangle - angle offset along the stream smallest= (False) calculate for smallest eigenvalue direction rather than for middle OUTPUT: p(angle_perp|dangle) HISTORY: 2013-12-06 - Written - Bovy (IAS) """ if isinstance(angleperp,(int,float,numpy.float32,numpy.float64)): angleperp= numpy.array([angleperp]) out= numpy.zeros(len(angleperp)) out= numpy.array([\ integrate.quad(self._pangledAnglet,0.,self._tdisrupt, (ap,dangle,smallest))[0] for ap in angleperp]) return out @physical_conversion('angle',pop=True) def meanangledAngle(self,dangle,smallest=False): """ NAME: meanangledAngle PURPOSE: calculate the mean perpendicular angle at a given angle INPUT: dangle - angle offset along the stream smallest= (False) calculate for smallest eigenvalue direction rather than for middle OUTPUT: mean perpendicular angle HISTORY: 2013-12-06 - Written - Bovy (IAS) """ if smallest: eigIndx= 0 else: eigIndx= 1 aplow= numpy.amax([numpy.sqrt(self._sortedSigOEig[eigIndx])\ *self._tdisrupt*5., self._sigangle]) num= integrate.quad(lambda x: x*self.pangledAngle(x,dangle,smallest), aplow,-aplow)[0] denom= integrate.quad(self.pangledAngle,aplow,-aplow, (dangle,smallest))[0] if denom == 0.: return numpy.nan else: return num/denom @physical_conversion('angle',pop=True) def sigangledAngle(self,dangle,assumeZeroMean=True,smallest=False, simple=False): """ NAME: sigangledAngle PURPOSE: calculate the dispersion in the perpendicular angle at a given angle INPUT: dangle - angle offset along the stream assumeZeroMean= (True) if True, assume that the mean is zero (should be) smallest= (False) calculate for smallest eigenvalue direction rather than for middle simple= (False), if True, return an even simpler estimate OUTPUT: dispersion in the perpendicular angle at this angle HISTORY: 2013-12-06 - Written - Bovy (IAS) """ if smallest: eigIndx= 0 else: eigIndx= 1 if simple: dt= self.meantdAngle(dangle,use_physical=False) return numpy.sqrt(self._sigangle2 +self._sortedSigOEig[eigIndx]*dt**2.) aplow= numpy.amax([numpy.sqrt(self._sortedSigOEig[eigIndx])*self._tdisrupt*5., self._sigangle]) numsig2= integrate.quad(lambda x: x**2.*self.pangledAngle(x,dangle), aplow,-aplow)[0] if not assumeZeroMean: nummean= integrate.quad(lambda x: x*self.pangledAngle(x,dangle), aplow,-aplow)[0] else: nummean= 0. denom= integrate.quad(self.pangledAngle,aplow,-aplow,(dangle,))[0] if denom == 0.: return numpy.nan else: return numpy.sqrt(numsig2/denom-(nummean/denom)**2.) def _pangledAnglet(self,t,angleperp,dangle,smallest): """p(angle_perp|angle_par,time)""" if smallest: eigIndx= 0 else: eigIndx= 1 if isinstance(angleperp,(int,float,numpy.float32,numpy.float64)): angleperp= numpy.array([angleperp]) t= numpy.array([t]) out= numpy.zeros_like(angleperp) tindx= t < self._tdisrupt out[tindx]=\ numpy.exp(-0.5*angleperp[tindx]**2.\ /(t[tindx]**2.*self._sortedSigOEig[eigIndx]+self._sigangle2))/\ numpy.sqrt(t[tindx]**2.*self._sortedSigOEig[eigIndx]+self._sigangle2)\ *self.ptdAngle(t[t < self._tdisrupt],dangle) return out ################APPROXIMATE FREQUENCY-ANGLE TRANSFORMATION##################### def _approxaA(self,R,vR,vT,z,vz,phi,interp=True,cindx=None): """ NAME: _approxaA PURPOSE: return action-angle coordinates for a point based on the linear approximation around the stream track INPUT: R,vR,vT,z,vz,phi - phase-space coordinates of the given point interp= (True), if True, use the interpolated track cindx= index of the closest point on the (interpolated) stream track if not given, determined from the dimensions given OUTPUT: (Or,Op,Oz,ar,ap,az) HISTORY: 2013-12-03 - Written - Bovy (IAS) 2015-11-12 - Added weighted sum of two nearest Jacobians to help with smoothness - Bovy (UofT) """ if isinstance(R,(int,float,numpy.float32,numpy.float64)): #Scalar input R= numpy.array([R]) vR= numpy.array([vR]) vT= numpy.array([vT]) z= numpy.array([z]) vz= numpy.array([vz]) phi= numpy.array([phi]) X= R*numpy.cos(phi) Y= R*numpy.sin(phi) Z= z if cindx is None: closestIndx= [self._find_closest_trackpoint(X[ii],Y[ii],Z[ii], z[ii],vz[ii],phi[ii], interp=interp, xy=True,usev=False) for ii in range(len(R))] else: closestIndx= cindx out= numpy.empty((6,len(R))) for ii in range(len(R)): dxv= numpy.empty(6) if interp: dxv[0]= R[ii]-self._interpolatedObsTrack[closestIndx[ii],0] dxv[1]= vR[ii]-self._interpolatedObsTrack[closestIndx[ii],1] dxv[2]= vT[ii]-self._interpolatedObsTrack[closestIndx[ii],2] dxv[3]= z[ii]-self._interpolatedObsTrack[closestIndx[ii],3] dxv[4]= vz[ii]-self._interpolatedObsTrack[closestIndx[ii],4] dxv[5]= phi[ii]-self._interpolatedObsTrack[closestIndx[ii],5] jacIndx= self._find_closest_trackpoint(R[ii],vR[ii],vT[ii], z[ii],vz[ii],phi[ii], interp=False, xy=False) else: dxv[0]= R[ii]-self._ObsTrack[closestIndx[ii],0] dxv[1]= vR[ii]-self._ObsTrack[closestIndx[ii],1] dxv[2]= vT[ii]-self._ObsTrack[closestIndx[ii],2] dxv[3]= z[ii]-self._ObsTrack[closestIndx[ii],3] dxv[4]= vz[ii]-self._ObsTrack[closestIndx[ii],4] dxv[5]= phi[ii]-self._ObsTrack[closestIndx[ii],5] jacIndx= closestIndx[ii] # Find 2nd closest Jacobian point for smoothing dmJacIndx= (X[ii]-self._ObsTrackXY[jacIndx,0])**2.\ +(Y[ii]-self._ObsTrackXY[jacIndx,1])**2.\ +(Z[ii]-self._ObsTrackXY[jacIndx,2])**2. if jacIndx == 0: jacIndx2= jacIndx+1 dmJacIndx2= (X[ii]-self._ObsTrackXY[jacIndx+1,0])**2.\ +(Y[ii]-self._ObsTrackXY[jacIndx+1,1])**2.\ +(Z[ii]-self._ObsTrackXY[jacIndx+1,2])**2. elif jacIndx == self._nTrackChunks-1: jacIndx2= jacIndx-1 dmJacIndx2= (X[ii]-self._ObsTrackXY[jacIndx-1,0])**2.\ +(Y[ii]-self._ObsTrackXY[jacIndx-1,1])**2.\ +(Z[ii]-self._ObsTrackXY[jacIndx-1,2])**2. else: dm1= (X[ii]-self._ObsTrackXY[jacIndx-1,0])**2.\ +(Y[ii]-self._ObsTrackXY[jacIndx-1,1])**2.\ +(Z[ii]-self._ObsTrackXY[jacIndx-1,2])**2. dm2= (X[ii]-self._ObsTrackXY[jacIndx+1,0])**2.\ +(Y[ii]-self._ObsTrackXY[jacIndx+1,1])**2.\ +(Z[ii]-self._ObsTrackXY[jacIndx+1,2])**2. if dm1 < dm2: jacIndx2= jacIndx-1 dmJacIndx2= dm1 else: jacIndx2= jacIndx+1 dmJacIndx2= dm2 ampJacIndx= numpy.sqrt(dmJacIndx)/(numpy.sqrt(dmJacIndx)\ +numpy.sqrt(dmJacIndx2)) #Make sure phi hasn't wrapped around if dxv[5] > numpy.pi: dxv[5]-= 2.*numpy.pi elif dxv[5] < -numpy.pi: dxv[5]+= 2.*numpy.pi #Apply closest jacobians out[:,ii]= numpy.dot((1.-ampJacIndx)*self._alljacsTrack[jacIndx,:,:] +ampJacIndx*self._alljacsTrack[jacIndx2,:,:], dxv) if interp: out[:,ii]+= self._interpolatedObsTrackAA[closestIndx[ii]] else: out[:,ii]+= self._ObsTrackAA[closestIndx[ii]] return out def _approxaAInv(self,Or,Op,Oz,ar,ap,az,interp=True): """ NAME: _approxaAInv PURPOSE: return R,vR,... coordinates for a point based on the linear approximation around the stream track INPUT: Or,Op,Oz,ar,ap,az - phase space coordinates in frequency-angle space interp= (True), if True, use the interpolated track OUTPUT: (R,vR,vT,z,vz,phi) HISTORY: 2013-12-22 - Written - Bovy (IAS) """ if isinstance(Or,(int,float,numpy.float32,numpy.float64)): #Scalar input Or= numpy.array([Or]) Op= numpy.array([Op]) Oz= numpy.array([Oz]) ar= numpy.array([ar]) ap= numpy.array([ap]) az= numpy.array([az]) #Calculate apar, angle offset along the stream closestIndx= [self._find_closest_trackpointaA(Or[ii],Op[ii],Oz[ii], ar[ii],ap[ii],az[ii], interp=interp)\ for ii in range(len(Or))] out= numpy.empty((6,len(Or))) for ii in range(len(Or)): dOa= numpy.empty(6) if interp: dOa[0]= Or[ii]-self._interpolatedObsTrackAA[closestIndx[ii],0] dOa[1]= Op[ii]-self._interpolatedObsTrackAA[closestIndx[ii],1] dOa[2]= Oz[ii]-self._interpolatedObsTrackAA[closestIndx[ii],2] dOa[3]= ar[ii]-self._interpolatedObsTrackAA[closestIndx[ii],3] dOa[4]= ap[ii]-self._interpolatedObsTrackAA[closestIndx[ii],4] dOa[5]= az[ii]-self._interpolatedObsTrackAA[closestIndx[ii],5] jacIndx= self._find_closest_trackpointaA(Or[ii],Op[ii],Oz[ii], ar[ii],ap[ii],az[ii], interp=False) else: dOa[0]= Or[ii]-self._ObsTrackAA[closestIndx[ii],0] dOa[1]= Op[ii]-self._ObsTrackAA[closestIndx[ii],1] dOa[2]= Oz[ii]-self._ObsTrackAA[closestIndx[ii],2] dOa[3]= ar[ii]-self._ObsTrackAA[closestIndx[ii],3] dOa[4]= ap[ii]-self._ObsTrackAA[closestIndx[ii],4] dOa[5]= az[ii]-self._ObsTrackAA[closestIndx[ii],5] jacIndx= closestIndx[ii] # Find 2nd closest Jacobian point for smoothing da= numpy.stack(\ numpy.meshgrid(_TWOPIWRAPS+ar[ii]-self._progenitor_angle[0], _TWOPIWRAPS+ap[ii]-self._progenitor_angle[1], _TWOPIWRAPS+az[ii]-self._progenitor_angle[2], indexing='xy')).T\ .reshape((len(_TWOPIWRAPS)**3,3)) dapar= self._sigMeanSign\ *numpy.dot(da[numpy.argmin(numpy.linalg.norm(\ numpy.cross(da,self._dsigomeanProgDirection), axis=1))], self._dsigomeanProgDirection) dmJacIndx= numpy.fabs(dapar-self._thetasTrack[jacIndx]) if jacIndx == 0: jacIndx2= jacIndx+1 dmJacIndx2= numpy.fabs(dapar-self._thetasTrack[jacIndx+1]) elif jacIndx == self._nTrackChunks-1: jacIndx2= jacIndx-1 dmJacIndx2= numpy.fabs(dapar-self._thetasTrack[jacIndx-1]) else: dm1= numpy.fabs(dapar-self._thetasTrack[jacIndx-1]) dm2= numpy.fabs(dapar-self._thetasTrack[jacIndx+1]) if dm1 < dm2: jacIndx2= jacIndx-1 dmJacIndx2= dm1 else: jacIndx2= jacIndx+1 dmJacIndx2= dm2 ampJacIndx= dmJacIndx/(dmJacIndx+dmJacIndx2) #Make sure the angles haven't wrapped around if dOa[3] > numpy.pi: dOa[3]-= 2.*numpy.pi elif dOa[3] < -numpy.pi: dOa[3]+= 2.*numpy.pi if dOa[4] > numpy.pi: dOa[4]-= 2.*numpy.pi elif dOa[4] < -numpy.pi: dOa[4]+= 2.*numpy.pi if dOa[5] > numpy.pi: dOa[5]-= 2.*numpy.pi elif dOa[5] < -numpy.pi: dOa[5]+= 2.*numpy.pi #Apply closest jacobian out[:,ii]= numpy.dot((1.-ampJacIndx)*self._allinvjacsTrack[jacIndx,:,:] +ampJacIndx*self._allinvjacsTrack[jacIndx2,:,:], dOa) if interp: out[:,ii]+= self._interpolatedObsTrack[closestIndx[ii]] else: out[:,ii]+= self._ObsTrack[closestIndx[ii]] return out ################################EVALUATE THE DF################################ def __call__(self,*args,**kwargs): """ NAME: __call__ PURPOSE: evaluate the DF INPUT: Either: a) R,vR,vT,z,vz,phi ndarray [nobjects] b) (Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) tuple if aAInput where: Omegar - radial frequency Omegaphi - azimuthal frequency Omegaz - vertical frequency angler - radial angle anglephi - azimuthal angle anglez - vertical angle c) Orbit instance or list thereof log= if True, return the natural log aaInput= (False) if True, option b above OUTPUT: value of DF HISTORY: 2013-12-03 - Written - Bovy (IAS) """ #First parse log log= kwargs.pop('log',True) dOmega, dangle= self.prepData4Call(*args,**kwargs) #Omega part dOmega4dfOmega= dOmega\ -numpy.tile(self._dsigomeanProg.T,(dOmega.shape[1],1)).T logdfOmega= -0.5*numpy.sum(dOmega4dfOmega* numpy.dot(self._sigomatrixinv, dOmega4dfOmega), axis=0)-0.5*self._sigomatrixLogdet\ +numpy.log(numpy.fabs(numpy.dot(self._dsigomeanProgDirection,dOmega))) #Angle part dangle2= numpy.sum(dangle**2.,axis=0) dOmega2= numpy.sum(dOmega**2.,axis=0) dOmegaAngle= numpy.sum(dOmega*dangle,axis=0) logdfA= -0.5/self._sigangle2*(dangle2-dOmegaAngle**2./dOmega2)\ -2.*self._lnsigangle-0.5*numpy.log(dOmega2) #Finite stripping part a0= dOmegaAngle/numpy.sqrt(2.)/self._sigangle/numpy.sqrt(dOmega2) ad= numpy.sqrt(dOmega2)/numpy.sqrt(2.)/self._sigangle\ *(self._tdisrupt-dOmegaAngle/dOmega2) loga= numpy.log((special.erf(a0)+special.erf(ad))/2.) #divided by 2 st 0 for well-within the stream out= logdfA+logdfOmega+loga+self._logmeandetdOdJp if log: return out else: return numpy.exp(out) def prepData4Call(self,*args,**kwargs): """ NAME: prepData4Call PURPOSE: prepare stream data for the __call__ method INPUT: __call__ inputs OUTPUT: (dOmega,dangle); wrt the progenitor; each [3,nobj] HISTORY: 2013-12-04 - Written - Bovy (IAS) """ #First calculate the actionAngle coordinates if they're not given #as such freqsAngles= self._parse_call_args(*args,**kwargs) dOmega= freqsAngles[:3,:]\ -numpy.tile(self._progenitor_Omega.T,(freqsAngles.shape[1],1)).T dangle= freqsAngles[3:,:]\ -numpy.tile(self._progenitor_angle.T,(freqsAngles.shape[1],1)).T #Assuming single wrap, resolve large angle differences (wraps should be marginalized over) dangle[(dangle < -4.)]+= 2.*numpy.pi dangle[(dangle > 4.)]-= 2.*numpy.pi return (dOmega,dangle) def _parse_call_args(self,*args,**kwargs): """Helper function to parse the arguments to the __call__ and related functions, return [6,nobj] array of frequencies (:3) and angles (3:)""" interp= kwargs.get('interp',self._useInterp) if len(args) == 5: raise OSError("Must specify phi for streamdf") elif len(args) == 6: if kwargs.get('aAInput',False): if isinstance(args[0],(int,float,numpy.float32,numpy.float64)): out= numpy.empty((6,1)) else: out= numpy.empty((6,len(args[0]))) for ii in range(6): out[ii,:]= args[ii] return out else: return self._approxaA(*args,interp=interp) elif isinstance(args[0],Orbit): if len(args[0].shape) > 1: raise RuntimeError("Evaluating streamdf with Orbit instances with multi-dimensional shapes is not supported") #pragma: no cover o= args[0] return self._approxaA(o.R(),o.vR(),o.vT(),o.z(),o.vz(),o.phi(), interp=interp) elif isinstance(args[0],list) and isinstance(args[0][0],Orbit): if numpy.any([len(no) > 1 for no in args[0]]): raise RuntimeError('Only single-object Orbit instances can be passed to DF instances at this point') #pragma: no cover R, vR, vT, z, vz, phi= [], [], [], [], [], [] for o in args[0]: R.append(o.R()) vR.append(o.vR()) vT.append(o.vT()) z.append(o.z()) vz.append(o.vz()) phi.append(o.phi()) return self._approxaA(numpy.array(R),numpy.array(vR), numpy.array(vT),numpy.array(z), numpy.array(vz),numpy.array(phi), interp=interp) def callMarg(self,xy,**kwargs): """ NAME: callMarg PURPOSE: evaluate the DF, marginalizing over some directions, in Galactocentric rectangular coordinates (or in observed l,b,D,vlos,pmll,pmbb) coordinates) INPUT: xy - phase-space point [X,Y,Z,vX,vY,vZ]; the distribution of the dimensions set to None is returned interp= (object-wide interp default) if True, use the interpolated stream track cindx= index of the closest point on the (interpolated) stream track if not given, determined from the dimensions given nsigma= (3) number of sigma to marginalize the DF over (approximate sigma) ngl= (5) order of Gauss-Legendre integration lb= (False) if True, xy contains [l,b,D,vlos,pmll,pmbb] in [deg,deg,kpc,km/s,mas/yr,mas/yr] and the marginalized PDF in these coordinates is returned vo= (220) circular velocity to normalize with when lb=True ro= (8) Galactocentric radius to normalize with when lb=True R0= (8) Galactocentric radius of the Sun (kpc) Zsun= (0.0208) Sun's height above the plane (kpc) vsun= ([-11.1,241.92,7.25]) Sun's motion in cylindrical coordinates (vR positive away from center) OUTPUT: p(xy) marginalized over missing directions in xy HISTORY: 2013-12-16 - Written - Bovy (IAS) """ coordGiven= numpy.array([not x is None for x in xy],dtype='bool') if numpy.sum(coordGiven) == 6: raise NotImplementedError("When specifying all coordinates, please use __call__ instead of callMarg") #First construct the Gaussian approximation at this xy gaussmean, gaussvar= self.gaussApprox(xy,**kwargs) cholvar, chollower= stable_cho_factor(gaussvar) #Now Gauss-legendre integrate over missing directions ngl= kwargs.get('ngl',5) nsigma= kwargs.get('nsigma',3) glx, glw= numpy.polynomial.legendre.leggauss(ngl) coordEval= [] weightEval= [] jj= 0 baseX= (glx+1)/2. baseX= list(baseX) baseX.extend(-(glx+1)/2.) baseX= numpy.array(baseX) baseW= glw baseW= list(baseW) baseW.extend(glw) baseW= numpy.array(baseW) for ii in range(6): if not coordGiven[ii]: coordEval.append(nsigma*baseX) weightEval.append(baseW) jj+= 1 else: coordEval.append(xy[ii]*numpy.ones(1)) weightEval.append(numpy.ones(1)) mgrid= numpy.meshgrid(*coordEval,indexing='ij') mgridNotGiven= numpy.array([mgrid[ii].flatten() for ii in range(6) if not coordGiven[ii]]) mgridNotGiven= numpy.dot(cholvar,mgridNotGiven) jj= 0 if coordGiven[0]: iX= mgrid[0] else: iX= mgridNotGiven[jj]+gaussmean[jj] jj+= 1 if coordGiven[1]: iY= mgrid[1] else: iY= mgridNotGiven[jj]+gaussmean[jj] jj+= 1 if coordGiven[2]: iZ= mgrid[2] else: iZ= mgridNotGiven[jj]+gaussmean[jj] jj+= 1 if coordGiven[3]: ivX= mgrid[3] else: ivX= mgridNotGiven[jj]+gaussmean[jj] jj+= 1 if coordGiven[4]: ivY= mgrid[4] else: ivY= mgridNotGiven[jj]+gaussmean[jj] jj+= 1 if coordGiven[5]: ivZ= mgrid[5] else: ivZ= mgridNotGiven[jj]+gaussmean[jj] jj+= 1 iXw, iYw, iZw, ivXw, ivYw, ivZw=\ numpy.meshgrid(*weightEval,indexing='ij') if kwargs.get('lb',False): #Convert to Galactocentric cylindrical coordinates #Setup coordinate transformation kwargs vo= kwargs.get('vo',self._vo) ro= kwargs.get('ro',self._ro) R0= kwargs.get('R0',self._R0) Zsun= kwargs.get('Zsun',self._Zsun) vsun= kwargs.get('vsun',self._vsun) tXYZ= coords.lbd_to_XYZ(iX.flatten(),iY.flatten(), iZ.flatten(), degree=True) iR,iphi,iZ= coords.XYZ_to_galcencyl(tXYZ[:,0],tXYZ[:,1], tXYZ[:,2], Xsun=R0,Zsun=Zsun).T tvxvyvz= coords.vrpmllpmbb_to_vxvyvz(ivX.flatten(), ivY.flatten(), ivZ.flatten(), tXYZ[:,0],tXYZ[:,1], tXYZ[:,2],XYZ=True) ivR,ivT,ivZ= coords.vxvyvz_to_galcencyl(tvxvyvz[:,0], tvxvyvz[:,1], tvxvyvz[:,2], iR,iphi,iZ, galcen=True, vsun=vsun, Xsun=R0,Zsun=Zsun).T iR/= ro iZ/= ro ivR/= vo ivT/= vo ivZ/= vo else: #Convert to cylindrical coordinates iR,iphi,iZ=\ coords.rect_to_cyl(iX.flatten(),iY.flatten(),iZ.flatten()) ivR,ivT,ivZ=\ coords.rect_to_cyl_vec(ivX.flatten(),ivY.flatten(), ivZ.flatten(), iR,iphi,iZ,cyl=True) #Add the additional Jacobian dXdY/dldb... if necessary if kwargs.get('lb',False): #Find the nearest track point interp= kwargs.get('interp',self._useInterp) if not 'cindx' in kwargs: cindx= self._find_closest_trackpointLB(*xy,interp=interp, usev=True) else: cindx= kwargs['cindx'] #Only l,b,d,... to Galactic X,Y,Z,... is necessary because going #from Galactic to Galactocentric has Jacobian determinant 1 if interp: addLogDet= self._interpolatedTrackLogDetJacLB[cindx] else: addLogDet= self._trackLogDetJacLB[cindx] else: addLogDet= 0. logdf= self(iR,ivR,ivT,iZ,ivZ,iphi,log=True) return logsumexp(logdf +numpy.log(iXw.flatten()) +numpy.log(iYw.flatten()) +numpy.log(iZw.flatten()) +numpy.log(ivXw.flatten()) +numpy.log(ivYw.flatten()) +numpy.log(ivZw.flatten()))\ +0.5*numpy.log(numpy.linalg.det(gaussvar))\ +addLogDet def gaussApprox(self,xy,**kwargs): """ NAME: gaussApprox PURPOSE: return the mean and variance of a Gaussian approximation to the stream DF at a given phase-space point in Galactocentric rectangular coordinates (distribution is over missing directions) INPUT: xy - phase-space point [X,Y,Z,vX,vY,vZ]; the distribution of the dimensions set to None is returned interp= (object-wide interp default) if True, use the interpolated stream track cindx= index of the closest point on the (interpolated) stream track if not given, determined from the dimensions given lb= (False) if True, xy contains [l,b,D,vlos,pmll,pmbb] in [deg,deg,kpc,km/s,mas/yr,mas/yr] and the Gaussian approximation in these coordinates is returned OUTPUT: (mean,variance) of the approximate Gaussian DF for the missing directions in xy HISTORY: 2013-12-12 - Written - Bovy (IAS) """ interp= kwargs.get('interp',self._useInterp) lb= kwargs.get('lb',False) #What are we looking for coordGiven= numpy.array([not x is None for x in xy],dtype='bool') nGiven= numpy.sum(coordGiven) #First find the nearest track point if not 'cindx' in kwargs and lb: cindx= self._find_closest_trackpointLB(*xy,interp=interp, usev=True) elif not 'cindx' in kwargs and not lb: cindx= self._find_closest_trackpoint(*xy,xy=True,interp=interp, usev=True) else: cindx= kwargs['cindx'] #Get the covariance matrix if interp and lb: tcov= self._interpolatedAllErrCovsLBUnscaled[cindx] tmean= self._interpolatedObsTrackLB[cindx] elif interp and not lb: tcov= self._interpolatedAllErrCovsXY[cindx] tmean= self._interpolatedObsTrackXY[cindx] elif not interp and lb: tcov= self._allErrCovsLBUnscaled[cindx] tmean= self._ObsTrackLB[cindx] elif not interp and not lb: tcov= self._allErrCovsXY[cindx] tmean= self._ObsTrackXY[cindx] if lb:#Apply scale factors tcov= copy.copy(tcov) tcov*= numpy.tile(self._ErrCovsLBScale,(6,1)) tcov*= numpy.tile(self._ErrCovsLBScale,(6,1)).T #Fancy indexing to recover V22, V11, and V12; V22, V11, V12 as in Appendix B of 0905.2979v1 V11indx0= numpy.array([[ii for jj in range(6-nGiven)] for ii in range(6) if not coordGiven[ii]]) V11indx1= numpy.array([[ii for ii in range(6) if not coordGiven[ii]] for jj in range(6-nGiven)]) V11= tcov[V11indx0,V11indx1] V22indx0= numpy.array([[ii for jj in range(nGiven)] for ii in range(6) if coordGiven[ii]]) V22indx1= numpy.array([[ii for ii in range(6) if coordGiven[ii]] for jj in range(nGiven)]) V22= tcov[V22indx0,V22indx1] V12indx0= numpy.array([[ii for jj in range(nGiven)] for ii in range(6) if not coordGiven[ii]]) V12indx1= numpy.array([[ii for ii in range(6) if coordGiven[ii]] for jj in range(6-nGiven)]) V12= tcov[V12indx0,V12indx1] #Also get m1 and m2, again following Appendix B of 0905.2979v1 m1= tmean[True^coordGiven] m2= tmean[coordGiven] #conditional mean and variance V22inv= numpy.linalg.inv(V22) v2= numpy.array([xy[ii] for ii in range(6) if coordGiven[ii]]) condMean= m1+numpy.dot(V12,numpy.dot(V22inv,v2-m2)) condVar= V11-numpy.dot(V12,numpy.dot(V22inv,V12.T)) return (condMean,condVar) ################################SAMPLE THE DF################################## def sample(self,n,returnaAdt=False,returndt=False,interp=None, xy=False,lb=False): """ NAME: sample PURPOSE: sample from the DF INPUT: n - number of points to return returnaAdt= (False) if True, return (Omega,angle,dt) returndT= (False) if True, also return the time since the star was stripped interp= (object-wide default) use interpolation of the stream track xy= (False) if True, return Galactocentric rectangular coordinates lb= (False) if True, return Galactic l,b,d,vlos,pmll,pmbb coordinates OUTPUT: (R,vR,vT,z,vz,phi) of points on the stream in 6,N array HISTORY: 2013-12-22 - Written - Bovy (IAS) """ #First sample frequencies Om,angle,dt= self._sample_aAt(n) if returnaAdt: if _APY_UNITS and self._voSet and self._roSet: Om=\ units.Quantity(\ Om*conversion.freq_in_Gyr(self._vo,self._ro), unit=1/units.Gyr) angle= units.Quantity(angle,unit=units.rad) dt= units.Quantity(\ dt*conversion.time_in_Gyr(self._vo,self._ro), unit=units.Gyr) return (Om,angle,dt) if interp is None: interp= self._useInterp #Propagate to R,vR,etc. RvR= self._approxaAInv(Om[0,:],Om[1,:],Om[2,:], angle[0,:],angle[1,:],angle[2,:], interp=interp) if returndt and not xy and not lb: if _APY_UNITS and self._voSet and self._roSet: return (units.Quantity(RvR[0]*self._ro,unit=units.kpc), units.Quantity(RvR[1]*self._vo,unit=units.km/units.s), units.Quantity(RvR[2]*self._vo,unit=units.km/units.s), units.Quantity(RvR[3]*self._ro,unit=units.kpc), units.Quantity(RvR[4]*self._vo,unit=units.km/units.s), units.Quantity(RvR[5],unit=units.rad), units.Quantity(\ dt*conversion.time_in_Gyr(self._vo,self._ro), unit=units.Gyr)) return (RvR[0],RvR[1],RvR[2],RvR[3],RvR[4],RvR[5],dt) elif not xy and not lb: if _APY_UNITS and self._voSet and self._roSet: return (units.Quantity(RvR[0]*self._ro,unit=units.kpc), units.Quantity(RvR[1]*self._vo,unit=units.km/units.s), units.Quantity(RvR[2]*self._vo,unit=units.km/units.s), units.Quantity(RvR[3]*self._ro,unit=units.kpc), units.Quantity(RvR[4]*self._vo,unit=units.km/units.s), units.Quantity(RvR[5],unit=units.rad)) return RvR if xy: sX= RvR[0]*numpy.cos(RvR[5]) sY= RvR[0]*numpy.sin(RvR[5]) sZ= RvR[3] svX, svY, svZ=\ coords.cyl_to_rect_vec(RvR[1],RvR[2],RvR[4],RvR[5]) out= numpy.empty((6,n)) out[0]= sX out[1]= sY out[2]= sZ out[3]= svX out[4]= svY out[5]= svZ if returndt: if _APY_UNITS and self._voSet and self._roSet: return (units.Quantity(out[0]*self._ro,unit=units.kpc), units.Quantity(out[1]*self._ro,unit=units.kpc), units.Quantity(out[2]*self._ro,unit=units.kpc), units.Quantity(out[3]*self._vo,unit=units.km/units.s), units.Quantity(out[4]*self._vo,unit=units.km/units.s), units.Quantity(out[5]*self._vo,unit=units.km/units.s), units.Quantity(\ dt*conversion.time_in_Gyr(self._vo,self._ro), unit=units.Gyr)) return (out[0],out[1],out[2],out[3],out[4],out[5],dt) else: if _APY_UNITS and self._voSet and self._roSet: return (units.Quantity(out[0]*self._ro,unit=units.kpc), units.Quantity(out[1]*self._ro,unit=units.kpc), units.Quantity(out[2]*self._ro,unit=units.kpc), units.Quantity(out[3]*self._vo,unit=units.km/units.s), units.Quantity(out[4]*self._vo,unit=units.km/units.s), units.Quantity(out[5]*self._vo,unit=units.km/units.s)) return out if lb: vo= self._vo ro= self._ro R0= self._R0 Zsun= self._Zsun vsun= self._vsun XYZ= coords.galcencyl_to_XYZ(RvR[0]*ro,RvR[5],RvR[3]*ro, Xsun=R0,Zsun=Zsun).T vXYZ= coords.galcencyl_to_vxvyvz(RvR[1]*vo,RvR[2]*vo,RvR[4]*vo, RvR[5], vsun=vsun,Xsun=R0,Zsun=Zsun).T slbd=coords.XYZ_to_lbd(XYZ[0],XYZ[1],XYZ[2], degree=True) svlbd= coords.vxvyvz_to_vrpmllpmbb(vXYZ[0],vXYZ[1],vXYZ[2], slbd[:,0],slbd[:,1], slbd[:,2], degree=True) out= numpy.empty((6,n)) out[0]= slbd[:,0] out[1]= slbd[:,1] out[2]= slbd[:,2] out[3]= svlbd[:,0] out[4]= svlbd[:,1] out[5]= svlbd[:,2] if returndt: if _APY_UNITS and self._voSet and self._roSet: return (units.Quantity(out[0],unit=units.deg), units.Quantity(out[1],unit=units.deg), units.Quantity(out[2],unit=units.kpc), units.Quantity(out[3],unit=units.km/units.s), units.Quantity(out[4],unit=units.mas/units.yr), units.Quantity(out[5],unit=units.mas/units.yr), units.Quantity(\ dt*conversion.time_in_Gyr(self._vo,self._ro), unit=units.Gyr)) return (out[0],out[1],out[2],out[3],out[4],out[5],dt) else: if _APY_UNITS and self._voSet and self._roSet: return (units.Quantity(out[0],unit=units.deg), units.Quantity(out[1],unit=units.deg), units.Quantity(out[2],unit=units.kpc), units.Quantity(out[3],unit=units.km/units.s), units.Quantity(out[4],unit=units.mas/units.yr), units.Quantity(out[5],unit=units.mas/units.yr)) return out def _sample_aAt(self,n): """Sampling frequencies, angles, and times part of sampling""" #Sample frequency along largest eigenvalue using ARS dO1s=\ ars.ars([0.,0.],[True,False], [self._meandO-numpy.sqrt(self._sortedSigOEig[2]), self._meandO+numpy.sqrt(self._sortedSigOEig[2])], _h_ars,_hp_ars,nsamples=n, hxparams=(self._meandO,self._sortedSigOEig[2]), maxn=100) dO1s= numpy.array(dO1s)*self._sigMeanSign dO2s= numpy.random.normal(size=n)*numpy.sqrt(self._sortedSigOEig[1]) dO3s= numpy.random.normal(size=n)*numpy.sqrt(self._sortedSigOEig[0]) #Rotate into dOs in R,phi,z coordinates dO= numpy.vstack((dO3s,dO2s,dO1s)) dO= numpy.dot(self._sigomatrixEig[1][:,self._sigomatrixEigsortIndx], dO) Om= dO+numpy.tile(self._progenitor_Omega.T,(n,1)).T #Also generate angles da= numpy.random.normal(size=(3,n))*self._sigangle #And a random time dt= self.sample_t(n) #Integrate the orbits relative to the progenitor da+= dO*numpy.tile(dt,(3,1)) angle= da+numpy.tile(self._progenitor_angle.T,(n,1)).T return (Om,angle,dt) def sample_t(self,n): """ NAME: sample_t PURPOSE: generate a stripping time (time since stripping); simple implementation could be replaced by more complicated distributions in sub-classes of streamdf INPUT: n - number of points to return OUTPUT: array of n stripping times HISTORY: 2015-09-16 - Written - Bovy (UofT) """ return numpy.random.uniform(size=n)*self._tdisrupt def _h_ars(x,params): """ln p(Omega) for ARS""" mO, sO2= params return -0.5*(x-mO)**2./sO2+numpy.log(x) def _hp_ars(x,params): """d ln p(Omega) / d Omega for ARS""" mO, sO2= params return -(x-mO)/sO2+1./x def _determine_stream_track_single(aA,progenitorTrack,trackt, progenitor_angle,sigMeanSign, dsigomeanProgDirection,meanOmega, thetasTrack): #Setup output allAcfsTrack= numpy.empty(9) alljacsTrack= numpy.empty((6,6)) allinvjacsTrack= numpy.empty((6,6)) ObsTrack= numpy.empty(6) ObsTrackAA= numpy.empty(6) detdOdJ= numpy.empty(6) #Calculate tacfs= aA.actionsFreqsAngles(progenitorTrack(trackt)) allAcfsTrack[0]= tacfs[0][0] allAcfsTrack[1]= tacfs[1][0] allAcfsTrack[2]= tacfs[2][0] for jj in range(3,9): allAcfsTrack[jj]= tacfs[jj] tjac= calcaAJac(progenitorTrack(trackt).vxvv[0], aA, dxv=None,actionsFreqsAngles=True, lb=False, _initacfs=tacfs) alljacsTrack[:,:]= tjac[3:,:] tinvjac= numpy.linalg.inv(tjac[3:,:]) allinvjacsTrack[:,:]= tinvjac #Also store detdOdJ jindx= numpy.array([True,True,True,False,False,False,True,True,True], dtype='bool') dOdJ= numpy.dot(tjac[3:,:],numpy.linalg.inv(tjac[jindx,:]))[0:3,0:3] detdOdJ= numpy.linalg.det(dOdJ) theseAngles= numpy.mod(progenitor_angle\ +thetasTrack\ *sigMeanSign\ *dsigomeanProgDirection, 2.*numpy.pi) ObsTrackAA[3:]= theseAngles diffAngles= theseAngles-allAcfsTrack[6:] diffAngles[(diffAngles > numpy.pi)]= diffAngles[(diffAngles > numpy.pi)]-2.*numpy.pi diffAngles[(diffAngles < -numpy.pi)]= diffAngles[(diffAngles < -numpy.pi)]+2.*numpy.pi thisFreq= meanOmega(thetasTrack) ObsTrackAA[:3]= thisFreq diffFreqs= thisFreq-allAcfsTrack[3:6] ObsTrack[:]= numpy.dot(tinvjac, numpy.hstack((diffFreqs,diffAngles))) ObsTrack[0]+= \ progenitorTrack(trackt).R() ObsTrack[1]+= \ progenitorTrack(trackt).vR() ObsTrack[2]+= \ progenitorTrack(trackt).vT() ObsTrack[3]+= \ progenitorTrack(trackt).z() ObsTrack[4]+= \ progenitorTrack(trackt).vz() ObsTrack[5]+= \ progenitorTrack(trackt).phi() return [allAcfsTrack,alljacsTrack,allinvjacsTrack,ObsTrack,ObsTrackAA, detdOdJ] def _determine_stream_track_TM_single(aAT, progenitor_j, progenitor_Omega, progenitor_angle, dOdJ,dJdO, sigMeanSign, dsigomeanProgDirection, meanOmega, thetasTrack): #Setup output detdOdJ= numpy.empty(6) # Calculate track thisFreq= meanOmega(thetasTrack) theseAngles= numpy.mod(progenitor_angle\ +thetasTrack\ *sigMeanSign\ *dsigomeanProgDirection, 2.*numpy.pi) # Compute thisActions from thisFreq and dJ/dO near the progenitor thisActions= numpy.dot(dJdO,thisFreq-progenitor_Omega)+progenitor_j # Compute (x,v) using TM, also compute the Jacobian xvJacHess= aAT.xvJacobianFreqs(\ thisActions[0],thisActions[1],thisActions[2], numpy.array([theseAngles[0]]),numpy.array([theseAngles[1]]), numpy.array([theseAngles[2]])) # Output ObsTrack= xvJacHess[0] alljacsTrackTemp= numpy.linalg.inv(xvJacHess[1][0]) alljacsTrack= copy.copy(alljacsTrackTemp) # dOdJ here because it might be more precise alljacsTrack[:3,:3]= numpy.dot(dOdJ,alljacsTrackTemp[:3,:3]) alljacsTrack[3:,:3]= numpy.dot(dOdJ,alljacsTrackTemp[3:,:3]) allinvjacsTrack= numpy.linalg.inv(alljacsTrack) ObsTrackAA= numpy.empty(6) ObsTrackAA[:3]= thisFreq ObsTrackAA[3:]= theseAngles detdOdJ= numpy.linalg.det(xvJacHess[2]) return [alljacsTrack,allinvjacsTrack,ObsTrack,ObsTrackAA,detdOdJ] def _determine_stream_track_TM_approxConstantTrackFreq(aAT, progenitor_j, progenitor_Omega, progenitor_angle, dOdJ,dJdO, sigMeanSign, dsigomeanProgDirection, meanOmega, thetasTrack): #Setup output detdOdJ= numpy.empty(6) # Calculate track thisFreq= meanOmega(thetasTrack[0]) theseAngles= numpy.mod(numpy.tile(progenitor_angle,(len(thetasTrack),1))\ +numpy.tile(thetasTrack,(3,1)).T\ *sigMeanSign\ *dsigomeanProgDirection, 2.*numpy.pi) # Compute thisActions from thisFreq and dJ/dO near the progenitor thisActions= numpy.dot(dJdO,thisFreq-progenitor_Omega)+progenitor_j # Compute (x,v) using TM, also compute the Jacobian xvJacHess= aAT.xvJacobianFreqs(\ thisActions[0],thisActions[1],thisActions[2], theseAngles[:,0],theseAngles[:,1],theseAngles[:,2]) # Output ObsTrack= xvJacHess[0] alljacsTrack= numpy.empty((len(thetasTrack),6,6)) allinvjacsTrack= numpy.empty((len(thetasTrack),6,6)) detdOdJ= numpy.empty(len(thetasTrack)) for ii in range(len(thetasTrack)): alljacsTrackTemp= numpy.linalg.inv(xvJacHess[1][ii]) alljacsTrack[ii]= copy.copy(alljacsTrackTemp) # dOdJ here because it might be more precise alljacsTrack[ii,:3,:3]= numpy.dot(dOdJ,alljacsTrackTemp[:3,:3]) alljacsTrack[ii,3:,:3]= numpy.dot(dOdJ,alljacsTrackTemp[3:,:3]) allinvjacsTrack[ii]= numpy.linalg.inv(alljacsTrack[ii]) detdOdJ= numpy.linalg.det(xvJacHess[2]) ObsTrackAA= numpy.empty((len(thetasTrack),6)) ObsTrackAA[:,:3]= thisFreq ObsTrackAA[:,3:]= theseAngles return [alljacsTrack,allinvjacsTrack,ObsTrack,ObsTrackAA,detdOdJ] def _determine_stream_spread_single(sigomatrixEig, thetasTrack, sigOmega, sigAngle, allinvjacsTrack): """sigAngle input may either be a function that returns the dispersion in perpendicular angle as a function of parallel angle, or a value""" #Estimate the spread in all frequencies and angles sigObig2= sigOmega(thetasTrack)**2. tsigOdiag= copy.copy(sigomatrixEig[0]) tsigOdiag[numpy.argmax(tsigOdiag)]= sigObig2 tsigO= numpy.dot(sigomatrixEig[1], numpy.dot(numpy.diag(tsigOdiag), numpy.linalg.inv(sigomatrixEig[1]))) #angles if hasattr(sigAngle,'__call__'): sigangle2= sigAngle(thetasTrack)**2. else: sigangle2= sigAngle**2. tsigadiag= numpy.ones(3)*sigangle2 tsigadiag[numpy.argmax(tsigOdiag)]= 1. tsiga= numpy.dot(sigomatrixEig[1], numpy.dot(numpy.diag(tsigadiag), numpy.linalg.inv(sigomatrixEig[1]))) #correlations, assume half correlated for now (can be calculated) correlations= numpy.diag(0.5*numpy.ones(3))*numpy.sqrt(tsigOdiag*tsigadiag) correlations[numpy.argmax(tsigOdiag),numpy.argmax(tsigOdiag)]= 0. correlations= numpy.dot(sigomatrixEig[1], numpy.dot(correlations, numpy.linalg.inv(sigomatrixEig[1]))) #Now convert fullMatrix= numpy.empty((6,6)) fullMatrix[:3,:3]= tsigO fullMatrix[3:,3:]= tsiga fullMatrix[3:,:3]= correlations fullMatrix[:3,3:]= correlations.T return numpy.dot(allinvjacsTrack,numpy.dot(fullMatrix,allinvjacsTrack.T)) def calcaAJac(xv,aA,dxv=None,freqs=False,dOdJ=False,actionsFreqsAngles=False, lb=False,coordFunc=None, vo=220.,ro=8.,R0=8.,Zsun=0.0208,vsun=[-11.1,8.*30.24,7.25], _initacfs=None): """ NAME: calcaAJac PURPOSE: calculate the Jacobian d(J,theta)/d(x,v) INPUT: xv - phase-space point: Either 1) [R,vR,vT,z,vz,phi] 2) [l,b,D,vlos,pmll,pmbb] (if lb=True, see below) 3) list/array of 6 numbers that can be transformed into (normalized) R,vR,vT,z,vz,phi using coordFunc aA - actionAngle instance dxv - infinitesimal to use (rescaled for lb, so think fractionally)) freqs= (False) if True, go to frequencies rather than actions dOdJ= (False), actually calculate d Frequency / d action actionsFreqsAngles= (False) if True, calculate d(action,freq.,angle)/d (xv) lb= (False) if True, start with (l,b,D,vlos,pmll,pmbb) in (deg,deg,kpc,km/s,mas/yr,mas/yr) vo= (220) circular velocity to normalize with when lb=True ro= (8) Galactocentric radius to normalize with when lb=True R0= (8) Galactocentric radius of the Sun (kpc) Zsun= (0.0208) Sun's height above the plane (kpc) vsun= ([-11.1,241.92,7.25]) Sun's motion in cylindrical coordinates (vR positive away from center) coordFunc= (None) if set, this is a function that takes xv and returns R,vR,vT,z,vz,phi in normalized units (units where vc=1 at r=1 if the potential is normalized that way, for example) OUTPUT: Jacobian matrix HISTORY: 2013-11-25 - Written - Bovy (IAS) """ if lb: coordFunc= lambda x: lbCoordFunc(xv,vo,ro,R0,Zsun,vsun) if not coordFunc is None: R, vR, vT, z, vz, phi= coordFunc(xv) else: R, vR, vT, z, vz, phi= xv[0],xv[1],xv[2],xv[3],xv[4],xv[5] if dxv is None: dxv= 10.**-8.*numpy.ones(6) if lb: #Re-scale some of the differences, to be more natural dxv[0]*= 180./numpy.pi dxv[1]*= 180./numpy.pi dxv[2]*= ro dxv[3]*= vo dxv[4]*= vo/4.74047/xv[2] dxv[5]*= vo/4.74047/xv[2] if actionsFreqsAngles: jac= numpy.zeros((9,6)) else: jac= numpy.zeros((6,6)) if dOdJ: jac2= numpy.zeros((6,6)) if _initacfs is None: jr,lz,jz,Or,Ophi,Oz,ar,aphi,az\ = aA.actionsFreqsAngles(R,vR,vT,z,vz,phi) else: jr,lz,jz,Or,Ophi,Oz,ar,aphi,az\ = _initacfs for ii in range(6): temp= xv[ii]+dxv[ii] #Trick to make sure dxv is representable dxv[ii]= temp-xv[ii] xv[ii]+= dxv[ii] if not coordFunc is None: tR, tvR, tvT, tz, tvz, tphi= coordFunc(xv) else: tR, tvR, tvT, tz, tvz, tphi= xv[0],xv[1],xv[2],xv[3],xv[4],xv[5] tjr,tlz,tjz,tOr,tOphi,tOz,tar,taphi,taz\ = aA.actionsFreqsAngles(tR,tvR,tvT,tz,tvz,tphi) xv[ii]-= dxv[ii] angleIndx= 3 if actionsFreqsAngles: jac[0,ii]= (tjr-jr)/dxv[ii] jac[1,ii]= (tlz-lz)/dxv[ii] jac[2,ii]= (tjz-jz)/dxv[ii] jac[3,ii]= (tOr-Or)/dxv[ii] jac[4,ii]= (tOphi-Ophi)/dxv[ii] jac[5,ii]= (tOz-Oz)/dxv[ii] angleIndx= 6 elif freqs: jac[0,ii]= (tOr-Or)/dxv[ii] jac[1,ii]= (tOphi-Ophi)/dxv[ii] jac[2,ii]= (tOz-Oz)/dxv[ii] else: jac[0,ii]= (tjr-jr)/dxv[ii] jac[1,ii]= (tlz-lz)/dxv[ii] jac[2,ii]= (tjz-jz)/dxv[ii] if dOdJ: jac2[0,ii]= (tOr-Or)/dxv[ii] jac2[1,ii]= (tOphi-Ophi)/dxv[ii] jac2[2,ii]= (tOz-Oz)/dxv[ii] #For the angles, make sure we do not hit a turning point if tar-ar > numpy.pi: jac[angleIndx,ii]= (tar-ar-2.*numpy.pi)/dxv[ii] elif tar-ar < -numpy.pi: jac[angleIndx,ii]= (tar-ar+2.*numpy.pi)/dxv[ii] else: jac[angleIndx,ii]= (tar-ar)/dxv[ii] if taphi-aphi > numpy.pi: jac[angleIndx+1,ii]= (taphi-aphi-2.*numpy.pi)/dxv[ii] elif taphi-aphi < -numpy.pi: jac[angleIndx+1,ii]= (taphi-aphi+2.*numpy.pi)/dxv[ii] else: jac[angleIndx+1,ii]= (taphi-aphi)/dxv[ii] if taz-az > numpy.pi: jac[angleIndx+2,ii]= (taz-az-2.*numpy.pi)/dxv[ii] if taz-az < -numpy.pi: jac[angleIndx+2,ii]= (taz-az+2.*numpy.pi)/dxv[ii] else: jac[angleIndx+2,ii]= (taz-az)/dxv[ii] if dOdJ: jac2[3,:]= jac[3,:] jac2[4,:]= jac[4,:] jac2[5,:]= jac[5,:] jac= numpy.dot(jac2,numpy.linalg.inv(jac))[0:3,0:3] return jac def lbCoordFunc(xv,vo,ro,R0,Zsun,vsun): #Input is (l,b,D,vlos,pmll,pmbb) in (deg,deg,kpc,km/s,mas/yr,mas/yr) X,Y,Z= coords.lbd_to_XYZ(xv[0],xv[1],xv[2],degree=True) R,phi,Z= coords.XYZ_to_galcencyl(X,Y,Z, Xsun=R0,Zsun=Zsun) vx,vy,vz= coords.vrpmllpmbb_to_vxvyvz(xv[3],xv[4],xv[5], X,Y,Z,XYZ=True) vR,vT,vZ= coords.vxvyvz_to_galcencyl(vx,vy,vz,R,phi,Z,galcen=True, vsun=vsun,Xsun=R0,Zsun=Zsun) R/= ro Z/= ro vR/= vo vT/= vo vZ/= vo return (R,vR,vT,Z,vZ,phi) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/streamgapdf.py0000644000175100001710000021611014327773303016622 0ustar00runnerdocker# The DF of a gap in a tidal stream import copy import multiprocessing import warnings from functools import wraps import numpy from scipy import integrate, interpolate, special from ..orbit import Orbit from ..potential import (MovingObjectPotential, PlummerPotential, evaluateRforces) from ..potential import flatten as flatten_potential from ..util import (_rotate_to_arbitrary_vector, conversion, coords, galpyWarning, multi) from ..util.conversion import physical_conversion from . import streamdf from .df import df from .streamdf import _determine_stream_track_single def impact_check_range(func): """Decorator to check the range of interpolated kicks""" @wraps(func) def impact_wrapper(*args,**kwargs): if isinstance(args[1],numpy.ndarray): out= numpy.zeros(len(args[1])) goodIndx= (args[1] < args[0]._deltaAngleTrackImpact)*(args[1] > 0.) out[goodIndx]= func(args[0],args[1][goodIndx]) return out elif args[1] >= args[0]._deltaAngleTrackImpact or args[1] <= 0.: return 0. else: return func(*args,**kwargs) return impact_wrapper class streamgapdf(streamdf.streamdf): """The DF of a gap in a tidal stream""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: Initialize the DF of a gap in a stellar stream INPUT: streamdf args and kwargs Subhalo and impact parameters: impactb= impact parameter (can be Quantity) subhalovel= velocity of the subhalo shape=(3) (can be Quantity) timpact time since impact (can be Quantity) impact_angle= angle offset from progenitor at which the impact occurred (rad) (can be Quantity) Subhalo: specify either 1( mass and size of Plummer sphere or 2( general spherical-potential object (kick is numerically computed) 1( GM= mass of the subhalo (can be Quantity) rs= size parameter of the subhalo (can be Quantity) 2( subhalopot= galpy potential object or list thereof (should be spherical) 3( hernquist= (False) if True, use Hernquist kicks for GM/rs deltaAngleTrackImpact= (None) angle to estimate the stream track over to determine the effect of the impact [similar to deltaAngleTrack] (rad) nTrackChunksImpact= (floor(deltaAngleTrack/0.15)+1) number of chunks to divide the progenitor track in near the impact [similar to nTrackChunks] nKickPoints= (30xnTrackChunksImpact) number of points along the stream to compute the kicks at (kicks are then interpolated); '30' chosen such that higherorderTrack can be set to False and get calculations accurate to > 99% nokicksetup= (False) if True, only run as far as setting up the coordinate transformation at the time of impact (useful when using this in streampepperdf) spline_order= (3) order of the spline to interpolate the kicks with higherorderTrack= (False) if True, calculate the track using higher-order terms OUTPUT: object HISTORY: 2015-06-02 - Started - Bovy (IAS) """ df.__init__(self,ro=kwargs.get('ro',None),vo=kwargs.get('vo',None)) # Parse kwargs impactb= conversion.parse_length(kwargs.pop('impactb',1.),ro=self._ro) subhalovel= conversion.parse_velocity(\ kwargs.pop('subhalovel',numpy.array([0.,1.,0.])), vo=self._vo) hernquist= kwargs.pop('hernquist',False) GM= kwargs.pop('GM',None) if not GM is None: GM= conversion.parse_mass(GM,ro=self._ro,vo=self._vo) rs= kwargs.pop('rs',None) if not rs is None: rs= conversion.parse_length(rs,ro=self._ro) subhalopot= kwargs.pop('subhalopot',None) timpact= conversion.parse_time(kwargs.pop('timpact',1.), ro=self._ro,vo=self._vo) impact_angle= conversion.parse_angle(kwargs.pop('impact_angle',1.)) nokicksetup= kwargs.pop('nokicksetup',False) deltaAngleTrackImpact= kwargs.pop('deltaAngleTrackImpact',None) nTrackChunksImpact= kwargs.pop('nTrackChunksImpact',None) nKickPoints= kwargs.pop('nKickPoints',None) spline_order= kwargs.pop('spline_order',3) higherorderTrack= kwargs.pop('higherorderTrack',False) # For setup later nTrackChunks= kwargs.pop('nTrackChunks',None) interpTrack= kwargs.pop('interpTrack', streamdf._INTERPDURINGSETUP) useInterp= kwargs.pop('useInterp', streamdf._USEINTERP) # Analytical Plummer or general potential? self._general_kick= GM is None or rs is None if self._general_kick and subhalopot is None: raise OSError("One of (GM=, rs=) or subhalopot= needs to be set to specify the subhalo's structure") # Now run the regular streamdf setup, but without calculating the # stream track (nosetup=True) kwargs['nosetup']= True super().__init__(*args,**kwargs) # Setup the machinery to go between (x,v) and (Omega,theta) # near the impact self._determine_nTrackIterations(kwargs.get('nTrackIterations',None)) self._determine_deltaAngleTrackImpact(deltaAngleTrackImpact,timpact) self._determine_impact_coordtransform(self._deltaAngleTrackImpact, nTrackChunksImpact, timpact,impact_angle) # Set nKickPoints if nKickPoints is None: self._nKickPoints= 30*self._nTrackChunksImpact else: self._nKickPoints= nKickPoints if nokicksetup: return None # Compute \Delta Omega ( \Delta \theta_perp) and \Delta theta, # setup interpolating function self._determine_deltav_kick(impact_angle,impactb,subhalovel, GM,rs,subhalopot, spline_order,hernquist) self._determine_deltaOmegaTheta_kick(spline_order) # Then pass everything to the normal streamdf setup self.nInterpolatedTrackChunks= 201 #more expensive now self._higherorderTrack= higherorderTrack super()._determine_stream_track(nTrackChunks) self._useInterp= useInterp if interpTrack or self._useInterp: super()._interpolate_stream_track() super()._interpolate_stream_track_aA() super().calc_stream_lb() return None def pOparapar(self,Opar,apar): """ NAME: pOparapar PURPOSE: return the probability of a given parallel (frequency,angle) offset pair INPUT: Opar - parallel frequency offset (array) (can be Quantity) apar - parallel angle offset along the stream (scalar) (can be Quantity) OUTPUT: p(Opar,apar) HISTORY: 2015-11-17 - Written - Bovy (UofT) """ Opar= conversion.parse_frequency(Opar,ro=self._ro,vo=self._vo) apar= conversion.parse_angle(apar) if isinstance(Opar,(int,float,numpy.float32,numpy.float64)): Opar= numpy.array([Opar]) out= numpy.zeros(len(Opar)) # Compute ts and where they were at impact for all ts= apar/Opar apar_impact= apar-Opar*self._timpact dOpar_impact= self._kick_interpdOpar(apar_impact) Opar_b4impact= Opar-dOpar_impact # Evaluate the smooth model in the two regimes: # stripped before or after impact afterIndx= (ts < self._timpact)*(ts >= 0.) out[afterIndx]=\ super().pOparapar(Opar[afterIndx], apar) out[True^afterIndx]=\ super().pOparapar(Opar_b4impact[True^afterIndx], apar_impact[True^afterIndx], tdisrupt= self._tdisrupt-self._timpact) return out def _density_par(self,dangle,tdisrupt=None,approx=True, higherorder=None): """The raw density as a function of parallel angle, approx= use faster method that directly integrates the spline representation""" if higherorder is None: higherorder= self._higherorderTrack if tdisrupt is None: tdisrupt= self._tdisrupt if approx: return self._density_par_approx(dangle,tdisrupt, higherorder=higherorder) else: return integrate.quad(lambda T: numpy.sqrt(self._sortedSigOEig[2])\ *(1+T*T)/(1-T*T)**2.\ *self.pOparapar(T/(1-T*T)\ *numpy.sqrt(self._sortedSigOEig[2])\ +self._meandO,dangle), -1.,1.)[0] def _density_par_approx(self,dangle,tdisrupt,_return_array=False, higherorder=False): """Compute the density as a function of parallel angle using the spline representation + approximations""" # First construct the breakpoints for this dangle Oparb= (dangle-self._kick_interpdOpar_poly.x)/self._timpact # Find the lower limit of the integration in the pw-linear-kick approx. lowbindx,lowx= self.minOpar(dangle,tdisrupt,_return_raw=True) lowbindx= numpy.arange(len(Oparb)-1)[lowbindx] Oparb[lowbindx+1]= Oparb[lowbindx]-lowx # Now integrate between breakpoints out= (0.5/(1.+self._kick_interpdOpar_poly.c[-2]*self._timpact)\ *(special.erf(1./numpy.sqrt(2.*self._sortedSigOEig[2])\ *(Oparb[:-1]-self._kick_interpdOpar_poly.c[-1]-self._meandO))\ -special.erf(1./numpy.sqrt(2.*self._sortedSigOEig[2])*(numpy.roll(Oparb,-1)[:-1]-self._kick_interpdOpar_poly.c[-1]-self._meandO\ -self._kick_interpdOpar_poly.c[-2]*self._timpact*(Oparb-numpy.roll(Oparb,-1))[:-1])))) if _return_array: return out out= numpy.sum(out[:lowbindx+1]) if higherorder: # Add higher-order contribution out+= self._density_par_approx_higherorder(Oparb,lowbindx) # Add integration to infinity out+= 0.5*(1.+special.erf((self._meandO-Oparb[0])\ /numpy.sqrt(2.*self._sortedSigOEig[2]))) return out def _density_par_approx_higherorder(self,Oparb,lowbindx, _return_array=False, gaussxpolyInt=None): """Contribution from non-linear spline terms""" spline_order= self._kick_interpdOpar_raw._eval_args[2] if spline_order == 1: return 0. # Form all Gaussian-like integrals necessary ll= (numpy.roll(Oparb,-1)[:-1]-self._kick_interpdOpar_poly.c[-1]\ -self._meandO\ -self._kick_interpdOpar_poly.c[-2]*self._timpact\ *(Oparb-numpy.roll(Oparb,-1))[:-1])\ /numpy.sqrt(2.*self._sortedSigOEig[2]) ul= (Oparb[:-1]-self._kick_interpdOpar_poly.c[-1]-self._meandO)\ /numpy.sqrt(2.*self._sortedSigOEig[2]) if gaussxpolyInt is None: gaussxpolyInt=\ self._densMoments_approx_higherorder_gaussxpolyInts(\ ll,ul,spline_order+1) # Now multiply in the coefficients for each order powers= numpy.tile(numpy.arange(spline_order+1)[::-1], (len(ul),1)).T gaussxpolyInt*= -0.5*(-numpy.sqrt(2.))**(powers+1)\ *self._sortedSigOEig[2]**(0.5*(powers-1)) powers= numpy.tile(numpy.arange(spline_order+1)[::-1][:-2], (len(ul),1)).T for jj in range(spline_order+1): gaussxpolyInt[-jj-1]*= numpy.sum(\ self._kick_interpdOpar_poly.c[:-2] *self._timpact**powers /(1.+self._kick_interpdOpar_poly.c[-2]*self._timpact) **(powers+1) *special.binom(powers,jj) *(Oparb[:-1]-self._kick_interpdOpar_poly.c[-1]-self._meandO) **(powers-jj),axis=0) if _return_array: return numpy.sum(gaussxpolyInt,axis=0) else: return numpy.sum(gaussxpolyInt[:,:lowbindx+1]) def _densMoments_approx_higherorder_gaussxpolyInts(self,ll,ul,maxj): """Calculate all of the polynomial x Gaussian integrals occurring in the higher-order terms, recursively""" gaussxpolyInt= numpy.zeros((maxj,len(ul))) gaussxpolyInt[-1]= 1./numpy.sqrt(numpy.pi)\ *(numpy.exp(-ll**2.)-numpy.exp(-ul**2.)) gaussxpolyInt[-2]= 1./numpy.sqrt(numpy.pi)\ *(numpy.exp(-ll**2.)*ll-numpy.exp(-ul**2.)*ul)\ +0.5*(special.erf(ul)-special.erf(ll)) for jj in range(maxj-2): gaussxpolyInt[-jj-3]= 1./numpy.sqrt(numpy.pi)\ *(numpy.exp(-ll**2.)*ll**(jj+2)-numpy.exp(-ul**2.)*ul**(jj+2))\ +0.5*(jj+2)*gaussxpolyInt[-jj-1] return gaussxpolyInt def minOpar(self,dangle,tdisrupt=None,_return_raw=False): """ NAME: minOpar PURPOSE: return the approximate minimum parallel frequency at a given angle INPUT: dangle - parallel angle OUTPUT: minimum frequency that gets to this parallel angle HISTORY: 2015-12-28 - Written - Bovy (UofT) """ if tdisrupt is None: tdisrupt= self._tdisrupt # First construct the breakpoints for this dangle Oparb= (dangle-self._kick_interpdOpar_poly.x[:-1])/self._timpact # Find the lower limit of the integration in the pw-linear-kick approx. lowx= ((Oparb-self._kick_interpdOpar_poly.c[-1])\ *(tdisrupt-self._timpact)+Oparb*self._timpact-dangle)\ /((tdisrupt-self._timpact)\ *(1.+self._kick_interpdOpar_poly.c[-2]*self._timpact)\ +self._timpact) lowx[lowx < 0.]= numpy.inf lowbindx= numpy.argmin(lowx) if _return_raw: return (lowbindx,lowx[lowbindx]) else: return Oparb[lowbindx]-lowx[lowbindx] @physical_conversion('frequency',pop=True) def meanOmega(self,dangle,oned=False,tdisrupt=None,approx=True, higherorder=None): """ NAME: meanOmega PURPOSE: calculate the mean frequency as a function of angle, assuming a uniform time distribution up to a maximum time INPUT: dangle - angle offset oned= (False) if True, return the 1D offset from the progenitor (along the direction of disruption) approx= (True) if True, compute the mean Omega by direct integration of the spline representation higherorder= (object-wide default higherorderTrack) if True, include higher-order spline terms in the approximate computation OUTPUT: mean Omega HISTORY: 2015-11-17 - Written - Bovy (UofT) """ if higherorder is None: higherorder= self._higherorderTrack if tdisrupt is None: tdisrupt= self._tdisrupt if approx: num= self._meanOmega_num_approx(dangle,tdisrupt, higherorder=higherorder) else: num=\ integrate.quad(lambda T: (T/(1-T*T)\ *numpy.sqrt(self._sortedSigOEig[2])\ +self._meandO)\ *numpy.sqrt(self._sortedSigOEig[2])\ *(1+T*T)/(1-T*T)**2.\ *self.pOparapar(T/(1-T*T)\ *numpy.sqrt(self._sortedSigOEig[2])\ +self._meandO,dangle), -1.,1.)[0] denom= self._density_par(dangle,tdisrupt=tdisrupt,approx=approx, higherorder=higherorder) dO1D= num/denom if oned: return dO1D else: return self._progenitor_Omega+dO1D*self._dsigomeanProgDirection\ *self._sigMeanSign def _meanOmega_num_approx(self,dangle,tdisrupt,higherorder=False): """Compute the numerator going into meanOmega using the direct integration of the spline representation""" # First construct the breakpoints for this dangle Oparb= (dangle-self._kick_interpdOpar_poly.x)/self._timpact # Find the lower limit of the integration in the pw-linear-kick approx. lowbindx,lowx= self.minOpar(dangle,tdisrupt,_return_raw=True) lowbindx= numpy.arange(len(Oparb)-1)[lowbindx] Oparb[lowbindx+1]= Oparb[lowbindx]-lowx # Now integrate between breakpoints out= numpy.sum(((Oparb[:-1] +(self._meandO+self._kick_interpdOpar_poly.c[-1] -Oparb[:-1])/ (1.+self._kick_interpdOpar_poly.c[-2]*self._timpact)) *self._density_par_approx(dangle,tdisrupt, _return_array=True) +numpy.sqrt(self._sortedSigOEig[2]/2./numpy.pi)/ (1.+self._kick_interpdOpar_poly.c[-2]*self._timpact)**2. *(numpy.exp(-0.5*(Oparb[:-1] -self._kick_interpdOpar_poly.c[-1] -(1.+self._kick_interpdOpar_poly.c[-2]*self._timpact) *(Oparb-numpy.roll(Oparb,-1))[:-1] -self._meandO)**2. /self._sortedSigOEig[2]) -numpy.exp(-0.5*(Oparb[:-1]-self._kick_interpdOpar_poly.c[-1] -self._meandO)**2. /self._sortedSigOEig[2])))[:lowbindx+1]) if higherorder: # Add higher-order contribution out+= self._meanOmega_num_approx_higherorder(Oparb,lowbindx) # Add integration to infinity out+= 0.5*(numpy.sqrt(2./numpy.pi)*numpy.sqrt(self._sortedSigOEig[2])\ *numpy.exp(-0.5*(self._meandO-Oparb[0])**2.\ /self._sortedSigOEig[2]) +self._meandO *(1.+special.erf((self._meandO-Oparb[0]) /numpy.sqrt(2.*self._sortedSigOEig[2])))) return out def _meanOmega_num_approx_higherorder(self,Oparb,lowbindx): """Contribution from non-linear spline terms""" spline_order= self._kick_interpdOpar_raw._eval_args[2] if spline_order == 1: return 0. # Form all Gaussian-like integrals necessary ll= (numpy.roll(Oparb,-1)[:-1]-self._kick_interpdOpar_poly.c[-1]\ -self._meandO\ -self._kick_interpdOpar_poly.c[-2]*self._timpact\ *(Oparb-numpy.roll(Oparb,-1))[:-1])\ /numpy.sqrt(2.*self._sortedSigOEig[2]) ul= (Oparb[:-1]-self._kick_interpdOpar_poly.c[-1]-self._meandO)\ /numpy.sqrt(2.*self._sortedSigOEig[2]) gaussxpolyInt=\ self._densMoments_approx_higherorder_gaussxpolyInts(ll,ul, spline_order+2) firstTerm= Oparb[:-1]\ *self._density_par_approx_higherorder(\ Oparb,lowbindx,_return_array=True, gaussxpolyInt=copy.copy(gaussxpolyInt[1:])) # Now multiply in the coefficients for each order powers= numpy.tile(numpy.arange(spline_order+2)[::-1], (len(ul),1)).T gaussxpolyInt*= -0.5*(-numpy.sqrt(2.))**(powers+1)\ *self._sortedSigOEig[2]**(0.5*(powers-1)) powers= numpy.tile(numpy.arange(spline_order+1)[::-1][:-2], (len(ul),1)).T for jj in range(spline_order+2): gaussxpolyInt[-jj-1]*= numpy.sum(\ self._kick_interpdOpar_poly.c[:-2] *self._timpact**powers /(1.+self._kick_interpdOpar_poly.c[-2]*self._timpact) **(powers+2) *special.binom(powers+1,jj) *(Oparb[:-1]-self._kick_interpdOpar_poly.c[-1]-self._meandO) **(powers-jj+1),axis=0) out= numpy.sum(gaussxpolyInt,axis=0) out+= firstTerm return numpy.sum(out[:lowbindx+1]) def _determine_deltav_kick(self,impact_angle,impactb,subhalovel, GM,rs,subhalopot, spline_order,hernquist): # Store some impact parameters self._impactb= impactb self._subhalovel= subhalovel # Sign of delta angle tells us whether the impact happens to the # leading or trailing arm, self._sigMeanSign contains this info; # Checked before, but check it again in case impact_angle has changed if impact_angle > 0.: self._gap_leading= True else: self._gap_leading= False if (self._gap_leading and not self._leading) \ or (not self._gap_leading and self._leading): raise ValueError('Modeling leading (trailing) impact for trailing (leading) arm; this is not allowed because it is nonsensical in this framework') self._impact_angle= numpy.fabs(impact_angle) # Interpolate the track near the gap in (x,v) at the kick_thetas self._interpolate_stream_track_kick() self._interpolate_stream_track_kick_aA() # Then compute delta v along the track if self._general_kick: self._kick_deltav=\ impulse_deltav_general_curvedstream(self._kick_interpolatedObsTrackXY[:,3:], self._kick_interpolatedObsTrackXY[:,:3], self._impactb, self._subhalovel, self._kick_ObsTrackXY_closest[:3], self._kick_ObsTrackXY_closest[3:], subhalopot) else: if hernquist: deltav_func= impulse_deltav_hernquist_curvedstream else: deltav_func= impulse_deltav_plummer_curvedstream self._kick_deltav= \ deltav_func(self._kick_interpolatedObsTrackXY[:,3:], self._kick_interpolatedObsTrackXY[:,:3], self._impactb, self._subhalovel, self._kick_ObsTrackXY_closest[:3], self._kick_ObsTrackXY_closest[3:], GM,rs) return None def _determine_deltaOmegaTheta_kick(self,spline_order): # Propagate deltav(angle) -> delta (Omega,theta) [angle] # Cylindrical coordinates of the perturbed points vXp= self._kick_interpolatedObsTrackXY[:,3]+self._kick_deltav[:,0] vYp= self._kick_interpolatedObsTrackXY[:,4]+self._kick_deltav[:,1] vZp= self._kick_interpolatedObsTrackXY[:,5]+self._kick_deltav[:,2] vRp,vTp,vZp=\ coords.rect_to_cyl_vec(vXp,vYp,vZp, self._kick_interpolatedObsTrack[:,0], self._kick_interpolatedObsTrack[:,5], self._kick_interpolatedObsTrack[:,3], cyl=True) # We will abuse streamdf functions for doing the (O,a) -> (R,vR) # coordinate transformation, to do this, we assign some of the # attributes related to the track near the impact to the equivalent # attributes related to the track at the present time, carefully # removing this again to avoid confusion (as much as possible) self._interpolatedObsTrack= self._kick_interpolatedObsTrack self._ObsTrack= self._gap_ObsTrack self._interpolatedObsTrackXY= self._kick_interpolatedObsTrackXY self._ObsTrackXY= self._gap_ObsTrackXY self._alljacsTrack= self._gap_alljacsTrack self._interpolatedObsTrackAA= self._kick_interpolatedObsTrackAA self._ObsTrackAA= self._gap_ObsTrackAA self._nTrackChunks= self._nTrackChunksImpact Oap= self._approxaA(self._kick_interpolatedObsTrack[:,0], vRp,vTp, self._kick_interpolatedObsTrack[:,3], vZp, self._kick_interpolatedObsTrack[:,5],interp=True, cindx=range(len(self._kick_interpolatedObsTrackAA))) # Remove attributes again to avoid confusion later delattr(self,'_interpolatedObsTrack') delattr(self,'_ObsTrack') delattr(self,'_interpolatedObsTrackXY') delattr(self,'_ObsTrackXY') delattr(self,'_alljacsTrack') delattr(self,'_interpolatedObsTrackAA') delattr(self,'_ObsTrackAA') delattr(self,'_nTrackChunks') # Generate (dO,da)[angle_offset] and interpolate (raw here, see below # for form that checks range) self._kick_dOap= Oap.T-self._kick_interpolatedObsTrackAA self._kick_interpdOr_raw=\ interpolate.InterpolatedUnivariateSpline(\ self._kick_interpolatedThetasTrack,self._kick_dOap[:,0], k=spline_order) self._kick_interpdOp_raw=\ interpolate.InterpolatedUnivariateSpline(\ self._kick_interpolatedThetasTrack,self._kick_dOap[:,1], k=spline_order) self._kick_interpdOz_raw=\ interpolate.InterpolatedUnivariateSpline(\ self._kick_interpolatedThetasTrack,self._kick_dOap[:,2], k=spline_order) self._kick_interpdar_raw=\ interpolate.InterpolatedUnivariateSpline(\ self._kick_interpolatedThetasTrack,self._kick_dOap[:,3], k=spline_order) self._kick_interpdap_raw=\ interpolate.InterpolatedUnivariateSpline(\ self._kick_interpolatedThetasTrack,self._kick_dOap[:,4], k=spline_order) self._kick_interpdaz_raw=\ interpolate.InterpolatedUnivariateSpline(\ self._kick_interpolatedThetasTrack,self._kick_dOap[:,5], k=spline_order) # Also interpolate parallel and perpendicular frequencies self._kick_dOaparperp=\ numpy.dot(self._kick_dOap[:,:3], self._sigomatrixEig[1][:,self._sigomatrixEigsortIndx]) self._kick_dOaparperp[:,2]*= self._sigMeanSign self._kick_interpdOpar_raw=\ interpolate.InterpolatedUnivariateSpline(\ self._kick_interpolatedThetasTrack, numpy.dot(self._kick_dOap[:,:3],self._dsigomeanProgDirection)\ *self._sigMeanSign, k=spline_order) # to get zeros with sproot self._kick_interpdOperp0_raw=\ interpolate.InterpolatedUnivariateSpline(\ self._kick_interpolatedThetasTrack,self._kick_dOaparperp[:,0], k=spline_order) self._kick_interpdOperp1_raw=\ interpolate.InterpolatedUnivariateSpline(\ self._kick_interpolatedThetasTrack,self._kick_dOaparperp[:,1], k=spline_order) # Also construct derivative of dOpar self._kick_interpdOpar_dapar= self._kick_interpdOpar_raw.derivative(1) # Also construct piecewise-polynomial representation of dOpar, # removing intervals at the start and end with zero range ppoly= interpolate.PPoly.from_spline(\ self._kick_interpdOpar_raw._eval_args) nzIndx=\ numpy.nonzero((numpy.roll(ppoly.x,-1)-ppoly.x > 0)\ *(numpy.arange(len(ppoly.x)) < len(ppoly.x)//2)\ +(ppoly.x-numpy.roll(ppoly.x,1) > 0)\ *(numpy.arange(len(ppoly.x)) >= len(ppoly.x)//2)) self._kick_interpdOpar_poly= interpolate.PPoly(\ ppoly.c[:,nzIndx[0][:-1]],ppoly.x[nzIndx[0]]) return None # Functions that evaluate the interpolated kicks, but also check the range @impact_check_range def _kick_interpdOpar(self,da): return self._kick_interpdOpar_raw(da) @impact_check_range def _kick_interpdOperp0(self,da): return self._kick_interpdOperp0_raw(da) @impact_check_range def _kick_interpdOperp1(self,da): return self._kick_interpdOperp1_raw(da) @impact_check_range def _kick_interpdOr(self,da): return self._kick_interpdOr_raw(da) @impact_check_range def _kick_interpdOp(self,da): return self._kick_interpdOp_raw(da) @impact_check_range def _kick_interpdOz(self,da): return self._kick_interpdOz_raw(da) @impact_check_range def _kick_interpdar(self,da): return self._kick_interpdar_raw(da) @impact_check_range def _kick_interpdap(self,da): return self._kick_interpdap_raw(da) @impact_check_range def _kick_interpdaz(self,da): return self._kick_interpdaz_raw(da) def _interpolate_stream_track_kick(self): """Build interpolations of the stream track near the kick""" if hasattr(self,'_kick_interpolatedThetasTrack'): #pragma: no cover self._store_closest() return None #Already did this # Setup the trackpoints where the kick will be computed, covering the # full length of the stream self._kick_interpolatedThetasTrack= \ numpy.linspace(self._gap_thetasTrack[0], self._gap_thetasTrack[-1], self._nKickPoints) TrackX= self._gap_ObsTrack[:,0]*numpy.cos(self._gap_ObsTrack[:,5]) TrackY= self._gap_ObsTrack[:,0]*numpy.sin(self._gap_ObsTrack[:,5]) TrackZ= self._gap_ObsTrack[:,3] TrackvX, TrackvY, TrackvZ=\ coords.cyl_to_rect_vec(self._gap_ObsTrack[:,1], self._gap_ObsTrack[:,2], self._gap_ObsTrack[:,4], self._gap_ObsTrack[:,5]) #Interpolate self._kick_interpTrackX=\ interpolate.InterpolatedUnivariateSpline(self._gap_thetasTrack, TrackX,k=3) self._kick_interpTrackY=\ interpolate.InterpolatedUnivariateSpline(self._gap_thetasTrack, TrackY,k=3) self._kick_interpTrackZ=\ interpolate.InterpolatedUnivariateSpline(self._gap_thetasTrack, TrackZ,k=3) self._kick_interpTrackvX=\ interpolate.InterpolatedUnivariateSpline(self._gap_thetasTrack, TrackvX,k=3) self._kick_interpTrackvY=\ interpolate.InterpolatedUnivariateSpline(self._gap_thetasTrack, TrackvY,k=3) self._kick_interpTrackvZ=\ interpolate.InterpolatedUnivariateSpline(self._gap_thetasTrack, TrackvZ,k=3) #Now store an interpolated version of the stream track self._kick_interpolatedObsTrackXY= numpy.empty((len(self._kick_interpolatedThetasTrack),6)) self._kick_interpolatedObsTrackXY[:,0]=\ self._kick_interpTrackX(self._kick_interpolatedThetasTrack) self._kick_interpolatedObsTrackXY[:,1]=\ self._kick_interpTrackY(self._kick_interpolatedThetasTrack) self._kick_interpolatedObsTrackXY[:,2]=\ self._kick_interpTrackZ(self._kick_interpolatedThetasTrack) self._kick_interpolatedObsTrackXY[:,3]=\ self._kick_interpTrackvX(self._kick_interpolatedThetasTrack) self._kick_interpolatedObsTrackXY[:,4]=\ self._kick_interpTrackvY(self._kick_interpolatedThetasTrack) self._kick_interpolatedObsTrackXY[:,5]=\ self._kick_interpTrackvZ(self._kick_interpolatedThetasTrack) #Also in cylindrical coordinates self._kick_interpolatedObsTrack= \ numpy.empty((len(self._kick_interpolatedThetasTrack),6)) tR,tphi,tZ= coords.rect_to_cyl(self._kick_interpolatedObsTrackXY[:,0], self._kick_interpolatedObsTrackXY[:,1], self._kick_interpolatedObsTrackXY[:,2]) tvR,tvT,tvZ=\ coords.rect_to_cyl_vec(self._kick_interpolatedObsTrackXY[:,3], self._kick_interpolatedObsTrackXY[:,4], self._kick_interpolatedObsTrackXY[:,5], tR,tphi,tZ,cyl=True) self._kick_interpolatedObsTrack[:,0]= tR self._kick_interpolatedObsTrack[:,1]= tvR self._kick_interpolatedObsTrack[:,2]= tvT self._kick_interpolatedObsTrack[:,3]= tZ self._kick_interpolatedObsTrack[:,4]= tvZ self._kick_interpolatedObsTrack[:,5]= tphi self._store_closest() return None def _store_closest(self): # Also store (x,v) for the point of closest approach self._kick_ObsTrackXY_closest= numpy.array([\ self._kick_interpTrackX(self._impact_angle), self._kick_interpTrackY(self._impact_angle), self._kick_interpTrackZ(self._impact_angle), self._kick_interpTrackvX(self._impact_angle), self._kick_interpTrackvY(self._impact_angle), self._kick_interpTrackvZ(self._impact_angle)]) return None def _interpolate_stream_track_kick_aA(self): """Build interpolations of the stream track near the impact in action-angle coordinates""" if hasattr(self,'_kick_interpolatedObsTrackAA'): #pragma: no cover return None #Already did this #Calculate 1D meanOmega on a fine grid in angle and interpolate dmOs= numpy.array([\ super(streamgapdf,self).meanOmega(da,oned=True, tdisrupt=self._tdisrupt -self._timpact, use_physical=False) for da in self._kick_interpolatedThetasTrack]) self._kick_interpTrackAAdmeanOmegaOneD=\ interpolate.InterpolatedUnivariateSpline(\ self._kick_interpolatedThetasTrack,dmOs,k=3) #Build the interpolated AA self._kick_interpolatedObsTrackAA=\ numpy.empty((len(self._kick_interpolatedThetasTrack),6)) for ii in range(len(self._kick_interpolatedThetasTrack)): self._kick_interpolatedObsTrackAA[ii,:3]=\ self._progenitor_Omega+dmOs[ii]*self._dsigomeanProgDirection\ *self._gap_sigMeanSign self._kick_interpolatedObsTrackAA[ii,3:]=\ self._progenitor_angle+self._kick_interpolatedThetasTrack[ii]\ *self._dsigomeanProgDirection*self._gap_sigMeanSign\ -self._timpact*self._progenitor_Omega self._kick_interpolatedObsTrackAA[ii,3:]=\ numpy.mod(self._kick_interpolatedObsTrackAA[ii,3:],2.*numpy.pi) return None def _determine_deltaAngleTrackImpact(self,deltaAngleTrackImpact,timpact): self._timpact= timpact deltaAngleTrackLim = (self._sigMeanOffset+4.) * numpy.sqrt( self._sortedSigOEig[2]) * (self._tdisrupt-self._timpact) if deltaAngleTrackImpact is None: deltaAngleTrackImpact= deltaAngleTrackLim else: if deltaAngleTrackImpact > deltaAngleTrackLim: warnings.warn("WARNING: deltaAngleTrackImpact angle range large compared to plausible value", galpyWarning) self._deltaAngleTrackImpact= deltaAngleTrackImpact return None def _determine_impact_coordtransform(self,deltaAngleTrackImpact, nTrackChunksImpact, timpact,impact_angle): """Function that sets up the transformation between (x,v) and (O,theta)""" # Integrate the progenitor backward to the time of impact self._gap_progenitor_setup() # Sign of delta angle tells us whether the impact happens to the # leading or trailing arm, self._sigMeanSign contains this info if impact_angle > 0.: self._gap_leading= True else: self._gap_leading= False if (self._gap_leading and not self._leading) \ or (not self._gap_leading and self._leading): raise ValueError('Modeling leading (trailing) impact for trailing (leading) arm; this is not allowed because it is nonsensical in this framework') self._gap_sigMeanSign= 1. if (self._gap_leading and self._progenitor_Omega_along_dOmega/self._sigMeanSign < 0.) \ or (not self._gap_leading and self._progenitor_Omega_along_dOmega/self._sigMeanSign > 0.): self._gap_sigMeanSign= -1. # Determine how much orbital time is necessary for the progenitor's orbit at the time of impact to cover the part of the stream near the impact; we cover the whole leading (or trailing) part of the stream if nTrackChunksImpact is None: #default is floor(self._deltaAngleTrackImpact/0.15)+1 self._nTrackChunksImpact= int(numpy.floor(self._deltaAngleTrackImpact/0.15))+1 else: self._nTrackChunksImpact= nTrackChunksImpact if self._nTrackChunksImpact < 4: self._nTrackChunksImpact= 4 dt= self._deltaAngleTrackImpact\ /self._progenitor_Omega_along_dOmega\ /self._sigMeanSign*self._gap_sigMeanSign self._gap_trackts= numpy.linspace(0.,2*dt,2*self._nTrackChunksImpact-1) #to be sure that we cover it #Instantiate an auxiliaryTrack, which is an Orbit instance at the mean frequency of the stream, and zero angle separation wrt the progenitor; prog_stream_offset is the offset between this track and the progenitor at zero angle (same as in streamdf, but just done at the time of impact rather than the current time) prog_stream_offset=\ _determine_stream_track_single(self._aA, self._gap_progenitor, self._timpact, # around the t of imp self._progenitor_angle-self._timpact*self._progenitor_Omega, self._gap_sigMeanSign, self._dsigomeanProgDirection, lambda da: super(streamgapdf,self).meanOmega(da,offset_sign=self._gap_sigMeanSign,tdisrupt=self._tdisrupt-self._timpact,use_physical=False), 0.) #angle = 0 auxiliaryTrack= Orbit(prog_stream_offset[3]) if dt < 0.: self._gap_trackts= numpy.linspace(0.,-2.*dt,2*self._nTrackChunksImpact-1) #Flip velocities before integrating auxiliaryTrack= auxiliaryTrack.flip() auxiliaryTrack.integrate(self._gap_trackts,self._pot) if dt < 0.: #Flip velocities again auxiliaryTrack.orbit[...,1]= -auxiliaryTrack.orbit[...,1] auxiliaryTrack.orbit[...,2]= -auxiliaryTrack.orbit[...,2] auxiliaryTrack.orbit[...,4]= -auxiliaryTrack.orbit[...,4] #Calculate the actions, frequencies, and angle for this auxiliary orbit acfs= self._aA.actionsFreqs(auxiliaryTrack(0.),maxn=3, use_physical=False) auxiliary_Omega= numpy.array([acfs[3],acfs[4],acfs[5]]).reshape(3\ ) auxiliary_Omega_along_dOmega= \ numpy.dot(auxiliary_Omega,self._dsigomeanProgDirection) # compute the transformation using _determine_stream_track_single allAcfsTrack= numpy.empty((self._nTrackChunksImpact,9)) alljacsTrack= numpy.empty((self._nTrackChunksImpact,6,6)) allinvjacsTrack= numpy.empty((self._nTrackChunksImpact,6,6)) thetasTrack= numpy.linspace(0.,self._deltaAngleTrackImpact, self._nTrackChunksImpact) ObsTrack= numpy.empty((self._nTrackChunksImpact,6)) ObsTrackAA= numpy.empty((self._nTrackChunksImpact,6)) detdOdJps= numpy.empty(self._nTrackChunksImpact) if self._multi is None: for ii in range(self._nTrackChunksImpact): multiOut= _determine_stream_track_single(self._aA, auxiliaryTrack, self._gap_trackts[ii]*numpy.fabs(self._progenitor_Omega_along_dOmega/auxiliary_Omega_along_dOmega), #this factor accounts for the difference in frequency between the progenitor and the auxiliary track, no timpact bc gap_tracks is relative to timpact self._progenitor_angle-self._timpact*self._progenitor_Omega, self._gap_sigMeanSign, self._dsigomeanProgDirection, lambda da: super(streamgapdf,self).meanOmega(da,offset_sign=self._gap_sigMeanSign,tdisrupt=self._tdisrupt-self._timpact,use_physical=False), thetasTrack[ii]) allAcfsTrack[ii,:]= multiOut[0] alljacsTrack[ii,:,:]= multiOut[1] allinvjacsTrack[ii,:,:]= multiOut[2] ObsTrack[ii,:]= multiOut[3] ObsTrackAA[ii,:]= multiOut[4] detdOdJps[ii]= multiOut[5] else: multiOut= multi.parallel_map(\ (lambda x: _determine_stream_track_single(self._aA, auxiliaryTrack, self._gap_trackts[x]*numpy.fabs(self._progenitor_Omega_along_dOmega/auxiliary_Omega_along_dOmega), #this factor accounts for the difference in frequency between the progenitor and the auxiliary track, no timpact bc gap_tracks is relative to timpact self._progenitor_angle-self._timpact*self._progenitor_Omega, self._gap_sigMeanSign, self._dsigomeanProgDirection, lambda da: super(streamgapdf,self).meanOmega(da,offset_sign=self._gap_sigMeanSign,tdisrupt=self._tdisrupt-self._timpact,use_physical=False), thetasTrack[x])), range(self._nTrackChunksImpact), numcores=numpy.amin([self._nTrackChunksImpact, multiprocessing.cpu_count(), self._multi])) for ii in range(self._nTrackChunksImpact): allAcfsTrack[ii,:]= multiOut[ii][0] alljacsTrack[ii,:,:]= multiOut[ii][1] allinvjacsTrack[ii,:,:]= multiOut[ii][2] ObsTrack[ii,:]= multiOut[ii][3] ObsTrackAA[ii,:]= multiOut[ii][4] detdOdJps[ii]= multiOut[ii][5] #Repeat the track calculation using the previous track, to get closer to it for nn in range(self.nTrackIterations): if self._multi is None: for ii in range(self._nTrackChunksImpact): multiOut= _determine_stream_track_single(self._aA, Orbit(ObsTrack[ii,:]), 0., self._progenitor_angle-self._timpact*self._progenitor_Omega, self._gap_sigMeanSign, self._dsigomeanProgDirection, lambda da: super(streamgapdf,self).meanOmega(da,offset_sign=self._gap_sigMeanSign,tdisrupt=self._tdisrupt-self._timpact,use_physical=False), thetasTrack[ii]) allAcfsTrack[ii,:]= multiOut[0] alljacsTrack[ii,:,:]= multiOut[1] allinvjacsTrack[ii,:,:]= multiOut[2] ObsTrack[ii,:]= multiOut[3] ObsTrackAA[ii,:]= multiOut[4] detdOdJps[ii]= multiOut[5] else: multiOut= multi.parallel_map(\ (lambda x: _determine_stream_track_single(self._aA,Orbit(ObsTrack[x,:]),0., self._progenitor_angle-self._timpact*self._progenitor_Omega, self._gap_sigMeanSign, self._dsigomeanProgDirection, lambda da: super(streamgapdf,self).meanOmega(da,offset_sign=self._gap_sigMeanSign,tdisrupt=self._tdisrupt-self._timpact,use_physical=False), thetasTrack[x])), range(self._nTrackChunksImpact), numcores=numpy.amin([self._nTrackChunksImpact, multiprocessing.cpu_count(), self._multi])) for ii in range(self._nTrackChunksImpact): allAcfsTrack[ii,:]= multiOut[ii][0] alljacsTrack[ii,:,:]= multiOut[ii][1] allinvjacsTrack[ii,:,:]= multiOut[ii][2] ObsTrack[ii,:]= multiOut[ii][3] ObsTrackAA[ii,:]= multiOut[ii][4] detdOdJps[ii]= multiOut[ii][5] #Store the track self._gap_thetasTrack= thetasTrack self._gap_ObsTrack= ObsTrack self._gap_ObsTrackAA= ObsTrackAA self._gap_allAcfsTrack= allAcfsTrack self._gap_alljacsTrack= alljacsTrack self._gap_allinvjacsTrack= allinvjacsTrack self._gap_detdOdJps= detdOdJps self._gap_meandetdOdJp= numpy.mean(self._gap_detdOdJps) self._gap_logmeandetdOdJp= numpy.log(self._gap_meandetdOdJp) #Also calculate _ObsTrackXY in XYZ,vXYZ coordinates self._gap_ObsTrackXY= numpy.empty_like(self._gap_ObsTrack) TrackX= self._gap_ObsTrack[:,0]*numpy.cos(self._gap_ObsTrack[:,5]) TrackY= self._gap_ObsTrack[:,0]*numpy.sin(self._gap_ObsTrack[:,5]) TrackZ= self._gap_ObsTrack[:,3] TrackvX, TrackvY, TrackvZ=\ coords.cyl_to_rect_vec(self._gap_ObsTrack[:,1], self._gap_ObsTrack[:,2], self._gap_ObsTrack[:,4], self._gap_ObsTrack[:,5]) self._gap_ObsTrackXY[:,0]= TrackX self._gap_ObsTrackXY[:,1]= TrackY self._gap_ObsTrackXY[:,2]= TrackZ self._gap_ObsTrackXY[:,3]= TrackvX self._gap_ObsTrackXY[:,4]= TrackvY self._gap_ObsTrackXY[:,5]= TrackvZ return None def _gap_progenitor_setup(self): """Setup an Orbit instance that's the progenitor integrated backwards""" self._gap_progenitor= self._progenitor().flip() # new orbit, flip velocities # Make sure we do not use physical coordinates self._gap_progenitor.turn_physical_off() # Now integrate backward in time until tdisrupt ts= numpy.linspace(0.,self._tdisrupt,1001) self._gap_progenitor.integrate(ts,self._pot) # Flip its velocities, should really write a function for this self._gap_progenitor.orbit[...,1]= -self._gap_progenitor.orbit[...,1] self._gap_progenitor.orbit[...,2]= -self._gap_progenitor.orbit[...,2] self._gap_progenitor.orbit[...,4]= -self._gap_progenitor.orbit[...,4] return None ################################SAMPLE THE DF################################## def _sample_aAt(self,n): """Sampling frequencies, angles, and times part of sampling, for stream with gap""" # Use streamdf's _sample_aAt to generate unperturbed frequencies, # angles Om,angle,dt= super()._sample_aAt(n) # Now rewind angles by timpact, apply the kicks, and run forward again dangle_at_impact= angle-numpy.tile(self._progenitor_angle.T,(n,1)).T\ -(Om-numpy.tile(self._progenitor_Omega.T,(n,1)).T)*self._timpact dangle_par_at_impact= numpy.dot(dangle_at_impact.T, self._dsigomeanProgDirection)\ *self._gap_sigMeanSign # Calculate and apply kicks (points not yet released have zero kick) dOr= self._kick_interpdOr(dangle_par_at_impact) dOp= self._kick_interpdOp(dangle_par_at_impact) dOz= self._kick_interpdOz(dangle_par_at_impact) Om[0,:]+= dOr Om[1,:]+= dOp Om[2,:]+= dOz angle[0,:]+=\ self._kick_interpdar(dangle_par_at_impact)+dOr*self._timpact angle[1,:]+=\ self._kick_interpdap(dangle_par_at_impact)+dOp*self._timpact angle[2,:]+=\ self._kick_interpdaz(dangle_par_at_impact)+dOz*self._timpact return (Om,angle,dt) def impulse_deltav_plummer(v,y,b,w,GM,rs): """ NAME: impulse_deltav_plummer PURPOSE: calculate the delta velocity to due an encounter with a Plummer sphere in the impulse approximation; allows for arbitrary velocity vectors, but y is input as the position along the stream INPUT: v - velocity of the stream (nstar,3) y - position along the stream (nstar) b - impact parameter w - velocity of the Plummer sphere (3) GM - mass of the Plummer sphere (in natural units) rs - size of the Plummer sphere OUTPUT: deltav (nstar,3) HISTORY: 2015-04-30 - Written based on Erkal's expressions - Bovy (IAS) """ if len(v.shape) == 1: v= numpy.reshape(v,(1,3)) y= numpy.reshape(y,(1,1)) nv= v.shape[0] # Build the rotation matrices and their inverse rot= _rotation_vy(v) rotinv= _rotation_vy(v,inv=True) # Rotate the Plummer sphere's velocity to the stream frames tilew= numpy.sum(rot*numpy.tile(w,(nv,3,1)),axis=-1) # Use Denis' expressions wperp= numpy.sqrt(tilew[:,0]**2.+tilew[:,2]**2.) wpar= numpy.sqrt(numpy.sum(v**2.,axis=1))-tilew[:,1] wmag2= wpar**2.+wperp**2. wmag= numpy.sqrt(wmag2) out= numpy.empty_like(v) denom= wmag*((b**2.+rs**2.)*wmag2+wperp**2.*y**2.) out[:,0]= (b*wmag2*tilew[:,2]/wperp-y*wpar*tilew[:,0])/denom out[:,1]= -wperp**2.*y/denom out[:,2]= -(b*wmag2*tilew[:,0]/wperp+y*wpar*tilew[:,2])/denom # deal w/ perpendicular impacts wperp0Indx= numpy.fabs(wperp) < 10.**-10. out[wperp0Indx,0]= (b*wmag2[wperp0Indx]-y[wperp0Indx]*wpar[wperp0Indx]*tilew[wperp0Indx,0])/denom[wperp0Indx] out[wperp0Indx,2]=-(b*wmag2[wperp0Indx]+y[wperp0Indx]*wpar[wperp0Indx]*tilew[wperp0Indx,2])/denom[wperp0Indx] # Rotate back to the original frame return 2.0*GM*numpy.sum(\ rotinv*numpy.swapaxes(numpy.tile(out.T,(3,1,1)).T,1,2),axis=-1) def impulse_deltav_plummer_curvedstream(v,x,b,w,x0,v0,GM,rs): """ NAME: impulse_deltav_plummer_curvedstream PURPOSE: calculate the delta velocity to due an encounter with a Plummer sphere in the impulse approximation; allows for arbitrary velocity vectors, and arbitrary position along the stream INPUT: v - velocity of the stream (nstar,3) x - position along the stream (nstar,3) b - impact parameter w - velocity of the Plummer sphere (3) x0 - point of closest approach v0 - velocity of point of closest approach GM - mass of the Plummer sphere (in natural units) rs - size of the Plummer sphere OUTPUT: deltav (nstar,3) HISTORY: 2015-05-04 - Written based on above - SANDERS """ if len(v.shape) == 1: v= numpy.reshape(v,(1,3)) if len(x.shape) == 1: x= numpy.reshape(x,(1,3)) b0 = numpy.cross(w,v0) b0 *= b/numpy.sqrt(numpy.sum(b0**2)) b_ = b0+x-x0 w = w-v wmag = numpy.sqrt(numpy.sum(w**2,axis=1)) bdotw=numpy.sum(b_*w,axis=1)/wmag denom= wmag*(numpy.sum(b_**2,axis=1)+rs**2-bdotw**2) denom = 1./denom return -2.0*GM*((b_.T-bdotw*w.T/wmag)*denom).T def HernquistX(s): """ Computes X function from equations (33) & (34) of Hernquist (1990) """ if(s<0.): raise ValueError("s must be positive in Hernquist X function") elif(s<1.): return numpy.log((1+numpy.sqrt(1-s*s))/s)/numpy.sqrt(1-s*s) elif(s==1.): return 1. else: return numpy.arccos(1./s)/numpy.sqrt(s*s-1) def impulse_deltav_hernquist(v,y,b,w,GM,rs): """ NAME: impulse_deltav_hernquist PURPOSE: calculate the delta velocity to due an encounter with a Hernquist sphere in the impulse approximation; allows for arbitrary velocity vectors, but y is input as the position along the stream INPUT: v - velocity of the stream (nstar,3) y - position along the stream (nstar) b - impact parameter w - velocity of the Hernquist sphere (3) GM - mass of the Hernquist sphere (in natural units) rs - size of the Hernquist sphere OUTPUT: deltav (nstar,3) HISTORY: 2015-08-13 SANDERS, using Wyn Evans calculation """ if len(v.shape) == 1: v= numpy.reshape(v,(1,3)) nv= v.shape[0] # Build the rotation matrices and their inverse rot= _rotation_vy(v) rotinv= _rotation_vy(v,inv=True) # Rotate the Plummer sphere's velocity to the stream frames tilew= numpy.sum(rot*numpy.tile(w,(nv,3,1)),axis=-1) wperp= numpy.sqrt(tilew[:,0]**2.+tilew[:,2]**2.) wpar= numpy.sqrt(numpy.sum(v**2.,axis=1))-tilew[:,1] wmag2= wpar**2.+wperp**2. wmag= numpy.sqrt(wmag2) B = numpy.sqrt(b**2.+wperp**2.*y**2./wmag2) denom= wmag*(B**2-rs**2) denom=1./denom s = numpy.sqrt(2.*B/(rs+B)) HernquistXv=numpy.vectorize(HernquistX) Xfac = 1.-2.*rs/(rs+B)*HernquistXv(s) out= numpy.empty_like(v) out[:,0]= (b*tilew[:,2]/wperp-y*wpar*tilew[:,0]/wmag2)*denom*Xfac out[:,1]= -wperp**2.*y*denom*Xfac/wmag2 out[:,2]= -(b*tilew[:,0]/wperp+y*wpar*tilew[:,2]/wmag2)*denom*Xfac # deal w/ perpendicular impacts wperp0Indx= numpy.fabs(wperp) < 10.**-10. out[wperp0Indx,0]= (b-y[wperp0Indx]*wpar[wperp0Indx]*tilew[wperp0Indx,0]/wmag2[wperp0Indx])*denom[wperp0Indx]*Xfac[wperp0Indx] out[wperp0Indx,2]=-(b+y[wperp0Indx]*wpar[wperp0Indx]*tilew[wperp0Indx,2]/wmag2[wperp0Indx])*denom[wperp0Indx]*Xfac[wperp0Indx] # Rotate back to the original frame return 2.0*GM*numpy.sum(\ rotinv*numpy.swapaxes(numpy.tile(out.T,(3,1,1)).T,1,2),axis=-1) def impulse_deltav_hernquist_curvedstream(v,x,b,w,x0,v0,GM,rs): """ NAME: impulse_deltav_plummer_hernquist PURPOSE: calculate the delta velocity to due an encounter with a Hernquist sphere in the impulse approximation; allows for arbitrary velocity vectors, and arbitrary position along the stream INPUT: v - velocity of the stream (nstar,3) x - position along the stream (nstar,3) b - impact parameter w - velocity of the Hernquist sphere (3) x0 - point of closest approach v0 - velocity of point of closest approach GM - mass of the Hernquist sphere (in natural units) rs - size of the Hernquist sphere OUTPUT: deltav (nstar,3) HISTORY: 2015-08-13 - SANDERS, using Wyn Evans calculation """ if len(v.shape) == 1: v= numpy.reshape(v,(1,3)) if len(x.shape) == 1: x= numpy.reshape(x,(1,3)) b0 = numpy.cross(w,v0) b0 *= b/numpy.sqrt(numpy.sum(b0**2)) b_ = b0+x-x0 w = w-v wmag = numpy.sqrt(numpy.sum(w**2,axis=1)) bdotw=numpy.sum(b_*w,axis=1)/wmag B = numpy.sqrt(numpy.sum(b_**2,axis=1)-bdotw**2) denom= wmag*(B**2-rs**2) denom=1./denom s = numpy.sqrt(2.*B/(rs+B)) HernquistXv=numpy.vectorize(HernquistX) Xfac = 1.-2.*rs/(rs+B)*HernquistXv(s) return -2.0*GM*((b_.T-bdotw*w.T/wmag)*Xfac*denom).T def _a_integrand(T,y,b,w,pot,compt): t = T/(1-T*T) X = b+w*t+y*numpy.array([0,1,0]) r = numpy.sqrt(numpy.sum(X**2)) return (1+T*T)/(1-T*T)**2*evaluateRforces(pot,r,0.)*X[compt]/r def _deltav_integrate(y,b,w,pot): return numpy.array([integrate.quad(_a_integrand,-1.,1.,args=(y,b,w,pot,i))[0] for i in range(3)]) def impulse_deltav_general(v,y,b,w,pot): """ NAME: impulse_deltav_general PURPOSE: calculate the delta velocity to due an encounter with a general spherical potential in the impulse approximation; allows for arbitrary velocity vectors, but y is input as the position along the stream INPUT: v - velocity of the stream (nstar,3) y - position along the stream (nstar) b - impact parameter w - velocity of the subhalo (3) pot - Potential object or list thereof (should be spherical) OUTPUT: deltav (nstar,3) HISTORY: 2015-05-04 - SANDERS 2015-06-15 - Tweak to use galpy' potential objects - Bovy (IAS) """ pot= flatten_potential(pot) if len(v.shape) == 1: v= numpy.reshape(v,(1,3)) nv= v.shape[0] # Build the rotation matrices and their inverse rot= _rotation_vy(v) rotinv= _rotation_vy(v,inv=True) # Rotate the subhalo's velocity to the stream frames tilew= numpy.sum(rot*numpy.tile(w,(nv,3,1)),axis=-1) tilew[:,1]-=numpy.sqrt(numpy.sum(v**2.,axis=1)) wmag = numpy.sqrt(tilew[:,0]**2+tilew[:,2]**2) b0 = b*numpy.array([-tilew[:,2]/wmag,numpy.zeros(nv),tilew[:,0]/wmag]).T return numpy.array(list(map(lambda i:numpy.sum(i[3] *_deltav_integrate(i[0],i[1],i[2],pot).T,axis=-1) ,zip(y,b0,tilew,rotinv)))) def impulse_deltav_general_curvedstream(v,x,b,w,x0,v0,pot): """ NAME: impulse_deltav_general_curvedstream PURPOSE: calculate the delta velocity to due an encounter with a general spherical potential in the impulse approximation; allows for arbitrary velocity vectors and arbitrary shaped streams INPUT: v - velocity of the stream (nstar,3) x - position along the stream (nstar,3) b - impact parameter w - velocity of the subhalo (3) x0 - position of closest approach (3) v0 - velocity of stream at closest approach (3) pot - Potential object or list thereof (should be spherical) OUTPUT: deltav (nstar,3) HISTORY: 2015-05-04 - SANDERS 2015-06-15 - Tweak to use galpy' potential objects - Bovy (IAS) """ pot= flatten_potential(pot) if len(v.shape) == 1: v= numpy.reshape(v,(1,3)) if len(x.shape) == 1: x= numpy.reshape(x,(1,3)) b0 = numpy.cross(w,v0) b0 *= b/numpy.sqrt(numpy.sum(b0**2)) b_ = b0+x-x0 return numpy.array(list(map(lambda i:_deltav_integrate(0.,i[1],i[0],pot) ,zip(w-v,b_)))) def impulse_deltav_general_orbitintegration(v,x,b,w,x0,v0,pot,tmax,galpot, tmaxfac=10.,nsamp=1000, integrate_method='symplec4_c'): """ NAME: impulse_deltav_general_orbitintegration PURPOSE: calculate the delta velocity to due an encounter with a general spherical potential NOT in the impulse approximation by integrating each particle in the underlying galactic potential; allows for arbitrary velocity vectors and arbitrary shaped streams. INPUT: v - velocity of the stream (nstar,3) x - position along the stream (nstar,3) b - impact parameter w - velocity of the subhalo (3) x0 - position of closest approach (3) v0 - velocity of stream at closest approach (3) pot - Potential object or list thereof (should be spherical) tmax - maximum integration time galpot - galpy Potential object or list thereof nsamp(1000) - number of forward integration points integrate_method= ('symplec4_c') orbit integrator to use (see Orbit.integrate) OUTPUT: deltav (nstar,3) HISTORY: 2015-08-17 - SANDERS """ galpot= flatten_potential(galpot) if len(v.shape) == 1: v= numpy.reshape(v,(1,3)) if len(x.shape) == 1: x= numpy.reshape(x,(1,3)) nstar,ndim=numpy.shape(v) b0 = numpy.cross(w,v0) b0 *= b/numpy.sqrt(numpy.sum(b0**2)) times = numpy.linspace(0.,tmax,nsamp) xres = numpy.zeros(shape=(len(x),nsamp*2-1,3)) R, phi, z= coords.rect_to_cyl(x[:,0],x[:,1],x[:,2]) vR, vp, vz= coords.rect_to_cyl_vec(v[:,0],v[:,1],v[:,2], R,phi,z,cyl=True) for i in range(nstar): o = Orbit([R[i],vR[i],vp[i],z[i],vz[i],phi[i]]) o.integrate(times,galpot,method=integrate_method) xres[i,nsamp:,0]=o.x(times)[1:] xres[i,nsamp:,1]=o.y(times)[1:] xres[i,nsamp:,2]=o.z(times)[1:] oreverse = o.flip() oreverse.integrate(times,galpot,method=integrate_method) xres[i,:nsamp,0]=oreverse.x(times)[::-1] xres[i,:nsamp,1]=oreverse.y(times)[::-1] xres[i,:nsamp,2]=oreverse.z(times)[::-1] times = numpy.concatenate((-times[::-1],times[1:])) nsamp = len(times) X = b0+xres-x0-numpy.outer(times,w) r = numpy.sqrt(numpy.sum(X**2,axis=-1)) acc = (numpy.reshape(evaluateRforces(pot,r.flatten(),0.),(nstar,nsamp))/r)[:,:,numpy.newaxis]*X return integrate.simps(acc,x=times,axis=1) def impulse_deltav_general_fullplummerintegration(v,x,b,w,x0,v0,galpot,GM,rs, tmaxfac=10.,N=1000, integrate_method='symplec4_c'): """ NAME: impulse_deltav_general_fullplummerintegration PURPOSE: calculate the delta velocity to due an encounter with a moving Plummer sphere and galactic potential relative to just in galactic potential INPUT: v - velocity of the stream (nstar,3) x - position along the stream (nstar,3) b - impact parameter w - velocity of the subhalo (3) x0 - position of closest approach (3) v0 - velocity of stream at closest approach (3) galpot - Galaxy Potential object GM - mass of Plummer rs - scale of Plummer tmaxfac(10) - multiple of rs/fabs(w - v0) to use for time integration interval N(1000) - number of forward integration points integrate_method('symplec4_c') - orbit integrator to use (see Orbit.integrate) OUTPUT: deltav (nstar,3) HISTORY: 2015-08-18 - SANDERS """ galpot= flatten_potential(galpot) if len(v.shape) == 1: v= numpy.reshape(v,(1,3)) if len(x.shape) == 1: x= numpy.reshape(x,(1,3)) nstar,ndim=numpy.shape(v) b0 = numpy.cross(w,v0) b0 *= b/numpy.sqrt(numpy.sum(b0**2)) X = x0-b0 # Setup Plummer orbit R, phi, z= coords.rect_to_cyl(X[0],X[1],X[2]) vR, vp, vz= coords.rect_to_cyl_vec(w[0],w[1],w[2],R,phi,z,cyl=True) tmax = tmaxfac*rs/numpy.sqrt(numpy.sum((w-v0)**2)) times = numpy.linspace(0.,tmax,N) dtimes = numpy.linspace(-tmax,tmax,2*N) o = Orbit(vxvv=[R,-vR,-vp,z,-vz,phi]) o.integrate(times,galpot,method=integrate_method) oplum = o(times[-1]).flip() oplum.integrate(dtimes,galpot,method=integrate_method) plumpot = MovingObjectPotential(orbit=oplum, pot=PlummerPotential(amp=GM,b=rs)) # Now integrate each particle backwards in galaxy potential, forwards in combined potential and backwards again in galaxy and take diff deltav = numpy.zeros((nstar,3)) R, phi, z= coords.rect_to_cyl(x[:,0],x[:,1],x[:,2]) vR, vp, vz= coords.rect_to_cyl_vec(v[:,0],v[:,1],v[:,2], R,phi,z,cyl=True) for i in range(nstar): ostar= Orbit(vxvv=[R[i],-vR[i],-vp[i],z[i],-vz[i],phi[i]]) ostar.integrate(times,galpot,method=integrate_method) oboth = ostar(times[-1]).flip() oboth.integrate(dtimes,[galpot,plumpot],method=integrate_method) ogalpot = oboth(times[-1]).flip() ogalpot.integrate(times,galpot,method=integrate_method) deltav[i][0] = -ogalpot.vx(times[-1]) - v[i][0] deltav[i][1] = -ogalpot.vy(times[-1]) - v[i][1] deltav[i][2] = -ogalpot.vz(times[-1]) - v[i][2] return deltav def _astream_integrand_x(t,y,v,b,w,b2,w2,wperp,wperp2,wpar,GSigma,rs2): return GSigma(t)*(b*w2*w[2]/wperp-(y-v*t)*wpar*w[0])\ /((b2+rs2)*w2+wperp2*(y-v*t)**2.) def _astream_integrand_y(t,y,v,b2,w2,wperp2,GSigma,rs2): return GSigma(t)*(y-v*t)/((b2+rs2)*w2+wperp2*(y-v*t)**2.) def _astream_integrand_z(t,y,v,b,w,b2,w2,wperp,wperp2,wpar,GSigma,rs2): return -GSigma(t)*(b*w2*w[0]/wperp+(y-v*t)*wpar*w[2])\ /((b2+rs2)*w2+wperp2*(y-v*t)**2.) def impulse_deltav_plummerstream(v,y,b,w,GSigma,rs,tmin=None,tmax=None): """ NAME: impulse_deltav_plummerstream PURPOSE: calculate the delta velocity to due an encounter with a Plummer-softened stream in the impulse approximation; allows for arbitrary velocity vectors, but y is input as the position along the stream INPUT: v - velocity of the stream (nstar,3) y - position along the stream (nstar) b - impact parameter w - velocity of the Plummer sphere (3) GSigma - surface density of the Plummer-softened stream (in natural units); should be a function of time rs - size of the Plummer sphere tmin, tmax= (None) minimum and maximum time to consider for GSigma (need to be set) OUTPUT: deltav (nstar,3) HISTORY: 2015-11-14 - Written - Bovy (UofT) """ if len(v.shape) == 1: v= numpy.reshape(v,(1,3)) y= numpy.reshape(y,(1,1)) if tmax is None or tmax is None: raise ValueError("tmin= and tmax= need to be set") nv= v.shape[0] vmag= numpy.sqrt(numpy.sum(v**2.,axis=1)) # Build the rotation matrices and their inverse rot= _rotation_vy(v) rotinv= _rotation_vy(v,inv=True) # Rotate the perturbing stream's velocity to the stream frames tilew= numpy.sum(rot*numpy.tile(w,(nv,3,1)),axis=-1) # Use similar expressions to Denis' wperp= numpy.sqrt(tilew[:,0]**2.+tilew[:,2]**2.) wpar= numpy.sqrt(numpy.sum(v**2.,axis=1))-tilew[:,1] wmag2= wpar**2.+wperp**2. wmag= numpy.sqrt(wmag2) b2= b**2. rs2= rs**2. wperp2= wperp**2. out= numpy.empty_like(v) out[:,0]= [1./wmag[ii]\ *integrate.quad(_astream_integrand_x, tmin,tmax,args=(y[ii], vmag[ii],b,tilew[ii], b2,wmag2[ii], wperp[ii],wperp2[ii], wpar[ii], GSigma,rs2))[0] for ii in range(len(y))] out[:,1]= [-wperp2[ii]/wmag[ii]\ *integrate.quad(_astream_integrand_y, tmin,tmax,args=(y[ii], vmag[ii], b2,wmag2[ii], wperp2[ii], GSigma,rs2))[0] for ii in range(len(y))] out[:,2]= [1./wmag[ii]\ *integrate.quad(_astream_integrand_z , tmin,tmax,args=(y[ii], vmag[ii],b,tilew[ii], b2,wmag2[ii], wperp[ii],wperp2[ii], wpar[ii], GSigma,rs2))[0] for ii in range(len(y))] # Rotate back to the original frame return 2.0*numpy.sum(\ rotinv*numpy.swapaxes(numpy.tile(out.T,(3,1,1)).T,1,2),axis=-1) def _astream_integrand(t,b_,orb,tx,w,GSigma,rs2,tmin,compt): teval= tx-tmin-t b__= b_+numpy.array([orb.x(teval),orb.y(teval),orb.z(teval)]) w = w-numpy.array([orb.vx(teval),orb.vy(teval),orb.vz(teval)]) wmag = numpy.sqrt(numpy.sum(w**2)) bdotw=numpy.sum(b__*w)/wmag denom= wmag*(numpy.sum(b__**2)+rs2-bdotw**2) denom = 1./denom return -2.0*GSigma(t)*(((b__.T-bdotw*w.T/wmag)*denom).T)[compt] def _astream_integrate(b_,orb,tx,w,GSigma,rs2,otmin,tmin,tmax): return numpy.array([integrate.quad(_astream_integrand,tmin,tmax, args=(b_,orb,tx,w,GSigma,rs2, otmin,i))[0] \ for i in range(3)]) def impulse_deltav_plummerstream_curvedstream(v,x,t,b,w,x0,v0,GSigma,rs, galpot,tmin=None,tmax=None): """ NAME: impulse_deltav_plummerstream_curvedstream PURPOSE: calculate the delta velocity to due an encounter with a Plummer sphere in the impulse approximation; allows for arbitrary velocity vectors, and arbitrary position along the stream; velocities and positions are assumed to lie along an orbit INPUT: v - velocity of the stream (nstar,3) x - position along the stream (nstar,3) t - times at which (v,x) are reached, wrt the closest impact t=0 (nstar) b - impact parameter w - velocity of the Plummer sphere (3) x0 - point of closest approach v0 - velocity of point of closest approach GSigma - surface density of the Plummer-softened stream (in natural units); should be a function of time rs - size of the Plummer sphere galpot - galpy Potential object or list thereof tmin, tmax= (None) minimum and maximum time to consider for GSigma OUTPUT: deltav (nstar,3) HISTORY: 2015-11-20 - Written based on Plummer sphere above - Bovy (UofT) """ galpot= flatten_potential(galpot) if len(v.shape) == 1: v= numpy.reshape(v,(1,3)) if len(x.shape) == 1: x= numpy.reshape(x,(1,3)) # Integrate an orbit to use to figure out where each (v,x) is at each time R, phi, z= coords.rect_to_cyl(x0[0],x0[1],x0[2]) vR, vT, vz= coords.rect_to_cyl_vec(v0[0],v0[1],v0[2],R,phi,z,cyl=True) # First back, then forward to cover the entire range with 1 orbit o= Orbit([R,vR,vT,z,vz,phi]).flip() ts= numpy.linspace(0.,numpy.fabs(numpy.amin(t)+tmin),101) o.integrate(ts,galpot) o= o(ts[-1]).flip() ts= numpy.linspace(0.,numpy.amax(t)+tmax-numpy.amin(t)-tmin,201) o.integrate(ts,galpot) # Calculate kicks b0 = numpy.cross(w,v0) b0 *= b/numpy.sqrt(numpy.sum(b0**2)) return numpy.array(list(map(lambda i:_astream_integrate(\ b0-x0,o,i,w,GSigma,rs**2.,numpy.amin(t)+tmin, tmin,tmax),t))) def _rotation_vy(v,inv=False): return _rotate_to_arbitrary_vector(v,[0,1,0],inv) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/streamspraydf.py0000644000175100001710000003202614327773303017213 0ustar00runnerdockerimport numpy from ..df.df import df from ..orbit import Orbit from ..potential import evaluateRforces from ..potential import flatten as flatten_potential from ..potential import rtide from ..util import _rotate_to_arbitrary_vector, conversion, coords from ..util._optional_deps import _APY_LOADED, _APY_UNITS if _APY_LOADED: from astropy import units class streamspraydf(df): def __init__(self,progenitor_mass,progenitor=None, pot=None,rtpot=None, tdisrupt=None,leading=True, center=None,centerpot=None, meankvec=[2.,0.,0.3,0.,0.,0.], sigkvec=[0.4,0.,0.4,0.5,0.5,0.], ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a stream spray DF model of a tidal stream INPUT: progenitor_mass - mass of the progenitor (can be Quantity) tdisrupt= (5 Gyr) time since start of disruption (can be Quantity) leading= (True) if True, model the leading part of the stream if False, model the trailing part progenitor= progenitor orbit as Orbit instance (will be re-integrated, so don't bother integrating the orbit before) meankvec= (Fardal+2015-ish defaults) sigkvec= (Fardal+2015-ish defaults) pot = (None) potential for integrating orbits rtpot = (pot) potential for calculating tidal radius and circular velocity (should generally be the same as pot, but sometimes you need to drop parts of the potential that don't allow the tidal radius / circular velocity to be computed, such as velocity-dependent forces; when using center, rtpot should be the relevant potential in the frame of the center, thus, also being different from pot) center = (None) Orbit instance that represents the center around which the progenitor is orbiting for the purpose of stream formation; allows for a stream to be generated from a progenitor orbiting a moving object, like a satellite galaxy. Integrated internally using centerpot. centerpot = (pot) potential for calculating the orbit of the center; this might be different from the potential that the progenitor is integrated in if, for example, dynamical friction is important for the orbit of the center (if it's a satellite). OUTPUT: Instance HISTORY: 2018-07-31 - Written - Bovy (UofT) 2021-05-05 - Added center keyword - Yansong Qian (UofT) """ df.__init__(self,ro=ro,vo=vo) self._progenitor_mass= conversion.parse_mass(progenitor_mass, ro=self._ro,vo=self._vo) self._tdisrupt= 5./conversion.time_in_Gyr(self._vo,self._ro) \ if tdisrupt is None \ else conversion.parse_time(tdisrupt,ro=self._ro,vo=self._vo) if pot is None: #pragma: no cover raise OSError("pot= must be set") self._pot= flatten_potential(pot) self._rtpot=self._pot if rtpot is None else flatten_potential(rtpot) assert conversion.physical_compatible(self,self._pot), 'Physical conversion for the potential is not consistent with that of the streamspraydf object being initialized' assert conversion.physical_compatible(self,self._rtpot), 'Physical conversion for the rt potential is not consistent with that of the streamspraydf object being initialized' # Set up progenitor orbit assert conversion.physical_compatible(self,progenitor), 'Physical conversion for the progenitor Orbit object is not consistent with that of the streamspraydf object being initialized' self._orig_progenitor= progenitor # Store so we can use its ro/vo/etc. self._progenitor= progenitor() self._progenitor.turn_physical_off() self._progenitor_times= numpy.linspace(0.,-self._tdisrupt,10001) self._progenitor.integrate(self._progenitor_times,self._pot) self._meankvec= numpy.array(meankvec) self._sigkvec= numpy.array(sigkvec) # Set up center orbit if given if not center is None: self._centerpot=self._pot if centerpot is None \ else flatten_potential(centerpot) assert conversion.physical_compatible(self,self._centerpot), 'Physical conversion for the center potential is not consistent with that of the streamspraydf object being initialized' self._center= center() self._center.turn_physical_off() self._center.integrate(self._progenitor_times,self._centerpot) else: self._center= None if leading: self._meankvec*= -1. return None def sample(self,n,return_orbit=True,returndt=False,integrate=True): """ NAME: sample PURPOSE: sample from the DF INPUT: n - number of points to return return_orbit= (True) If True, the output phase-space positions is an orbit.Orbit object, if False, the output is (R,vR,vT,z,vz,phi) returndt= (False) if True, also return the time since the star was stripped integrate= (True) if True, integrate the orbits to the present time, if False, return positions at stripping (probably want to combine with returndt=True then to make sense of them!) xy= (False) if True, return Galactocentric rectangular coordinates lb= (False) if True, return Galactic l,b,d,vlos,pmll,pmbb coordinates OUTPUT: Orbit instance or (R,vR,vT,z,vz,phi) of points on the stream in 6,N array (set of 6 Quantities when physical output is on); optionally the time is included as well. The ro/vo unit-conversion parameters and the zo/solarmotion parameters as well as whether physical outputs are on match the settings of the progenitor Orbit given to the class initialization HISTORY: 2018-07-31 - Written - Bovy (UofT) 2022-05-18 - Made output Orbit ro/vo/zo/solarmotion/roSet/voSet match that of the progenitor orbit - Bovy (UofT) """ # First sample times dt= numpy.random.uniform(size=n)*self._tdisrupt # Build all rotation matrices rot, rot_inv= self._setup_rot(dt) # Compute progenitor position in the instantaneous frame, # relative to the center orbit if necessary centerx= self._progenitor.x(-dt) centery= self._progenitor.y(-dt) centerz= self._progenitor.z(-dt) centervx= self._progenitor.vx(-dt) centervy= self._progenitor.vy(-dt) centervz= self._progenitor.vz(-dt) if not self._center is None: centerx-= self._center.x(-dt) centery-= self._center.y(-dt) centerz-= self._center.z(-dt) centervx-= self._center.vx(-dt) centervy-= self._center.vy(-dt) centervz-= self._center.vz(-dt) xyzpt= numpy.einsum('ijk,ik->ij',rot, numpy.array([centerx,centery,centerz]).T) vxyzpt= numpy.einsum('ijk,ik->ij',rot, numpy.array([centervx,centervy,centervz]).T) Rpt,phipt,Zpt= coords.rect_to_cyl(xyzpt[:,0],xyzpt[:,1],xyzpt[:,2]) vRpt,vTpt,vZpt= coords.rect_to_cyl_vec(vxyzpt[:,0],vxyzpt[:,1],vxyzpt[:,2], Rpt,phipt,Zpt,cyl=True) # Sample positions and velocities in the instantaneous frame k= self._meankvec+numpy.random.normal(size=n)[:,numpy.newaxis]*self._sigkvec try: rtides= rtide(self._rtpot,Rpt,Zpt,phi=phipt, t=-dt,M=self._progenitor_mass,use_physical=False) vcs= numpy.sqrt(-Rpt *evaluateRforces(self._rtpot,Rpt,Zpt,phi=phipt,t=-dt, use_physical=False)) except (ValueError,TypeError): rtides= numpy.array([rtide(self._rtpot,Rpt[ii],Zpt[ii],phi=phipt[ii], t=-dt[ii],M=self._progenitor_mass,use_physical=False) for ii in range(len(Rpt))]) vcs= numpy.array([numpy.sqrt(-Rpt[ii] *evaluateRforces(self._rtpot,Rpt[ii],Zpt[ii],phi=phipt[ii],t=-dt[ii], use_physical=False)) for ii in range(len(Rpt))]) rtides_as_frac= rtides/Rpt RpZst= numpy.array([Rpt+k[:,0]*rtides, phipt+k[:,5]*rtides_as_frac, k[:,3]*rtides_as_frac]).T vRTZst= numpy.array([vRpt*(1.+k[:,1]), vTpt+k[:,2]*vcs*rtides_as_frac, k[:,4]*vcs*rtides_as_frac]).T # Now rotate these back to the galactocentric frame xst,yst,zst= coords.cyl_to_rect(RpZst[:,0],RpZst[:,1],RpZst[:,2]) vxst,vyst,vzst= coords.cyl_to_rect_vec(vRTZst[:,0],vRTZst[:,1],vRTZst[:,2],RpZst[:,1]) xyzs= numpy.einsum('ijk,ik->ij',rot_inv,numpy.array([xst,yst,zst]).T) vxyzs= numpy.einsum('ijk,ik->ij',rot_inv,numpy.array([vxst,vyst,vzst]).T) absx= xyzs[:,0] absy= xyzs[:,1] absz= xyzs[:,2] absvx= vxyzs[:,0] absvy= vxyzs[:,1] absvz= vxyzs[:,2] if not self._center is None: absx+= self._center.x(-dt) absy+= self._center.y(-dt) absz+= self._center.z(-dt) absvx+= self._center.vx(-dt) absvy+= self._center.vy(-dt) absvz+= self._center.vz(-dt) Rs,phis,Zs= coords.rect_to_cyl(absx,absy,absz) vRs,vTs,vZs= coords.rect_to_cyl_vec(absvx,absvy,absvz, Rs,phis,Zs,cyl=True) out= numpy.empty((6,n)) if integrate: # Now integrate the orbits for ii in range(n): o= Orbit([Rs[ii],vRs[ii],vTs[ii],Zs[ii],vZs[ii],phis[ii]]) o.integrate(numpy.linspace(-dt[ii],0.,10001),self._pot) o= o(0.) out[:,ii]= [o.R(),o.vR(),o.vT(),o.z(),o.vz(),o.phi()] else: out[0]= Rs out[1]= vRs out[2]= vTs out[3]= Zs out[4]= vZs out[5]= phis if return_orbit: # Output Orbit ro/vo/zo/solarmotion/roSet/voSet match progenitor o= Orbit(vxvv=out.T, ro=self._orig_progenitor._ro, vo=self._orig_progenitor._vo, zo=self._orig_progenitor._zo, solarmotion= self._orig_progenitor._solarmotion) if not self._orig_progenitor._roSet: o._roSet= False if not self._orig_progenitor._voSet: o._voSet= False out= o elif _APY_UNITS and self._voSet and self._roSet: out= (out[0]*self._ro*units.kpc, out[1]*self._vo*units.km/units.s, out[2]*self._vo*units.km/units.s, out[3]*self._ro*units.kpc, out[4]*self._vo*units.km/units.s, out[5]*units.rad) dt= dt*conversion.time_in_Gyr(self._vo,self._ro)*units.Gyr if returndt: return (out,dt) else: return out def _setup_rot(self,dt): n= len(dt) centerx= self._progenitor.x(-dt) centery= self._progenitor.y(-dt) centerz= self._progenitor.z(-dt) if self._center is None: L= self._progenitor.L(-dt) # Compute relative angular momentum to the center orbit else: centerx-= self._center.x(-dt) centery-= self._center.y(-dt) centerz-= self._center.z(-dt) centervx= self._progenitor.vx(-dt)-self._center.vx(-dt) centervy= self._progenitor.vy(-dt)-self._center.vy(-dt) centervz= self._progenitor.vz(-dt)-self._center.vz(-dt) L= numpy.array([centery*centervz-centerz*centervy, centerz*centervx-centerx*centervz, centerx*centervy-centery*centervx]).T Lnorm= L/numpy.tile(numpy.sqrt(numpy.sum(L**2.,axis=1)),(3,1)).T z_rot= numpy.swapaxes(_rotate_to_arbitrary_vector(numpy.atleast_2d(Lnorm),[0.,0.,1],inv=True),1,2) z_rot_inv= numpy.swapaxes(_rotate_to_arbitrary_vector(numpy.atleast_2d(Lnorm),[0.,0.,1],inv=False),1,2) xyzt= numpy.einsum('ijk,ik->ij',z_rot, numpy.array([centerx,centery,centerz]).T) Rt= numpy.sqrt(xyzt[:,0]**2.+xyzt[:,1]**2.) cosphi, sinphi= xyzt[:,0]/Rt,xyzt[:,1]/Rt pa_rot= numpy.array([[cosphi,-sinphi,numpy.zeros(n)], [sinphi,cosphi,numpy.zeros(n)], [numpy.zeros(n),numpy.zeros(n),numpy.ones(n)]]).T pa_rot_inv= numpy.array([[cosphi,sinphi,numpy.zeros(n)], [-sinphi,cosphi,numpy.zeros(n)], [numpy.zeros(n),numpy.zeros(n),numpy.ones(n)]]).T rot= numpy.einsum('ijk,ikl->ijl',pa_rot,z_rot) rot_inv= numpy.einsum('ijk,ikl->ijl',z_rot_inv,pa_rot_inv) return (rot,rot_inv) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/df/surfaceSigmaProfile.py0000644000175100001710000001305414327773303020261 0ustar00runnerdocker############################################################################### # surfaceSigmaProfile: classes that implement different surface-mass and # radial velocity dispersion profiles # # Includes the following: # surfaceSigmaProfile - top-level class that represents a surface # density profile and a sigma_R profile # expSurfaceSigmaProfile - class that represents an exponential surface # density profile and an exponential sigma_R # profile ############################################################################### import numpy class surfaceSigmaProfile: """Class that contains the surface density and sigma_R^2 profile""" def __init__(self): """Place holder for implementations of this class""" return None def formatStringParams(self): """ NAME: formatStringParams PURPOSE: when writing the parameters of this profile, what format-strings to use? This function defaults to '%6.4f' for each parameter in self._params INPUT: OUTPUT: tuple of format-strings HISTORY: 2010-03-28 - Written - Bovy (NYU) """ out= [] for param in self._params: out.append('%6.4f') return out def outputParams(self): """ NAME: outputParams PURPOSE:' return a list of parameters of this profile, to create filenames INPUT: OUTPUT: tuple of parameters (given to % ...) HISTORY: 2010-03-28 - Written - Bovy """ return tuple(self._params) def surfacemass(self,R,log=False): """ NAME: surfacemass PURPOSE: return the surface density profile at this R INPUT: R - Galactocentric radius (/ro) log - if True, return the log (default: False) OUTPUT: Sigma(R) HISTORY: 2010-03-26 - Written - Bovy (NYU) """ raise NotImplementedError("'surfacemass' function not implemented for this surfaceSigmaProfile class") def sigma2(self,R,log=False): """ NAME: sigma2 PURPOSE: return the radial velocity variance at this R INPUT: R - Galactocentric radius (/ro) log - if True, return the log (default: False) OUTPUT: sigma^2(R) HISTORY: 2010-03-26 - Written - Bovy (NYU) """ raise NotImplementedError("'sigma2' function not implemented for this surfaceSigmaProfile class") class expSurfaceSigmaProfile(surfaceSigmaProfile): """Exponential surface density and sigma_R^2 class""" def __init__(self,params=(1./3.,1.0,0.2)): """ NAME: __init__ PURPOSE: initialize an exponential surface density and sigma_R^2 profile INPUT: params - tuple/list of (surface scale-length,sigma scale-length, sigma(ro)) (NOTE: *not* sigma2 scale-length) OUTPUT: HISTORY: 2010-03-26 - Written - Bovy (NYU) """ surfaceSigmaProfile.__init__(self) self._params= params def surfacemass(self,R,log=False): """ NAME: surfacemass PURPOSE: return the surface density profile at this R INPUT: R - Galactocentric radius (/ro) log - if True, return the log (default: False) OUTPUT: Sigma(R) HISTORY: 2010-03-26 - Written - Bovy (NYU) """ if log: return -R/self._params[0] else: return numpy.exp(-R/self._params[0]) def surfacemassDerivative(self,R,log=False): """ NAME: surfacemassDerivative PURPOSE: return the derivative wrt R of the surface density profile at this R INPUT: R - Galactocentric radius (/ro) log - if True, return the derivative of the log (default: False) OUTPUT: Sigma'(R) or (log Sigma(r) )' HISTORY: 2010-03-26 - Written - Bovy (NYU) """ if log: return -1./self._params[0] else: return -numpy.exp(-R/self._params[0])/self._params[0] def sigma2(self,R,log=False): """ NAME: sigma2 PURPOSE: return the radial velocity variance at this R INPUT: R - Galactocentric radius (/ro) log - if True, return the log (default: False) OUTPUT: sigma^2(R) HISTORY: 2010-03-26 - Written - Bovy (NYU) """ if log: return 2.*numpy.log(self._params[2])-2.*(R-1.)/self._params[1] else: return self._params[2]**2.*numpy.exp(-2.*(R-1.)/self._params[1]) def sigma2Derivative(self,R,log=False): """ NAME: sigmaDerivative PURPOSE: return the derivative wrt R of the sigma_R^2 profile at this R INPUT: R - Galactocentric radius (/ro) log - if True, return the derivative of the log (default: False) OUTPUT: Sigma_R^2'(R) or (log Sigma_R^2(r) )' HISTORY: 2011-03-24 - Written - Bovy (NYU) """ if log: return -2./self._params[1] else: return self._params[2]**2.*numpy.exp(-2.*(R-1.)/self._params[1])\ *(-2./self._params[1]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667233486.7054672 galpy-1.8.1/galpy/orbit/0000755000175100001710000000000014327773317014505 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/orbit/Orbits.py0000644000175100001710000065215514327773303016332 0ustar00runnerdockerimport os import sys _PY3= sys.version > '3' import copy import json import string import warnings from functools import wraps from random import choice from string import ascii_lowercase import numpy import scipy from packaging.version import parse as parse_version from scipy import interpolate, optimize _SCIPY_VERSION= parse_version(scipy.__version__) if _SCIPY_VERSION < parse_version('0.10'): #pragma: no cover from scipy.maxentropy import logsumexp elif _SCIPY_VERSION < parse_version('0.19'): #pragma: no cover from scipy.misc import logsumexp else: from scipy.special import logsumexp from ..potential import (LcE, PotentialError, _isNonAxi, evaluatelinearPotentials, evaluateplanarPotentials, evaluatePotentials) from ..potential import flatten as flatten_potential from ..potential import rE, rl, toPlanarPotential from ..potential.DissipativeForce import _isDissipative from ..potential.Potential import _check_c from ..util import conversion, coords, galpyWarning, galpyWarningVerbose, plot from ..util._optional_deps import (_APY3, _APY_COORD_LOADED, _APY_GE_31, _APY_LOADED, _APY_UNITS, _ASTROQUERY_LOADED, _NUMEXPR_LOADED) from ..util.conversion import physical_compatible, physical_conversion from ..util.coords import _K from .integrateFullOrbit import integrateFullOrbit, integrateFullOrbit_c from .integrateLinearOrbit import (_ext_loaded, integrateLinearOrbit, integrateLinearOrbit_c) from .integratePlanarOrbit import (integratePlanarOrbit, integratePlanarOrbit_c, integratePlanarOrbit_dxdv) ext_loaded= _ext_loaded if _APY_LOADED: from astropy import units # Separate like this, because coordinates don't work in Pyodide astropy (2/25/22) if _APY_COORD_LOADED: from astropy import coordinates from astropy.coordinates import SkyCoord if _ASTROQUERY_LOADED: from astroquery.simbad import Simbad from ..util import config if _APY_LOADED: vxvv_units= [units.kpc,units.km/units.s,units.km/units.s, units.kpc,units.km/units.s,units.rad] # Set default numcores for integrate w/ parallel map using OMP_NUM_THREADS try: _NUMCORES= int(os.environ['OMP_NUM_THREADS']) except KeyError: import multiprocessing _NUMCORES= multiprocessing.cpu_count() # named_objects file def _named_objects_key_formatting(name): # Remove punctuation, spaces, and make lowercase if _PY3: out_name= name.translate(\ str.maketrans('', '',string.punctuation)).replace(' ', '').lower() else: #pragma: no cover out_name= str(name).translate(None,string.punctuation)\ .replace(' ', '').lower() return out_name _known_objects= None _known_objects_original_keys= None # these are use for auto-completion _known_objects_collections_original_keys= None _known_objects_synonyms_original_keys= None _known_objects_keys_updated= False def _load_named_objects(): global _known_objects global _known_objects_original_keys global _known_objects_collections_original_keys global _known_objects_synonyms_original_keys if not _known_objects: with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'named_objects.json')) as jsonfile: _known_objects= json.load(jsonfile) # Save original keys for auto-completion _known_objects_original_keys= copy.copy(list(_known_objects.keys())) _known_objects_collections_original_keys= \ copy.copy(list(_known_objects['_collections'].keys())) _known_objects_synonyms_original_keys= \ copy.copy(list(_known_objects['_synonyms'].keys())) # Add synonyms as duplicates for name in _known_objects['_synonyms']: _known_objects[name]= \ _known_objects[_known_objects['_synonyms'][name]] return None def _update_keys_named_objects(): global _known_objects_keys_updated if not _known_objects_keys_updated: # Format the keys of the known objects dictionary, first collections old_keys= list(_known_objects['_collections'].keys()) for old_key in old_keys: _known_objects['_collections']\ [_named_objects_key_formatting(old_key)]= \ _known_objects['_collections'].pop(old_key) # Then the objects themselves old_keys= list(_known_objects.keys()) old_keys.remove('_collections') old_keys.remove('_synonyms') for old_key in old_keys: _known_objects[_named_objects_key_formatting(old_key)]= \ _known_objects.pop(old_key) _known_objects_keys_updated= True # Auto-completion try: # pragma: no cover from IPython import get_ipython _load_named_objects() def name_completer(ipython,event): try: # encapsulate in try/except to avoid *any* error out= copy.copy(_known_objects_original_keys) out.remove('_collections') out.remove('_synonyms') out.extend(_known_objects_collections_original_keys) out.extend(_known_objects_synonyms_original_keys) out.extend(['ro=','vo=','zo=','solarmotion=']) except: pass return out get_ipython().set_hook('complete_command',name_completer, re_key=".*from_name") except: pass def shapeDecorator(func): """Decorator to return Orbits outputs with the correct shape""" @wraps(func) def shape_wrapper(*args,**kwargs): dontreshape= kwargs.get('dontreshape',False) result= func(*args,**kwargs) if dontreshape: return result elif args[0].shape == (): return result[0] else: return numpy.reshape(result,args[0].shape+result.shape[1:]) return shape_wrapper class Orbit: """ Class representing single and multiple orbits. """ def __init__(self,vxvv=None,ro=None,vo=None,zo=None,solarmotion=None, radec=False,uvw=False,lb=False): """ NAME: __init__ PURPOSE: Initialize an Orbit instance INPUT: vxvv - initial conditions (must all have the same phase-space dimension); can be either a) astropy (>v3.0) SkyCoord with arbitrary shape, including velocities (note that this turns *on* physical output even if ro and vo are not given) b) array of arbitrary shape (shape,phasedim) (shape of the orbits, followed by the phase-space dimension of the orbit); shape information is retained and used in outputs; elements can be either 1) in Galactocentric cylindrical coordinates with phase-space coordinates arranged as [R,vR,vT(,z,vz,phi)]; needs to be in internal units (for Quantity input; see 'list' option below) 2) [ra,dec,d,mu_ra, mu_dec,vlos] in [deg,deg,kpc,mas/yr,mas/yr,km/s] (ICRS; mu_ra = mu_ra * cos dec); (for Quantity input, see 'list' option below); 4) [ra,dec,d,U,V,W] in [deg,deg,kpc,km/s,km/s,kms]; (for Quantity input; see 'list' option below); ICRS frame 5) (l,b,d,mu_l, mu_b, vlos) in [deg,deg,kpc,mas/yr,mas/yr,km/s) (mu_l = mu_l * cos b); (for Quantity input; see 'list' option below) 6) [l,b,d,U,V,W] in [deg,deg,kpc,km/s,km/s,kms]; (for Quantity input; see 'list' option below) 5) and 6) also work when leaving out b and mu_b/W c) lists of initial conditions, entries can be 1) individual Orbit instances (of single objects) 2) Quantity arrays arranged as in section 2) above (so things like [R,vR,vT,z,vz,phi], where R, vR, ... can be arbitrary shape Quantity arrays) 3) list of Quantities (so things like [R1,vR1,..,], where R1, vR1, ... are scalar Quantities 4) None: assumed to be the Sun; if None occurs in a list it is assumed to be the Sun *and all other items in the list are assumed to be [ra,dec,...]*; cannot be combined with Quantity lists (2 and 3 above) 5) lists of scalar phase-space coordinates arranged as in b) (so things like [R,vR,...] where R,vR are scalars in internal units OPTIONAL INPUTS: ro - distance from vantage point to GC (kpc; can be Quantity) vo - circular velocity at ro (km/s; can be Quantity) zo - offset toward the NGP of the Sun wrt the plane (kpc; can be Quantity; default = 20.8 pc from Bennett & Bovy 2019) solarmotion - 'hogg' or 'dehnen', or 'schoenrich', or value in [-U,V,W]; can be Quantity OUTPUT: instance HISTORY: 2018-10-13 - Written - Mathew Bub (UofT) 2019-01-01 - Better handling of unit/coordinate-conversion parameters and consistency checks - Bovy (UofT) 2019-02-01 - Handle array of SkyCoords in a faster way by making use of the fact that array of SkyCoords is processed correctly by Orbit 2019-02-18 - Don't support radec, lb, or uvw keywords to avoid slow coordinate transformations that would require ugly code to fix - Bovy (UofT) 2019-03-19 - Allow array vxvv and arbitrary shapes - Bovy (UofT) """ # First deal with None = Sun if vxvv is None: # Assume one wants the Sun vxvv= numpy.array([0.,0.,0.,0.,0.,0.]) radec= True elif isinstance(vxvv,(list, tuple)): if None in vxvv: vxvv= [[0.,0.,0.,0.,0.,0.] if tvxvv is None else tvxvv for tvxvv in vxvv] radec= True # Set ro, vo, zo, solarmotion based on input, SkyCoord vxvv, ... self._setup_parse_coordtransform(vxvv,ro,vo,zo,solarmotion, radec,lb) # Determine and record input shape and flatten for further processing if _APY_COORD_LOADED and isinstance(vxvv,SkyCoord): input_shape= vxvv.shape vxvv=vxvv.flatten() elif isinstance(vxvv,numpy.ndarray): input_shape= vxvv.shape[:-1] vxvv= numpy.atleast_2d(vxvv) vxvv= vxvv.reshape((numpy.prod(vxvv.shape[:-1]),vxvv.shape[-1])) elif isinstance(vxvv,(list, tuple)): if isinstance(vxvv[0],Orbit): vxvv= self._setup_parse_listofOrbits(vxvv,ro,vo,zo,solarmotion) input_shape= (len(vxvv),) vxvv= numpy.array(vxvv) elif _APY_LOADED and isinstance(vxvv[0],units.Quantity): # Case where vxvv= [R,vR,...] or [ra,dec,...] with Quantities input_shape= vxvv[0].shape vxvv= [s.flatten() for s in vxvv] # Keep as list, is fine later... elif _APY_LOADED and isinstance(vxvv[0],list) \ and isinstance(vxvv[0][0],units.Quantity): # Case where vxvv= [[R1,vR1,...],[R2,vR2,...]] # or [[ra1,dec1,...],[ra2,dec2,...]] with Quantities input_shape= (len(vxvv),) pdim= len(vxvv[0]) stack= [] for pp in range(pdim): stack.append(\ numpy.array([tvxvv[pp].to(vxvv[0][pp].unit).value for tvxvv in vxvv])\ *vxvv[0][pp].unit) vxvv= stack # Keep as list, is fine later... elif numpy.ndim(vxvv[0]) == 0: # Scalar, so assume single object vxvv= [vxvv] input_shape= () vxvv= numpy.array(vxvv) else: input_shape= (len(vxvv),) vxvv= numpy.array(vxvv) if isinstance(vxvv,numpy.ndarray) and vxvv.dtype == 'object': # if diff. phasedim, object array is created raise RuntimeError("All individual orbits in an Orbit class must have the same phase-space dimensionality") #: Tuple of Orbit dimensions self.shape= input_shape self._setup_parse_vxvv(vxvv,radec,lb,uvw) # Check that we have a valid phase-space dim (often messed up by not # transposing the input array to the correct shape) if self.phasedim() < 2 or self.phasedim() > 6: if len(self.vxvv) > 1 and len(self.vxvv) < 7: raise RuntimeError(f"Invalid phase-space dimension {self.phasedim():d} for {len(self.vxvv):d} objects; perhaps you meant to transpose the input?") else: raise RuntimeError(f"Invalid phase-space dimension: phasedim = {self.phasedim():d}, but should be between 2 and 6") #: Total number of elements in the Orbit instance self.size= 1 if self.shape == () else len(self.vxvv) if self.dim() == 1: # For the 1D case, solar position/velocity is not used currently self._zo= None self._solarmotion= None def _setup_parse_coordtransform(self,vxvv,ro,vo,zo,solarmotion, radec,lb): # Parse coordinate-transformation inputs with units ro= conversion.parse_length_kpc(ro) zo= conversion.parse_length_kpc(zo) vo= conversion.parse_velocity_kms(vo) # if vxvv is SkyCoord, preferentially use its ro and zo if _APY_COORD_LOADED and isinstance(vxvv,SkyCoord): if not _APY3: # pragma: no cover raise ImportError('Orbit initialization using an astropy SkyCoord requires astropy >3.0') if zo is None and not vxvv.z_sun is None: zo= vxvv.z_sun.to(units.kpc).value elif not vxvv.z_sun is None: if numpy.fabs(zo-vxvv.z_sun.to(units.kpc).value) > 1e-8: raise ValueError("Orbit initialization's zo different from SkyCoord's z_sun; these should be the same for consistency") elif zo is None and not vxvv.galcen_distance is None: zo= 0. if ro is None and not vxvv.galcen_distance is None: ro= numpy.sqrt(vxvv.galcen_distance.to(units.kpc).value**2. -zo**2.) elif not vxvv.galcen_distance is None and \ numpy.fabs(ro**2.+zo**2.-vxvv.galcen_distance.to(units.kpc).value**2.) > 1e-10: warnings.warn("Orbit's initialization normalization ro and zo are incompatible with SkyCoord's galcen_distance (should have galcen_distance^2 = ro^2 + zo^2)",galpyWarning) # If at this point ro/vo not set, use default from config if (_APY_COORD_LOADED and isinstance(vxvv,SkyCoord)) or radec or lb: if ro is None: ro= config.__config__.getfloat('normalization','ro') if vo is None: vo= config.__config__.getfloat('normalization','vo') # If at this point zo not set, use default if zo is None: zo= 0.0208 # if vxvv is SkyCoord, preferentially use its solarmotion if _APY_COORD_LOADED and isinstance(vxvv,SkyCoord) \ and not vxvv.galcen_v_sun is None: sc_solarmotion= vxvv.galcen_v_sun.d_xyz.to(units.km/units.s).value sc_solarmotion[0]= -sc_solarmotion[0] # right->left sc_solarmotion[1]-= vo if solarmotion is None: solarmotion= sc_solarmotion # If at this point solarmotion not set, use default if solarmotion is None: solarmotion= 'schoenrich' if isinstance(solarmotion,str) and solarmotion.lower() == 'hogg': vsolar= numpy.array([-10.1,4.0,6.7]) elif isinstance(solarmotion,str) and solarmotion.lower() == 'dehnen': vsolar= numpy.array([-10.,5.25,7.17]) elif isinstance(solarmotion,str) \ and solarmotion.lower() == 'schoenrich': vsolar= numpy.array([-11.1,12.24,7.25]) else: vsolar= numpy.array(conversion.parse_velocity_kms(solarmotion)) # If both vxvv SkyCoord with vsun and solarmotion set, check the same if _APY_COORD_LOADED and isinstance(vxvv,SkyCoord) \ and not vxvv.galcen_v_sun is None: if numpy.any(numpy.fabs(sc_solarmotion-vsolar) > 1e-8): raise ValueError("Orbit initialization's solarmotion parameter not compatible with SkyCoord's galcen_v_sun; these should be the same for consistency (this may be because you did not set vo; galcen_v_sun = solarmotion+vo for consistency)") # Now store all coordinate-transformation parameters and save whether # ro/vo are set (they are considered to be set if they have been # determined at this point, even if they were not explicitly set if vo is None: self._vo= config.__config__.getfloat('normalization','vo') self._voSet= False else: self._vo= vo self._voSet= True if ro is None: self._ro= config.__config__.getfloat('normalization','ro') self._roSet= False else: self._ro= ro self._roSet= True self._zo= zo self._solarmotion= vsolar return None def _setup_parse_listofOrbits(self,vxvv,ro,vo,zo,solarmotion): # Only implement lists of scalar Orbit for now if not numpy.all([o.shape == () for o in vxvv]): raise RuntimeError("Initializing an Orbit instance with a list of Orbit instances only supports lists of single Orbit instances") # Need to check that coordinate-transformation parameters are # consistent between given orbits and between this instance's # initialization and the given orbits; if not explicitly given # for this instance, fall back onto list's parameters ros= numpy.array([o._ro for o in vxvv]) vos= numpy.array([o._vo for o in vxvv]) zos= numpy.array([o._zo for o in vxvv]) solarmotions= numpy.array([o._solarmotion for o in vxvv]) if numpy.any(numpy.fabs(ros-ros[0]) > 1e-10): raise RuntimeError("All individual orbits given to an Orbit class must have the same ro unit-conversion parameter") if numpy.any(numpy.fabs(vos-vos[0]) > 1e-10): raise RuntimeError("All individual orbits given to an Orbit class must have the same vo unit-conversion parameter") if not zos[0] is None and numpy.any(numpy.fabs(zos-zos[0]) > 1e-10): raise RuntimeError("All individual orbits given to an Orbit class must have the same zo solar offset") if not solarmotions[0] is None and \ numpy.any(numpy.fabs(solarmotions-solarmotions[0]) > 1e-10): raise RuntimeError("All individual orbits given to an Orbit class must have the same solar motion") if self._roSet: if numpy.fabs(ros[0]-self._ro) > 1e-10: raise RuntimeError("All individual orbits given to an Orbit class must have the same ro unit-conversion parameter as used in the initialization call") else: self._ro= vxvv[0]._ro self._roSet= vxvv[0]._roSet if self._voSet: if numpy.fabs(vos[0]-self._vo) > 1e-10: raise RuntimeError("All individual orbits given to an Orbit class must have the same vo unit-conversion parameter as used in the initialization call") else: self._vo= vxvv[0]._vo self._voSet= vxvv[0]._voSet if not zo is None: if numpy.fabs(zos[0]-self._zo) > 1e-10: raise RuntimeError("All individual orbits given to an Orbit class must have the same zo solar offset parameter as used in the initialization call") else: self._zo= vxvv[0]._zo if not solarmotion is None: if numpy.any(numpy.fabs(solarmotions[0]-self._solarmotion) > 1e-10): raise RuntimeError("All individual orbits given to an Orbit class must have the same solar motion as used in the initialization call") else: self._solarmotion= vxvv[0]._solarmotion # shape of o.vxvv is (1,phasedim) due to internal storage return [list(o.vxvv[0]) for o in vxvv] def _setup_parse_vxvv(self,vxvv,radec,lb,uvw): if _APY_COORD_LOADED and isinstance(vxvv,SkyCoord): galcen_v_sun= coordinates.CartesianDifferential(\ numpy.array([-self._solarmotion[0], self._solarmotion[1]+self._vo, self._solarmotion[2]])*units.km/units.s) gc_frame= coordinates.Galactocentric(\ galcen_distance=numpy.sqrt(self._ro**2.+self._zo**2.)\ *units.kpc, z_sun=self._zo*units.kpc,galcen_v_sun=galcen_v_sun) vxvvg= vxvv.transform_to(gc_frame) if _APY_GE_31: vxvvg.representation_type= 'cylindrical' else: #pragma: no cover vxvvg.representation= 'cylindrical' R= vxvvg.rho.to(units.kpc).value/self._ro phi= numpy.pi-vxvvg.phi.to(units.rad).value z= vxvvg.z.to(units.kpc).value/self._ro try: vR= vxvvg.d_rho.to(units.km/units.s).value/self._vo except TypeError: raise TypeError("SkyCoord given to Orbit initialization does not have velocity data, which is required to setup an Orbit") vT= -(vxvvg.d_phi*vxvvg.rho)\ .to(units.km/units.s, equivalencies=units.dimensionless_angles()).value/self._vo vz= vxvvg.d_z.to(units.km/units.s).value/self._vo vxvv= numpy.array([R,vR,vT,z,vz,phi]) # Make sure radec and lb are False (issue #402) radec= False lb= False elif not isinstance(vxvv,(list, tuple)): vxvv= vxvv.T # (norb,phasedim) --> (phasedim,norb) easier later if not (_APY_COORD_LOADED and isinstance(vxvv,SkyCoord)) and (radec or lb): if radec: if _APY_LOADED and isinstance(vxvv[0],units.Quantity): ra, dec= vxvv[0].to(units.deg).value, \ vxvv[1].to(units.deg).value else: ra, dec= vxvv[0], vxvv[1] l,b= coords.radec_to_lb(ra,dec,degree=True,epoch=None).T _extra_rot= True elif len(vxvv) == 4: l, b= vxvv[0], numpy.zeros_like(vxvv[0]) _extra_rot= False else: l,b= vxvv[0],vxvv[1] _extra_rot= True if _APY_LOADED and isinstance(l,units.Quantity): l= l.to(units.deg).value if _APY_LOADED and isinstance(b,units.Quantity): b= b.to(units.deg).value if uvw: if _APY_LOADED and isinstance(vxvv[2],units.Quantity): X,Y,Z= coords.lbd_to_XYZ(l,b,vxvv[2].to(units.kpc).value, degree=True).T else: X,Y,Z= coords.lbd_to_XYZ(l,b,vxvv[2],degree=True).T vx= conversion.parse_velocity_kms(vxvv[3]) vy= conversion.parse_velocity_kms(vxvv[4]) vz= conversion.parse_velocity_kms(vxvv[5]) else: if radec: if _APY_LOADED and isinstance(vxvv[3],units.Quantity): pmra, pmdec= vxvv[3].to(units.mas/units.yr).value, \ vxvv[4].to(units.mas/units.yr).value else: pmra, pmdec= vxvv[3], vxvv[4] pmll, pmbb= coords.pmrapmdec_to_pmllpmbb(pmra,pmdec,ra,dec, degree=True, epoch=None).T d, vlos= vxvv[2], vxvv[5] elif len(vxvv) == 4: pmll, pmbb= vxvv[2], numpy.zeros_like(vxvv[2]) d, vlos= vxvv[1], vxvv[3] else: pmll, pmbb= vxvv[3], vxvv[4] d, vlos= vxvv[2], vxvv[5] d= conversion.parse_length_kpc(d) vlos= conversion.parse_velocity_kms(vlos) if _APY_LOADED and isinstance(pmll,units.Quantity): pmll= pmll.to(units.mas/units.yr).value if _APY_LOADED and isinstance(pmbb,units.Quantity): pmbb= pmbb.to(units.mas/units.yr).value X,Y,Z,vx,vy,vz= coords.sphergal_to_rectgal(l,b,d, vlos,pmll, pmbb, degree=True).T X/= self._ro Y/= self._ro Z/= self._ro vx/= self._vo vy/= self._vo vz/= self._vo vsun= numpy.array([0.,1.,0.,])+self._solarmotion/self._vo R, phi, z= coords.XYZ_to_galcencyl(X,Y,Z,Zsun=self._zo/self._ro, _extra_rot=_extra_rot).T vR, vT,vz= coords.vxvyvz_to_galcencyl(vx,vy,vz, R,phi,z, vsun=vsun, Xsun=1.,Zsun=self._zo/self._ro, galcen=True, _extra_rot=_extra_rot).T if lb and len(vxvv) == 4: vxvv= numpy.array([R,vR,vT,phi]) else: vxvv= numpy.array([R,vR,vT,z,vz,phi]) # Parse vxvv if it consists of Quantities if _APY_LOADED and isinstance(vxvv[0],units.Quantity): # Need to set ro and vo, default if not specified, so need to # turn them on self._roSet= True self._voSet= True new_vxvv= [vxvv[0].to(vxvv_units[0]).value/self._ro, vxvv[1].to(vxvv_units[1]).value/self._vo] if len(vxvv) > 2: new_vxvv.append(vxvv[2].to(vxvv_units[2]).value/self._vo) if len(vxvv) == 4: new_vxvv.append(vxvv[3].to(vxvv_units[5]).value) elif len(vxvv) > 4: new_vxvv.append(vxvv[3].to(vxvv_units[3]).value/self._ro) new_vxvv.append(vxvv[4].to(vxvv_units[4]).value/self._vo) if len(vxvv) == 6: new_vxvv.append(vxvv[5].to(vxvv_units[5]).value) vxvv= numpy.array(new_vxvv) # (phasedim,norb) --> (norb,phasedim) again and store self.vxvv= vxvv.T return None @classmethod def from_name(cls,*args,**kwargs): """ NAME: from_name PURPOSE: given the name of an object or a list of names, retrieve coordinate information for that object from SIMBAD and return a corresponding orbit INPUT: name - the name of the object or list of names; when loading a collection of objects (like 'mwglobularclusters'), lists are not allowed +standard Orbit initialization keywords: ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) zo= offset toward the NGP of the Sun wrt the plane (kpc; can be Quantity; default = 20.8 pc from Bennett & Bovy 2019) solarmotion= 'hogg' or 'dehnen', or 'schoenrich', or value in [-U,V,W]; can be Quantity OUTPUT: orbit containing the phase space coordinates of the named object HISTORY: 2018-07-15 - Written - Mathew Bub (UofT) 2019-05-21 - Generalized to multiple objects and incorporated into Orbits - Bovy (UofT) """ if not _APY_LOADED: # pragma: no cover raise ImportError('astropy needs to be installed to use ' 'Orbit.from_name') _load_named_objects() _update_keys_named_objects() # Stack coordinate-transform parameters, so they can be changed... obs= numpy.array([kwargs.get('ro',None), kwargs.get('vo',None), kwargs.get('zo',None), kwargs.get('solarmotion',None)], dtype='object') if len(args) > 1: name= [n for n in args] elif isinstance(args[0],list): name= args[0] else: this_name= _named_objects_key_formatting(args[0]) if this_name in _known_objects['_collections'].keys(): name= _known_objects['_collections'][this_name] else: name= args[0] if isinstance(name,str): out= cls(vxvv=_from_name_oneobject(name,obs),radec=True, ro=obs[0],vo=obs[1],zo=obs[2],solarmotion=obs[3]) else: # assume list all_vxvv= [] for tname in name: all_vxvv.append(_from_name_oneobject(tname,obs)) out= cls(vxvv=all_vxvv,radec=True, ro=obs[0],vo=obs[1],zo=obs[2],solarmotion=obs[3]) out._name= numpy.char.array(name) return out @property @shapeDecorator def name(self): return self._name @classmethod def from_fit(cls,init_vxvv,vxvv,vxvv_err=None,pot=None, radec=False,lb=False, customsky=False,lb_to_customsky=None, pmllpmbb_to_customsky=None, tintJ=10,ntintJ=1000,integrate_method='dopr54_c', ro=None,vo=None,zo=None,solarmotion=None, disp=False): """ NAME: from_fit PURPOSE: Initialize an Orbit using a fit to data INPUT: init_vxvv - initial guess for the fit (same representation [e.g.,radec=True] as vxvv data, except when customsky, then init_vxvv is assumed to be ra,dec) vxvv - [:,6] array of positions and velocities along the orbit (if not lb=True or radec=True, these need to be in natural units [/ro,/vo], cannot be Quantities) vxvv_err= [:,6] array of errors on positions and velocities along the orbit (if None, these are set to 0.01) (if not lb=True or radec=True, these need to be in natural units [/ro,/vo], cannot be Quantities) pot= Potential to fit the orbit in Keywords related to the input data: radec= if True, input vxvv and vxvv are [ra,dec,d,mu_ra, mu_dec,vlos] in [deg,deg,kpc,mas/yr,mas/yr,km/s] (all J2000.0; mu_ra = mu_ra * cos dec); the attributes of the current Orbit are used to convert between these coordinates and Galactocentric coordinates lb= if True, input vxvv and vxvv are [long,lat,d,mu_ll, mu_bb,vlos] in [deg,deg,kpc,mas/yr,mas/yr,km/s] (mu_ll = mu_ll * cos lat); the attributes of the current Orbit are used to convert between these coordinates and Galactocentric coordinates customsky= if True, input vxvv and vxvv_err are [custom long,custom lat,d,mu_customll, mu_custombb,vlos] in [deg,deg,kpc,mas/yr,mas/yr,km/s] (mu_ll = mu_ll * cos lat) where custom longitude and custom latitude are a custom set of sky coordinates (e.g., ecliptic) and the proper motions are also expressed in these coordinates; you need to provide the functions lb_to_customsky and pmllpmbb_to_customsky to convert to the custom sky coordinates (these should have the same inputs and outputs as lb_to_radec and pmllpmbb_to_pmrapmdec); the attributes of the current Orbit are used to convert between these coordinates and Galactocentric coordinates obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer (in kpc and km/s; entries can be Quantity) (default=Object-wide default) Cannot be an Orbit instance with the orbit of the reference point, as w/ the ra etc. functions Y is ignored and always assumed to be zero lb_to_customsky= function that converts l,b,degree=False to the custom sky coordinates (like lb_to_radec); needs to be given when customsky=True pmllpmbb_to_customsky= function that converts pmll,pmbb,l,b,degree=False to proper motions in the custom sky coordinates (like pmllpmbb_to_pmrapmdec); needs to be given when customsky=True Keywords related to the orbit integrations: tintJ= (default: 10) time to integrate orbits for fitting the orbit (can be Quantity) ntintJ= (default: 1000) number of time-integration points integrate_method= (default: 'dopr54_c') integration method to use Keywords related to the coordinate transformation: ro= distance from vantage point to GC (kpc; can be Quantity) vo= circular velocity at ro (km/s; can be Quantity) zo= offset toward the NGP of the Sun wrt the plane (kpc; can be Quantity; default = 20.8 pc from Bennett & Bovy 2019) solarmotion= 'hogg' or 'dehnen', or 'schoenrich', or value in [-U,V,W]; can be Quantity disp= (False) display the optimizer's convergence message OUTPUT: Orbit instance HISTORY: 2014-06-17 - Written - Bovy (IAS) 2019-05-22 - Incorporated into new Orbit class as from_fit - Bovy (UofT) """ pot= flatten_potential(pot) # Setup Orbit instance for initialization to, among other things, # parse the coordinate-transformation keywords init_orbit= cls(init_vxvv,radec=radec or customsky, lb=lb,ro=ro,vo=vo,zo=zo, solarmotion=solarmotion) _check_potential_dim(init_orbit,pot) _check_consistent_units(init_orbit,pot) if radec or lb or customsky: obs, ro, vo= _parse_radec_kwargs(init_orbit, {'ro':init_orbit._ro, 'vo':init_orbit._vo}, vel=True,dontpop=True) else: obs, ro, vo= None, None, None if customsky \ and (lb_to_customsky is None or pmllpmbb_to_customsky is None): raise OSError('if customsky=True, the functions lb_to_customsky and pmllpmbb_to_customsky need to be given') new_vxvv, maxLogL= _fit_orbit(init_orbit,vxvv,vxvv_err,pot, radec=radec,lb=lb, customsky=customsky, lb_to_customsky=lb_to_customsky, pmllpmbb_to_customsky=pmllpmbb_to_customsky, tintJ=tintJ,ntintJ=ntintJ, integrate_method=integrate_method, ro=ro,vo=vo,obs=obs,disp=disp) #Setup with these new initial conditions return cls(new_vxvv, ro=ro,vo=vo,zo=zo,solarmotion=solarmotion) def __len__(self): return 1 if self.shape == () else self.shape[0] def dim(self): """ NAME: dim PURPOSE: return the dimension of the Orbit INPUT: (none) OUTPUT: dimension HISTORY: 2011-02-03 - Written - Bovy (NYU) """ pdim= self.phasedim() if pdim == 2: return 1 elif pdim == 3 or pdim == 4: return 2 elif pdim == 5 or pdim == 6: return 3 def phasedim(self): """ NAME: phasedim PURPOSE: return the phase-space dimension of the problem (2 for 1D, 3 for 2D-axi, 4 for 2D, 5 for 3D-axi, 6 for 3D) INPUT: (none) OUTPUT: phase-space dimension HISTORY: 2018-12-20 - Written - Bovy (UofT) """ return self.vxvv.shape[-1] def __getattr__(self,name): """ NAME: __getattr__ PURPOSE: get or evaluate an attribute for this Orbit instance INPUT: name - name of the attribute OUTPUT: if the attribute is callable, a function to evaluate the attribute for each Orbit; otherwise a list of attributes HISTORY: 2018-10-13 - Written - Mathew Bub (UofT) 2019-02-28 - Implement all plotting function - Bovy (UofT) """ # Catch all plotting functions if 'plot' in name: def _plot(*args,**kwargs): kwargs['d1']= kwargs.get('d1','t') kwargs['d2']= name.split('plot')[1] if ('E' in kwargs['d2'] or kwargs['d2'] == 'Jacobi') \ and kwargs.pop('normed',False): kwargs['d2']+= 'norm' return self.plot(*args,**kwargs) # Assign documentation if 'E' in name or 'Jacobi' in name: Estring= """pot= Potential instance or list of instances in which the orbit was integrated normed= if set, plot {quant}(t)/{quant}(0) rather than {quant}(t) """.format(quant=name.split('plot')[1]) else: Estring= '' _plot.__doc__= """ NAME: plot{quant} PURPOSE: plot {quant}(t) along the orbit INPUT: d1= first dimension to plot ('x', 'y', 'R', 'vR', 'vT', 'z', 'vz', ...); can also be an expression, like 'R*vR', or a user-defined function of time (e.g., lambda t: o.R(t) for R) {Estring}ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output matplotlib.plot inputs+galpy.util.plot.plot inputs OUTPUT: sends plot to output device HISTORY: 2019-04-13 - Written - Bovy (UofT) """.format(quant=name.split('plot')[1],Estring=Estring) return _plot else: raise AttributeError("'{}' object has no attribute '{}'"\ .format(self.__class__.__name__, name)) def __getitem__(self,key): """ NAME: __getitem__ PURPOSE: get a subset of this instance's orbits INPUT: key - slice OUTPUT: For single item: Orbit instance, for multiple items: another Orbit instance HISTORY: 2018-12-31 - Written - Bovy (UofT) """ indx_array= numpy.arange(self.size).reshape(self.shape) indx_array= indx_array[key] flat_indx_array= indx_array.flatten() orbits_list= self.vxvv[flat_indx_array] # Transfer new shape shape_kwargs= {} shape_kwargs['shape']= indx_array.shape # Transfer physical physical_kwargs= {} physical_kwargs['_roSet']= self._roSet physical_kwargs['_voSet']= self._voSet physical_kwargs['_ro']= self._ro physical_kwargs['_vo']= self._vo physical_kwargs['_zo']= self._zo physical_kwargs['_solarmotion']= self._solarmotion # Also transfer all attributes related to integration if hasattr(self,'orbit'): integrate_kwargs= {} integrate_kwargs['t']= self.t integrate_kwargs['_integrate_t_asQuantity']= \ self._integrate_t_asQuantity integrate_kwargs['orbit']= \ copy.deepcopy(self.orbit[flat_indx_array]) integrate_kwargs['_pot']= self._pot else: integrate_kwargs= None # Other things to transfer misc_kwargs= {} if hasattr(self,'_name'): misc_kwargs['_name']= self._name[flat_indx_array] return self._from_slice(orbits_list,integrate_kwargs, shape_kwargs,physical_kwargs, misc_kwargs) @classmethod def _from_slice(cls,orbits_list,integrate_kwargs,shape_kwargs, physical_kwargs,misc_kwargs): out= cls(vxvv=orbits_list) # Set shape out.shape= shape_kwargs['shape'] # Transfer attributes related to physical for kw in physical_kwargs: out.__dict__[kw]= physical_kwargs[kw] # Also transfer all attributes related to integration if not integrate_kwargs is None: for kw in integrate_kwargs: out.__dict__[kw]= integrate_kwargs[kw] # Transfer miscellaneous attributes for kw in misc_kwargs: out.__dict__[kw]= misc_kwargs[kw] return out def reshape(self,newshape): """ NAME: reshape PURPOSE: Change the shape of the Orbit instance INPUT: newshape - new shape (int or tuple of ints; see numpy.reshape) OUTPUT: (none; re-shaping is done in-place) HISTORY: 2019-03-20 - Written - Bovy (UofT) """ # We reshape a dummy numpy array to use numpy.reshape's parsing dummy= numpy.empty(self.shape) try: dummy= dummy.reshape(newshape) except ValueError: raise (ValueError('cannot reshape Orbit of shape %s into shape %s' % (self.shape,newshape))) from None self.shape= dummy.shape return None ############################ CUSTOM IMPLEMENTED ORBIT FUNCTIONS################ def turn_physical_off(self): """ NAME: turn_physical_off PURPOSE: turn off automatic returning of outputs in physical units INPUT: (none) OUTPUT: (none) HISTORY: 2019-02-28 - Written - Bovy (UofT) """ self._roSet= False self._voSet= False return None def turn_physical_on(self,ro=None,vo=None): """ NAME: turn_physical_on PURPOSE: turn on automatic returning of outputs in physical units INPUT: ro= reference distance (kpc; can be Quantity) vo= reference velocity (km/s; can be Quantity) OUTPUT: (none) HISTORY: 2019-02-28 - Written - Bovy (UofT) 2020-04-22 - Don't turn on a parameter when it is False - Bovy (UofT) """ if not ro is False: self._roSet= True if not vo is False: self._voSet= True if not ro is None and ro: self._ro= conversion.parse_length_kpc(ro) if not vo is None and vo: self._vo= conversion.parse_velocity_kms(vo) return None def integrate(self,t,pot,method='symplec4_c',progressbar=True, dt=None,numcores=_NUMCORES, force_map=False): """ NAME: integrate PURPOSE: integrate this Orbit instance with multiprocessing INPUT: t - list of times at which to output (0 has to be in this!) (can be Quantity) pot - potential instance or list of instances method = 'odeint' for scipy's odeint 'leapfrog' for a simple leapfrog implementation 'leapfrog_c' for a simple leapfrog implementation in C 'symplec4_c' for a 4th order symplectic integrator in C 'symplec6_c' for a 6th order symplectic integrator in C 'rk4_c' for a 4th-order Runge-Kutta integrator in C 'rk6_c' for a 6-th order Runge-Kutta integrator in C 'dopr54_c' for a 5-4 Dormand-Prince integrator in C 'dop853' for a 8-5-3 Dormand-Prince integrator in Python 'dop853_c' for a 8-5-3 Dormand-Prince integrator in C progressbar= (True) if True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!) dt - if set, force the integrator to use this basic stepsize; must be an integer divisor of output stepsize (only works for the C integrators that use a fixed stepsize) (can be Quantity) numcores - number of cores to use for Python-based multiprocessing (pure Python or using force_map=True); default = OMP_NUM_THREADS force_map= (False) if True, force use of Python-based multiprocessing (not recommended) OUTPUT: None (get the actual orbit using getOrbit()) HISTORY: 2018-10-13 - Written as parallel_map applied to regular Orbit integration - Mathew Bub (UofT) 2018-12-26 - Written to use OpenMP C implementation - Bovy (UofT) """ if method.lower() not in ['odeint', 'leapfrog', 'dop853', 'leapfrog_c', 'symplec4_c', 'symplec6_c', 'rk4_c', 'rk6_c', 'dopr54_c', 'dop853_c']: raise ValueError(f'{method:s} is not a valid `method`') pot= flatten_potential(pot) _check_potential_dim(self,pot) _check_consistent_units(self,pot) # Parse t if _APY_LOADED and isinstance(t,units.Quantity): self._integrate_t_asQuantity= True t= conversion.parse_time(t,ro=self._ro,vo=self._vo) else: self._integrate_t_asQuantity= False if _APY_LOADED and not dt is None and isinstance(dt,units.Quantity): dt= conversion.parse_time(dt,ro=self._ro,vo=self._vo) from ..potential import MWPotential if pot == MWPotential: warnings.warn("Use of MWPotential as a Milky-Way-like potential is deprecated; galpy.potential.MWPotential2014, a potential fit to a large variety of dynamical constraints (see Bovy 2015), is the preferred Milky-Way-like potential in galpy", galpyWarning) if not _check_integrate_dt(t,dt): raise ValueError('dt input (integrator stepsize) for Orbit.integrate must be an integer divisor of the output stepsize') # Delete attributes for interpolation and rperi etc. determination if hasattr(self,'_orbInterp'): delattr(self,'_orbInterp') if hasattr(self,'rs'): delattr(self,'rs') if self.dim() == 2: thispot= toPlanarPotential(pot) else: thispot= pot self.t= numpy.array(t) self._pot= thispot #First check that the potential has C if '_c' in method: if not ext_loaded or not _check_c(self._pot): if ('leapfrog' in method or 'symplec' in method): method= 'leapfrog' else: method= 'odeint' if not ext_loaded: # pragma: no cover warnings.warn("Cannot use C integration because C extension not loaded (using %s instead)" % (method), galpyWarning) else: warnings.warn("Cannot use C integration because some of the potentials are not implemented in C (using %s instead)" % (method), galpyWarning) # Now check that we aren't trying to integrate a dissipative force # with a symplectic integrator if _isDissipative(self._pot) and ('leapfrog' in method or 'symplec' in method): if '_c' in method: method= 'dopr54_c' else: method= 'odeint' warnings.warn("Cannot use symplectic integration because some of the included forces are dissipative (using non-symplectic integrator %s instead)" % (method), galpyWarning) # Implementation with parallel_map in Python if not '_c' in method or not ext_loaded or force_map: if self.dim() == 1: out, msg= integrateLinearOrbit(self._pot,self.vxvv,t,method, progressbar=progressbar, numcores=numcores,dt=dt) elif self.dim() == 2: out, msg= integratePlanarOrbit(self._pot,self.vxvv,t,method, progressbar=progressbar, numcores=numcores,dt=dt) else: out, msg= integrateFullOrbit(self._pot,self.vxvv,t,method, progressbar=progressbar, numcores=numcores,dt=dt) else: warnings.warn("Using C implementation to integrate orbits", galpyWarningVerbose) if self.dim() == 1: out, msg= integrateLinearOrbit_c(self._pot, numpy.copy(self.vxvv), t,method, progressbar=progressbar, dt=dt) else: if self.phasedim() == 3 \ or self.phasedim() == 5: #We hack this by putting in a dummy phi=0 vxvvs= numpy.pad(self.vxvv,((0,0),(0,1)), 'constant',constant_values=0) else: vxvvs= numpy.copy(self.vxvv) if self.dim() == 2: out, msg= integratePlanarOrbit_c(self._pot,vxvvs, t,method, progressbar=progressbar, dt=dt) else: out, msg= integrateFullOrbit_c(self._pot,vxvvs, t,method, progressbar=progressbar, dt=dt) if self.phasedim() == 3 \ or self.phasedim() == 5: out= out[:,:,:-1] # Store orbit internally self.orbit= out # Check whether r ever < minr if dynamical friction is included # and warn if so # or if using interpSphericalPotential and r < rmin or r > rmax from ..potential import (ChandrasekharDynamicalFrictionForce, interpSphericalPotential) if numpy.any([isinstance(p,ChandrasekharDynamicalFrictionForce) for p in flatten_potential([pot])]): # make sure pot=list lpot= flatten_potential([pot]) cdf_indx= numpy.arange(len(lpot))[\ numpy.array([isinstance(p,ChandrasekharDynamicalFrictionForce) for p in lpot],dtype='bool')][0] if numpy.any(self.r(self.t,use_physical=False) \ < lpot[cdf_indx]._minr): warnings.warn("""Orbit integration with """ """ChandrasekharDynamicalFrictionForce """ """entered domain where r < minr and """ """ChandrasekharDynamicalFrictionForce is """ """turned off; initialize """ """ChandrasekharDynamicalFrictionForce with a """ """smaller minr to avoid this if you wish """ """(but note that you want to turn it off """ """close to the center for an object that """ """sinks all the way to r=0, to avoid """ """numerical instabilities)""", galpyWarning) elif numpy.any([isinstance(p,interpSphericalPotential) for p in flatten_potential([pot])]): # make sure pot=list lpot= flatten_potential([pot]) isp_indx= numpy.arange(len(lpot))[\ numpy.array([isinstance(p,interpSphericalPotential) for p in lpot],dtype='bool')][0] if numpy.any(self.r(self.t,use_physical=False) \ < lpot[isp_indx]._rmin) \ or numpy.any(self.r(self.t,use_physical=False) \ > lpot[isp_indx]._rmax): warnings.warn("""Orbit integration with """ """interpSphericalPotential visited radii """ """outside of the interpolation range; """ """initialize interpSphericalPotential """ """with a wider radial range to avoid this """ """if you wish (min/max r = {:.3f},{:.3f}"""\ .format(self.rperi(),self.rap()), galpyWarning) return None def integrate_dxdv(self,dxdv,t,pot,method='dopr54_c', progressbar=True,dt=None, numcores=_NUMCORES,force_map=False, rectIn=False,rectOut=False): """ NAME: integrate_dxdv PURPOSE: integrate the orbit and a small area of phase space INPUT: dxdv - [dR,dvR,dvT,dphi], shape=(*input_shape,4) t - list of times at which to output (0 has to be in this!) (can be Quantity) pot - potential instance or list of instances progressbar= (True) if True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!) dt - if set, force the integrator to use this basic stepsize; must be an integer divisor of output stepsize (only works for the C integrators that use a fixed stepsize) (can be Quantity) method = 'odeint' for scipy's odeint 'rk4_c' for a 4th-order Runge-Kutta integrator in C 'rk6_c' for a 6-th order Runge-Kutta integrator in C 'dopr54_c' for a 5-4 Dormand-Prince integrator in C 'dopr853_c' for a 8-5-3 Dormand-Prince integrator in C rectIn= (False) if True, input dxdv is in rectangular coordinates rectOut= (False) if True, output dxdv (that in orbit_dxdv) is in rectangular coordinates numcores - number of cores to use for Python-based multiprocessing (pure Python or using force_map=True); default = OMP_NUM_THREADS force_map= (False) if True, force use of Python-based multiprocessing (not recommended) OUTPUT: (none) (get the actual orbit using getOrbit_dxdv(), the orbit that is integrated alongside with dxdv is stored as usual, any previous regular orbit integration will be erased!) HISTORY: 2011-10-17 - Written - Bovy (IAS) 2014-06-29 - Added rectIn and rectOut - Bovy (IAS) 2019-05-21 - Parallelized and incorporated into new Orbits class - Bovy (UofT) """ if not self.phasedim() == 4: raise AttributeError('integrate_dxdv is only implemented for 4D (planar) orbits') if method.lower() not in ['odeint', 'dop853', 'rk4_c', 'rk6_c', 'dopr54_c', 'dop853_c']: if 'leapfrog' in method.lower() or 'symplec' in method.lower(): raise ValueError(f'{method:s} is not a valid `method for integrate_dxdv, because symplectic integrators cannot be used`') else: raise ValueError(f'{method:s} is not a valid `method for integrate_dxdv`') pot= flatten_potential(pot) _check_potential_dim(self,pot) _check_consistent_units(self,pot) # Parse t if _APY_LOADED and isinstance(t,units.Quantity): self._integrate_t_asQuantity= True t= conversion.parse_time(t,ro=self._ro,vo=self._vo) else: self._integrate_t_asQuantity= False if not dt is None: dt= conversion.parse_time(dt,ro=self._ro,vo=self._vo) # Parse dxdv dxdv= numpy.array(dxdv) if dxdv.ndim > 1: dxdv= dxdv.reshape((numpy.prod(dxdv.shape[:-1]),dxdv.shape[-1])) else: dxdv= numpy.atleast_2d(dxdv) # Delete attributes for interpolation and rperi etc. determination if hasattr(self,'_orbInterp'): delattr(self,'_orbInterp') if hasattr(self,'rs'): delattr(self,'rs') if self.dim() == 2: thispot= toPlanarPotential(pot) self.t= numpy.array(t) self._pot_dxdv= thispot self._pot= thispot #First check that the potential has C if '_c' in method: allHasC= _check_c(pot) and _check_c(pot,dxdv=True) if not ext_loaded or \ (not allHasC and not 'leapfrog' in method and not 'symplec' in method): method= 'odeint' if not ext_loaded: # pragma: no cover warnings.warn("Cannot use C integration because C extension not loaded (using %s instead)" % (method), galpyWarning) else: warnings.warn("Using odeint because not all used potential have adequate C implementations to integrate phase-space volumes",galpyWarning) # Implementation with parallel_map in Python if True or not '_c' in method or not ext_loaded or force_map: if self.dim() == 2: out, msg= integratePlanarOrbit_dxdv(self._pot,self.vxvv,dxdv, t,method,rectIn,rectOut, progressbar=progressbar, numcores=numcores,dt=dt) # Store orbit internally self.orbit_dxdv= out self.orbit= self.orbit_dxdv[...,:4] return None def flip(self,inplace=False): """ NAME: flip PURPOSE: 'flip' an orbit's initial conditions such that the velocities are minus the original velocities; useful for quick backward integration; returns a new Orbit instance INPUT: inplace= (False) if True, flip the orbit in-place, that is, without returning a new instance and also flip the velocities of the integrated orbit (if it exists) OUTPUT: Orbit instance that has the velocities of the current orbit flipped (inplace=False) or just flips all velocities of current instance (inplace=True) HISTORY: 2019-03-02 - Written - Bovy (UofT) """ if inplace: self.vxvv[...,1]= -self.vxvv[...,1] if self.phasedim() > 2: self.vxvv[...,2]= -self.vxvv[...,2] if self.phasedim() > 4: self.vxvv[...,4]= -self.vxvv[...,4] if hasattr(self,'orbit'): self.orbit[...,1]= -self.orbit[...,1] if self.phasedim() > 2: self.orbit[...,2]= -self.orbit[...,2] if self.phasedim() > 4: self.orbit[...,4]= -self.orbit[...,4] if hasattr(self,"_orbInterp"): delattr(self,"_orbInterp") return None orbSetupKwargs= {'ro':self._ro, 'vo':self._vo, 'zo':self._zo, 'solarmotion':self._solarmotion} if self.phasedim() == 2: orbSetupKwargs.pop('zo',None) orbSetupKwargs.pop('solarmotion',None) out= Orbit(numpy.array([self.vxvv[...,0],-self.vxvv[...,1]]).T, **orbSetupKwargs) elif self.phasedim() == 3: out= Orbit(numpy.array([self.vxvv[...,0],-self.vxvv[...,1], -self.vxvv[...,2]]).T,**orbSetupKwargs) elif self.phasedim() == 4: out= Orbit(numpy.array([self.vxvv[...,0],-self.vxvv[...,1], -self.vxvv[...,2],self.vxvv[...,3]]).T, **orbSetupKwargs) elif self.phasedim() == 5: out= Orbit(numpy.array([self.vxvv[...,0],-self.vxvv[...,1], -self.vxvv[...,2],self.vxvv[...,3], -self.vxvv[...,4]]).T,**orbSetupKwargs) elif self.phasedim() == 6: out= Orbit(numpy.array([self.vxvv[...,0],-self.vxvv[...,1], -self.vxvv[...,2],self.vxvv[...,3], -self.vxvv[...,4],self.vxvv[...,5]]).T, **orbSetupKwargs) out._roSet= self._roSet out._voSet= self._voSet # Make sure the output has the same shape as the original Orbit out.reshape(self.shape) return out @shapeDecorator def getOrbit(self): """ NAME: getOrbit PURPOSE: return previously calculated orbits INPUT: (none) OUTPUT: array orbit[*input_shape,nt,nphasedim] HISTORY: 2019-03-02 - Written - Bovy (UofT) """ return self.orbit.copy() @shapeDecorator def getOrbit_dxdv(self): """ NAME: getOrbit_dxdv PURPOSE: return a previously calculated integration of a small phase-space volume (with integrate_dxdv) INPUT: (none) OUTPUT: array orbit[*input_shape,nt,nphasedim] HISTORY: 2019-05-21 - Written - Bovy (UofT) """ return self.orbit_dxdv[...,4:].copy() @physical_conversion('energy') @shapeDecorator def E(self,*args,**kwargs): """ NAME: E PURPOSE: calculate the energy INPUT: t - (optional) time at which to get the energy (can be Quantity) pot= Potential instance or list of such instances vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: energy [*input_shape,nt] HISTORY: 2019-03-01 - Written - Bovy (UofT) """ if not kwargs.get('pot',None) is None: kwargs['pot']= flatten_potential(kwargs.get('pot')) _check_consistent_units(self,kwargs.get('pot',None)) if not 'pot' in kwargs or kwargs['pot'] is None: try: pot= self._pot except AttributeError: raise AttributeError("Integrate orbits or specify pot=") if 'pot' in kwargs and kwargs['pot'] is None: kwargs.pop('pot') else: pot= kwargs.pop('pot') if self.dim() == 2: pot= toPlanarPotential(pot) if len(args) > 0: t= args[0] else: t= 0. #Get orbit thiso= self._call_internal(*args,**kwargs) onet= (len(thiso.shape) == 2) if onet: thiso= thiso[:,numpy.newaxis,:] t= numpy.atleast_1d(t) if self.phasedim() == 2: try: out= (evaluatelinearPotentials(\ pot,thiso[0], t=numpy.tile(t,thiso[0].T.shape[:-1]+(1,)).T, use_physical=False)\ +thiso[1]**2./2.).T except (ValueError,TypeError,IndexError): out= (numpy.array([[evaluatelinearPotentials(\ pot,thiso[0][ii][jj],t=t[ii], use_physical=False) for ii in range(len(thiso[0]))] for jj in range(self.size)])\ +(thiso[1]**2./2.).T) elif self.phasedim() == 3: try: out= (evaluateplanarPotentials(\ pot,thiso[0], t=numpy.tile(t,thiso[0].T.shape[:-1]+(1,)).T, use_physical=False)\ +thiso[1]**2./2.+thiso[2]**2./2.).T except (ValueError,TypeError,IndexError): out= (numpy.array([[evaluateplanarPotentials(\ pot,thiso[0][ii][jj],t=t[ii], use_physical=False) for ii in range(len(thiso[0]))] for jj in range(self.size)]) +(thiso[1]**2./2.+thiso[2]**2./2.).T) elif self.phasedim() == 4: try: out= (evaluateplanarPotentials(\ pot,thiso[0],phi=thiso[-1], t=numpy.tile(t,thiso[0].T.shape[:-1]+(1,)).T, use_physical=False)\ +thiso[1]**2./2.+thiso[2]**2./2.).T except (ValueError,TypeError,IndexError): out= (numpy.array([[evaluateplanarPotentials(\ pot,thiso[0][ii][jj],t=t[ii], phi=thiso[-1][ii][jj], use_physical=False) for ii in range(len(thiso[0]))] for jj in range(self.size)]) +(thiso[1]**2./2.+thiso[2]**2./2.).T) elif self.phasedim() == 5: z= kwargs.get('_z',1.)*thiso[3] # For ER and Ez vz= kwargs.get('_vz',1.)*thiso[4] # For ER and Ez try: out= (evaluatePotentials(\ pot,thiso[0],z, t=numpy.tile(t,thiso[0].T.shape[:-1]+(1,)).T, use_physical=False)\ +thiso[1]**2./2.+thiso[2]**2./2.+vz**2./2.).T except (ValueError,TypeError,IndexError): out= (numpy.array([[evaluatePotentials(\ pot,thiso[0][ii][jj], z[ii][jj], t=t[ii], use_physical=False) for ii in range(len(thiso[0]))] for jj in range(self.size)]) +(thiso[1]**2./2.+thiso[2]**2./2.+vz**2./2.).T) elif self.phasedim() == 6: z= kwargs.get('_z',1.)*thiso[3] # For ER and Ez vz= kwargs.get('_vz',1.)*thiso[4] # For ER and Ez try: out= (evaluatePotentials(\ pot,thiso[0],z,phi=thiso[-1], t=numpy.tile(t,thiso[0].T.shape[:-1]+(1,)).T, use_physical=False)\ +thiso[1]**2./2.+thiso[2]**2./2.+vz**2./2.).T except (ValueError,TypeError,IndexError): out= (numpy.array([[evaluatePotentials(\ pot,thiso[0][ii][jj], z[ii][jj], t=t[ii], phi=thiso[-1][ii][jj], use_physical=False) for ii in range(len(thiso[0]))] for jj in range(self.size)]) +(thiso[1]**2./2.+thiso[2]**2./2.+vz**2./2.).T) if onet: return out[:,0] else: return out @physical_conversion('action') @shapeDecorator def L(self,*args,**kwargs): """ NAME: L PURPOSE: calculate the angular momentum at time t INPUT: t - (optional) time at which to get the angular momentum (can be Quantity) ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: angular momentum [*input_shape,nt,3] HISTORY: 2019-03-01 - Written - Bovy (UofT) """ if self.dim() == 1: raise AttributeError("'linear Orbit has no angular momentum") #Get orbit if self.dim() == 2: thiso= self._call_internal(*args,**kwargs) return (thiso[0]*thiso[2]).T elif self.phasedim() == 5: raise AttributeError("You must track the azimuth to get the angular momentum of a 3D Orbit") else: # phasedim == 6 old_physical= kwargs.get('use_physical',None) kwargs['use_physical']= False kwargs['dontreshape']= True vx= self.vx(*args,**kwargs) vy= self.vy(*args,**kwargs) vz= self.vz(*args,**kwargs) x= self.x(*args,**kwargs) y= self.y(*args,**kwargs) z= self.z(*args,**kwargs) out= numpy.zeros(x.shape+(3,)) out[...,0]= y*vz-z*vy out[...,1]= z*vx-x*vz out[...,2]= x*vy-y*vx if not old_physical is None: kwargs['use_physical']= old_physical else: kwargs.pop('use_physical') kwargs.pop('dontreshape') return out @physical_conversion('action') @shapeDecorator def Lz(self,*args,**kwargs): """ NAME: Lz PURPOSE: calculate the z-component of the angular momentum at time t INPUT: t - (optional) time at which to get the angular momentum (can be Quantity) ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: z-component of the angular momentum [*input_shape,nt] HISTORY: 2019-03-01 - Written - Bovy (UofT) """ thiso= self._call_internal(*args,**kwargs) return (thiso[0]*thiso[2]).T @physical_conversion('energy') @shapeDecorator def ER(self,*args,**kwargs): """ NAME: ER PURPOSE: calculate the radial energy INPUT: t - (optional) time at which to get the radial energy (can be Quantity) pot= Potential instance or list of such instances vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output (can be Quantity) OUTPUT: radial energy [*input_shape,nt] HISTORY: 2019-03-01 - Written - Bovy (UofT) """ old_physical= kwargs.get('use_physical',None) kwargs['use_physical']= False kwargs['_z']= 0. kwargs['_vz']= 0. kwargs['dontreshape']= True out= self.E(*args,**kwargs) if not old_physical is None: kwargs['use_physical']= old_physical else: kwargs.pop('use_physical') kwargs.pop('_z') kwargs.pop('_vz') kwargs.pop('dontreshape') return out @physical_conversion('energy') @shapeDecorator def Ez(self,*args,**kwargs): """ NAME: Ez PURPOSE: calculate the vertical energy INPUT: t - (optional) time at which to get the vertical energy (can be Quantity) pot= Potential instance or list of such instances vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output (can be Quantity) OUTPUT: vertical energy [*input_shape,nt] HISTORY: 2019-03-01 - Written - Bovy (UofT) """ old_physical= kwargs.get('use_physical',None) kwargs['use_physical']= False kwargs['dontreshape']= True tE= self.E(*args,**kwargs) kwargs['_z']= 0. kwargs['_vz']= 0. out= tE-self.E(*args,**kwargs) if not old_physical is None: kwargs['use_physical']= old_physical else: kwargs.pop('use_physical') kwargs.pop('_z') kwargs.pop('_vz') kwargs.pop('dontreshape') return out @physical_conversion('energy') @shapeDecorator def Jacobi(self,*args,**kwargs): """ NAME: Jacobi PURPOSE: calculate the Jacobi integral E - Omega L INPUT: t - (optional) time at which to get the Jacobi integral (can be Quantity) OmegaP= pattern speed (can be Quantity) pot= potential instance or list of such instances vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: Jacobi integral [*input_shape,nt] HISTORY: 2019-03-01 - Written - Bovy (UofT) """ if not kwargs.get('pot',None) is None: kwargs['pot']= flatten_potential(kwargs.get('pot')) _check_consistent_units(self,kwargs.get('pot',None)) if not 'OmegaP' in kwargs or kwargs['OmegaP'] is None: OmegaP= 1. if not 'pot' in kwargs or kwargs['pot'] is None: try: pot= self._pot except AttributeError: raise AttributeError("Integrate orbit or specify pot=") else: pot= kwargs['pot'] if isinstance(pot,list): for p in pot: if hasattr(p,'OmegaP'): OmegaP= p.OmegaP() break else: if hasattr(pot,'OmegaP'): OmegaP= pot.OmegaP() kwargs.pop('OmegaP',None) else: OmegaP= kwargs.pop('OmegaP') OmegaP= conversion.parse_frequency(OmegaP,ro=self._ro,vo=self._vo) #Make sure you are not using physical coordinates old_physical= kwargs.get('use_physical',None) kwargs['use_physical']= False kwargs['dontreshape']= True if not isinstance(OmegaP,(int,float)) and len(OmegaP) == 3: if isinstance(OmegaP,list): thisOmegaP= numpy.array(OmegaP) else: thisOmegaP= OmegaP tL= self.L(*args,**kwargs) if len(tL.shape) == 2: # 1 time out= self.E(*args,**kwargs)-numpy.einsum('i,ji->j', thisOmegaP,tL) else: out= self.E(*args,**kwargs)-numpy.einsum('i,jki->jk', thisOmegaP,tL) else: out= self.E(*args,**kwargs)-OmegaP*self.Lz(*args,**kwargs) if not old_physical is None: kwargs['use_physical']= old_physical else: kwargs.pop('use_physical') kwargs.pop('dontreshape') return out def _setupaA(self,pot=None,type='staeckel',**kwargs): """ NAME: _setupaA PURPOSE: set up an actionAngle module for this Orbit INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' OUTPUT: HISTORY: 2019-02-25 - Written based on OrbitTop._setupaA - Bovy (UofT) """ from .. import actionAngle if not pot is None: pot= flatten_potential(pot) if self.dim() == 2 and (type == 'staeckel' or type == 'adiabatic'): # No reason to do Staeckel or adiabatic... type= 'spherical' elif self.dim() == 1: raise RuntimeError("Orbit action-angle methods are not supported for 1D orbits") delta= kwargs.pop('delta',None) if not delta is None: delta= conversion.parse_length(delta,ro=self._ro) b= kwargs.pop('b',None) if not b is None: b= conversion.parse_length(b,ro=self._ro) if pot is None: try: pot= self._pot except AttributeError: raise AttributeError("Integrate orbit or specify pot=") if hasattr(self,'_aA'): if (not pot is None and pot != self._aAPot) \ or (not type is None and type != self._aAType) \ or (not delta is None and hasattr(self._aA,'_delta') and numpy.any(delta != self._aA._delta)) \ or (delta is None and hasattr(self,'_aA_delta_automagic') and not self._aA_delta_automagic) \ or (not b is None and hasattr(self._aA,'_aAI') and numpy.any(b != self._aA._aAI.b)) \ or ('ip' in kwargs and hasattr(self._aA,'_aAI') and (numpy.any(kwargs['ip'].b != self._aA._aAI.b) \ or numpy.any(kwargs['ip']._amp != self._aA._aAI.amp))): for attr in list(self.__dict__): if '_aA' in attr: delattr(self,attr) else: return None _check_consistent_units(self,pot) self._aAPot= pot self._aAType= type #Setup if self._aAType.lower() == 'adiabatic': self._aA= actionAngle.actionAngleAdiabatic(pot=self._aAPot, **kwargs) elif self._aAType.lower() == 'staeckel': # try to make sure this is not 0 tz= self.z(use_physical=False,dontreshape=True)\ +(numpy.fabs(self.z(use_physical=False, dontreshape=True)) < 1e-8) \ * (2.*(self.z(use_physical=False, dontreshape=True) >= 0)-1.)*1e-10 self._aA_delta_automagic= False if delta is None: self._aA_delta_automagic= True try: delta= actionAngle.estimateDeltaStaeckel(\ self._aAPot, self.R(use_physical=False,dontreshape=True), tz,no_median=True,use_physical=False) except PotentialError as e: if 'deriv' in str(e): raise PotentialError('Automagic calculation of delta parameter for Staeckel approximation failed because the necessary second derivatives of the given potential are not implemented; set delta= explicitly (to a single value or an array with the same shape as the orbits') elif 'non-axi' in str(e): raise PotentialError('Automagic calculation of delta parameter for Staeckel approximation failed because the given potential is not axisymmetric; pass an axisymmetric potential instead') else: #pragma: no cover raise if numpy.all(delta == 1e-6): self._setupaA(pot=pot,type='spherical') else: if hasattr(delta,"__len__"): delta[delta < 1e-6]= 1e-6 self._aA= actionAngle.actionAngleStaeckel(pot=self._aAPot, delta=delta, **kwargs) elif self._aAType.lower() == 'isochroneapprox': from ..actionAngle import actionAngleIsochroneApprox self._aA= actionAngleIsochroneApprox(pot=self._aAPot,b=b, **kwargs) elif self._aAType.lower() == 'spherical': self._aA= actionAngle.actionAngleSpherical(pot=self._aAPot, **kwargs) return None def _setup_EccZmaxRperiRap(self,pot=None,**kwargs): """Internal function to compute e,zmax,rperi,rap and cache it for re-use""" self._setupaA(pot=pot,**kwargs) if hasattr(self,'_aA_ecc'): return None if self.dim() == 3: # try to make sure this is not 0 tz= self.z(use_physical=False,dontreshape=True)\ +(numpy.fabs(self.z(use_physical=False, dontreshape=True)) < 1e-8) \ * (2.*(self.z(use_physical=False, dontreshape=True) >= 0)-1.)*1e-10 tvz= self.vz(use_physical=False,dontreshape=True) elif self.dim() == 2: tz= numpy.zeros(self.size) tvz= numpy.zeros(self.size) # self.dim() == 1 error caught by _setupaA self._aA_ecc, self._aA_zmax, self._aA_rperi, self._aA_rap=\ self._aA.EccZmaxRperiRap(self.R(use_physical=False, dontreshape=True), self.vR(use_physical=False, dontreshape=True), self.vT(use_physical=False, dontreshape=True), tz,tvz, use_physical=False) return None def _setup_actionsFreqsAngles(self,pot=None,**kwargs): """Internal function to compute the actions, frequencies, and angles and cache them for re-use""" self._setupaA(pot=pot,**kwargs) if hasattr(self,'_aA_jr'): return None if self.dim() == 3: # try to make sure this is not 0 tz= self.z(use_physical=False,dontreshape=True)\ +(numpy.fabs(self.z(use_physical=False, dontreshape=True)) < 1e-8) \ * (2.*(self.z(use_physical=False, dontreshape=True) >= 0)-1.)*1e-10 tvz= self.vz(use_physical=False,dontreshape=True) elif self.dim() == 2: tz= numpy.zeros(self.size) tvz= numpy.zeros(self.size) # self.dim() == 1 error caught by _setupaA self._aA_jr, self._aA_jp, self._aA_jz, \ self._aA_Or, self._aA_Op, self._aA_Oz, \ self._aA_wr, self._aA_wp, self._aA_wz= \ self._aA.actionsFreqsAngles(\ self.R(use_physical=False,dontreshape=True), self.vR(use_physical=False,dontreshape=True), self.vT(use_physical=False,dontreshape=True), tz,tvz, self.phi(use_physical=False,dontreshape=True), use_physical=False) return None def _setup_actions(self,pot=None,**kwargs): """Internal function to compute the actions and cache them for re-use (used for methods that don't support frequencies and angles)""" self._setupaA(pot=pot,**kwargs) if hasattr(self,'_aA_jr'): return None if self.dim() == 3: # try to make sure this is not 0 tz= self.z(use_physical=False,dontreshape=True)\ +(numpy.fabs(self.z(use_physical=False, dontreshape=True)) < 1e-8) \ * (2.*(self.z(use_physical=False, dontreshape=True) >= 0)-1.)*1e-10 tvz= self.vz(use_physical=False,dontreshape=True) elif self.dim() == 2: tz= numpy.zeros(self.size) tvz= numpy.zeros(self.size) # self.dim() == 1 error caught by _setupaA self._aA_jr, self._aA_jp, self._aA_jz= self._aA(\ self.R(use_physical=False,dontreshape=True), self.vR(use_physical=False,dontreshape=True), self.vT(use_physical=False,dontreshape=True), tz,tvz, self.phi(use_physical=False,dontreshape=True), use_physical=False) return None @shapeDecorator def e(self,analytic=False,pot=None,**kwargs): """ NAME: e PURPOSE: calculate the eccentricity, either numerically from the numerical orbit integration or using analytical means INPUT: analytic(= False) compute this analytically pot - potential to use for analytical calculation For 3D orbits different approximations for analytic=True are available (see the EccZmaxRperiRap method of actionAngle modules): type= ('staeckel') type of actionAngle module to use 1) 'adiabatic': assuming motion splits into R and z 2) 'staeckel': assuming motion splits into u and v of prolate spheroidal coordinate system, exact for Staeckel potentials (incl. all spherical potentials) 3) 'spherical': for spherical potentials, exact +actionAngle module setup kwargs for the corresponding actionAngle modules (actionAngleAdiabatic, actionAngleStaeckel, and actionAngleSpherical) OUTPUT: eccentricity [*input_shape] HISTORY: 2019-02-25 - Written - Bovy (UofT) """ if analytic: self._setup_EccZmaxRperiRap(pot=pot,**kwargs) return self._aA_ecc if not hasattr(self,'orbit'): raise AttributeError("Integrate the orbit first or use analytic=True for approximate eccentricity") rs= self.r(self.t,use_physical=False,dontreshape=True) return (numpy.amax(rs,axis=-1)-numpy.amin(rs,axis=-1))\ /(numpy.amax(rs,axis=-1)+numpy.amin(rs,axis=-1)) @physical_conversion('position') @shapeDecorator def rap(self,analytic=False,pot=None,**kwargs): """ NAME: rap PURPOSE: calculate the apocenter radius, either numerically from the numerical orbit integration or using analytical means INPUT: analytic(= False) compute this analytically pot - potential to use for analytical calculation For 3D orbits different approximations for analytic=True are available (see the EccZmaxRperiRap method of actionAngle modules): type= ('staeckel') type of actionAngle module to use 1) 'adiabatic': assuming motion splits into R and z 2) 'staeckel': assuming motion splits into u and v of prolate spheroidal coordinate system, exact for Staeckel potentials (incl. all spherical potentials) 3) 'spherical': for spherical potentials, exact +actionAngle module setup kwargs for the corresponding actionAngle modules (actionAngleAdiabatic, actionAngleStaeckel, and actionAngleSpherical) ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: R_ap [*input_shape] HISTORY: 2019-02-25 - Written - Bovy (UofT) """ if analytic: self._setup_EccZmaxRperiRap(pot=pot,**kwargs) return self._aA_rap if not hasattr(self,'orbit'): raise AttributeError("Integrate the orbit first or use analytic=True for approximate eccentricity") rs= self.r(self.t,use_physical=False,dontreshape=True) return numpy.amax(rs,axis=-1) @physical_conversion('position') @shapeDecorator def rperi(self,analytic=False,pot=None,**kwargs): """ NAME: rperi PURPOSE: calculate the pericenter radius, either numerically from the numerical orbit integration or using analytical means INPUT: analytic(= False) compute this analytically pot - potential to use for analytical calculation For 3D orbits different approximations for analytic=True are available (see the EccZmaxRperiRap method of actionAngle modules): type= ('staeckel') type of actionAngle module to use 1) 'adiabatic': assuming motion splits into R and z 2) 'staeckel': assuming motion splits into u and v of prolate spheroidal coordinate system, exact for Staeckel potentials (incl. all spherical potentials) 3) 'spherical': for spherical potentials, exact +actionAngle module setup kwargs for the corresponding actionAngle modules (actionAngleAdiabatic, actionAngleStaeckel, and actionAngleSpherical) ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: R_peri [*input_shape] HISTORY: 2019-02-25 - Written - Bovy (UofT) """ if analytic: self._setup_EccZmaxRperiRap(pot=pot,**kwargs) return self._aA_rperi if not hasattr(self,'orbit'): raise AttributeError("Integrate the orbit first or use analytic=True for approximate eccentricity") rs= self.r(self.t,use_physical=False,dontreshape=True) return numpy.amin(rs,axis=-1) @physical_conversion('position') @shapeDecorator def rguiding(self,*args,**kwargs): """ NAME: rguiding PURPOSE: calculate the guiding-center radius (the radius of a circular orbit with the same angular momentum) INPUT: pot= potential instance or list of such instances ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: R_guiding [*input_shape,nt] HISTORY: 2019-03-02 - Written as thin wrapper around Potential.rl - Bovy (UofT) """ pot= kwargs.get('pot',self.__dict__.get('_pot',None)) if pot is None: raise RuntimeError("You need to specify the potential as pot= to compute the guiding-center radius") flatten_potential(pot) if _isNonAxi(pot): raise RuntimeError('Potential given to rguiding is non-axisymmetric, but rguiding requires an axisymmetric potential') _check_consistent_units(self,pot) Lz= numpy.atleast_1d(self.Lz(*args,use_physical=False, dontreshape=True)) Lz_shape= Lz.shape Lz= Lz.flatten() if len(Lz) > 500: # Build interpolation grid, 500 ~ 1s precomputergLzgrid= numpy.linspace(numpy.nanmin(Lz), numpy.nanmax(Lz), 500) rls= numpy.array([rl(pot,lz,use_physical=False) for lz in precomputergLzgrid]) #Spline interpolate return interpolate.InterpolatedUnivariateSpline(\ precomputergLzgrid,rls,k=3)(Lz).reshape(Lz_shape) else: return numpy.array([rl(pot,lz,use_physical=False) for lz in Lz]).reshape(Lz_shape) @physical_conversion('position') @shapeDecorator def rE(self,*args,**kwargs): """ NAME: rE PURPOSE: calculate the radius of a circular orbit with the same energy INPUT: pot= potential instance or list of such instances ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: r_E [*input_shape,nt] HISTORY: 2022-04-07 - Written as thin wrapper around Potential.rE - Bovy (UofT) """ pot= kwargs.get('pot',self.__dict__.get('_pot',None)) if pot is None: raise RuntimeError("You need to specify the potential as pot= to compute rE") flatten_potential(pot) if _isNonAxi(pot): raise RuntimeError('Potential given to rE is non-axisymmetric, but rE requires an axisymmetric potential') _check_consistent_units(self,pot) E= numpy.atleast_1d(self.E(*args,pot=pot,use_physical=False,dontreshape=True)) E_shape= E.shape E= E.flatten() if len(E) > 500: # Build interpolation grid precomputerEEgrid= numpy.linspace(numpy.nanmin(E), numpy.nanmax(E), 500) rEs= numpy.array([rE(pot,tE,use_physical=False) for tE in precomputerEEgrid]) #Spline interpolate return interpolate.InterpolatedUnivariateSpline(\ precomputerEEgrid,rEs,k=3)(E).reshape(E_shape) else: return numpy.array([rE(pot,tE,use_physical=False) for tE in E]).reshape(E_shape) @physical_conversion('action') @shapeDecorator def LcE(self,*args,**kwargs): """ NAME: LcE PURPOSE: calculate the angular momentum of a circular orbit with the same energy INPUT: pot= potential instance or list of such instances ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: L_c(E) [*input_shape,nt] HISTORY: 2022-04-07 - Written - Bovy (UofT) """ pot= kwargs.pop('pot',self.__dict__.get('_pot',None)) if pot is None: raise RuntimeError("You need to specify the potential as pot= to compute LcE") flatten_potential(pot) if _isNonAxi(pot): raise RuntimeError('Potential given to LcE is non-axisymmetric, but LcE requires an axisymmetric potential') _check_consistent_units(self,pot) E= numpy.atleast_1d(self.E(*args,pot=pot,use_physical=False,dontreshape=True)) E_shape= E.shape E= E.flatten() if len(E) > 500: # Build interpolation grid precomputeLcEEgrid= numpy.linspace(numpy.nanmin(E), numpy.nanmax(E), 500) LcEs= numpy.array([LcE(pot,tE,use_physical=False) for tE in precomputeLcEEgrid]) #Spline interpolate return interpolate.InterpolatedUnivariateSpline(\ precomputeLcEEgrid,LcEs,k=3)(E).reshape(E_shape) else: return numpy.array([LcE(pot,tE,use_physical=False) for tE in E]).reshape(E_shape) @physical_conversion('position') @shapeDecorator def zmax(self,analytic=False,pot=None,**kwargs): """ NAME: zmax PURPOSE: calculate the maximum vertical height, either numerically from the numerical orbit integration or using analytical means INPUT: analytic(= False) compute this analytically pot - potential to use for analytical calculation For 3D orbits different approximations for analytic=True are available (see the EccZmaxRperiRap method of actionAngle modules): type= ('staeckel') type of actionAngle module to use 1) 'adiabatic': assuming motion splits into R and z 2) 'staeckel': assuming motion splits into u and v of prolate spheroidal coordinate system, exact for Staeckel potentials (incl. all spherical potentials) 3) 'spherical': for spherical potentials, exact +actionAngle module setup kwargs for the corresponding actionAngle modules (actionAngleAdiabatic, actionAngleStaeckel, and actionAngleSpherical) ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: Z_max [*input_shape] HISTORY: 2019-02-25 - Written - Bovy (UofT) """ if analytic: self._setup_EccZmaxRperiRap(pot=pot,**kwargs) return self._aA_zmax if not hasattr(self,'orbit'): raise AttributeError("Integrate the orbit first or use analytic=True for approximate eccentricity") return numpy.amax(numpy.fabs(self.z(self.t,use_physical=False, dontreshape=True)), axis=-1) @physical_conversion('action') @shapeDecorator def jr(self,pot=None,**kwargs): """ NAME: jr PURPOSE: calculate the radial action INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: jr [*input_shape] HISTORY: 2019-02-27 - Written - Bovy (UofT) """ try: self._setup_actionsFreqsAngles(pot=pot,**kwargs) except NotImplementedError: self._setup_actions(pot=pot,**kwargs) return self._aA_jr @physical_conversion('action') @shapeDecorator def jp(self,pot=None,**kwargs): """ NAME: jp PURPOSE: calculate the azimuthal action INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: jp [*input_shape] HISTORY: 2019-02-26 - Written - Bovy (UofT) """ try: self._setup_actionsFreqsAngles(pot=pot,**kwargs) except NotImplementedError: # pragma: no cover self._setup_actions(pot=pot,**kwargs) return self._aA_jp @physical_conversion('action') @shapeDecorator def jz(self,pot=None,**kwargs): """ NAME: jz PURPOSE: calculate the vertical action INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: jz [*input_shape] HISTORY: 2019-02-27 - Written - Bovy (UofT) """ try: self._setup_actionsFreqsAngles(pot=pot,**kwargs) except NotImplementedError: # pragma: no cover self._setup_actions(pot=pot,**kwargs) return self._aA_jz @physical_conversion('angle') @shapeDecorator def wr(self,pot=None,**kwargs): """ NAME: wr PURPOSE: calculate the radial angle INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs OUTPUT: wr [*input_shape] HISTORY: 2019-02-27 - Written - Bovy (UofT) """ self._setup_actionsFreqsAngles(pot=pot,**kwargs) return self._aA_wr @physical_conversion('angle') @shapeDecorator def wp(self,pot=None,**kwargs): """ NAME: wp PURPOSE: calculate the azimuthal angle INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs OUTPUT: wp [*input_shape] HISTORY: 2019-02-27 - Written - Bovy (UofT) """ self._setup_actionsFreqsAngles(pot=pot,**kwargs) return self._aA_wp @physical_conversion('angle') @shapeDecorator def wz(self,pot=None,**kwargs): """ NAME: wz PURPOSE: calculate the vertical angle INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs OUTPUT: wz [*input_shape] HISTORY: 2019-02-27 - Written - Bovy (UofT) """ self._setup_actionsFreqsAngles(pot=pot,**kwargs) return self._aA_wz @physical_conversion('time') @shapeDecorator def Tr(self,pot=None,**kwargs): """ NAME: Tr PURPOSE: calculate the radial period INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: Tr [*input_shape] HISTORY: 2019-02-27 - Written - Bovy (UofT) """ self._setup_actionsFreqsAngles(pot=pot,**kwargs) return 2.*numpy.pi/self._aA_Or @physical_conversion('time') @shapeDecorator def Tp(self,pot=None,**kwargs): """ NAME: Tp PURPOSE: calculate the azimuthal period INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: Tp [*input_shape] HISTORY: 2019-02-27 - Written - Bovy (UofT) """ self._setup_actionsFreqsAngles(pot=pot,**kwargs) return 2.*numpy.pi/self._aA_Op @shapeDecorator def TrTp(self,pot=None,**kwargs): """ NAME: TrTp PURPOSE: the 'ratio' between the radial and azimuthal period Tr/Tphi*pi INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs OUTPUT: Tr/Tp*pi [*input_shape] HISTORY: 2019-02-27 - Written - Bovy (UofT) """ self._setup_actionsFreqsAngles(pot=pot,**kwargs) return self._aA_Op/self._aA_Or*numpy.pi @physical_conversion('time') @shapeDecorator def Tz(self,pot=None,**kwargs): """ NAME: Tz PURPOSE: calculate the vertical period INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: Tz [*input_shape] HISTORY: 2019-02-27 - Written - Bovy (UofT) """ self._setup_actionsFreqsAngles(pot=pot,**kwargs) return 2.*numpy.pi/self._aA_Oz @physical_conversion('frequency') @shapeDecorator def Or(self,pot=None,**kwargs): """ NAME: Or PURPOSE: calculate the radial frequency INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: Or [*input_shape] HISTORY: 2019-02-27 - Written - Bovy (UofT) """ self._setup_actionsFreqsAngles(pot=pot,**kwargs) return self._aA_Or @physical_conversion('frequency') @shapeDecorator def Op(self,pot=None,**kwargs): """ NAME: Op PURPOSE: calculate the azimuthal frequency INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: Op [*input_shape] HISTORY: 2019-02-27 - Written - Bovy (UofT) """ self._setup_actionsFreqsAngles(pot=pot,**kwargs) return self._aA_Op @physical_conversion('frequency') @shapeDecorator def Oz(self,pot=None,**kwargs): """ NAME: Oz PURPOSE: calculate the vertical frequency INPUT: pot - potential type= ('staeckel') type of actionAngle module to use 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' +actionAngle module setup kwargs ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: Oz [*input_shape] HISTORY: 2019-02-27 - Written - Bovy (UofT) """ self._setup_actionsFreqsAngles(pot=pot,**kwargs) return self._aA_Oz @physical_conversion('time') def time(self,*args,**kwargs): """ NAME: time PURPOSE: return the times at which the orbit is sampled INPUT: t - (default: integration times) time at which to get the time (for consistency reasons); default is to return the list of times at which the orbit is sampled ro= (Object-wide default) physical scale for distances to use to convert vo= (Object-wide default) physical scale for velocities to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: t(t) HISTORY: 2019-02-28 - Written - Bovy (UofT) """ if len(args) == 0: try: return self.t.copy() except AttributeError: return 0. else: out= args[0] return conversion.parse_time(out,ro=self._ro,vo=self._vo) @physical_conversion('position') @shapeDecorator def R(self,*args,**kwargs): """ NAME: R PURPOSE: return cylindrical radius at time t INPUT: t - (optional) time at which to get the radius (can be Quantity) ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: R(t) [*input_shape,nt] HISTORY: 2019-02-01 - Written - Bovy (UofT) """ return self._call_internal(*args,**kwargs)[0].T @physical_conversion('position') @shapeDecorator def r(self,*args,**kwargs): """ NAME: r PURPOSE: return spherical radius at time t INPUT: t - (optional) time at which to get the radius ro= (Object-wide default) physical scale for distances to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: r(t) [*input_shape,nt] HISTORY: 2019-02-20 - Written - Bovy (UofT) """ thiso= self._call_internal(*args,**kwargs) if self.dim() == 3: return numpy.sqrt(thiso[0]**2.+thiso[3]**2.).T else: return numpy.fabs(thiso[0]).T @physical_conversion('velocity') @shapeDecorator def vR(self,*args,**kwargs): """ NAME: vR PURPOSE: return radial velocity at time t INPUT: t - (optional) time at which to get the radial velocity vo= (Object-wide default) physical scale for velocities to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: vR(t) [*input_shape,nt] HISTORY: 2019-02-20 - Written - Bovy (UofT) """ return self._call_internal(*args,**kwargs)[1].T @physical_conversion('velocity') @shapeDecorator def vT(self,*args,**kwargs): """ NAME: vT PURPOSE: return rotational velocity at time t INPUT: t - (optional) time at which to get the rotational velocity vo= (Object-wide default) physical scale for velocities to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: vT(t) [*input_shape,nt] HISTORY: 2019-02-20 - Written - Bovy (UofT) """ return self._call_internal(*args,**kwargs)[2].T @physical_conversion('position') @shapeDecorator def z(self,*args,**kwargs): """ NAME: z PURPOSE: return vertical height INPUT: t - (optional) time at which to get the vertical height ro= (Object-wide default) physical scale for distances to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: z(t) [*input_shape,nt] HISTORY: 2019-02-20 - Written - Bovy (UofT) """ if self.dim() < 3: raise AttributeError("linear and planar orbits do not have z()") return self._call_internal(*args,**kwargs)[3].T @physical_conversion('velocity') @shapeDecorator def vz(self,*args,**kwargs): """ NAME: vz PURPOSE: return vertical velocity INPUT: t - (optional) time at which to get the vertical velocity vo= (Object-wide default) physical scale for velocities to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: vz(t) [*input_shape,nt] HISTORY: 2019-02-20 - Written - Bovy (UofT) """ if self.dim() < 3: raise AttributeError("linear and planar orbits do not have vz()") return self._call_internal(*args,**kwargs)[4].T @physical_conversion('angle') @shapeDecorator def phi(self,*args,**kwargs): """ NAME: phi PURPOSE: return azimuth INPUT: t - (optional) time at which to get the azimuth OUTPUT: phi(t) [*input_shape,nt] in [-pi,pi] HISTORY: 2019-02-20 - Written - Bovy (UofT) """ if self.phasedim() != 4 and self.phasedim() != 6: raise AttributeError("Orbit must track azimuth to use phi()") return self._call_internal(*args,**kwargs)[-1].T @physical_conversion('position') @shapeDecorator def x(self,*args,**kwargs): """ NAME: x PURPOSE: return x INPUT: t - (optional) time at which to get x ro= (Object-wide default) physical scale for distances to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: x(t) [*input_shape,nt] HISTORY: 2019-02-20 - Written - Bovy (UofT) """ thiso= self._call_internal(*args,**kwargs) if self.dim() == 1: return thiso[0].T elif self.phasedim() != 4 and self.phasedim() != 6: raise AttributeError("Orbit must track azimuth to use x()") else: return (thiso[0]*numpy.cos(thiso[-1,:])).T @physical_conversion('position') @shapeDecorator def y(self,*args,**kwargs): """ NAME: y PURPOSE: return y INPUT: t - (optional) time at which to get y ro= (Object-wide default) physical scale for distances to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: y(t) [*input_shape,nt] HISTORY: 2019-02-20 - Written - Bovy (UofT) """ thiso= self._call_internal(*args,**kwargs) if self.phasedim() != 4 and self.phasedim() != 6: raise AttributeError("Orbit must track azimuth to use y()") else: return (thiso[0]*numpy.sin(thiso[-1,:])).T @physical_conversion('velocity') @shapeDecorator def vx(self,*args,**kwargs): """ NAME: vx PURPOSE: return x velocity at time t INPUT: t - (optional) time at which to get the velocity vo= (Object-wide default) physical scale for velocities to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: vx(t) [*input_shape,nt] HISTORY: 2019-02-20 - Written - Bovy (UofT) """ thiso= self._call_internal(*args,**kwargs) if self.dim() == 1: return thiso[1].T elif self.phasedim() != 4 and self.phasedim() != 6: raise AttributeError("Orbit must track azimuth to use vx()") else: return (thiso[1]*numpy.cos(thiso[-1]) -thiso[2]*numpy.sin(thiso[-1])).T @physical_conversion('velocity') @shapeDecorator def vy(self,*args,**kwargs): """ NAME: vy PURPOSE: return y velocity at time t INPUT: t - (optional) time at which to get the velocity vo= (Object-wide default) physical scale for velocities to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: vy(t) [*input_shape,nt] HISTORY: 2019-02-20 - Written - Bovy (UofT) """ thiso= self._call_internal(*args,**kwargs) if self.phasedim() != 4 and self.phasedim() != 6: raise AttributeError("Orbit must track azimuth to use vy()") else: return (thiso[2]*numpy.cos(thiso[-1]) +thiso[1]*numpy.sin(thiso[-1])).T @physical_conversion('frequency-kmskpc') @shapeDecorator def vphi(self,*args,**kwargs): """ NAME: vphi PURPOSE: return angular velocity INPUT: t - (optional) time at which to get the angular velocity vo= (Object-wide default) physical scale for velocities to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: vphi(t) [*input_shape,nt] HISTORY: 2019-02-20 - Written - Bovy (UofT) """ thiso= self._call_internal(*args,**kwargs) return (thiso[2]/thiso[0]).T @physical_conversion('velocity') @shapeDecorator def vr(self,*args,**kwargs): """ NAME: vr PURPOSE: return spherical radial velocity. For < 3 dimensions returns vR INPUT: t - (optional) time at which to get the radial velocity vo= (Object-wide default) physical scale for velocities to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: vr(t) [*input_shape,nt] HISTORY: 2020-07-01 - Written - James Lane (UofT) """ thiso = self._call_internal(*args,**kwargs) if self.dim() == 3: r = numpy.sqrt(thiso[0]**2.+thiso[3]**2.) return ((thiso[0]*thiso[1]+thiso[3]*thiso[4])/r).T else: return thiso[1].T @physical_conversion('velocity') @shapeDecorator def vtheta(self,*args,**kwargs): """ NAME: vtheta PURPOSE: return spherical polar velocity INPUT: t - (optional) time at which to get the theta velocity vo= (Object-wide default) physical scale for velocities to use to convert use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: vtheta(t) [*input_shape,nt] HISTORY: 2020-07-01 - Written - James Lane (UofT) """ thiso = self._call_internal(*args,**kwargs) if not self.dim() == 3: raise AttributeError("Orbit must be 3D to use vtheta()") else: r = numpy.sqrt( thiso[0]**2.+thiso[3]**2.) return ((thiso[1]*thiso[3]-thiso[0]*thiso[4])/r).T @physical_conversion('angle') @shapeDecorator def theta(self,*args,**kwargs): """ NAME: theta PURPOSE: return spherical polar angle INPUT: t - (optional) time at which to get the angle OUTPUT: theta(t) [*input_shape,nt] HISTORY: 2020-07-01 - Written - James Lane (UofT) """ thiso = self._call_internal(*args,**kwargs) if self.dim() != 3: raise AttributeError("Orbit must be 3D to use theta()") else: return numpy.arctan2(thiso[0],thiso[3]) @physical_conversion('angle_deg') @shapeDecorator def ra(self,*args,**kwargs): """ NAME: ra PURPOSE: return the right ascension INPUT: t - (optional) time at which to get ra obs=[X,Y,Z] - (optional) position of observer (in kpc) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) OUTPUT: ra(t) [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'ra') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _radec(self,thiso,*args,**kwargs).T[0]\ .reshape(thiso_shape[1:]).T @physical_conversion('angle_deg') @shapeDecorator def dec(self,*args,**kwargs): """ NAME: dec PURPOSE: return the declination INPUT: t - (optional) time at which to get dec obs=[X,Y,Z] - (optional) position of observer (in kpc) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) OUTPUT: dec(t) [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'dec') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _radec(self,thiso,*args,**kwargs).T[1]\ .reshape(thiso_shape[1:]).T @physical_conversion('angle_deg') @shapeDecorator def ll(self,*args,**kwargs): """ NAME: ll PURPOSE: return Galactic longitude INPUT: t - (optional) time at which to get ll obs=[X,Y,Z] - (optional) position of observer (in kpc) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) OUTPUT: l(t) [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'ll') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _lbd(self,thiso,*args,**kwargs).T[0].reshape(thiso_shape[1:]).T @physical_conversion('angle_deg') @shapeDecorator def bb(self,*args,**kwargs): """ NAME: bb PURPOSE: return Galactic latitude INPUT: t - (optional) time at which to get bb obs=[X,Y,Z] - (optional) position of observer (in kpc) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) OUTPUT: b(t) [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT """ _check_roSet(self,kwargs,'bb') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _lbd(self,thiso,*args,**kwargs).T[1].reshape(thiso_shape[1:]).T @physical_conversion('position_kpc') @shapeDecorator def dist(self,*args,**kwargs): """ NAME: dist PURPOSE: return distance from the observer in kpc INPUT: t - (optional) time at which to get dist obs=[X,Y,Z] - (optional) position of observer (in kpc) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) OUTPUT: dist(t) in kpc [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'dist') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _lbd(self,thiso,*args,**kwargs).T[2].reshape(thiso_shape[1:]).T @physical_conversion('proper-motion_masyr') @shapeDecorator def pmra(self,*args,**kwargs): """ NAME: pmra PURPOSE: return proper motion in right ascension (in mas/yr) INPUT: t - (optional) time at which to get pmra obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer (in kpc and km/s) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) vo= velocity in km/s corresponding to v=1. (default=Object-wide default) OUTPUT: pm_ra(t) in mas / yr [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'pmra') _check_voSet(self,kwargs,'pmra') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _pmrapmdec(self,thiso,*args,**kwargs).T[0]\ .reshape(thiso_shape[1:]).T @physical_conversion('proper-motion_masyr') @shapeDecorator def pmdec(self,*args,**kwargs): """ NAME: pmdec PURPOSE: return proper motion in declination (in mas/yr) INPUT: t - (optional) time at which to get pmdec obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer (in kpc and km/s) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) vo= velocity in km/s corresponding to v=1. (default=Object-wide default) OUTPUT: pm_dec(t) in mas/yr [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'pmdec') _check_voSet(self,kwargs,'pmdec') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _pmrapmdec(self,thiso,*args,**kwargs).T[1]\ .reshape(thiso_shape[1:]).T @physical_conversion('proper-motion_masyr') @shapeDecorator def pmll(self,*args,**kwargs): """ NAME: pmll PURPOSE: return proper motion in Galactic longitude (in mas/yr) INPUT: t - (optional) time at which to get pmll obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer (in kpc and km/s) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) vo= velocity in km/s corresponding to v=1. (default=Object-wide default) OUTPUT: pm_l(t) in mas/yr [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'pmll') _check_voSet(self,kwargs,'pmll') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _lbdvrpmllpmbb(self,thiso,*args,**kwargs).T[4]\ .reshape(thiso_shape[1:]).T @physical_conversion('proper-motion_masyr') @shapeDecorator def pmbb(self,*args,**kwargs): """ NAME: pmbb PURPOSE: return proper motion in Galactic latitude (in mas/yr) INPUT: t - (optional) time at which to get pmbb obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer (in kpc and km/s) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) vo= velocity in km/s corresponding to v=1. (default=Object-wide default) OUTPUT: pm_b(t) in mas/yr [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'pmbb') _check_voSet(self,kwargs,'pmbb') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _lbdvrpmllpmbb(self,thiso,*args,**kwargs).T[5]\ .reshape(thiso_shape[1:]).T @physical_conversion('velocity_kms') @shapeDecorator def vlos(self,*args,**kwargs): """ NAME: vlos PURPOSE: return the line-of-sight velocity (in km/s) INPUT: t - (optional) time at which to get vlos obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer (in kpc and km/s) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) vo= velocity in km/s corresponding to v=1. (default=Object-wide default) OUTPUT: vlos(t) in km/s [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'vlos') _check_voSet(self,kwargs,'vlos') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _lbdvrpmllpmbb(self,thiso,*args,**kwargs).T[3]\ .reshape(thiso_shape[1:]).T @shapeDecorator def vra(self,*args,**kwargs): """ NAME: vra PURPOSE: return velocity in right ascension (km/s) INPUT: t - (optional) time at which to get vra (can be Quantity) obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer in the Galactocentric frame (in kpc and km/s) (default=[8.0,0.,0.,0.,220.,0.]; entries can be Quantity) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) OUTPUT: v_ra(t) in km/s [*input_shape] HISTORY: 2019-02-28 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'vra') _check_voSet(self,kwargs,'vra') kwargs['dontreshape']= True dist= self.dist(*args,**kwargs) if _APY_UNITS and isinstance(dist,units.Quantity): result= units.Quantity(dist.to(units.kpc).value*_K* self.pmra(*args,**kwargs)\ .to(units.mas/units.yr).value, unit=units.km/units.s) else: result= dist*_K*self.pmra(*args,**kwargs) kwargs.pop('dontreshape') return result @shapeDecorator def vdec(self,*args,**kwargs): """ NAME: vdec PURPOSE: return velocity in declination (km/s) INPUT: t - (optional) time at which to get vdec (can be Quantity) obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer in the Galactocentric frame (in kpc and km/s) (default=[8.0,0.,0.,0.,220.,0.]; entries can be Quantity) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) OUTPUT: v_dec(t) in km/s [*input_shape] HISTORY: 2019-02-28 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'vdec') _check_voSet(self,kwargs,'vdec') kwargs['dontreshape']= True dist= self.dist(*args,**kwargs) if _APY_UNITS and isinstance(dist,units.Quantity): result= units.Quantity(dist.to(units.kpc).value*_K* self.pmdec(*args,**kwargs)\ .to(units.mas/units.yr).value, unit=units.km/units.s) else: result= dist*_K*self.pmdec(*args,**kwargs) kwargs.pop('dontreshape') return result @shapeDecorator def vll(self,*args,**kwargs): """ NAME: vll PURPOSE: return the velocity in Galactic longitude (km/s) INPUT: t - (optional) time at which to get vll (can be Quantity) obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer in the Galactocentric frame (in kpc and km/s) (default=[8.0,0.,0.,0.,220.,0.]; entries can be Quantity) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) OUTPUT: v_l(t) in km/s [*input_shape] HISTORY: 2019-02-28 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'vll') _check_voSet(self,kwargs,'vll') kwargs['dontreshape']= True dist= self.dist(*args,**kwargs) if _APY_UNITS and isinstance(dist,units.Quantity): result= units.Quantity(dist.to(units.kpc).value*_K* self.pmll(*args,**kwargs)\ .to(units.mas/units.yr).value, unit=units.km/units.s) else: result= dist*_K*self.pmll(*args,**kwargs) kwargs.pop('dontreshape') return result @shapeDecorator def vbb(self,*args,**kwargs): """ NAME: vbb PURPOSE: return velocity in Galactic latitude (km/s) INPUT: t - (optional) time at which to get vbb (can be Quantity) obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer in the Galactocentric frame (in kpc and km/s) (default=[8.0,0.,0.,0.,220.,0.]; entries can be Quantity) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) OUTPUT: v_b(t) in km/s [*input_shape] HISTORY: 2019-02-28 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'vbb') _check_voSet(self,kwargs,'vbb') kwargs['dontreshape']= True dist= self.dist(*args,**kwargs) if _APY_UNITS and isinstance(dist,units.Quantity): result= units.Quantity(dist.to(units.kpc).value*_K* self.pmbb(*args,**kwargs)\ .to(units.mas/units.yr).value, unit=units.km/units.s) else: result= dist*_K*self.pmbb(*args,**kwargs) kwargs.pop('dontreshape') return result @physical_conversion('position_kpc') @shapeDecorator def helioX(self,*args,**kwargs): """ NAME: helioX PURPOSE: return Heliocentric Galactic rectangular x-coordinate (aka "X") INPUT: t - (optional) time at which to get X obs=[X,Y,Z] - (optional) position and velocity of observer (in kpc and km/s) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) OUTPUT: helioX(t) in kpc [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'helioX') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _helioXYZ(self,thiso,*args,**kwargs)[0]\ .reshape(thiso_shape[1:]).T @physical_conversion('position_kpc') @shapeDecorator def helioY(self,*args,**kwargs): """ NAME: helioY PURPOSE: return Heliocentric Galactic rectangular y-coordinate (aka "Y") INPUT: t - (optional) time at which to get Y obs=[X,Y,Z] - (optional) position and velocity of observer (in kpc and km/s) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) OUTPUT: helioY(t) in kpc [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'helioY') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _helioXYZ(self,thiso,*args,**kwargs)[1]\ .reshape(thiso_shape[1:]).T @physical_conversion('position_kpc') @shapeDecorator def helioZ(self,*args,**kwargs): """ NAME: helioZ PURPOSE: return Heliocentric Galactic rectangular z-coordinate (aka "Z") INPUT: t - (optional) time at which to get Z obs=[X,Y,Z] - (optional) position and velocity of observer (in kpc and km/s) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) OUTPUT: helioZ(t) in kpc [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'helioZ') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _helioXYZ(self,thiso,*args,**kwargs)[2]\ .reshape(thiso_shape[1:]).T @physical_conversion('velocity_kms') @shapeDecorator def U(self,*args,**kwargs): """ NAME: U PURPOSE: return Heliocentric Galactic rectangular x-velocity (aka "U") INPUT: t - (optional) time at which to get U obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer (in kpc and km/s) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) vo= velocity in km/s corresponding to v=1. (default=Object-wide default) OUTPUT: U(t) in km/s [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'U') _check_voSet(self,kwargs,'U') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _XYZvxvyvz(self,thiso,*args,**kwargs)[3]\ .reshape(thiso_shape[1:]).T @physical_conversion('velocity_kms') @shapeDecorator def V(self,*args,**kwargs): """ NAME: V PURPOSE: return Heliocentric Galactic rectangular y-velocity (aka "V") INPUT: t - (optional) time at which to get U obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer (in kpc and km/s) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) vo= velocity in km/s corresponding to v=1. (default=Object-wide default) OUTPUT: V(t) in km/s [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'V') _check_voSet(self,kwargs,'V') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _XYZvxvyvz(self,thiso,*args,**kwargs)[4]\ .reshape(thiso_shape[1:]).T @physical_conversion('velocity_kms') @shapeDecorator def W(self,*args,**kwargs): """ NAME: W PURPOSE: return Heliocentric Galactic rectangular z-velocity (aka "W") INPUT: t - (optional) time at which to get W obs=[X,Y,Z,vx,vy,vz] - (optional) position and velocity of observer (in kpc and km/s) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) vo= velocity in km/s corresponding to v=1. (default=Object-wide default) OUTPUT: W(t) in km/s [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ _check_roSet(self,kwargs,'W') _check_voSet(self,kwargs,'W') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) return _XYZvxvyvz(self,thiso,*args,**kwargs)[5]\ .reshape(thiso_shape[1:]).T @shapeDecorator def SkyCoord(self,*args,**kwargs): """ NAME: SkyCoord PURPOSE: return the positions and velocities as an astropy SkyCoord INPUT: t - (optional) time at which to get the position obs=[X,Y,Z] - (optional) position of observer (in kpc) (default=Object-wide default) OR Orbit object that corresponds to the orbit of the observer; Note that when Y is non-zero, the coordinate system is rotated around z such that Y'=0 ro= distance in kpc corresponding to R=1. (default=Object-wide default) vo= velocity in km/s corresponding to v=1. (default=Object-wide default) OUTPUT: SkyCoord(t) [*input_shape,nt] HISTORY: 2019-02-21 - Written - Bovy (UofT) """ kwargs.pop('quantity',None) # rm useless keyword to no conflict later kwargs['dontreshape']= True _check_roSet(self,kwargs,'SkyCoord') thiso= self._call_internal(*args,**kwargs) thiso_shape= thiso.shape thiso= thiso.reshape((thiso_shape[0],-1)) radec= _radec(self,thiso,*args,**kwargs).T\ .reshape((2,)+thiso_shape[1:]) tdist= self.dist(quantity=False,*args,**kwargs).T if not _APY3: # pragma: no cover kwargs.pop('dontreshape') return coordinates.SkyCoord(radec[0]*units.degree, radec[1]*units.degree, distance=tdist*units.kpc, frame='icrs').T _check_voSet(self,kwargs,'SkyCoord') pmrapmdec= _pmrapmdec(self,thiso,*args,**kwargs).T\ .reshape((2,)+thiso_shape[1:]) tvlos= self.vlos(quantity=False,*args,**kwargs).T kwargs.pop('dontreshape') # Also return the Galactocentric frame used v_sun= coordinates.CartesianDifferential(\ numpy.array([-self._solarmotion[0], self._solarmotion[1]+self._vo, self._solarmotion[2]])*units.km/units.s) return coordinates.SkyCoord(radec[0]*units.degree, radec[1]*units.degree, distance=tdist*units.kpc, pm_ra_cosdec=pmrapmdec[0]\ *units.mas/units.yr, pm_dec=pmrapmdec[1]*units.mas/units.yr, radial_velocity=tvlos*units.km/units.s, frame='icrs', galcen_distance=\ numpy.sqrt(self._ro**2.+self._zo**2.)\ *units.kpc, z_sun=self._zo*units.kpc, galcen_v_sun=v_sun).T def __call__(self,*args,**kwargs): """ NAME: __call__ PURPOSE: return the orbits at time t INPUT: t - desired time (can be Quantity) OUTPUT: an Orbit instance with initial conditions set to the phase-space at time t; shape of new Orbit is (shape_old,nt) HISTORY: 2019-03-05 - Written - Bovy (UofT) 2019-03-20 - Implemented multiple times --> Orbits - Bovy (UofT) """ orbSetupKwargs= {'ro':self._ro, 'vo':self._vo, 'zo':self._zo, 'solarmotion':self._solarmotion} thiso= self._call_internal(*args,**kwargs) out= Orbit(vxvv=numpy.reshape(thiso.T,self.shape+thiso.T.shape[1:]), **orbSetupKwargs) out._roSet= self._roSet out._voSet= self._voSet return out def _call_internal(self,*args,**kwargs): """ NAME: _call_internal PURPOSE: return the orbits vector at time t (like OrbitTop's __call__) INPUT: t - desired time OUTPUT: [R,vR,vT,z,vz(,phi)] or [R,vR,vT(,phi)] depending on the orbit; shape = [phasedim,nt,norb] HISTORY: 2019-02-01 - Started - Bovy (UofT) 2019-02-18 - Written interpolation part - Bovy (UofT) """ if len(args) == 0 or (not hasattr(self,'t') and args[0] == 0. ): return numpy.array(self.vxvv).T elif not hasattr(self,'t'): raise ValueError("Integrate instance before evaluating it at a specific time") else: t= args[0] # Parse t, first check whether we are dealing with the common case # where one wants all integrated times # 2nd line: scalar Quantities have __len__, but raise TypeError # for scalars t_exact_integration_times= hasattr(t,'__len__') \ and not (_APY_LOADED and isinstance(t,units.Quantity) and t.isscalar) \ and (len(t) == len(self.t)) \ and numpy.all(t == self.t) if _APY_LOADED and isinstance(t,units.Quantity): t= conversion.parse_time(t,ro=self._ro,vo=self._vo) # Need to re-evaluate now that t has changed... t_exact_integration_times= hasattr(t,'__len__') \ and (len(t) == len(self.t)) \ and numpy.all(t == self.t) elif '_integrate_t_asQuantity' in self.__dict__ \ and self._integrate_t_asQuantity \ and not t_exact_integration_times: # Not doing hasattr in above elif, bc currently slow due to overwrite of __getattribute__ warnings.warn("You specified integration times as a Quantity, but are evaluating at times not specified as a Quantity; assuming that time given is in natural (internal) units (multiply time by unit to get output at physical time)",galpyWarning) if t_exact_integration_times: # Common case where one wants all integrated times return self.orbit.T.copy() elif isinstance(t,(int,float,numpy.number)) and hasattr(self,'t') \ and t in list(self.t): return numpy.array(self.orbit[:,list(self.t).index(t),:]).T else: if isinstance(t,(int,float,numpy.number)): nt= 1 t= numpy.atleast_1d(t) else: nt= len(t) if numpy.any(t > numpy.nanmax(self.t)) \ or numpy.any(t < numpy.nanmin(self.t)): raise ValueError('Found time value not in the integration time domain') try: self._setupOrbitInterp() except: out= numpy.zeros((self.phasedim(),nt,self.size)) for jj in range(nt): try: indx= list(self.t).index(t[jj]) except ValueError: raise LookupError("Orbit interpolaton failed; integrate on finer grid") out[:,jj]= self.orbit[:,indx].T return out #should always have nt > 1, bc otherwise covered by above out= numpy.empty((self.phasedim(),nt,self.size)) # Evaluating RectBivariateSpline on grid requires sorted arrays sindx= numpy.argsort(t) t= t[sindx] usindx= numpy.argsort(sindx) # to later unsort if self.phasedim() == 4 or self.phasedim() == 6: #Unpack interpolated x and y to R and phi x= self._orbInterp[0](t,self._orb_indx_4orbInterp) y= self._orbInterp[-1](t,self._orb_indx_4orbInterp) out[0]= numpy.sqrt(x*x+y*y) out[-1]= numpy.arctan2(y,x) for ii in range(1,self.phasedim()-1): out[ii]= self._orbInterp[ii](t,self._orb_indx_4orbInterp) else: for ii in range(self.phasedim()): out[ii]= self._orbInterp[ii](t,self._orb_indx_4orbInterp) if nt == 1: return out[:,0] else: t= t[usindx] return out[:,usindx] def toPlanar(self): """ NAME: toPlanar PURPOSE: convert 3D orbits into 2D orbits INPUT: (none) OUTPUT: planar Orbit instance HISTORY: 2019-03-02 - Written - Bovy (UofT) """ orbSetupKwargs= {'ro':self._ro, 'vo':self._vo, 'zo':self._zo, 'solarmotion':self._solarmotion} if self.phasedim() == 6: vxvv= self.vxvv[:,[0,1,2,5]] elif self.phasedim() == 5: vxvv= self.vxvv[:,[0,1,2]] else: raise AttributeError("planar or linear Orbit does not have the toPlanar attribute") out= Orbit(vxvv=vxvv,**orbSetupKwargs) out._roSet= self._roSet out._voSet= self._voSet return out def toLinear(self): """ NAME: toLinear PURPOSE: convert 3D orbits into 1D orbits (z) INPUT: (none) OUTPUT: linear Orbit instance HISTORY: 2019-03-02 - Written - Bovy (UofT) """ orbSetupKwargs= {'ro':self._ro, 'vo':self._vo} if self.dim() == 3: vxvv= self.vxvv[:,[3,4]] else: raise AttributeError("planar or linear Orbit does not have the toPlanar attribute") out= Orbit(vxvv=vxvv,**orbSetupKwargs) out._roSet= self._roSet out._voSet= self._voSet return out def _setupOrbitInterp(self): if hasattr(self,"_orbInterp"): return None # Setup one interpolation / phasedim, for all orbits simultaneously # First check that times increase if hasattr(self,"t"): #Orbit has been integrated if self.t[1] < self.t[0]: #must be backward sindx= numpy.argsort(self.t) # sort self.t= self.t[sindx] self.orbit= self.orbit[:,sindx] usindx= numpy.argsort(sindx) # to later unsort orbInterp= [] orb_indx= numpy.arange(self.size) for ii in range(self.phasedim()): if (self.phasedim() == 4 or self.phasedim() == 6) and ii == 0: #Interpolate x and y rather than R and phi to avoid issues w/ phase wrapping if self.size == 1: orbInterp.append(_1DInterp(\ self.t, self.orbit[0,:,0]*numpy.cos(self.orbit[0,:,-1]))) else: orbInterp.append(interpolate.RectBivariateSpline(\ self.t,orb_indx, (self.orbit[:,:,0]*numpy.cos(self.orbit[:,:,-1])).T, ky=1,s=0.)) elif (self.phasedim() == 4 or self.phasedim() == 6) and \ ii == self.phasedim()-1: if self.size == 1: orbInterp.append(_1DInterp(\ self.t, self.orbit[0,:,0]*numpy.sin(self.orbit[0,:,-1]))) else: orbInterp.append(interpolate.RectBivariateSpline(\ self.t,orb_indx, (self.orbit[:,:,0]*numpy.sin(self.orbit[:,:,-1])).T, ky=1,s=0.)) else: if self.size == 1: orbInterp.append(_1DInterp(self.t,self.orbit[0,:,ii])) else: orbInterp.append(interpolate.RectBivariateSpline(\ self.t,orb_indx,self.orbit[:,:,ii].T,ky=1,s=0.)) self._orbInterp= orbInterp self._orb_indx_4orbInterp= orb_indx try: #unsort self.t= self.t[usindx] self.orbit= self.orbit[:,usindx] except: pass return None def _parse_plot_quantity(self,quant,**kwargs): """Internal function to parse a quantity to be plotted based on input data""" # Cannot be using Quantity output kwargs['quantity']= False if callable(quant): out= quant(self.t) if out.shape == self.t.shape: out= numpy.tile(out,(len(self.vxvv),1)) return out def _eval(q): # Check those that don't have the exact name of the function if q == 't': # Typically expect this to have same shape as other quantities return numpy.tile(self.time(self.t,**kwargs), (len(self.vxvv),1)) elif q == 'Enorm': return (self.E(self.t,**kwargs).T/self.E(0.,**kwargs)).T elif q == 'Eznorm': return (self.Ez(self.t,**kwargs).T/self.Ez(0.,**kwargs)).T elif q == 'ERnorm': return (self.ER(self.t,**kwargs).T/self.ER(0.,**kwargs)).T elif q == 'Jacobinorm': return (self.Jacobi(self.t,**kwargs).T/self.Jacobi(0.,**kwargs)).T else: # these are exact, e.g., 'x' for self.x return self.__getattribute__(q)(self.t,**kwargs) try: return _eval(quant) except AttributeError: pass if _NUMEXPR_LOADED: import numexpr else: #pragma: no cover raise ImportError('Parsing the quantity to be plotted failed; if you are trying to plot an expression, please make sure to install numexpr first') # Figure out the variables in the expression to be computed to plot try: vars= numexpr.NumExpr(quant).input_names except TypeError as err: raise TypeError(f'Parsing the expression {quant} failed, with error message:\n"{err}"') # Construct dictionary of necessary parameters vars_dict= {} for var in vars: vars_dict[var]= _eval(var) return numexpr.evaluate(quant,local_dict=vars_dict) def plot(self,*args,**kwargs): """ NAME: plot PURPOSE: plot a previously calculated orbit (with reasonable defaults) INPUT: d1= first dimension to plot ('x', 'y', 'R', 'vR', 'vT', 'z', 'vz', ...); can also be an expression, like 'R*vR', or a user-defined function of time (e.g., lambda t: o.R(t) for R) d2= second dimension to plot; can also be an expression, like 'R*vR', or a user-defined function of time (e.g., lambda t: o.R(t) for R) ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output matplotlib.plot inputs+galpy.util.plot.plot inputs OUTPUT: sends plot to output device HISTORY: 2010-07-26 - Written - Bovy (NYU) 2010-09-22 - Adapted to more general framework - Bovy (NYU) 2013-11-29 - added ra,dec kwargs and other derived quantities - Bovy (IAS) 2014-06-11 - Support for plotting in physical coordinates - Bovy (IAS) 2017-11-28 - Allow arbitrary functions of time to be plotted - Bovy (UofT) 2019-04-13 - Edited for multiple Orbits - Bovy (UofT) """ if (kwargs.get('use_physical',False) \ and kwargs.get('ro',self._roSet)) or \ (not 'use_physical' in kwargs \ and kwargs.get('ro',self._roSet)): labeldict= {'t':r'$t\ (\mathrm{Gyr})$','R':r'$R\ (\mathrm{kpc})$', 'vR':r'$v_R\ (\mathrm{km\,s}^{-1})$', 'vT':r'$v_T\ (\mathrm{km\,s}^{-1})$', 'z':r'$z\ (\mathrm{kpc})$', 'vz':r'$v_z\ (\mathrm{km\,s}^{-1})$','phi':r'$\phi$', 'r':r'$r\ (\mathrm{kpc})$', 'x':r'$x\ (\mathrm{kpc})$','y':r'$y\ (\mathrm{kpc})$', 'vx':r'$v_x\ (\mathrm{km\,s}^{-1})$', 'vy':r'$v_y\ (\mathrm{km\,s}^{-1})$', 'E':r'$E\,(\mathrm{km}^2\,\mathrm{s}^{-2})$', 'Ez':r'$E_z\,(\mathrm{km}^2\,\mathrm{s}^{-2})$', 'ER':r'$E_R\,(\mathrm{km}^2\,\mathrm{s}^{-2})$', 'Enorm':r'$E(t)/E(0.)$', 'Eznorm':r'$E_z(t)/E_z(0.)$', 'ERnorm':r'$E_R(t)/E_R(0.)$', 'Jacobi':r'$E-\Omega_p\,L\,(\mathrm{km}^2\,\mathrm{s}^{-2})$', 'Jacobinorm':r'$(E-\Omega_p\,L)(t)/(E-\Omega_p\,L)(0)$'} else: labeldict= {'t':r'$t$','R':r'$R$','vR':r'$v_R$','vT':r'$v_T$', 'z':r'$z$','vz':r'$v_z$','phi':r'$\phi$', 'r':r'$r$', 'x':r'$x$','y':r'$y$','vx':r'$v_x$','vy':r'$v_y$', 'E':r'$E$','Enorm':r'$E(t)/E(0.)$', 'Ez':r'$E_z$','Eznorm':r'$E_z(t)/E_z(0.)$', 'ER':r'$E_R$','ERnorm':r'$E_R(t)/E_R(0.)$', 'Jacobi':r'$E-\Omega_p\,L$', 'Jacobinorm':r'$(E-\Omega_p\,L)(t)/(E-\Omega_p\,L)(0)$'} labeldict.update({'ra':r'$\alpha\ (\mathrm{deg})$', 'dec':r'$\delta\ (\mathrm{deg})$', 'll':r'$l\ (\mathrm{deg})$', 'bb':r'$b\ (\mathrm{deg})$', 'dist':r'$d\ (\mathrm{kpc})$', 'pmra':r'$\mu_\alpha\ (\mathrm{mas\,yr}^{-1})$', 'pmdec':r'$\mu_\delta\ (\mathrm{mas\,yr}^{-1})$', 'pmll':r'$\mu_l\ (\mathrm{mas\,yr}^{-1})$', 'pmbb':r'$\mu_b\ (\mathrm{mas\,yr}^{-1})$', 'vlos':r'$v_\mathrm{los}\ (\mathrm{km\,s}^{-1})$', 'helioX':r'$X\ (\mathrm{kpc})$', 'helioY':r'$Y\ (\mathrm{kpc})$', 'helioZ':r'$Z\ (\mathrm{kpc})$', 'U':r'$U\ (\mathrm{km\,s}^{-1})$', 'V':r'$V\ (\mathrm{km\,s}^{-1})$', 'W':r'$W\ (\mathrm{km\,s}^{-1})$'}) # Cannot be using Quantity output kwargs['quantity']= False #Defaults if not 'd1' in kwargs and not 'd2' in kwargs: if self.phasedim() == 3: d1= 'R' d2= 'vR' elif self.phasedim() == 4: d1= 'x' d2= 'y' elif self.phasedim() == 2: d1= 'x' d2= 'vx' elif self.phasedim() == 5 or self.phasedim() == 6: d1= 'R' d2= 'z' elif not 'd1' in kwargs: d2= kwargs.pop('d2') d1= 't' elif not 'd2' in kwargs: d1= kwargs.pop('d1') d2= 't' else: d1= kwargs.pop('d1') d2= kwargs.pop('d2') kwargs['dontreshape']= True x= numpy.atleast_2d(self._parse_plot_quantity(d1,**kwargs)) y= numpy.atleast_2d(self._parse_plot_quantity(d2,**kwargs)) kwargs.pop('dontreshape') kwargs.pop('ro',None) kwargs.pop('vo',None) kwargs.pop('obs',None) kwargs.pop('use_physical',None) kwargs.pop('pot',None) kwargs.pop('OmegaP',None) kwargs.pop('quantity',None) auto_scale= not 'xrange' in kwargs and not 'yrange' in kwargs \ and not kwargs.get('overplot',False) labels= kwargs.pop('label',[f'Orbit {ii+1}' for ii in range(self.size)]) if self.size == 1 and isinstance(labels,str): labels= [labels] #Plot if not 'xlabel' in kwargs: kwargs['xlabel']= labeldict.get(d1,fr'${d1}$') if not 'ylabel' in kwargs: kwargs['ylabel']= labeldict.get(d2,fr'${d2}$') for ii,(tx,ty) in enumerate(zip(x,y)): kwargs['label']= labels[ii] line2d= plot.plot(tx,ty,*args,**kwargs)[0] kwargs['overplot']= True if auto_scale: line2d.axes.autoscale(enable=True) plot._add_ticks() return [line2d] def plot3d(self,*args,**kwargs): """ NAME: plot3d PURPOSE: plot 3D aspects of an Orbit INPUT: d1= first dimension to plot ('x', 'y', 'R', 'vR', 'vT', 'z', 'vz', ...); can also be an expression, like 'R*vR', or a user-defined function of time (e.g., lambda t: o.R(t) for R) d2= second dimension to plot d3= third dimension to plot ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output galpy.util.plot.plot3d args and kwargs OUTPUT: plot HISTORY: 2010-07-26 - Written - Bovy (NYU) 2010-09-22 - Adapted to more general framework - Bovy (NYU) 2010-01-08 - Adapted to 3D - Bovy (NYU) 2013-11-29 - added ra,dec kwargs and other derived quantities - Bovy (IAS) 2014-06-11 - Support for plotting in physical coordinates - Bovy (IAS) 2017-11-28 - Allow arbitrary functions of time to be plotted - Bovy (UofT) 2019-04-13 - Adapted for multiple orbits - Bovy (UofT) """ if (kwargs.get('use_physical',False) \ and kwargs.get('ro',self._roSet)) or \ (not 'use_physical' in kwargs \ and kwargs.get('ro',self._roSet)): labeldict= {'t':r'$t\ (\mathrm{Gyr})$','R':r'$R\ (\mathrm{kpc})$', 'vR':r'$v_R\ (\mathrm{km\,s}^{-1})$', 'vT':r'$v_T\ (\mathrm{km\,s}^{-1})$', 'z':r'$z\ (\mathrm{kpc})$', 'vz':r'$v_z\ (\mathrm{km\,s}^{-1})$','phi':r'$\phi$', 'r':r'$r\ (\mathrm{kpc})$', 'x':r'$x\ (\mathrm{kpc})$','y':r'$y\ (\mathrm{kpc})$', 'vx':r'$v_x\ (\mathrm{km\,s}^{-1})$', 'vy':r'$v_y\ (\mathrm{km\,s}^{-1})$'} else: labeldict= {'t':r'$t$','R':r'$R$','vR':r'$v_R$','vT':r'$v_T$', 'z':r'$z$','vz':r'$v_z$','phi':r'$\phi$', 'r':r'$r$','x':r'$x$','y':r'$y$', 'vx':r'$v_x$','vy':r'$v_y$'} labeldict.update({'ra':r'$\alpha\ (\mathrm{deg})$', 'dec':r'$\delta\ (\mathrm{deg})$', 'll':r'$l\ (\mathrm{deg})$', 'bb':r'$b\ (\mathrm{deg})$', 'dist':r'$d\ (\mathrm{kpc})$', 'pmra':r'$\mu_\alpha\ (\mathrm{mas\,yr}^{-1})$', 'pmdec':r'$\mu_\delta\ (\mathrm{mas\,yr}^{-1})$', 'pmll':r'$\mu_l\ (\mathrm{mas\,yr}^{-1})$', 'pmbb':r'$\mu_b\ (\mathrm{mas\,yr}^{-1})$', 'vlos':r'$v_\mathrm{los}\ (\mathrm{km\,s}^{-1})$', 'helioX':r'$X\ (\mathrm{kpc})$', 'helioY':r'$Y\ (\mathrm{kpc})$', 'helioZ':r'$Z\ (\mathrm{kpc})$', 'U':r'$U\ (\mathrm{km\,s}^{-1})$', 'V':r'$V\ (\mathrm{km\,s}^{-1})$', 'W':r'$W\ (\mathrm{km\,s}^{-1})$'}) # Cannot be using Quantity output kwargs['quantity']= False #Defaults if not 'd1' in kwargs and not 'd2' in kwargs and not 'd3' in kwargs: if self.phasedim() == 3: d1= 'R' d2= 'vR' d3= 'vT' elif self.phasedim() == 4: d1= 'x' d2= 'y' d3= 'vR' elif self.phasedim() == 2: raise AttributeError("Cannot plot 3D aspects of 1D orbits") elif self.phasedim() == 5: d1= 'R' d2= 'vR' d3= 'z' elif self.phasedim() == 6: d1= 'x' d2= 'y' d3= 'z' elif not ('d1' in kwargs and 'd2' in kwargs and 'd3' in kwargs): raise AttributeError("Please provide 'd1', 'd2', and 'd3'") else: d1= kwargs.pop('d1') d2= kwargs.pop('d2') d3= kwargs.pop('d3') kwargs['dontreshape']= True x= numpy.atleast_2d(self._parse_plot_quantity(d1,**kwargs)) y= numpy.atleast_2d(self._parse_plot_quantity(d2,**kwargs)) z= numpy.atleast_2d(self._parse_plot_quantity(d3,**kwargs)) kwargs.pop('dontreshape') kwargs.pop('ro',None) kwargs.pop('vo',None) kwargs.pop('obs',None) kwargs.pop('use_physical',None) kwargs.pop('quantity',None) auto_scale= not 'xrange' in kwargs and not 'yrange' in kwargs \ and not 'zrange' in kwargs and not kwargs.get('overplot',False) #Plot if not 'xlabel' in kwargs: kwargs['xlabel']= labeldict.get(d1,fr'${d1}$') if not 'ylabel' in kwargs: kwargs['ylabel']= labeldict.get(d2,fr'${d2}$') if not 'zlabel' in kwargs: kwargs['zlabel']= labeldict.get(d3,fr'${d3}$') for tx,ty,tz in zip(x,y,z): line3d= plot.plot3d(tx,ty,tz,*args,**kwargs)[0] kwargs['overplot']= True if auto_scale: line3d.axes.autoscale(enable=True) plot._add_ticks() return [line3d] def animate(self,*args,**kwargs): #pragma: no cover """ NAME: animate PURPOSE: animate a previously calculated orbit (with reasonable defaults) INPUT: d1= first dimension to plot ('x', 'y', 'R', 'vR', 'vT', 'z', 'vz', ...); can be list with up to three entries for three subplots; each entry can also be a user-defined function of time (e.g., lambda t: o.R(t) for R) d2= second dimension to plot; can be list with up to three entries for three subplots; each entry can also be a user-defined function of time (e.g., lambda t: o.R(t) for R) width= (600) width of output div in px height= (400) height of output div in px xlabel= (pre-defined labels) label for the first dimension (or list of labels if d1 is a list); should only have to be specified when using a function as d1 and can then specify as, e.g., [None,'YOUR LABEL',None] if d1 is a list of three xs and the first and last are standard entries) ylabel= (pre-defined labels) label for the second dimension (or list of labels if d2 is a list); should only have to be specified when using a function as d2 and can then specify as, e.g., [None,'YOUR LABEL',None] if d1 is a list of three xs and the first and last are standard entries) json_filename= (None) if set, save the data necessary for the figure in this filename (e.g., json_filename= 'orbit_data/orbit.json'); this path is also used in the output HTML, so needs to be accessible staticPlot= (False) if True, create a static plot that doesn't allow zooming, panning, etc. ro= (Object-wide default) physical scale for distances to use to convert (can be Quantity) vo= (Object-wide default) physical scale for velocities to use to convert (can be Quantity) use_physical= use to override Object-wide default for using a physical scale for output OUTPUT: IPython.display.HTML object with code to animate the orbit; can be directly shown in jupyter notebook or embedded in HTML pages; get a text version of the HTML using the _repr_html_() function HISTORY: 2017-09-17-24 - Written - Bovy (UofT) 2019-03-11 - Adapted for multiple orbits - Bovy (UofT) """ try: from IPython.display import HTML except ImportError: raise ImportError("Orbit.animate requires ipython/jupyter to be installed") if (kwargs.get('use_physical',False) \ and kwargs.get('ro',self._roSet)) or \ (not 'use_physical' in kwargs \ and kwargs.get('ro',self._roSet)): labeldict= {'t':'t (Gyr)', 'R':'R (kpc)', 'vR':'v_R (km/s)', 'vT':'v_T (km/s)', 'z':'z (kpc)', 'vz':'v_z (km/s)', 'phi':'azimuthal angle', 'r':'r (kpc)', 'x':'x (kpc)', 'y':'y (kpc)', 'vx':'v_x (km/s)', 'vy':'v_y (km/s)', 'E':'E (km^2/s^2)', 'Ez':'E_z (km^2/s^2)', 'ER':'E_R (km^2/s^2)', 'Enorm':'E(t)/E(0.)', 'Eznorm':'E_z(t)/E_z(0.)', 'ERnorm':'E_R(t)/E_R(0.)', 'Jacobi':'E-Omega_p L (km^2/s^2)', 'Jacobinorm':'(E-Omega_p L)(t)/(E-Omega_p L)(0)'} else: labeldict= {'t':'t','R':'R','vR':'v_R','vT':'v_T', 'z':'z','vz':'v_z','phi':r'azimuthal angle', 'r':'r', 'x':'x','y':'y','vx':'v_x','vy':'v_y', 'E':'E','Enorm':'E(t)/E(0.)', 'Ez':'E_z','Eznorm':'E_z(t)/E_z(0.)', 'ER':r'E_R','ERnorm':r'E_R(t)/E_R(0.)', 'Jacobi':r'E-Omega_p L', 'Jacobinorm':r'(E-Omega_p L)(t)/(E-Omega_p L)(0)'} labeldict.update({'ra':'RA (deg)', 'dec':'Dec (deg)', 'll':'Galactic lon (deg)', 'bb':'Galactic lat (deg)', 'dist':'distance (kpc)', 'pmra':'pmRA (mas/yr)', 'pmdec':'pmDec (mas/yr)', 'pmll':'pmGlon (mas/yr)', 'pmbb':'pmGlat (mas/yr)', 'vlos':'line-of-sight vel (km/s)', 'helioX':'X (kpc)', 'helioY':'Y (kpc)', 'helioZ':'Z (kpc)', 'U':'U (km/s)', 'V':'V (km/s)', 'W':'W (km/s)'}) # Cannot be using Quantity output kwargs['quantity']= False #Defaults if not 'd1' in kwargs and not 'd2' in kwargs: if self.phasedim() == 3: d1= 'R' d2= 'vR' elif self.phasedim() == 4: d1= 'x' d2= 'y' elif self.phasedim() == 2: d1= 'x' d2= 'vx' elif self.dim() == 3: d1= 'R' d2= 'z' elif not 'd1' in kwargs: d2= kwargs.pop('d2') d1= 't' elif not 'd2' in kwargs: d1= kwargs.pop('d1') d2= 't' else: d1= kwargs.pop('d1') d2= kwargs.pop('d2') xs= [] ys= [] xlabels= [] ylabels= [] if isinstance(d1,str) or callable(d1): d1s= [d1] d2s= [d2] else: d1s= d1 d2s= d2 if len(d1s) > 3: raise ValueError('Orbit.animate only works for up to three subplots') all_xlabel= kwargs.get('xlabel',[None for d in d1]) all_ylabel= kwargs.get('ylabel',[None for d in d2]) for d1,d2, xlabel, ylabel in zip(d1s,d2s,all_xlabel,all_ylabel): #Get x and y for each subplot kwargs['dontreshape']= True x= self._parse_plot_quantity(d1,**kwargs) y= self._parse_plot_quantity(d2,**kwargs) kwargs.pop('dontreshape') xs.append(x) ys.append(y) if xlabel is None: xlabels.append(labeldict.get(d1,r'\mathrm{No\ xlabel\ specified}')) else: xlabels.append(xlabel) if ylabel is None: ylabels.append(labeldict.get(d2,r'\mathrm{No\ ylabel\ specified}')) else: ylabels.append(ylabel) kwargs.pop('ro',None) kwargs.pop('vo',None) kwargs.pop('obs',None) kwargs.pop('use_physical',None) kwargs.pop('pot',None) kwargs.pop('OmegaP',None) kwargs.pop('quantity',None) width= kwargs.pop('width',600) height= kwargs.pop('height',400) load_jslibs= kwargs.pop('load_jslibs',True) if load_jslibs: load_jslibs_code= """ """ else: load_jslibs_code= "" # Dump data to HTML nplots= len(xs) jsonDict= {} for ii in range(nplots): for jj in range(self.size): jsonDict['x%i_%i' % (ii+1,jj)]= xs[ii][jj].tolist() jsonDict['y%i_%i' % (ii+1,jj)]= ys[ii][jj].tolist() json_filename= kwargs.pop('json_filename',None) if json_filename is None: jd= json.dumps(jsonDict) json_code= f""" let data= JSON.parse('{jd}');""" close_json_code= "" else: with open(json_filename,'w') as jfile: json.dump(jsonDict,jfile) json_code= f"""Plotly.d3.json('{json_filename}',function(data){{""" close_json_code= "});" self.divid= 'galpy-'\ +''.join(choice(ascii_lowercase) for i in range(24)) button_width= 419.51+4.*10. button_margin_left= int(numpy.round((width-button_width)/2.)) if button_margin_left < 0: button_margin_left= 0 # Configuration options config= """{{staticPlot: {staticPlot}}}"""\ .format(staticPlot='true' if kwargs.pop('staticPlot',False) else 'false') # Layout for multiple plots if len(d1s) == 1: xmin= [0,0,0] xmax= [1,1,1] elif len(d1s) == 2: xmin= [0,0.55,0] xmax= [0.45,1,1] elif len(d1s) == 3: xmin= [0,0.365,0.73] xmax= [0.27,0.635,1] # Colors line_colors= ['#1f77b4', # muted blue '#ff7f0e', # safety orange '#2ca02c', # cooked asparagus green '#d62728', # brick red '#9467bd', # muted purple '#8c564b', # chestnut brown '#e377c2', # raspberry yogurt pink '#7f7f7f', # middle gray '#bcbd22', # curry yellow-green '#17becf'] # blue-teal # When there are more than these # of colors needed, make up randoms if self.size > len(line_colors): line_colors.extend(["#%06x" % numpy.random.randint(0, 0xFFFFFF) for ii in range(self.size-len(line_colors))]) layout= """{{ xaxis: {{ title: '{xlabel}', domain: [{xmin},{xmax}], }}, yaxis: {{title: '{ylabel}'}}, margin: {{t: 20}}, hovermode: 'closest', showlegend: false, """.format(xlabel=xlabels[0],ylabel=ylabels[0],xmin=xmin[0],xmax=xmax[0]) for ii in range(1,nplots): layout+= """ xaxis{idx}: {{ title: '{xlabel}', anchor: 'y{idx}', domain: [{xmin},{xmax}], }}, yaxis{idx}: {{ title: '{ylabel}', anchor: 'x{idx}', }}, """.format(idx=ii+1,xlabel=xlabels[ii],ylabel=ylabels[ii], xmin=xmin[ii],xmax=xmax[ii]) layout+="""}""" # First plot setup_trace1= """ let trace1= {{ x: data.x1_0.slice(0,numPerFrame), y: data.y1_0.slice(0,numPerFrame), mode: 'lines', line: {{ shape: 'spline', width: 0.8, color: '{line_color}', }}, }}; let trace2= {{ x: data.x1_0.slice(0,numPerFrame), y: data.y1_0.slice(0,numPerFrame), mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, }}; """.format(line_color=line_colors[0]) traces_cumul= """trace1,trace2""" for ii in range(1,self.size): setup_trace1+= """ let trace{trace_num_1}= {{ x: data.x1_{trace_indx}.slice(0,numPerFrame), y: data.y1_{trace_indx}.slice(0,numPerFrame), mode: 'lines', line: {{ shape: 'spline', width: 0.8, color: '{line_color}', }}, }}; let trace{trace_num_2}= {{ x: data.x1_{trace_indx}.slice(0,numPerFrame), y: data.y1_{trace_indx}.slice(0,numPerFrame), mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, }}; """.format(trace_indx=str(ii),trace_num_1=str(2*ii+1),trace_num_2=str(2*ii+2), line_color=line_colors[ii]) traces_cumul+= f""",trace{str(2*ii+1)},trace{str(2*ii+2)}""" x_data_list = """""" y_data_list = """""" trace_num_10_list = """""" trace_num_20_list = """""" for jj in range(len(d1s)): for ii in range(0, self.size): x_data_list += """data.x{jj}_{trace_indx}.slice(trace_slice_begin,trace_slice_end), """.format(jj=jj+1, divid=self.divid, trace_indx=str(ii)) y_data_list += """data.y{jj}_{trace_indx}.slice(trace_slice_begin,trace_slice_end), """.format(jj=jj+1, divid=self.divid, trace_indx=str(ii)) trace_num_10_list += f"""{str(2*jj*self.size + 2 * ii + 1 - 1)}, """ trace_num_20_list += f"""{str(2*jj*self.size + 2 * ii + 2 - 1)}, """ # Additional traces for additional plots if len(d1s) > 1: setup_trace2= """ let trace{trace_num_1}= {{ x: data.x2_0.slice(0,numPerFrame), y: data.y2_0.slice(0,numPerFrame), xaxis: 'x2', yaxis: 'y2', mode: 'lines', line: {{ shape: 'spline', width: 0.8, color: '{line_color}', }}, }}; let trace{trace_num_2}= {{ x: data.x2_0.slice(0,numPerFrame), y: data.y2_0.slice(0,numPerFrame), xaxis: 'x2', yaxis: 'y2', mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, }}; """.format(line_color=line_colors[0],trace_num_1=str(2*self.size+1), trace_num_2=str(2*self.size+2)) traces_cumul+= f""",trace{str(2*self.size+1)},trace{str(2*self.size+2)}""" for ii in range(1,self.size): setup_trace2+= """ let trace{trace_num_1}= {{ x: data.x2_{trace_indx}.slice(0,numPerFrame), y: data.y2_{trace_indx}.slice(0,numPerFrame), xaxis: 'x2', yaxis: 'y2', mode: 'lines', line: {{ shape: 'spline', width: 0.8, color: '{line_color}', }}, }}; let trace{trace_num_2}= {{ x: data.x2_{trace_indx}.slice(0,numPerFrame), y: data.y2_{trace_indx}.slice(0,numPerFrame), xaxis: 'x2', yaxis: 'y2', mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, }}; """.format(line_color=line_colors[ii],trace_indx=str(ii), trace_num_1=str(2*self.size+2*ii+1), trace_num_2=str(2*self.size+2*ii+2)) traces_cumul+= f""",trace{str(2*self.size+2*ii+1)},trace{str(2*self.size+2*ii+2)}""" else: # else for "if there is a 2nd panel" setup_trace2= """ let traces= [{traces_cumul}]; """.format(traces_cumul=traces_cumul) if len(d1s) > 2: setup_trace3= """ let trace{trace_num_1}= {{ x: data.x3_0.slice(0,numPerFrame), y: data.y3_0.slice(0,numPerFrame), xaxis: 'x3', yaxis: 'y3', mode: 'lines', line: {{ shape: 'spline', width: 0.8, color: '{line_color}', }}, }}; let trace{trace_num_2}= {{ x: data.x3_0.slice(0,numPerFrame), y: data.y3_0.slice(0,numPerFrame), xaxis: 'x3', yaxis: 'y3', mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, }}; """.format(line_color=line_colors[0],trace_num_1=str(4*self.size+1), trace_num_2=str(4*self.size+2)) traces_cumul+= f""",trace{str(4*self.size+1)},trace{str(4*self.size+2)}""" for ii in range(1,self.size): setup_trace3+= """ let trace{trace_num_1}= {{ x: data.x3_{trace_indx}.slice(0,numPerFrame), y: data.y3_{trace_indx}.slice(0,numPerFrame), xaxis: 'x3', yaxis: 'y3', mode: 'lines', line: {{ shape: 'spline', width: 0.8, color: '{line_color}', }}, }}; let trace{trace_num_2}= {{ x: data.x3_{trace_indx}.slice(0,numPerFrame), y: data.y3_{trace_indx}.slice(0,numPerFrame), xaxis: 'x3', yaxis: 'y3', mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, }}; """.format(line_color=line_colors[ii],trace_indx=str(ii), trace_num_1=str(4*self.size+2*ii+1), trace_num_2=str(4*self.size+2*ii+2), trace_num_10=str(4*self.size+2*ii+1-1), trace_num_20=str(4*self.size+2*ii+2-1)) traces_cumul+= f""",trace{str(4*self.size+2*ii+1)},trace{str(4*self.size+2*ii+2)}""" setup_trace3 += """ let traces= [{traces_cumul}]; """.format(traces_cumul=traces_cumul) elif len(d1s) > 1: # elif for "if there is a 3rd panel setup_trace3= """ let traces= [{traces_cumul}]; """.format(traces_cumul=traces_cumul) else: # else for "if there is a 3rd or 2nd panel" (don't think we can get here!) setup_trace3= "" return HTML("""
{load_jslibs_code} """.format(json_code=json_code,close_json_code=close_json_code, divid=self.divid,width=width,height=height, button_margin_left=button_margin_left,config=config, layout=layout,load_jslibs_code=load_jslibs_code, x_data_list=x_data_list, y_data_list=y_data_list, trace_num_10_list=trace_num_10_list, trace_num_20_list=trace_num_20_list, setup_trace1=setup_trace1,setup_trace2=setup_trace2, setup_trace3=setup_trace3, trace_num_list= [ii for ii in range(self.size * len(d1s))])) class _1DInterp: """Class to simulate 2D interpolation when using a single orbit""" def __init__(self,t,y,k=3): self._ip= interpolate.InterpolatedUnivariateSpline(t,y,k=k) def __call__(self,t,indx): return self._ip(t)[:,None] def _from_name_oneobject(name,obs): """ NAME: _from_name_oneobject PURPOSE: Query Simbad for the phase-space coordinates of one object INPUT: name - name of the object obs - numpy.array of [ro,vo,zo,solarmotion] that can be altered OUTPUT: [ra,dec,dist,pmra,pmdec,vlos] HISTORY: 2018-07-15 - Written - Mathew Bub (UofT) 2019-06-16 - Added named_objects - Bovy (UofT) """ # First check whether this is a named_object this_name= _named_objects_key_formatting(name) # Find the object in the file? if this_name in _known_objects.keys(): if 'ra' in _known_objects[this_name].keys(): vxvv= [_known_objects[this_name]['ra'], _known_objects[this_name]['dec'], _known_objects[this_name]['distance'], _known_objects[this_name]['pmra'], _known_objects[this_name]['pmdec'], _known_objects[this_name]['vlos']] # If you add another way, need to convert to ra,dec,... bc from_name # expects that if obs[0] is None and \ 'ro' in _known_objects[this_name].keys(): obs[0]= _known_objects[this_name]['ro'] if obs[1] is None and \ 'vo' in _known_objects[this_name].keys(): obs[1]= _known_objects[this_name]['vo'] if obs[2] is None and \ 'zo' in _known_objects[this_name].keys(): obs[2]= _known_objects[this_name]['zo'] if obs[3] is None and \ 'solarmotion' in _known_objects[this_name].keys(): obs[3]= _known_objects[this_name]['solarmotion'] return vxvv if not _ASTROQUERY_LOADED: # pragma: no cover raise ImportError('astroquery needs to be installed to use ' 'Orbit.from_name when not using a known ' 'object (i.e., when querying Simbad)') # setup a SIMBAD query with the appropriate fields simbad= Simbad() simbad.add_votable_fields('ra(d)', 'dec(d)', 'pmra', 'pmdec', 'rv_value', 'plx', 'distance') simbad.remove_votable_fields('main_id', 'coordinates') # query SIMBAD for the named object try: simbad_table= simbad.query_object(name) except OSError: # pragma: no cover raise OSError('failed to connect to SIMBAD') if not simbad_table: raise ValueError(f'failed to find {name} in SIMBAD') # check that the necessary coordinates have been found missing= simbad_table.mask if (any(missing['RA_d', 'DEC_d', 'PMRA', 'PMDEC', 'RV_VALUE'][0]) or all(missing['PLX_VALUE', 'Distance_distance'][0])): raise ValueError('failed to find some coordinates for {} in ' 'SIMBAD'.format(name)) ra, dec, pmra, pmdec, vlos= simbad_table['RA_d', 'DEC_d', 'PMRA', 'PMDEC', 'RV_VALUE'][0] # get a distance value if not missing['PLX_VALUE'][0]: dist= 1./simbad_table['PLX_VALUE'][0] else: dist_str= str(simbad_table['Distance_distance'][0]) + \ simbad_table['Distance_unit'][0] dist= units.Quantity(dist_str).to(units.kpc).value return [ra,dec,dist,pmra,pmdec,vlos] def _fit_orbit(orb,vxvv,vxvv_err,pot,radec=False,lb=False, customsky=False,lb_to_customsky=None, pmllpmbb_to_customsky=None, tintJ=100,ntintJ=1000,integrate_method='dopr54_c', ro=None,vo=None,obs=None,disp=False): """Fit an orbit to data in a given potential""" # Need to turn this off for speed coords._APY_COORDS_ORIG= coords._APY_COORDS coords._APY_COORDS= False #Import here, because otherwise there is an infinite loop of imports from ..actionAngle import actionAngle, actionAngleIsochroneApprox #Mock this up, bc we want to use its orbit-integration routines class mockActionAngleIsochroneApprox(actionAngleIsochroneApprox): def __init__(self,tintJ,ntintJ,pot,integrate_method='dopr54_c'): actionAngle.__init__(self) self._tintJ= tintJ self._ntintJ=ntintJ self._tsJ= numpy.linspace(0.,self._tintJ,self._ntintJ) self._integrate_dt= None self._pot= pot self._integrate_method= integrate_method return None tmockAA= mockActionAngleIsochroneApprox(tintJ,ntintJ,pot, integrate_method=integrate_method) opt_vxvv= optimize.fmin_powell(_fit_orbit_mlogl,orb.vxvv, args=(vxvv,vxvv_err,pot,radec,lb, customsky,lb_to_customsky, pmllpmbb_to_customsky, tmockAA, ro,vo,obs), disp=disp) maxLogL= -_fit_orbit_mlogl(opt_vxvv,vxvv,vxvv_err,pot,radec,lb, customsky,lb_to_customsky,pmllpmbb_to_customsky, tmockAA, ro,vo,obs) coords._APY_COORDS= coords._APY_COORDS_ORIG return (opt_vxvv,maxLogL) def _fit_orbit_mlogl(new_vxvv,vxvv,vxvv_err,pot,radec,lb, customsky,lb_to_customsky,pmllpmbb_to_customsky, tmockAA, ro,vo,obs): """The log likelihood for fitting an orbit""" #Use this _parse_args routine, which does forward and backward integration iR,ivR,ivT,iz,ivz,iphi= tmockAA._parse_args(True,False, new_vxvv[0], new_vxvv[1], new_vxvv[2], new_vxvv[3], new_vxvv[4], new_vxvv[5]) if radec or lb or customsky: #Need to transform to (l,b), (ra,dec), or a custom set #First transform to X,Y,Z,vX,vY,vZ (Galactic) X,Y,Z = coords.galcencyl_to_XYZ(iR.flatten(),iphi.flatten(), iz.flatten(), Xsun=obs[0]/ro, Zsun=obs[2]/ro).T vX,vY,vZ = coords.galcencyl_to_vxvyvz(ivR.flatten(),ivT.flatten(), ivz.flatten(),iphi.flatten(), vsun=numpy.array(\ obs[3:6])/vo,Xsun=obs[0]/ro,Zsun=obs[2]/ro).T bad_indx= (X == 0.)*(Y == 0.)*(Z == 0.) if True in bad_indx: X[bad_indx]+= ro/10000. lbdvrpmllpmbb= coords.rectgal_to_sphergal(X*ro,Y*ro,Z*ro, vX*vo,vY*vo,vZ*vo, degree=True) if lb: orb_vxvv= numpy.array([lbdvrpmllpmbb[:,0], lbdvrpmllpmbb[:,1], lbdvrpmllpmbb[:,2], lbdvrpmllpmbb[:,4], lbdvrpmllpmbb[:,5], lbdvrpmllpmbb[:,3]]).T elif radec: #Further transform to ra,dec,pmra,pmdec radec= coords.lb_to_radec(lbdvrpmllpmbb[:,0], lbdvrpmllpmbb[:,1],degree=True, epoch=None) pmrapmdec= coords.pmllpmbb_to_pmrapmdec(lbdvrpmllpmbb[:,4], lbdvrpmllpmbb[:,5], lbdvrpmllpmbb[:,0], lbdvrpmllpmbb[:,1], degree=True, epoch=None) orb_vxvv= numpy.array([radec[:,0],radec[:,1], lbdvrpmllpmbb[:,2], pmrapmdec[:,0],pmrapmdec[:,1], lbdvrpmllpmbb[:,3]]).T elif customsky: #Further transform to ra,dec,pmra,pmdec customradec= lb_to_customsky(lbdvrpmllpmbb[:,0], lbdvrpmllpmbb[:,1],degree=True) custompmrapmdec= pmllpmbb_to_customsky(lbdvrpmllpmbb[:,4], lbdvrpmllpmbb[:,5], lbdvrpmllpmbb[:,0], lbdvrpmllpmbb[:,1], degree=True) orb_vxvv= numpy.array([customradec[:,0],customradec[:,1], lbdvrpmllpmbb[:,2], custompmrapmdec[:,0],custompmrapmdec[:,1], lbdvrpmllpmbb[:,3]]).T else: #shape=(2tintJ-1,6) orb_vxvv= numpy.array([iR.flatten(),ivR.flatten(),ivT.flatten(), iz.flatten(),ivz.flatten(),iphi.flatten()]).T out= 0. for ii in range(vxvv.shape[0]): sub_vxvv= (orb_vxvv-vxvv[ii,:].flatten())**2. #print(sub_vxvv[numpy.argmin(numpy.sum(sub_vxvv,axis=1))]) if not vxvv_err is None: sub_vxvv/= vxvv_err[ii,:]**2. else: sub_vxvv/= 0.01**2. out+= logsumexp(-0.5*numpy.sum(sub_vxvv,axis=1)) return -out def _check_roSet(orb,kwargs,funcName): """Function to check whether ro is set, because it's required for funcName""" if not orb._roSet and kwargs.get('ro',None) is None: warnings.warn(f"Method {funcName}(.) requires ro to be given at Orbit initialization or at method evaluation; using default ro which is {orb._ro:f} kpc", galpyWarning) def _check_voSet(orb,kwargs,funcName): """Function to check whether vo is set, because it's required for funcName""" if not orb._voSet and kwargs.get('vo',None) is None: warnings.warn(f"Method {funcName}(.) requires vo to be given at Orbit initialization or at method evaluation; using default vo which is {orb._vo:f} km/s", galpyWarning) def _helioXYZ(orb,thiso,*args,**kwargs): """Calculate heliocentric rectangular coordinates""" obs, ro, vo= _parse_radec_kwargs(orb,kwargs) if not len(thiso.shape) == 2: thiso= thiso.reshape((thiso.shape[0],1)) if len(thiso[:,0]) != 4 and len(thiso[:,0]) != 6: #pragma: no cover raise AttributeError("orbit must track azimuth to use radeclbd functions") elif len(thiso[:,0]) == 4: #planarOrbit if isinstance(obs,(numpy.ndarray,list)): X,Y,Z = coords.galcencyl_to_XYZ(\ thiso[0,:],thiso[3,:]-numpy.arctan2(obs[1],obs[0]),0., Xsun=numpy.sqrt(obs[0]**2.+obs[1]**2.)/ro, Zsun=obs[2]/ro,_extra_rot=False).T else: #Orbit instance obs.turn_physical_off() if obs.dim() == 2: X,Y,Z = coords.galcencyl_to_XYZ(\ thiso[0,:],thiso[3,:]-obs.phi(*args,**kwargs), numpy.zeros_like(thiso[0]), Xsun=obs.R(*args,**kwargs),Zsun=0.,_extra_rot=False).T else: X,Y,Z = coords.galcencyl_to_XYZ(\ thiso[0,:],thiso[3,:]-obs.phi(*args,**kwargs), numpy.zeros_like(thiso[0]), Xsun=obs.R(*args,**kwargs), Zsun=obs.z(*args,**kwargs),_extra_rot=False).T obs.turn_physical_on() else: #FullOrbit if isinstance(obs,(numpy.ndarray,list)): X,Y,Z = coords.galcencyl_to_XYZ(\ thiso[0,:],thiso[5,:]-numpy.arctan2(obs[1],obs[0]), thiso[3,:], Xsun=numpy.sqrt(obs[0]**2.+obs[1]**2.)/ro, Zsun=obs[2]/ro).T else: #Orbit instance obs.turn_physical_off() if obs.dim() == 2: X,Y,Z = coords.galcencyl_to_XYZ(\ thiso[0,:],thiso[5,:]-obs.phi(*args,**kwargs), thiso[3,:], Xsun=obs.R(*args,**kwargs),Zsun=0.).T else: X,Y,Z = coords.galcencyl_to_XYZ(\ thiso[0,:],thiso[5,:]-obs.phi(*args,**kwargs), thiso[3,:], Xsun=obs.R(*args,**kwargs), Zsun=obs.z(*args,**kwargs)).T obs.turn_physical_on() return (X*ro,Y*ro,Z*ro) def _lbd(orb,thiso,*args,**kwargs): """Calculate l,b, and d""" obs, ro, vo= _parse_radec_kwargs(orb,kwargs,dontpop=True) X,Y,Z= _helioXYZ(orb,thiso,*args,**kwargs) bad_indx= (X == 0.)*(Y == 0.)*(Z == 0.) if True in bad_indx: X[bad_indx]+= 1e-15 return coords.XYZ_to_lbd(X,Y,Z,degree=True) def _radec(orb,thiso,*args,**kwargs): """Calculate ra and dec""" lbd= _lbd(orb,thiso,*args,**kwargs) return coords.lb_to_radec(lbd[:,0],lbd[:,1],degree=True,epoch=None) def _XYZvxvyvz(orb,thiso,*args,**kwargs): """Calculate X,Y,Z,U,V,W""" obs, ro, vo= _parse_radec_kwargs(orb,kwargs,vel=True) if not len(thiso.shape) == 2: thiso= thiso.reshape((thiso.shape[0],1)) if len(thiso[:,0]) != 4 and len(thiso[:,0]) != 6: #pragma: no cover raise AttributeError("orbit must track azimuth to use radeclbduvw functions") elif len(thiso[:,0]) == 4: #planarOrbit if isinstance(obs,(numpy.ndarray,list)): Xsun= numpy.sqrt(obs[0]**2.+obs[1]**2.) X,Y,Z = coords.galcencyl_to_XYZ(\ thiso[0,:],thiso[3,:]-numpy.arctan2(obs[1],obs[0]), numpy.zeros_like(thiso[0]), Xsun=Xsun/ro,Zsun=obs[2]/ro,_extra_rot=False).T vX,vY,vZ = coords.galcencyl_to_vxvyvz(\ thiso[1,:],thiso[2,:],numpy.zeros_like(thiso[0]), thiso[3,:]-numpy.arctan2(obs[1],obs[0]), vsun=numpy.array(# have to rotate [obs[3]*obs[0]/Xsun+obs[4]*obs[1]/Xsun, -obs[3]*obs[1]/Xsun+obs[4]*obs[0]/Xsun, obs[5]])/vo, Xsun=Xsun/ro,Zsun=obs[2]/ro,_extra_rot=False).T else: #Orbit instance obs.turn_physical_off() if obs.dim() == 2: X,Y,Z = coords.galcencyl_to_XYZ(\ thiso[0,:],thiso[3,:]-obs.phi(*args,**kwargs), numpy.zeros_like(thiso[0]), Xsun=obs.R(*args,**kwargs),Zsun=0.,_extra_rot=False).T vX,vY,vZ = coords.galcencyl_to_vxvyvz(\ thiso[1,:],thiso[2,:],numpy.zeros_like(thiso[0]), thiso[3,:]-obs.phi(*args,**kwargs), vsun=numpy.array([\ obs.vR(*args,**kwargs),obs.vT(*args,**kwargs), 0.]), Xsun=obs.R(*args,**kwargs),Zsun=0.,_extra_rot=False).T else: X,Y,Z = coords.galcencyl_to_XYZ(\ thiso[0,:],thiso[3,:]-obs.phi(*args,**kwargs), numpy.zeros_like(thiso[0]), Xsun=obs.R(*args,**kwargs), Zsun=obs.z(*args,**kwargs),_extra_rot=False).T vX,vY,vZ = coords.galcencyl_to_vxvyvz(\ thiso[1,:],thiso[2,:],numpy.zeros_like(thiso[0]), thiso[3,:]-obs.phi(*args,**kwargs), vsun=numpy.array([\ obs.vR(*args,**kwargs), obs.vT(*args,**kwargs), obs.vz(*args,**kwargs)]), Xsun=obs.R(*args,**kwargs), Zsun=obs.z(*args,**kwargs),_extra_rot=False).T obs.turn_physical_on() else: #FullOrbit if isinstance(obs,(numpy.ndarray,list)): Xsun= numpy.sqrt(obs[0]**2.+obs[1]**2.) X,Y,Z = coords.galcencyl_to_XYZ(\ thiso[0,:],thiso[5,:]-numpy.arctan2(obs[1],obs[0]),thiso[3,:], Xsun=Xsun/ro,Zsun=obs[2]/ro).T vX,vY,vZ = coords.galcencyl_to_vxvyvz(\ thiso[1,:],thiso[2,:],thiso[4,:], thiso[5,:]-numpy.arctan2(obs[1],obs[0]), vsun=numpy.array(# have to rotate [obs[3]*obs[0]/Xsun+obs[4]*obs[1]/Xsun, -obs[3]*obs[1]/Xsun+obs[4]*obs[0]/Xsun, obs[5]])/vo, Xsun=Xsun/ro,Zsun=obs[2]/ro).T else: #Orbit instance obs.turn_physical_off() if obs.dim() == 2: X,Y,Z = coords.galcencyl_to_XYZ(\ thiso[0,:],thiso[5,:]-obs.phi(*args,**kwargs), thiso[3,:], Xsun=obs.R(*args,**kwargs),Zsun=0.).T vX,vY,vZ = coords.galcencyl_to_vxvyvz(\ thiso[1,:],thiso[2,:],thiso[4,:], thiso[5,:]-obs.phi(*args,**kwargs), vsun=numpy.array([\ obs.vR(*args,**kwargs),obs.vT(*args,**kwargs), 0.]), Xsun=obs.R(*args,**kwargs),Zsun=0.).T else: X,Y,Z = coords.galcencyl_to_XYZ(\ thiso[0,:],thiso[5,:]-obs.phi(*args,**kwargs), thiso[3,:], Xsun=obs.R(*args,**kwargs), Zsun=obs.z(*args,**kwargs)).T vX,vY,vZ = coords.galcencyl_to_vxvyvz(\ thiso[1,:],thiso[2,:],thiso[4,:], thiso[5,:]-obs.phi(*args,**kwargs), vsun=numpy.array([\ obs.vR(*args,**kwargs), obs.vT(*args,**kwargs), obs.vz(*args,**kwargs)]), Xsun=obs.R(*args,**kwargs), Zsun=obs.z(*args,**kwargs)).T obs.turn_physical_on() return (X*ro,Y*ro,Z*ro,vX*vo,vY*vo,vZ*vo) def _lbdvrpmllpmbb(orb,thiso,*args,**kwargs): """Calculate l,b,d,vr,pmll,pmbb""" obs, ro, vo= _parse_radec_kwargs(orb,kwargs,dontpop=True) X,Y,Z,vX,vY,vZ= _XYZvxvyvz(orb,thiso,*args,**kwargs) bad_indx= (X == 0.)*(Y == 0.)*(Z == 0.) if True in bad_indx: X[bad_indx]+= ro/10000. return coords.rectgal_to_sphergal(X,Y,Z,vX,vY,vZ,degree=True) def _pmrapmdec(orb,thiso,*args,**kwargs): """Calculate pmra and pmdec""" lbdvrpmllpmbb= _lbdvrpmllpmbb(orb,thiso,*args,**kwargs) return coords.pmllpmbb_to_pmrapmdec(lbdvrpmllpmbb[:,4], lbdvrpmllpmbb[:,5], lbdvrpmllpmbb[:,0], lbdvrpmllpmbb[:,1],degree=True, epoch=None) def _parse_radec_kwargs(orb,kwargs,vel=False,dontpop=False): if 'obs' in kwargs: obs= kwargs['obs'] if not dontpop: kwargs.pop('obs') if isinstance(obs,(list,numpy.ndarray)): if len(obs) == 2: obs= [obs[0],obs[1],0.] elif len(obs) == 4: obs= [obs[0],obs[1],0.,obs[2],obs[3],0.] for ii in range(len(obs)): if _APY_LOADED and isinstance(obs[ii],units.Quantity): if ii < 3: obs[ii]= conversion.parse_length_kpc(obs[ii]) else: obs[ii]= conversion.parse_velocity_kms(obs[ii]) else: if vel: obs= [orb._ro,0.,orb._zo, orb._solarmotion[0],orb._solarmotion[1]+orb._vo, orb._solarmotion[2]] else: obs= [orb._ro,0.,orb._zo] if 'ro' in kwargs: ro= conversion.parse_length_kpc(kwargs['ro']) if not dontpop: kwargs.pop('ro') else: ro= orb._ro if 'vo' in kwargs: vo= conversion.parse_velocity_kms(kwargs['vo']) if not dontpop: kwargs.pop('vo') else: vo= orb._vo return (obs,ro,vo) def _check_integrate_dt(t,dt): """Check that the stepsize in t is an integer x dt""" if dt is None: return True mult= round((t[1]-t[0])/dt) if numpy.fabs(mult*dt-t[1]+t[0]) < 10.**-10.: return True else: return False def _check_potential_dim(orb,pot): from ..potential import _dim # Don't deal with pot=None here, just dimensionality assert pot is None or orb.dim() <= _dim(pot), 'Orbit dimensionality is %i, but potential dimensionality is %i < %i; orbit needs to be of equal or lower dimensionality as the potential; you can reduce the dimensionality---if appropriate---of your orbit with orbit.toPlanar or orbit.toVertical' % (orb.dim(),_dim(pot),orb.dim()) assert pot is None or not (orb.dim() == 1 and _dim(pot) != 1), 'Orbit dimensionality is 1, but potential dimensionality is %i != 1; 1D orbits can only be integrated in 1D potentials; you convert your potential to a 1D potential---if appropriate---using potential.toVerticalPotential' % (_dim(pot)) def _check_consistent_units(orb,pot): if pot is None: return None assert physical_compatible(orb,pot), 'Physical conversion for the Orbit object is not consistent with that of the Potential given to it' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/orbit/__init__.py0000644000175100001710000000012014327773303016602 0ustar00runnerdockerfrom . import Orbits # # Functions # #none # # Classes # Orbit= Orbits.Orbit ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/orbit/integrateFullOrbit.py0000644000175100001710000010306514327773303020664 0ustar00runnerdockerimport ctypes import ctypes.util import warnings import numpy from numpy.ctypeslib import ndpointer from scipy import integrate from .. import potential from ..potential.Potential import (_evaluatephitorques, _evaluateRforces, _evaluatezforces) from ..util import _load_extension_libs, galpyWarning, symplecticode from ..util._optional_deps import _TQDM_LOADED from ..util.leung_dop853 import dop853 from ..util.multi import parallel_map from .integratePlanarOrbit import (_parse_integrator, _parse_scf_pot, _parse_tol, _prep_tfuncs) if _TQDM_LOADED: import tqdm _lib, _ext_loaded= _load_extension_libs.load_libgalpy() def _parse_pot(pot,potforactions=False,potfortorus=False): """Parse the potential so it can be fed to C""" #Figure out what's in pot if not isinstance(pot,list): pot= [pot] if (potforactions or potfortorus) \ and ( (len(pot) == 1 and isinstance(pot[0],potential.NullPotential)) or numpy.all([isinstance(p,potential.NullPotential) for p in pot]) ): raise NotImplementedError("Evaluating actions using the C backend is not supported for NullPotential instances") # Remove NullPotentials from list of Potentials containing other potentials purged_pot= [p for p in pot if not isinstance(p,potential.NullPotential)] if len(purged_pot) > 0: pot= purged_pot #Initialize everything pot_type= [] pot_args= [] pot_tfuncs= [] npot= len(pot) for p in pot: if isinstance(p,potential.LogarithmicHaloPotential): pot_type.append(0) if p.isNonAxi: pot_args.extend([p._amp,p._q,p._core2,p._1m1overb2]) else: pot_args.extend([p._amp,p._q,p._core2,2.]) # 1m1overb2 > 1: axi elif isinstance(p,potential.DehnenBarPotential): pot_type.append(1) pot_args.extend([p._amp*p._af,p._tform,p._tsteady,p._rb,p._omegab, p._barphi]) elif isinstance(p,potential.MiyamotoNagaiPotential): pot_type.append(5) pot_args.extend([p._amp,p._a,p._b]) elif isinstance(p,potential.PowerSphericalPotential): pot_type.append(7) pot_args.extend([p._amp,p.alpha]) elif isinstance(p,potential.HernquistPotential): pot_type.append(8) pot_args.extend([p._amp,p.a]) elif isinstance(p,potential.NFWPotential): pot_type.append(9) pot_args.extend([p._amp,p.a]) elif isinstance(p,potential.JaffePotential): pot_type.append(10) pot_args.extend([p._amp,p.a]) elif isinstance(p,potential.DoubleExponentialDiskPotential): pot_type.append(11) pot_args.extend([p._amp,-4.*numpy.pi*p._alpha*p._amp, p._alpha,p._beta,len(p._de_j1_xs)]) pot_args.extend(p._de_j0_xs) pot_args.extend(p._de_j1_xs) pot_args.extend(p._de_j0_weights) pot_args.extend(p._de_j1_weights) elif isinstance(p,potential.FlattenedPowerPotential): pot_type.append(12) pot_args.extend([p._amp,p.alpha,p.q2,p.core2]) elif isinstance(p,potential.interpRZPotential): pot_type.append(13) pot_args.extend([len(p._rgrid),len(p._zgrid)]) if p._logR: pot_args.extend([p._logrgrid[ii] for ii in range(len(p._rgrid))]) else: pot_args.extend([p._rgrid[ii] for ii in range(len(p._rgrid))]) pot_args.extend([p._zgrid[ii] for ii in range(len(p._zgrid))]) if hasattr(p,'_potGrid_splinecoeffs'): pot_args.extend([x for x in p._potGrid_splinecoeffs.flatten(order='C')]) else: # pragma: no cover warnings.warn("You are attempting to use the C implementation of interpRZPotential, but have not interpolated the potential itself; if you think this is needed for what you want to do, initialize the interpRZPotential instance with interpPot=True", galpyWarning) pot_args.extend(list(numpy.ones(len(p._rgrid)*len(p._zgrid)))) if hasattr(p,'_rforceGrid_splinecoeffs'): pot_args.extend([x for x in p._rforceGrid_splinecoeffs.flatten(order='C')]) else: # pragma: no cover warnings.warn("You are attempting to use the C implementation of interpRZPotential, but have not interpolated the Rforce; if you think this is needed for what you want to do, initialize the interpRZPotential instance with interpRforce=True", galpyWarning) pot_args.extend(list(numpy.ones(len(p._rgrid)*len(p._zgrid)))) if hasattr(p,'_zforceGrid_splinecoeffs'): pot_args.extend([x for x in p._zforceGrid_splinecoeffs.flatten(order='C')]) else: # pragma: no cover warnings.warn("You are attempting to use the C implementation of interpRZPotential, but have not interpolated the zforce; if you think this is needed for what you want to do, initialize the interpRZPotential instance with interpzforce=True", galpyWarning) pot_args.extend(list(numpy.ones(len(p._rgrid)*len(p._zgrid)))) pot_args.extend([p._amp,int(p._logR)]) elif isinstance(p,potential.IsochronePotential): pot_type.append(14) pot_args.extend([p._amp,p.b]) elif isinstance(p,potential.PowerSphericalPotentialwCutoff): pot_type.append(15) pot_args.extend([p._amp,p.alpha,p.rc]) elif isinstance(p,potential.MN3ExponentialDiskPotential): # Three Miyamoto-Nagai disks npot+= 2 pot_type.extend([5,5,5]) pot_args.extend([p._amp*p._mn3[0]._amp, p._mn3[0]._a,p._mn3[0]._b, p._amp*p._mn3[1]._amp, p._mn3[1]._a,p._mn3[1]._b, p._amp*p._mn3[2]._amp, p._mn3[2]._a,p._mn3[2]._b]) elif isinstance(p,potential.KuzminKutuzovStaeckelPotential): pot_type.append(16) pot_args.extend([p._amp,p._ac,p._Delta]) elif isinstance(p,potential.PlummerPotential): pot_type.append(17) pot_args.extend([p._amp,p._b]) elif isinstance(p,potential.PseudoIsothermalPotential): pot_type.append(18) pot_args.extend([p._amp,p._a]) elif isinstance(p,potential.KuzminDiskPotential): pot_type.append(19) pot_args.extend([p._amp,p._a]) elif isinstance(p,potential.BurkertPotential): pot_type.append(20) pot_args.extend([p._amp,p.a]) elif isinstance(p,potential.EllipsoidalPotential.EllipsoidalPotential): pot_args.append(p._amp) pot_args.extend([0.,0.,0.,0.,0.,0.]) # for caching # Potential specific parameters if isinstance(p,potential.TriaxialHernquistPotential): pot_type.append(21) pot_args.extend([2,p.a,p.a4]) # for psi, mdens, mdens_deriv elif isinstance(p,potential.TriaxialNFWPotential): pot_type.append(22) pot_args.extend([2,p.a,p.a3]) # for psi, mdens, mdens_deriv elif isinstance(p,potential.TriaxialJaffePotential): pot_type.append(23) pot_args.extend([2,p.a,p.a2]) # for psi, mdens, mdens_deriv elif isinstance(p,potential.PerfectEllipsoidPotential): pot_type.append(30) pot_args.extend([1,p.a2]) # for psi, mdens, mdens_deriv elif isinstance(p,potential.TriaxialGaussianPotential): pot_type.append(37) pot_args.extend([1,-p._twosigma2]) # for psi, mdens, mdens_deriv elif isinstance(p,potential.PowerTriaxialPotential): pot_type.append(38) pot_args.extend([1,p.alpha]) # for psi, mdens, mdens_deriv pot_args.extend([p._b2,p._c2,int(p._aligned)]) # Reg. Ellipsoidal if not p._aligned: pot_args.extend(list(p._rot.flatten())) else: pot_args.extend(list(numpy.eye(3).flatten())) # not actually used pot_args.append(p._glorder) pot_args.extend([p._glx[ii] for ii in range(p._glorder)]) # this adds some common factors to the integration weights pot_args.extend([-4.*numpy.pi*p._glw[ii]*p._b*p._c\ /numpy.sqrt(( 1.+(p._b2-1.)*p._glx[ii]**2.) *(1.+(p._c2-1.)*p._glx[ii]**2.)) for ii in range(p._glorder)]) elif isinstance(p,potential.SCFPotential): # Type 24, see stand-alone parser below pt,pa,ptf= _parse_scf_pot(p) pot_type.append(pt) pot_args.extend(pa) pot_tfuncs.extend(ptf) elif isinstance(p,potential.SoftenedNeedleBarPotential): pot_type.append(25) pot_args.extend([p._amp,p._a,p._b,p._c2,p._pa,p._omegab]) pot_args.extend([0.,0.,0.,0.,0.,0.,0.]) # for caching elif isinstance(p,potential.DiskSCFPotential): # Need to pull this apart into: (a) SCF part, (b) constituent # [Sigma_i,h_i] parts # (a) SCF, multiply in any add'l amp pt,pa,ptf= _parse_scf_pot(p._scf,extra_amp=p._amp) pot_type.append(pt) pot_args.extend(pa) pot_tfuncs.extend(ptf) # (b) constituent [Sigma_i,h_i] parts for Sigma,hz in zip(p._Sigma_dict,p._hz_dict): npot+= 1 pot_type.append(26) stype= Sigma.get('type','exp') if stype == 'exp' and not 'Rhole' in Sigma: pot_args.extend([3,0, 4.*numpy.pi*Sigma.get('amp',1.)*p._amp, Sigma.get('h',1./3.)]) elif stype == 'expwhole' \ or (stype == 'exp' and 'Rhole' in Sigma): pot_args.extend([4,1, 4.*numpy.pi*Sigma.get('amp',1.)*p._amp, Sigma.get('h',1./3.), Sigma.get('Rhole',0.5)]) hztype= hz.get('type','exp') if hztype == 'exp': pot_args.extend([0,hz.get('h',0.0375)]) elif hztype == 'sech2': pot_args.extend([1,hz.get('h',0.0375)]) elif isinstance(p, potential.SpiralArmsPotential): pot_type.append(27) pot_args.extend([len(p._Cs), p._amp, p._N, p._sin_alpha, p._tan_alpha, p._r_ref, p._phi_ref, p._Rs, p._H, p._omega]) pot_args.extend(p._Cs) # 30: PerfectEllipsoidPotential, done with others above # 31: KGPotential # 32: IsothermalDiskPotential elif isinstance(p,potential.DehnenCoreSphericalPotential): pot_type.append(33) pot_args.extend([p._amp,p.a]) elif isinstance(p,potential.DehnenSphericalPotential): pot_type.append(34) pot_args.extend([p._amp,p.a,p.alpha]) elif isinstance(p,potential.HomogeneousSpherePotential): pot_type.append(35) pot_args.extend([p._amp,p._R2,p._R3]) elif isinstance(p,potential.interpSphericalPotential): pot_type.append(36) pot_args.append(len(p._rgrid)) pot_args.extend(p._rgrid) pot_args.extend(p._rforce_grid) pot_args.extend([p._amp,p._rmin,p._rmax,p._total_mass, p._Phi0,p._Phimax]) # 37: TriaxialGaussianPotential, done with others above # 38: PowerTriaxialPotential, done with others above elif isinstance(p,potential.NonInertialFrameForce): pot_type.append(39) pot_args.append(p._amp) pot_args.extend([0.,0.,0.,0.,0.,0.,0.,0.,0.,0.]) # for caching pot_args.extend([p._rot_acc,p._lin_acc,p._omegaz_only, p._const_freq,p._Omega_as_func]) if p._Omega_as_func: pot_args.extend([0.,0.,0.,0.,0.,0.,0.]) else: if p._omegaz_only: pot_args.extend([0.,0.,p._Omega]) else: pot_args.extend(p._Omega) pot_args.append(p._Omega2) if not p._const_freq and p._omegaz_only: pot_args.extend([0.,0.,p._Omegadot]) elif not p._const_freq: pot_args.extend(p._Omegadot) else: pot_args.extend([0.,0.,0.]) if p._lin_acc: pot_tfuncs.extend([p._a0[0],p._a0[1],p._a0[2]]) if p._rot_acc: pot_tfuncs.extend([p._x0[0],p._x0[1],p._x0[2]]) pot_tfuncs.extend([p._v0[0],p._v0[1],p._v0[2]]) if p._Omega_as_func: if p._omegaz_only: pot_tfuncs.extend([p._Omega,p._Omegadot]) else: pot_tfuncs.extend([p._Omega[0],p._Omega[1],p._Omega[2], p._Omegadot[0],p._Omegadot[1],p._Omegadot[2]]) elif isinstance(p,potential.NullPotential): pot_type.append(40) # No arguments, zero forces ############################## WRAPPERS ############################### elif isinstance(p,potential.DehnenSmoothWrapperPotential): pot_type.append(-1) wrap_npot, wrap_pot_type, wrap_pot_args, wrap_pot_tfuncs= \ _parse_pot(p._pot, potforactions=potforactions,potfortorus=potfortorus) pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.extend([p._amp,p._tform,p._tsteady,int(p._grow)]) elif isinstance(p,potential.SolidBodyRotationWrapperPotential): pot_type.append(-2) # Not sure how to easily avoid this duplication wrap_npot, wrap_pot_type, wrap_pot_args, wrap_pot_tfuncs= \ _parse_pot(p._pot, potforactions=potforactions,potfortorus=potfortorus) pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.extend([p._amp,p._omega,p._pa]) elif isinstance(p,potential.CorotatingRotationWrapperPotential): pot_type.append(-4) # Not sure how to easily avoid this duplication wrap_npot, wrap_pot_type, wrap_pot_args, wrap_pot_tfuncs= \ _parse_pot(p._pot, potforactions=potforactions,potfortorus=potfortorus) pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.extend([p._amp,p._vpo,p._beta,p._pa,p._to]) elif isinstance(p,potential.GaussianAmplitudeWrapperPotential): pot_type.append(-5) wrap_npot, wrap_pot_type, wrap_pot_args, wrap_pot_tfuncs= \ _parse_pot(p._pot, potforactions=potforactions,potfortorus=potfortorus) pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.extend([p._amp,p._to,p._sigma2]) elif isinstance(p,potential.MovingObjectPotential): pot_type.append(-6) wrap_npot, wrap_pot_type, wrap_pot_args, wrap_pot_tfuncs= \ _parse_pot(p._pot, potforactions=potforactions,potfortorus=potfortorus) pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.extend([len(p._orb.t)]) pot_args.extend(p._orb.t) pot_args.extend(p._orb.x(p._orb.t,use_physical=False)) pot_args.extend(p._orb.y(p._orb.t,use_physical=False)) pot_args.extend(p._orb.z(p._orb.t,use_physical=False)) pot_args.extend([p._amp]) pot_args.extend([p._orb.t[0],p._orb.t[-1]]) #t_0, t_f elif isinstance(p,potential.ChandrasekharDynamicalFrictionForce): pot_type.append(-7) wrap_npot, wrap_pot_type, wrap_pot_args, wrap_pot_tfuncs= \ _parse_pot(p._dens_pot, potforactions=potforactions,potfortorus=potfortorus) pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.extend([len(p._sigmar_rs_4interp)]) pot_args.extend(p._sigmar_rs_4interp) pot_args.extend(p._sigmars_4interp) pot_args.extend([p._amp]) pot_args.extend([-1.,0.,0.,0.,0.,0.,0.,0.]) # for caching pot_args.extend([p._ms,p._rhm,p._gamma**2., -1 if not p._lnLambda else p._lnLambda, p._minr**2.]) pot_args.extend([p._sigmar_rs_4interp[0], p._sigmar_rs_4interp[-1]]) #r_0, r_f elif isinstance(p,potential.RotateAndTiltWrapperPotential): pot_type.append(-8) # Not sure how to easily avoid this duplication wrap_npot, wrap_pot_type, wrap_pot_args, wrap_pot_tfuncs= \ _parse_pot(p._pot, potforactions=potforactions,potfortorus=potfortorus) pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.extend([p._amp]) pot_args.extend([0.,0.,0.,0.,0.,0.]) # for caching pot_args.extend(list(p._rot.flatten())) pot_args.append(not p._norot) pot_args.append(not p._offset is None) pot_args.extend(list(p._offset) if not p._offset is None else [0.,0.,0.]) elif isinstance(p,potential.TimeDependentAmplitudeWrapperPotential): pot_type.append(-9) # Not sure how to easily avoid this duplication wrap_npot, wrap_pot_type, wrap_pot_args, wrap_pot_tfuncs= \ _parse_pot(p._pot, potforactions=potforactions,potfortorus=potfortorus) pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.append(p._amp) pot_tfuncs.append(p._A) pot_type= numpy.array(pot_type,dtype=numpy.int32,order='C') pot_args= numpy.array(pot_args,dtype=numpy.float64,order='C') return (npot,pot_type,pot_args,pot_tfuncs) def integrateFullOrbit_c(pot,yo,t,int_method,rtol=None,atol=None, progressbar=True,dt=None): """ NAME: integrateFullOrbit_c PURPOSE: C integrate an ode for a FullOrbit INPUT: pot - Potential or list of such instances yo - initial condition [q,p] , can be [N,6] or [6] t - set of times at which one wants the result int_method= 'leapfrog_c', 'rk4_c', 'rk6_c', 'symplec4_c' rtol, atol progressbar= (True) if True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!) dt= (None) force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators) OUTPUT: (y,err) y : array, shape (N,len(t),6) or (len(t),6) if N = 1 Array containing the value of y for each desired time in t, \ with the initial value y0 in the first row. err: error message, if not zero: 1 means maximum step reduction happened for adaptive integrators HISTORY: 2011-11-13 - Written - Bovy (IAS) 2018-12-21 - Adapted to allow multiple objects - Bovy (UofT) 2022-04-12 - Add progressbar - Bovy (UofT) """ if len(yo.shape) == 1: single_obj= True else: single_obj= False yo= numpy.atleast_2d(yo) nobj= len(yo) rtol, atol= _parse_tol(rtol,atol) npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot) pot_tfuncs= _prep_tfuncs(pot_tfuncs) int_method_c= _parse_integrator(int_method) if dt is None: dt= -9999.99 #Set up result array result= numpy.empty((nobj,len(t),6)) err= numpy.zeros(nobj,dtype=numpy.int32) #Set up progressbar progressbar*= _TQDM_LOADED if nobj > 1 and progressbar: pbar= tqdm.tqdm(total=nobj,leave=False) pbar_func_ctype= ctypes.CFUNCTYPE(None) pbar_c= pbar_func_ctype(pbar.update) else: # pragma: no cover pbar_c= None #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') integrationFunc= _lib.integrateFullOrbit integrationFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_double, ctypes.c_double, ctypes.c_double, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ctypes.c_int, ctypes.c_void_p] #Array requirements, first store old order f_cont= [yo.flags['F_CONTIGUOUS'], t.flags['F_CONTIGUOUS']] yo= numpy.require(yo,dtype=numpy.float64,requirements=['C','W']) t= numpy.require(t,dtype=numpy.float64,requirements=['C','W']) result= numpy.require(result,dtype=numpy.float64,requirements=['C','W']) err= numpy.require(err,dtype=numpy.int32,requirements=['C','W']) #Run the C code integrationFunc(ctypes.c_int(nobj), yo, ctypes.c_int(len(t)), t, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(dt), ctypes.c_double(rtol), ctypes.c_double(atol), result, err, ctypes.c_int(int_method_c), pbar_c) if nobj > 1 and progressbar: pbar.close() if numpy.any(err == -10): #pragma: no cover raise KeyboardInterrupt("Orbit integration interrupted by CTRL-C (SIGINT)") #Reset input arrays if f_cont[0]: yo= numpy.asfortranarray(yo) if f_cont[1]: t= numpy.asfortranarray(t) if single_obj: return (result[0],err[0]) else: return (result,err) def integrateFullOrbit_dxdv_c(pot,yo,dyo,t,int_method,rtol=None,atol=None): #pragma: no cover because not included in v1, uncover when included """ NAME: integrateFullOrbit_dxdv_c PURPOSE: C integrate an ode for a planarOrbit+phase space volume dxdv INPUT: pot - Potential or list of such instances yo - initial condition [q,p] dyo - initial condition [dq,dp] t - set of times at which one wants the result int_method= 'leapfrog_c', 'rk4_c', 'rk6_c', 'symplec4_c' rtol, atol OUTPUT: (y,err) y : array, shape (len(y0), len(t)) Array containing the value of y for each desired time in t, \ with the initial value y0 in the first row. err: error message if not zero, 1: maximum step reduction happened for adaptive integrators HISTORY: 2011-11-13 - Written - Bovy (IAS) """ rtol, atol= _parse_tol(rtol,atol) npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot) pot_tfuncs= _prep_tfuncs(pot_tfuncs) int_method_c= _parse_integrator(int_method) yo= numpy.concatenate((yo,dyo)) #Set up result array result= numpy.empty((len(t),12)) err= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') integrationFunc= _lib.integrateFullOrbit_dxdv integrationFunc.argtypes= [ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_double, ctypes.c_double, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int), ctypes.c_int] #Array requirements, first store old order f_cont= [yo.flags['F_CONTIGUOUS'], t.flags['F_CONTIGUOUS']] yo= numpy.require(yo,dtype=numpy.float64,requirements=['C','W']) t= numpy.require(t,dtype=numpy.float64,requirements=['C','W']) result= numpy.require(result,dtype=numpy.float64,requirements=['C','W']) #Run the C code integrationFunc(yo, ctypes.c_int(len(t)), t, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(rtol),ctypes.c_double(atol), result, ctypes.byref(err), ctypes.c_int(int_method_c)) if int(err.value) == -10: #pragma: no cover raise KeyboardInterrupt("Orbit integration interrupted by CTRL-C (SIGINT)") #Reset input arrays if f_cont[0]: yo= numpy.asfortranarray(yo) if f_cont[1]: t= numpy.asfortranarray(t) return (result,err.value) def integrateFullOrbit(pot,yo,t,int_method,rtol=None,atol=None, numcores=1,progressbar=True,dt=None): """ NAME: integrateFullOrbit PURPOSE: Integrate an ode for a FullOrbit INPUT: pot - Potential or list of such instances yo - initial condition [q,p], shape [N,5] or [N,6] t - set of times at which one wants the result int_method= 'leapfrog', 'odeint', or 'dop853' rtol, atol= tolerances (not always used...) numcores= (1) number of cores to use for multi-processing progressbar= (True) if True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!) dt= (None) force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators) OUTPUT: (y,err) y : array, shape (N,len(t),5/6) Array containing the value of y for each desired time in t, \ with the initial value y0 in the first row. err: error message, always zero for now HISTORY: 2010-08-01 - Written - Bovy (NYU) 2019-04-09 - Adapted to allow multiple objects and parallel mapping - Bovy (UofT) 2022-04-12 - Add progressbar - Bovy (UofT) """ nophi= False if not int_method.lower() == 'dop853' and not int_method == 'odeint': if len(yo[0]) == 5: nophi= True #We hack this by putting in a dummy phi=0 yo= numpy.pad(yo,((0,0),(0,1)),'constant',constant_values=0) if int_method.lower() == 'leapfrog': if rtol is None: rtol= 1e-8 def integrate_for_map(vxvv): #go to the rectangular frame this_vxvv= numpy.array([vxvv[0]*numpy.cos(vxvv[5]), vxvv[0]*numpy.sin(vxvv[5]), vxvv[3], vxvv[1]*numpy.cos(vxvv[5]) -vxvv[2]*numpy.sin(vxvv[5]), vxvv[2]*numpy.cos(vxvv[5]) +vxvv[1]*numpy.sin(vxvv[5]), vxvv[4]]) #integrate out= symplecticode.leapfrog(_rectForce,this_vxvv, t,args=(pot,),rtol=rtol) #go back to the cylindrical frame R= numpy.sqrt(out[:,0]**2.+out[:,1]**2.) phi= numpy.arccos(out[:,0]/R) phi[(out[:,1] < 0.)]= 2.*numpy.pi-phi[(out[:,1] < 0.)] vR= out[:,3]*numpy.cos(phi)+out[:,4]*numpy.sin(phi) vT= out[:,4]*numpy.cos(phi)-out[:,3]*numpy.sin(phi) out[:,3]= out[:,2] out[:,4]= out[:,5] out[:,0]= R out[:,1]= vR out[:,2]= vT out[:,5]= phi return out elif int_method.lower() == 'dop853' or int_method.lower() == 'odeint': if rtol is None: rtol= 1e-8 if int_method.lower() == 'dop853': integrator= dop853 extra_kwargs= {} else: integrator= integrate.odeint extra_kwargs= {'rtol':rtol} if len(yo[0]) == 5: def integrate_for_map(vxvv): l= vxvv[0]*vxvv[2] l2= l**2. init= [vxvv[0],vxvv[1],vxvv[3],vxvv[4]] intOut= integrator(_RZEOM,init,t=t,args=(pot,l2), **extra_kwargs) out= numpy.zeros((len(t),5)) out[:,0]= intOut[:,0] out[:,1]= intOut[:,1] out[:,3]= intOut[:,2] out[:,4]= intOut[:,3] out[:,2]= l/out[:,0] #post-process to remove negative radii neg_radii= (out[:,0] < 0.) out[neg_radii,0]= -out[neg_radii,0] return out else: def integrate_for_map(vxvv): vphi= vxvv[2]/vxvv[0] init= [vxvv[0],vxvv[1],vxvv[5],vphi,vxvv[3],vxvv[4]] intOut= integrator(_EOM,init,t=t,args=(pot,)) out= numpy.zeros((len(t),6)) out[:,0]= intOut[:,0] out[:,1]= intOut[:,1] out[:,2]= out[:,0]*intOut[:,3] out[:,3]= intOut[:,4] out[:,4]= intOut[:,5] out[:,5]= intOut[:,2] #post-process to remove negative radii neg_radii= (out[:,0] < 0.) out[neg_radii,0]= -out[neg_radii,0] out[neg_radii,3]+= numpy.pi return out else: # Assume we are forcing parallel_mapping of a C integrator... def integrate_for_map(vxvv): return integrateFullOrbit_c(pot,numpy.copy(vxvv), t,int_method,dt=dt)[0] if len(yo) == 1: # Can't map a single value... out= numpy.atleast_3d(integrate_for_map(yo[0]).T).T else: out= numpy.array(parallel_map(integrate_for_map,yo,numcores=numcores, progressbar=progressbar)) if nophi: out= out[:,:,:5] return out, numpy.zeros(len(yo)) def _RZEOM(y,t,pot,l2): """ NAME: _RZEOM PURPOSE: implements the EOM, i.e., the right-hand side of the differential equation, for a 3D orbit assuming conservation of angular momentum INPUT: y - current phase-space position t - current time pot - (list of) Potential instance(s) l2 - angular momentum squared OUTPUT: dy/dt HISTORY: 2010-04-16 - Written - Bovy (NYU) """ return [y[1], l2/y[0]**3.+_evaluateRforces(pot,y[0],y[2],t=t), y[3], _evaluatezforces(pot,y[0],y[2],t=t)] def _EOM(y,t,pot): """ NAME: _EOM PURPOSE: implements the EOM, i.e., the right-hand side of the differential equation, for a 3D orbit INPUT: y - current phase-space position t - current time pot - (list of) Potential instance(s) OUTPUT: dy/dt HISTORY: 2010-04-16 - Written - Bovy (NYU) """ l2= (y[0]**2.*y[3])**2. return [y[1], l2/y[0]**3.+_evaluateRforces(pot,y[0],y[4],phi=y[2],t=t, v=[y[1],y[0]*y[3],y[5]]), y[3], 1./y[0]**2.*(_evaluatephitorques(pot,y[0],y[4],phi=y[2],t=t, v=[y[1],y[0]*y[3],y[5]]) -2.*y[0]*y[1]*y[3]), y[5], _evaluatezforces(pot,y[0],y[4],phi=y[2],t=t, v=[y[1],y[0]*y[3],y[5]])] def _rectForce(x,pot,t=0.): """ NAME: _rectForce PURPOSE: returns the force in the rectangular frame INPUT: x - current position t - current time pot - (list of) Potential instance(s) OUTPUT: force HISTORY: 2011-02-02 - Written - Bovy (NYU) """ #x is rectangular so calculate R and phi R= numpy.sqrt(x[0]**2.+x[1]**2.) phi= numpy.arccos(x[0]/R) sinphi= x[1]/R cosphi= x[0]/R if x[1] < 0.: phi= 2.*numpy.pi-phi #calculate forces Rforce= _evaluateRforces(pot,R,x[2],phi=phi,t=t) phitorque= _evaluatephitorques(pot,R,x[2],phi=phi,t=t) return numpy.array([cosphi*Rforce-1./R*sinphi*phitorque, sinphi*Rforce+1./R*cosphi*phitorque, _evaluatezforces(pot,R,x[2],phi=phi,t=t)]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/orbit/integrateLinearOrbit.py0000644000175100001710000002665614327773303021206 0ustar00runnerdockerimport ctypes import ctypes.util import numpy from numpy.ctypeslib import ndpointer from scipy import integrate from .. import potential from ..potential.linearPotential import _evaluatelinearForces from ..potential.verticalPotential import verticalPotential from ..util import _load_extension_libs, symplecticode from ..util._optional_deps import _TQDM_LOADED from ..util.leung_dop853 import dop853 from ..util.multi import parallel_map from .integrateFullOrbit import _parse_pot as _parse_pot_full from .integratePlanarOrbit import _parse_integrator, _parse_tol, _prep_tfuncs if _TQDM_LOADED: import tqdm _lib, _ext_loaded= _load_extension_libs.load_libgalpy() def _parse_pot(pot): """Parse the potential so it can be fed to C""" from .integrateFullOrbit import _parse_scf_pot #Figure out what's in pot if not isinstance(pot,list): pot= [pot] #Initialize everything pot_type= [] pot_args= [] pot_tfuncs= [] npot= len(pot) for p in pot: # Prepare for wrappers NOT CURRENTLY SUPPORTED, SEE PLANAR OR FULL if isinstance(p,verticalPotential) \ and isinstance(p._Pot,potential.MN3ExponentialDiskPotential): # Need to do this one separately, because combination of many parts # Three Miyamoto-Nagai disks npot+= 2 pot_type.extend([5,5,5]) pot_args.extend([p._Pot._amp*p._Pot._mn3[0]._amp, p._Pot._mn3[0]._a,p._Pot._mn3[0]._b, p._R,p._phi, p._Pot._amp*p._Pot._mn3[1]._amp, p._Pot._mn3[1]._a,p._Pot._mn3[1]._b, p._R,p._phi, p._Pot._amp*p._Pot._mn3[2]._amp, p._Pot._mn3[2]._a,p._Pot._mn3[2]._b, p._R,p._phi]) elif isinstance(p,verticalPotential) \ and isinstance(p._Pot,potential.DiskSCFPotential): # Need to do this one separately, because combination of many parts # Need to pull this apart into: (a) SCF part, (b) constituent # [Sigma_i,h_i] parts # (a) SCF, multiply in any add'l amp pt,pa,ptf= _parse_scf_pot(p._Pot._scf,extra_amp=p._Pot._amp) pot_type.append(pt) pot_args.extend(pa) pot_tfuncs.extend(ptf) pot_args.extend([p._R,p._phi]) # (b) constituent [Sigma_i,h_i] parts for Sigma,hz in zip(p._Pot._Sigma_dict,p._Pot._hz_dict): npot+= 1 pot_type.append(26) stype= Sigma.get('type','exp') if stype == 'exp' and not 'Rhole' in Sigma: pot_args.extend([3,0, 4.*numpy.pi*Sigma.get('amp',1.)*p._Pot._amp, Sigma.get('h',1./3.)]) elif stype == 'expwhole' \ or (stype == 'exp' and 'Rhole' in Sigma): pot_args.extend([4,1, 4.*numpy.pi*Sigma.get('amp',1.)*p._Pot._amp, Sigma.get('h',1./3.), Sigma.get('Rhole',0.5)]) hztype= hz.get('type','exp') if hztype == 'exp': pot_args.extend([0,hz.get('h',0.0375)]) elif hztype == 'sech2': pot_args.extend([1,hz.get('h',0.0375)]) pot_args.extend([p._R,p._phi]) elif isinstance(p,potential.KGPotential): pot_type.append(31) pot_args.extend([p._amp,p._K,p._D2,2.*p._F]) elif isinstance(p,potential.IsothermalDiskPotential): pot_type.append(32) pot_args.extend([p._amp*p._sigma2/p._H,2.*p._H]) # All other potentials can be handled in the same way as follows: elif isinstance(p,verticalPotential): _,pt,pa,ptf= _parse_pot_full(p._Pot) pot_type.extend(pt) pot_args.extend(pa) pot_tfuncs.extend(ptf) pot_args.append(p._R) pot_args.append(p._phi) pot_type= numpy.array(pot_type,dtype=numpy.int32,order='C') pot_args= numpy.array(pot_args,dtype=numpy.float64,order='C') return (npot,pot_type,pot_args,pot_tfuncs) def integrateLinearOrbit_c(pot,yo,t,int_method,rtol=None,atol=None, progressbar=True,dt=None): """ NAME: integrateLinearOrbit_c PURPOSE: C integrate an ode for a LinearOrbit INPUT: pot - Potential or list of such instances yo - initial condition [q,p], can be [N,2] or [2] t - set of times at which one wants the result int_method= 'leapfrog_c', 'rk4_c', 'rk6_c', 'symplec4_c' rtol, atol progressbar= (True) if True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!) dt= (None) force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators) OUTPUT: (y,err) y : array, shape (N,len(t),2) or (len(y0),len(t)) if N=1 Array containing the value of y for each desired time in t, \ with the initial value y0 in the first row. err: error message, if not zero: 1 means maximum step reduction happened for adaptive integrators HISTORY: 2018-10-06 - Written - Bovy (UofT) 2018-10-14 - Adapted to allow multiple orbits to be integrated at once - Bovy (UofT) 2022-04-12 - Add progressbar - Bovy (UofT) """ if len(yo.shape) == 1: single_obj= True else: single_obj= False yo= numpy.atleast_2d(yo) nobj= len(yo) rtol, atol= _parse_tol(rtol,atol) npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot) pot_tfuncs= _prep_tfuncs(pot_tfuncs) int_method_c= _parse_integrator(int_method) if dt is None: dt= -9999.99 #Set up result array result= numpy.empty((nobj,len(t),2)) err= numpy.zeros(nobj,dtype=numpy.int32) #Set up progressbar progressbar*= _TQDM_LOADED if nobj > 1 and progressbar: pbar= tqdm.tqdm(total=nobj,leave=False) pbar_func_ctype= ctypes.CFUNCTYPE(None) pbar_c= pbar_func_ctype(pbar.update) else: # pragma: no cover pbar_c= None #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') integrationFunc= _lib.integrateLinearOrbit integrationFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_double, ctypes.c_double, ctypes.c_double, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ctypes.c_int, ctypes.c_void_p] #Array requirements, first store old order f_cont= [yo.flags['F_CONTIGUOUS'], t.flags['F_CONTIGUOUS']] yo= numpy.require(yo,dtype=numpy.float64,requirements=['C','W']) t= numpy.require(t,dtype=numpy.float64,requirements=['C','W']) result= numpy.require(result,dtype=numpy.float64,requirements=['C','W']) err= numpy.require(err,dtype=numpy.int32,requirements=['C','W']) #Run the C code integrationFunc(ctypes.c_int(nobj), yo, ctypes.c_int(len(t)), t, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(dt), ctypes.c_double(rtol),ctypes.c_double(atol), result, err, ctypes.c_int(int_method_c), pbar_c) if nobj > 1 and progressbar: pbar.close() if numpy.any(err == -10): #pragma: no cover raise KeyboardInterrupt("Orbit integration interrupted by CTRL-C (SIGINT)") #Reset input arrays if f_cont[0]: yo= numpy.asfortranarray(yo) if f_cont[1]: t= numpy.asfortranarray(t) if single_obj: return (result[0],err[0]) else: return (result,err) # Python integration functions def integrateLinearOrbit(pot,yo,t,int_method,rtol=None,atol=None,numcores=1, progressbar=True,dt=None): """ NAME: integrateLinearOrbit PURPOSE: Integrate an ode for a LinearOrbit INPUT: pot - Potential or list of such instances yo - initial condition [q,p], shape [N,2] t - set of times at which one wants the result int_method= 'leapfrog', 'odeint', or 'dop853' rtol, atol= tolerances (not always used...) numcores= (1) number of cores to use for multi-processing progressbar= (True) if True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!) dt= (None) force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators) OUTPUT: (y,err) y : array, shape (N,len(t),2) Array containing the value of y for each desired time in t, \ with the initial value y0 in the first row. err: error message, always zero for now HISTORY: 2010-07-13- Written - Bovy (NYU) 2019-04-08 - Adapted to allow multiple orbits to be integrated at once and moved to integrateLinearOrbit.py - Bovy (UofT) 2022-04-12 - Add progressbar - Bovy (UofT) """ if int_method.lower() == 'leapfrog': if rtol is None: rtol= 1e-8 def integrate_for_map(vxvv): return symplecticode.leapfrog(lambda x,t=t: \ _evaluatelinearForces(pot,x,t=t), numpy.array(vxvv), t,rtol=rtol) elif int_method.lower() == 'dop853': if rtol is None: rtol= 1e-8 def integrate_for_map(vxvv): return dop853(func=_linearEOM,x=vxvv,t=t,args=(pot,)) elif int_method.lower() == 'odeint': if rtol is None: rtol= 1e-8 def integrate_for_map(vxvv): return integrate.odeint(_linearEOM,vxvv,t,args=(pot,),rtol=rtol) else: # Assume we are forcing parallel_mapping of a C integrator... def integrate_for_map(vxvv): return integrateLinearOrbit_c(pot,numpy.copy(vxvv), t,int_method,dt=dt)[0] if len(yo) == 1: # Can't map a single value... return numpy.atleast_3d(integrate_for_map(yo[0]).T).T, 0 else: return (numpy.array(parallel_map(integrate_for_map,yo,numcores=numcores, progressbar=progressbar)), numpy.zeros(len(yo))) def _linearEOM(y,t,pot): """ NAME: linearEOM PURPOSE: the one-dimensional equation-of-motion INPUT: y - current phase-space position t - current time pot - (list of) linearPotential instance(s) OUTPUT: dy/dt HISTORY: 2010-07-13 - Bovy (NYU) """ return [y[1],_evaluatelinearForces(pot,y[0],t=t)] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/orbit/integratePlanarOrbit.py0000644000175100001710000012561114327773303021200 0ustar00runnerdockerimport ctypes import ctypes.util import numpy from numpy.ctypeslib import ndpointer from scipy import integrate from .. import potential from ..potential.planarPotential import (_evaluateplanarphitorques, _evaluateplanarPotentials, _evaluateplanarRforces, planarPotentialFromFullPotential, planarPotentialFromRZPotential) from ..potential.WrapperPotential import parentWrapperPotential from ..util import _load_extension_libs, symplecticode from ..util._optional_deps import _NUMBA_LOADED, _TQDM_LOADED from ..util.leung_dop853 import dop853 from ..util.multi import parallel_map if _TQDM_LOADED: import tqdm if _NUMBA_LOADED: from numba import cfunc, types _lib, _ext_loaded= _load_extension_libs.load_libgalpy() def _parse_pot(pot): """Parse the potential so it can be fed to C""" #Figure out what's in pot if not isinstance(pot,list): pot= [pot] # Remove NullPotentials from list of Potentials containing other potentials purged_pot= [p for p in pot if not isinstance(p,potential.NullPotential)] if len(purged_pot) > 0: pot= purged_pot #Initialize everything pot_type= [] pot_args= [] pot_tfuncs= [] npot= len(pot) for p in pot: # Prepare for wrappers if ((isinstance(p,planarPotentialFromFullPotential) \ or isinstance(p,planarPotentialFromRZPotential)) \ and isinstance(p._Pot,parentWrapperPotential)) \ or isinstance(p,parentWrapperPotential): if not isinstance(p,parentWrapperPotential): wrap_npot, wrap_pot_type, wrap_pot_args, wrap_pot_tfuncs= \ _parse_pot(potential.toPlanarPotential(p._Pot._pot)) else: wrap_npot, wrap_pot_type, wrap_pot_args, wrap_pot_tfuncs= \ _parse_pot(p._pot) if (isinstance(p,planarPotentialFromRZPotential) or isinstance(p,planarPotentialFromFullPotential) ) \ and isinstance(p._Pot,potential.LogarithmicHaloPotential): pot_type.append(0) if p._Pot.isNonAxi: pot_args.extend([p._Pot._amp,p._Pot._q, p._Pot._core2,p._Pot._1m1overb2]) else: pot_args.extend([p._Pot._amp,p._Pot._q,p._Pot._core2,2.]) # 1m1overb2 > 1: axi elif isinstance(p,planarPotentialFromFullPotential) \ and isinstance(p._Pot,potential.DehnenBarPotential): pot_type.append(1) pot_args.extend([p._Pot._amp*p._Pot._af,p._Pot._tform, p._Pot._tsteady,p._Pot._rb,p._Pot._omegab, p._Pot._barphi]) elif isinstance(p,potential.TransientLogSpiralPotential): pot_type.append(2) pot_args.extend([p._amp,p._A,p._to,p._sigma2,p._alpha,p._m, p._omegas,p._gamma]) elif isinstance(p,potential.SteadyLogSpiralPotential): pot_type.append(3) if p._tform is None: pot_args.extend([p._amp,float('nan'), float('nan'), p._A,p._alpha,p._m, p._omegas,p._gamma]) else: pot_args.extend([p._amp,p._tform,p._tsteady,p._A,p._alpha,p._m, p._omegas,p._gamma]) elif isinstance(p,potential.EllipticalDiskPotential): pot_type.append(4) if p._tform is None: pot_args.extend([p._amp,float('nan'), float('nan'), p._twophio,p._p,p._phib]) else: pot_args.extend([p._amp,p._tform,p._tsteady, p._twophio,p._p,p._phib]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.MiyamotoNagaiPotential): pot_type.append(5) pot_args.extend([p._Pot._amp,p._Pot._a,p._Pot._b]) elif isinstance(p,potential.LopsidedDiskPotential): pot_type.append(6) pot_args.extend([p._amp,p._mphio,p._p,p._phib]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.PowerSphericalPotential): pot_type.append(7) pot_args.extend([p._Pot._amp,p._Pot.alpha]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.HernquistPotential): pot_type.append(8) pot_args.extend([p._Pot._amp,p._Pot.a]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.NFWPotential): pot_type.append(9) pot_args.extend([p._Pot._amp,p._Pot.a]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.JaffePotential): pot_type.append(10) pot_args.extend([p._Pot._amp,p._Pot.a]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.DoubleExponentialDiskPotential): pot_type.append(11) pot_args.extend([p._Pot._amp, -4.*numpy.pi*p._Pot._alpha*p._Pot._amp, p._Pot._alpha,p._Pot._beta,len(p._Pot._de_j1_xs)]) pot_args.extend(p._Pot._de_j0_xs) pot_args.extend(p._Pot._de_j1_xs) pot_args.extend(p._Pot._de_j0_weights) pot_args.extend(p._Pot._de_j1_weights) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.FlattenedPowerPotential): pot_type.append(12) pot_args.extend([p._Pot._amp,p._Pot.alpha,p._Pot.core2]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.IsochronePotential): pot_type.append(14) pot_args.extend([p._Pot._amp,p._Pot.b]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.PowerSphericalPotentialwCutoff): pot_type.append(15) pot_args.extend([p._Pot._amp,p._Pot.alpha,p._Pot.rc]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.MN3ExponentialDiskPotential): # Three Miyamoto-Nagai disks npot+= 2 pot_type.extend([5,5,5]) pot_args.extend([p._Pot._amp*p._Pot._mn3[0]._amp, p._Pot._mn3[0]._a,p._Pot._mn3[0]._b, p._Pot._amp*p._Pot._mn3[1]._amp, p._Pot._mn3[1]._a,p._Pot._mn3[1]._b, p._Pot._amp*p._Pot._mn3[2]._amp, p._Pot._mn3[2]._a,p._Pot._mn3[2]._b]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.KuzminKutuzovStaeckelPotential): pot_type.append(16) pot_args.extend([p._Pot._amp,p._Pot._ac,p._Pot._Delta]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.PlummerPotential): pot_type.append(17) pot_args.extend([p._Pot._amp,p._Pot._b]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.PseudoIsothermalPotential): pot_type.append(18) pot_args.extend([p._Pot._amp,p._Pot._a]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.KuzminDiskPotential): pot_type.append(19) pot_args.extend([p._Pot._amp,p._Pot._a]) elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.BurkertPotential): pot_type.append(20) pot_args.extend([p._Pot._amp,p._Pot.a]) elif (isinstance(p,planarPotentialFromFullPotential) or isinstance(p,planarPotentialFromRZPotential)) \ and isinstance(p._Pot,potential.EllipsoidalPotential.EllipsoidalPotential): pot_args.append(p._Pot._amp) pot_args.extend([0.,0.,0.,0.,0.,0.]) # for caching if isinstance(p._Pot,potential.TriaxialHernquistPotential): pot_type.append(21) pot_args.extend([2,p._Pot.a,p._Pot.a4]) # for psi, mdens, mdens_deriv if isinstance(p._Pot,potential.TriaxialNFWPotential): pot_type.append(22) pot_args.extend([2,p._Pot.a,p._Pot.a3]) # for psi, mdens, mdens_deriv if isinstance(p._Pot,potential.TriaxialJaffePotential): pot_type.append(23) pot_args.extend([2,p._Pot.a,p._Pot.a2]) # for psi, mdens, mdens_deriv elif isinstance(p._Pot,potential.PerfectEllipsoidPotential): pot_type.append(30) pot_args.extend([1,p._Pot.a2]) # for psi, mdens, mdens_deriv elif isinstance(p._Pot,potential.TriaxialGaussianPotential): pot_type.append(37) pot_args.extend([1,-p._Pot._twosigma2]) # for psi, mdens, mdens_deriv elif isinstance(p._Pot,potential.PowerTriaxialPotential): pot_type.append(38) pot_args.extend([1,p._Pot.alpha]) # for psi, mdens, mdens_deriv pot_args.extend([p._Pot._b2,p._Pot._c2, int(p._Pot._aligned)]) # Reg. Ellipsoidal if not p._Pot._aligned: pot_args.extend(list(p._Pot._rot.flatten())) else: pot_args.extend(list(numpy.eye(3).flatten())) # not actually used pot_args.append(p._Pot._glorder) pot_args.extend([p._Pot._glx[ii] for ii in range(p._Pot._glorder)]) # this adds some common factors to the integration weights pot_args.extend([-4.*numpy.pi*p._Pot._glw[ii]*p._Pot._b*p._Pot._c\ /numpy.sqrt(( 1.+(p._Pot._b2-1.)*p._Pot._glx[ii]**2.) *(1.+(p._Pot._c2-1.)*p._Pot._glx[ii]**2.)) for ii in range(p._Pot._glorder)]) elif (isinstance(p,planarPotentialFromFullPotential) or isinstance(p,planarPotentialFromRZPotential)) \ and isinstance(p._Pot,potential.SCFPotential): pt,pa,ptf= _parse_scf_pot(p._Pot) pot_type.append(pt) pot_args.extend(pa) pot_tfuncs.extend(ptf) elif isinstance(p,planarPotentialFromFullPotential) \ and isinstance(p._Pot,potential.SoftenedNeedleBarPotential): pot_type.append(25) pot_args.extend([p._Pot._amp,p._Pot._a,p._Pot._b,p._Pot._c2, p._Pot._pa,p._Pot._omegab]) pot_args.extend([0.,0.,0.,0.,0.,0.,0.]) # for caching elif (isinstance(p,planarPotentialFromFullPotential) or isinstance(p,planarPotentialFromRZPotential)) \ and isinstance(p._Pot,potential.DiskSCFPotential): # Need to pull this apart into: (a) SCF part, (b) constituent # [Sigma_i,h_i] parts # (a) SCF, multiply in any add'l amp pt,pa,ptf= _parse_scf_pot(p._Pot._scf,extra_amp=p._Pot._amp) pot_type.append(pt) pot_args.extend(pa) pot_tfuncs.extend(ptf) # (b) constituent [Sigma_i,h_i] parts for Sigma,hz in zip(p._Pot._Sigma_dict,p._Pot._hz_dict): npot+= 1 pot_type.append(26) stype= Sigma.get('type','exp') if stype == 'exp' and not 'Rhole' in Sigma: pot_args.extend([3,0, 4.*numpy.pi*Sigma.get('amp',1.)*p._Pot._amp, Sigma.get('h',1./3.)]) elif stype == 'expwhole' \ or (stype == 'exp' and 'Rhole' in Sigma): pot_args.extend([4,1, 4.*numpy.pi*Sigma.get('amp',1.)*p._Pot._amp, Sigma.get('h',1./3.), Sigma.get('Rhole',0.5)]) hztype= hz.get('type','exp') if hztype == 'exp': pot_args.extend([0,hz.get('h',0.0375)]) elif hztype == 'sech2': pot_args.extend([1,hz.get('h',0.0375)]) elif isinstance(p,planarPotentialFromFullPotential) \ and isinstance(p._Pot, potential.SpiralArmsPotential): pot_type.append(27) pot_args.extend([len(p._Pot._Cs), p._Pot._amp, p._Pot._N, p._Pot._sin_alpha, p._Pot._tan_alpha, p._Pot._r_ref, p._Pot._phi_ref, p._Pot._Rs, p._Pot._H, p._Pot._omega]) pot_args.extend(p._Pot._Cs) elif isinstance(p,potential.CosmphiDiskPotential): pot_type.append(28) pot_args.extend([p._amp,p._mphio,p._p,p._mphib,p._m, p._rb,p._rbp,p._rb2p,p._r1p]) elif isinstance(p,potential.HenonHeilesPotential): pot_type.append(29) pot_args.extend([p._amp]) # 30: PerfectEllipsoidPotential, done with other EllipsoidalPotentials above # 31: KGPotential # 32: IsothermalDiskPotential elif isinstance(p, planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.DehnenCoreSphericalPotential): pot_type.append(33) pot_args.extend([p._Pot._amp,p._Pot.a]) elif isinstance(p, planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.DehnenSphericalPotential): pot_type.append(34) pot_args.extend([p._Pot._amp,p._Pot.a,p._Pot.alpha]) # 35: HomogeneousSpherePotential elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.HomogeneousSpherePotential): pot_type.append(35) pot_args.extend([p._Pot._amp,p._Pot._R2,p._Pot._R3]) # 36: interpSphericalPotential elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.interpSphericalPotential): pot_type.append(36) pot_args.append(len(p._Pot._rgrid)) pot_args.extend(p._Pot._rgrid) pot_args.extend(p._Pot._rforce_grid) pot_args.extend([p._Pot._amp,p._Pot._rmin,p._Pot._rmax, p._Pot._total_mass,p._Pot._Phi0,p._Pot._Phimax]) # 37: TriaxialGaussianPotential, done with other EllipsoidalPotentials above # 38: PowerTriaxialPotential, done with other EllipsoidalPotentials above elif isinstance(p,planarPotentialFromRZPotential) \ and isinstance(p._Pot,potential.NullPotential): pot_type.append(40) ############################## WRAPPERS ############################### elif ((isinstance(p,planarPotentialFromFullPotential) or isinstance(p,planarPotentialFromRZPotential)) \ and isinstance(p._Pot,potential.DehnenSmoothWrapperPotential)) \ or isinstance(p,potential.DehnenSmoothWrapperPotential): if not isinstance(p,potential.DehnenSmoothWrapperPotential): p= p._Pot pot_type.append(-1) # wrap_pot_type, args, and npot obtained before this horrible if pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.extend([p._amp,p._tform,p._tsteady,int(p._grow)]) elif ((isinstance(p,planarPotentialFromFullPotential) or isinstance(p,planarPotentialFromRZPotential)) \ and isinstance(p._Pot,potential.SolidBodyRotationWrapperPotential)) \ or isinstance(p,potential.SolidBodyRotationWrapperPotential): if not isinstance(p,potential.SolidBodyRotationWrapperPotential): p= p._Pot pot_type.append(-2) # wrap_pot_type, args, and npot obtained before this horrible if pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.extend([p._amp,p._omega,p._pa]) elif ((isinstance(p,planarPotentialFromFullPotential) or isinstance(p,planarPotentialFromRZPotential)) \ and isinstance(p._Pot,potential.CorotatingRotationWrapperPotential)) \ or isinstance(p,potential.CorotatingRotationWrapperPotential): if not isinstance(p,potential.CorotatingRotationWrapperPotential): p= p._Pot pot_type.append(-4) # wrap_pot_type, args, and npot obtained before this horrible if pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.extend([p._amp,p._vpo,p._beta,p._pa,p._to]) elif ((isinstance(p,planarPotentialFromFullPotential) or isinstance(p,planarPotentialFromRZPotential)) \ and isinstance(p._Pot,potential.GaussianAmplitudeWrapperPotential)) \ or isinstance(p,potential.GaussianAmplitudeWrapperPotential): if not isinstance(p,potential.GaussianAmplitudeWrapperPotential): p= p._Pot pot_type.append(-5) # wrap_pot_type, args, and npot obtained before this horrible if pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.extend([p._amp,p._to,p._sigma2]) elif ((isinstance(p,planarPotentialFromFullPotential) or isinstance(p,planarPotentialFromRZPotential)) \ and isinstance(p._Pot,potential.MovingObjectPotential)) \ or isinstance(p,potential.MovingObjectPotential): if not isinstance(p,potential.MovingObjectPotential): p= p._Pot pot_type.append(-6) wrap_npot, wrap_pot_type, wrap_pot_args, wrap_pot_tfuncs= \ _parse_pot(potential.toPlanarPotential(p._pot)) pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.extend([len(p._orb.t)]) pot_args.extend(p._orb.t) pot_args.extend(p._orb.x(p._orb.t,use_physical=False)) pot_args.extend(p._orb.y(p._orb.t,use_physical=False)) pot_args.extend([p._amp]) pot_args.extend([p._orb.t[0],p._orb.t[-1]]) #t_0, t_f elif ((isinstance(p,planarPotentialFromFullPotential) or isinstance(p,planarPotentialFromRZPotential)) \ and isinstance(p._Pot,potential.RotateAndTiltWrapperPotential)) \ or isinstance(p,potential.RotateAndTiltWrapperPotential): # pragma: no cover raise NotImplementedError('Planar orbit integration in C for RotateAndTiltWrapperPotential not implemented; please integrate an orbit with (z,vz) = (0,0) instead') # Note that potential.RotateAndTiltWrapperPotential would be -8 elif ((isinstance(p,planarPotentialFromFullPotential) or isinstance(p,planarPotentialFromRZPotential)) \ and isinstance(p._Pot,potential.TimeDependentAmplitudeWrapperPotential)) \ or isinstance(p,potential.TimeDependentAmplitudeWrapperPotential): if not isinstance(p,potential.TimeDependentAmplitudeWrapperPotential): p= p._Pot pot_type.append(-9) # wrap_pot_type, args, and npot obtained before this horrible if pot_args.append(wrap_npot) pot_type.extend(wrap_pot_type) pot_args.extend(wrap_pot_args) pot_tfuncs.extend(wrap_pot_tfuncs) pot_args.append(p._amp) pot_tfuncs.append(p._A) pot_type= numpy.array(pot_type,dtype=numpy.int32,order='C') pot_args= numpy.array(pot_args,dtype=numpy.float64,order='C') return (npot,pot_type,pot_args,pot_tfuncs) def _parse_integrator(int_method): """parse the integrator method to pass to C""" #Pick integrator if int_method.lower() == 'rk4_c': int_method_c= 1 elif int_method.lower() == 'rk6_c': int_method_c= 2 elif int_method.lower() == 'symplec4_c': int_method_c= 3 elif int_method.lower() == 'symplec6_c': int_method_c= 4 elif int_method.lower() == 'dopr54_c': int_method_c= 5 elif int_method.lower() == 'dop853_c': int_method_c= 6 else: int_method_c= 0 return int_method_c def _parse_tol(rtol,atol): """Parse the tolerance keywords""" #Process atol and rtol if rtol is None: rtol= -12.*numpy.log(10.) else: #pragma: no cover rtol= numpy.log(rtol) if atol is None: atol= -12.*numpy.log(10.) else: #pragma: no cover atol= numpy.log(atol) return (rtol,atol) def _parse_scf_pot(p,extra_amp=1.): # Stand-alone parser for SCF, bc re-used isNonAxi= p.isNonAxi pot_args= [p._a, isNonAxi] pot_args.extend(p._Acos.shape) pot_args.extend(extra_amp*p._amp*p._Acos.flatten(order='C')) if isNonAxi: pot_args.extend(extra_amp*p._amp*p._Asin.flatten(order='C')) pot_args.extend([-1.,0,0,0,0,0,0]) return (24,pot_args,[]) # latter is pot_tfuncs def _prep_tfuncs(pot_tfuncs): if len(pot_tfuncs) == 0: pot_tfuncs= None # NULL else: func_ctype= ctypes.CFUNCTYPE(ctypes.c_double, # Return type ctypes.c_double) # time try: # using numba if not _NUMBA_LOADED: raise nb_c_sig= types.double(types.double) func_pyarr= [cfunc(nb_c_sig,nopython=True)(a).ctypes for a in pot_tfuncs] except: # Any Exception, switch to regular ctypes wrapping func_pyarr= [func_ctype(a) for a in pot_tfuncs] pot_tfuncs= (func_ctype * len(func_pyarr))(*func_pyarr) return pot_tfuncs def integratePlanarOrbit_c(pot,yo,t,int_method,rtol=None,atol=None, progressbar=True,dt=None): """ NAME: integratePlanarOrbit_c PURPOSE: C integrate an ode for a planarOrbit INPUT: pot - Potential or list of such instances yo - initial condition [q,p], can be [N,4] or [4] t - set of times at which one wants the result int_method= 'leapfrog_c', 'rk4_c', 'rk6_c', 'symplec4_c', ... rtol, atol progressbar= (True) if True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!) dt= (None) force integrator to use this stepsize (default is to automatically determine one) OUTPUT: (y,err) y : array, shape (len(y0),len(t),4) Array containing the value of y for each desired time in t, \ with the initial value y0 in the first row. err: error message, if not zero: 1 means maximum step reduction happened for adaptive integrators HISTORY: 2011-10-03 - Written - Bovy (IAS) 2018-12-20 - Adapted to allow multiple objects - Bovy (UofT) 2022-04-12 - Add progressbar - Bovy (UofT) """ if len(yo.shape) == 1: single_obj= True else: single_obj= False yo= numpy.atleast_2d(yo) nobj= len(yo) rtol, atol= _parse_tol(rtol,atol) npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot) pot_tfuncs= _prep_tfuncs(pot_tfuncs) int_method_c= _parse_integrator(int_method) if dt is None: dt= -9999.99 #Set up result array result= numpy.empty((nobj,len(t),4)) err= numpy.zeros(nobj,dtype=numpy.int32) #Set up progressbar progressbar*= _TQDM_LOADED if nobj > 1 and progressbar: pbar= tqdm.tqdm(total=nobj,leave=False) pbar_func_ctype= ctypes.CFUNCTYPE(None) pbar_c= pbar_func_ctype(pbar.update) else: # pragma: no cover pbar_c= None #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') integrationFunc= _lib.integratePlanarOrbit integrationFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_double, ctypes.c_double, ctypes.c_double, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ctypes.c_int, ctypes.c_void_p] #Array requirements, first store old order f_cont= [yo.flags['F_CONTIGUOUS'], t.flags['F_CONTIGUOUS']] yo= numpy.require(yo,dtype=numpy.float64,requirements=['C','W']) t= numpy.require(t,dtype=numpy.float64,requirements=['C','W']) result= numpy.require(result,dtype=numpy.float64,requirements=['C','W']) err= numpy.require(err,dtype=numpy.int32,requirements=['C','W']) #Run the C code integrationFunc(ctypes.c_int(nobj), yo, ctypes.c_int(len(t)), t, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(dt), ctypes.c_double(rtol), ctypes.c_double(atol), result, err, ctypes.c_int(int_method_c), pbar_c) if nobj > 1 and progressbar: pbar.close() if numpy.any(err == -10): #pragma: no cover raise KeyboardInterrupt("Orbit integration interrupted by CTRL-C (SIGINT)") #Reset input arrays if f_cont[0]: yo= numpy.asfortranarray(yo) if f_cont[1]: t= numpy.asfortranarray(t) if single_obj: return (result[0],err[0]) else: return (result,err) def integratePlanarOrbit_dxdv_c(pot,yo,dyo,t,int_method,rtol=None,atol=None, dt=None): """ NAME: integratePlanarOrbit_dxdv_c PURPOSE: C integrate an ode for a planarOrbit+phase space volume dxdv INPUT: pot - Potential or list of such instances yo - initial condition [q,p] dyo - initial condition [dq,dp] t - set of times at which one wants the result int_method= 'leapfrog_c', 'rk4_c', 'rk6_c', 'symplec4_c' rtol, atol dt= (None) force integrator to use this stepsize (default is to automatically determine one)) OUTPUT: (y,err) y,dy : array, shape (len(y0),len(t),8) Array containing the value of y for each desired time in t, \ with the initial value y0 in the first row. err: error message if not zero, 1: maximum step reduction happened for adaptive integrators HISTORY: 2011-10-19 - Written - Bovy (IAS) """ rtol, atol= _parse_tol(rtol,atol) npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot) pot_tfuncs= _prep_tfuncs(pot_tfuncs) int_method_c= _parse_integrator(int_method) if dt is None: dt= -9999.99 yo= numpy.concatenate((yo,dyo)) #Set up result array result= numpy.empty((len(t),8)) err= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') integrationFunc= _lib.integratePlanarOrbit_dxdv integrationFunc.argtypes= [ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ctypes.c_double, ctypes.c_double, ctypes.c_double, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int), ctypes.c_int] #Array requirements, first store old order f_cont= [yo.flags['F_CONTIGUOUS'], t.flags['F_CONTIGUOUS']] yo= numpy.require(yo,dtype=numpy.float64,requirements=['C','W']) t= numpy.require(t,dtype=numpy.float64,requirements=['C','W']) result= numpy.require(result,dtype=numpy.float64,requirements=['C','W']) #Run the C code integrationFunc(yo, ctypes.c_int(len(t)), t, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(dt), ctypes.c_double(rtol),ctypes.c_double(atol), result, ctypes.byref(err), ctypes.c_int(int_method_c)) if err.value == -10: #pragma: no cover raise KeyboardInterrupt("Orbit integration interrupted by CTRL-C (SIGINT)") #Reset input arrays if f_cont[0]: yo= numpy.asfortranarray(yo) if f_cont[1]: t= numpy.asfortranarray(t) return (result,err.value) def integratePlanarOrbit(pot,yo,t,int_method,rtol=None,atol=None,numcores=1, progressbar=True,dt=None): """ NAME: integratePlanarOrbit PURPOSE: Integrate an ode for a planarOrbit INPUT: pot - Potential or list of such instances yo - initial condition [q,p], shape [N,3] or [N,4] t - set of times at which one wants the result int_method= 'leapfrog', 'odeint', or 'dop853' rtol, atol= tolerances (not always used...) numcores= (1) number of cores to use for multi-processing progressbar= (True) if True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!) dt= (None) force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators!) OUTPUT: (y,err) y : array, shape (N,len(t),3/4) Array containing the value of y for each desired time in t, \ with the initial value y0 in the first row. err: error message, always zero for now HISTORY: 2010-07-20 - Written - Bovy (NYU) 2019-04-09 - Adapted to allow multiple objects and parallel mapping - Bovy (UofT) 2022-04-12 - Add progressbar - Bovy (UofT) """ nophi= False if not int_method.lower() == 'dop853' and not int_method == 'odeint': if len(yo[0]) == 3: nophi= True #We hack this by putting in a dummy phi=0 yo= numpy.pad(yo,((0,0),(0,1)),'constant',constant_values=0) if int_method.lower() == 'leapfrog': if rtol is None: rtol= 1e-8 def integrate_for_map(vxvv): #go to the rectangular frame this_vxvv= numpy.array([vxvv[0]*numpy.cos(vxvv[3]), vxvv[0]*numpy.sin(vxvv[3]), vxvv[1]*numpy.cos(vxvv[3]) -vxvv[2]*numpy.sin(vxvv[3]), vxvv[2]*numpy.cos(vxvv[3]) +vxvv[1]*numpy.sin(vxvv[3])]) #integrate tmp_out= symplecticode.leapfrog(_planarRectForce,this_vxvv, t,args=(pot,),rtol=rtol) #go back to the cylindrical frame R= numpy.sqrt(tmp_out[:,0]**2.+tmp_out[:,1]**2.) phi= numpy.arccos(tmp_out[:,0]/R) phi[(tmp_out[:,1] < 0.)]= 2.*numpy.pi-phi[(tmp_out[:,1] < 0.)] vR= tmp_out[:,2]*numpy.cos(phi)+tmp_out[:,3]*numpy.sin(phi) vT= tmp_out[:,3]*numpy.cos(phi)-tmp_out[:,2]*numpy.sin(phi) out= numpy.zeros((len(t),4)) out[:,0]= R out[:,1]= vR out[:,2]= vT out[:,3]= phi return out elif int_method.lower() == 'dop853' or int_method.lower() == 'odeint': if rtol is None: rtol= 1e-8 if int_method.lower() == 'dop853': integrator= dop853 extra_kwargs= {} else: integrator= integrate.odeint extra_kwargs= {'rtol':rtol} if len(yo[0]) == 3: def integrate_for_map(vxvv): l= vxvv[0]*vxvv[2] l2= l**2. init= [vxvv[0],vxvv[1]] intOut= integrator(_planarREOM,init,t=t,args=(pot,l2), **extra_kwargs) out= numpy.zeros((len(t),3)) out[:,0]= intOut[:,0] out[:,1]= intOut[:,1] out[:,2]= l/out[:,0] #post-process to remove negative radii neg_radii= (out[:,0] < 0.) out[neg_radii,0]= -out[neg_radii,0] return out else: def integrate_for_map(vxvv): vphi= vxvv[2]/vxvv[0] init= [vxvv[0],vxvv[1],vxvv[3],vphi] intOut= integrator(_planarEOM,init,t=t,args=(pot,), **extra_kwargs) out= numpy.zeros((len(t),4)) out[:,0]= intOut[:,0] out[:,1]= intOut[:,1] out[:,3]= intOut[:,2] out[:,2]= out[:,0]*intOut[:,3] #post-process to remove negative radii neg_radii= (out[:,0] < 0.) out[neg_radii,0]= -out[neg_radii,0] out[neg_radii,3]+= numpy.pi return out else: # Assume we are forcing parallel_mapping of a C integrator... def integrate_for_map(vxvv): return integratePlanarOrbit_c(pot,numpy.copy(vxvv), t,int_method,dt=dt)[0] if len(yo) == 1: # Can't map a single value... out= numpy.atleast_3d(integrate_for_map(yo[0]).T).T else: out= numpy.array(parallel_map(integrate_for_map,yo,numcores=numcores, progressbar=progressbar)) if nophi: out= out[:,:,:3] return out, numpy.zeros(len(yo)) def integratePlanarOrbit_dxdv(pot,yo,dyo,t,int_method, rectIn,rectOut, rtol=None,atol=None, progressbar=True,dt=None,numcores=1): """ NAME: integratePlanarOrbit_dxdv PURPOSE: Integrate an ode for a planarOrbit+phase space volume dxdv INPUT: pot - Potential or list of such instances yo - initial condition [q,p], shape [N,4] dyo - initial condition [dq,dp], shape [N,4] t - set of times at which one wants the result int_method= 'odeint', 'dop853', 'dopr54_c', 'rk4_c', 'rk6_c' rectIn= (False) if True, input dyo is in rectangular coordinates rectOut= (False) if True, output dyo is in rectangular coordinates rtol, atol= tolerances (not always used...) numcores= (1) number of cores to use for multi-processing progressbar= (True) if True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!) dt= (None) force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators) OUTPUT: (y,err) y : array, shape (N,len(t),8) Array containing the value of y for each desired time in t, \ with the initial value y0 in the first row. err: error message, always zero for now HISTORY: 2011-10-17 - Written - Bovy (IAS) 2019-05-21 - Adapted to allow multiple objects and parallel mapping - Bovy (UofT) 2022-04-12 - Add progressbar - Bovy (UofT) """ #go to the rectangular frame this_yo= numpy.array([yo[:,0]*numpy.cos(yo[:,3]), yo[:,0]*numpy.sin(yo[:,3]), yo[:,1]*numpy.cos(yo[:,3]) -yo[:,2]*numpy.sin(yo[:,3]), yo[:,2]*numpy.cos(yo[:,3]) +yo[:,1]*numpy.sin(yo[:,3])]).T if not rectIn: this_dyo= numpy.array([numpy.cos(yo[:,3])*dyo[:,0] -yo[:,0]*numpy.sin(yo[:,3])*dyo[:,3], numpy.sin(yo[:,3])*dyo[:,0] +yo[:,0]*numpy.cos(yo[:,3])*dyo[:,3], -(yo[:,1]*numpy.sin(yo[:,3]) +yo[:,2]*numpy.cos(yo[:,3]))*dyo[:,3] +numpy.cos(yo[:,3])*dyo[:,1] -numpy.sin(yo[:,3])*dyo[:,2], (yo[:,1]*numpy.cos(yo[:,3]) -yo[:,2]*numpy.sin(yo[:,3]))*dyo[:,3] +numpy.sin(yo[:,3])*dyo[:,1] +numpy.cos(yo[:,3])*dyo[:,2]]).T else: this_dyo= dyo this_yo= numpy.hstack((this_yo,this_dyo)) if int_method.lower() == 'dop853' or int_method.lower() == 'odeint': if rtol is None: rtol= 1e-8 if int_method.lower() == 'dop853': integrator= dop853 extra_kwargs= {} else: integrator= integrate.odeint extra_kwargs= {'rtol':rtol} def integrate_for_map(vxvv): return integrator(_planarEOM_dxdv,vxvv,t=t,args=(pot,), **extra_kwargs) else: # Assume we are forcing parallel_mapping of a C integrator... def integrate_for_map(vxvv): return integratePlanarOrbit_dxdv_c(pot,numpy.copy(vxvv[:4]), numpy.copy(vxvv[4:]), t,int_method,dt=dt, rtol=rtol,atol=atol)[0] if len(this_yo) == 1: # Can't map a single value... out= numpy.atleast_3d(integrate_for_map(this_yo[0]).T).T else: out= numpy.array(parallel_map(integrate_for_map,this_yo, progressbar=progressbar, numcores=numcores)) #go back to the cylindrical frame R= numpy.sqrt(out[...,0]**2.+out[...,1]**2.) phi= numpy.arccos(out[...,0]/R) phi[(out[...,1] < 0.)]= 2.*numpy.pi-phi[(out[...,1] < 0.)] vR= out[...,2]*numpy.cos(phi)+out[...,3]*numpy.sin(phi) vT= out[...,3]*numpy.cos(phi)-out[...,2]*numpy.sin(phi) cp= numpy.cos(phi) sp= numpy.sin(phi) out[...,0]= R out[...,1]= vR out[...,2]= vT out[...,3]= phi if rectOut: out[...,4:]= out[...,4:] else: dR= cp*out[...,4]+sp*out[...,5] dphi= (cp*out[...,5]-sp*out[...,4])/R dvR= cp*out[...,6]+sp*out[...,7]+vT*dphi dvT= cp*out[...,7]-sp*out[...,6]-vR*dphi out[...,4]= dR out[...,7]= dphi out[...,5]= dvR out[...,6]= dvT return out, numpy.zeros(len(yo)) def _planarREOM(y,t,pot,l2): """ NAME: _planarREOM PURPOSE: implements the EOM, i.e., the right-hand side of the differential equation, for integrating a planar Orbit assuming angular momentum conservation INPUT: y - current phase-space position t - current time pot - (list of) Potential instance(s) l2 - angular momentum squared OUTPUT: dy/dt HISTORY: 2010-07-20 - Written - Bovy (NYU) """ return [y[1], l2/y[0]**3.+_evaluateplanarRforces(pot,y[0],t=t)] def _planarEOM(y,t,pot): """ NAME: _planarEOM PURPOSE: implements the EOM, i.e., the right-hand side of the differential equation, for integrating a general planar Orbit INPUT: y - current phase-space position t - current time pot - (list of) Potential instance(s) OUTPUT: dy/dt HISTORY: 2010-07-20 - Written - Bovy (NYU) """ l2= (y[0]**2.*y[3])**2. return [y[1], l2/y[0]**3.+_evaluateplanarRforces(pot,y[0],phi=y[2],t=t), y[3], 1./y[0]**2.*(_evaluateplanarphitorques(pot,y[0],phi=y[2],t=t)- 2.*y[0]*y[1]*y[3])] def _planarEOM_dxdv(x,t,pot): """ NAME: _planarEOM_dxdv PURPOSE: implements the EOM, i.e., the right-hand side of the differential equation, for integrating phase space differences, rectangular INPUT: x - current phase-space position t - current time pot - (list of) Potential instance(s) OUTPUT: dy/dt HISTORY: 2011-10-18 - Written - Bovy (IAS) """ #x is rectangular so calculate R and phi R= numpy.sqrt(x[0]**2.+x[1]**2.) phi= numpy.arccos(x[0]/R) sinphi= x[1]/R cosphi= x[0]/R if x[1] < 0.: phi= 2.*numpy.pi-phi #calculate forces Rforce= _evaluateplanarRforces(pot,R,phi=phi,t=t) phitorque= _evaluateplanarphitorques(pot,R,phi=phi,t=t) R2deriv= _evaluateplanarPotentials(pot,R,phi=phi,t=t,dR=2) phi2deriv= _evaluateplanarPotentials(pot,R,phi=phi,t=t,dphi=2) Rphideriv= _evaluateplanarPotentials(pot,R,phi=phi,t=t,dR=1,dphi=1) #Calculate derivatives and derivatives+time derivatives dFxdx= -cosphi**2.*R2deriv\ +2.*cosphi*sinphi/R**2.*phitorque\ +sinphi**2./R*Rforce\ +2.*sinphi*cosphi/R*Rphideriv\ -sinphi**2./R**2.*phi2deriv dFxdy= -sinphi*cosphi*R2deriv\ +(sinphi**2.-cosphi**2.)/R**2.*phitorque\ -cosphi*sinphi/R*Rforce\ -(cosphi**2.-sinphi**2.)/R*Rphideriv\ +cosphi*sinphi/R**2.*phi2deriv dFydx= -cosphi*sinphi*R2deriv\ +(sinphi**2.-cosphi**2.)/R**2.*phitorque\ +(sinphi**2.-cosphi**2.)/R*Rphideriv\ -sinphi*cosphi/R*Rforce\ +sinphi*cosphi/R**2.*phi2deriv dFydy= -sinphi**2.*R2deriv\ -2.*sinphi*cosphi/R**2.*phitorque\ -2.*sinphi*cosphi/R*Rphideriv\ +cosphi**2./R*Rforce\ -cosphi**2./R**2.*phi2deriv return numpy.array([x[2],x[3], cosphi*Rforce-1./R*sinphi*phitorque, sinphi*Rforce+1./R*cosphi*phitorque, x[6],x[7], dFxdx*x[4]+dFxdy*x[5], dFydx*x[4]+dFydy*x[5]]) def _planarRectForce(x,pot,t=0.): """ NAME: _planarRectForce PURPOSE: returns the planar force in the rectangular frame INPUT: x - current position t - current time pot - (list of) Potential instance(s) OUTPUT: force HISTORY: 2011-02-02 - Written - Bovy (NYU) """ #x is rectangular so calculate R and phi R= numpy.sqrt(x[0]**2.+x[1]**2.) phi= numpy.arccos(x[0]/R) sinphi= x[1]/R cosphi= x[0]/R if x[1] < 0.: phi= 2.*numpy.pi-phi #calculate forces Rforce= _evaluateplanarRforces(pot,R,phi=phi,t=t) phitorque= _evaluateplanarphitorques(pot,R,phi=phi,t=t) return numpy.array([cosphi*Rforce-1./R*sinphi*phitorque, sinphi*Rforce+1./R*cosphi*phitorque]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/orbit/named_objects.json0000644000175100001710000034163414327773303020203 0ustar00runnerdocker{ "_collections": { "MWglobularclusters": [ "NGC5286", "Terzan12", "Arp2", "NGC5024", "NGC6638", "Crater", "BH261", "NGC6553", "NGC6749", "NGC6528", "NGC4372", "NGC2808", "IC4499", "BH229", "NGC6642", "NGC6779", "NGC6541", "NGC6441", "Pal4", "NGC6341", "NGC5694", "NGC2298", "Ton2", "NGC6637", "NGC6325", "NGC4147", "NGC6366", "Pal7", "NGC5986", "NGC5927", "Terzan1", "NGC4833", "Pal8", "NGC7078", "NGC6517", "NGC6284", "Pal14", "NGC6539", "NGC7089", "NGC5272", "NGC362", "NGC6144", "NGC6287", "E3", "NGC6205", "NGC6402", "FSR1735", "Pal3", "NGC6256", "NGC6342", "Djorg2", "NGC6093", "NGC5139", "Terzan5", "NGC6333", "NGC6934", "NGC6101", "NGC6171", "NGC5466", "Pal5", "ESO45211", "NGC6266", "Pal15", "Pal13", "Terzan2", "NGC6540", "Terzan4", "BH184", "NGC5053", "NGC6723", "FSR1716", "BH176", "NGC6809", "NGC5897", "NGC6496", "NGC6715", "NGC6388", "Pal2", "NGC1261", "NGC6362", "Whiting1", "NGC6522", "NGC6254", "NGC6535", "NGC6440", "NGC6316", "NGC5634", "NGC7492", "Terzan9", "NGC6352", "Terzan7", "Terzan6", "NGC6235", "NGC5904", "NGC6626", "IC1257", "NGC5946", "NGC6717", "NGC288", "NGC2419", "Terzan10", "NGC6218", "Pal6", "NGC6426", "NGC6304", "NGC6273", "NGC6544", "NGC6624", "NGC6356", "Pal12", "Djorg1", "NGC6293", "NGC7006", "NGC104", "Pal11", "NGC5824", "Terzan3", "NGC6584", "NGC3201", "Rup106", "NGC6838", "NGC7099", "NGC6229", "NGC6139", "Pyxis", "NGC6121", "NGC6681", "NGC6652", "NGC1904", "NGC1851", "NGC6397", "Terzan8", "NGC6569", "NGC6981", "NGC6401", "NGC6760", "NGC6380", "NGC6355", "NGC4590", "Pal1", "NGC6656", "ESO28006", "NGC6558", "Pal10", "E1", "NGC6712", "NGC6752", "NGC6453", "NGC6864", "Eridanus", "ESO93-8", "BH140", "AM4", "Munoz1", "FSR1758", "Liller1", "UKS1", "VVVCL001", "Mercer5", "Laevens3", "Segue3" ], "MWsatellitegalaxies": [ "AntliaII", "AquariusII", "BootesI", "BootesII", "BootesIII", "CanesVenaticiI", "CanesVenaticiII", "Carina", "CarinaII", "CarinaIII", "ColumbaI", "ComaBerenices", "CraterII", "Draco", "DracoII", "EridanusII", "Fornax", "GrusI", "GrusII", "Hercules", "HorologiumI", "HydraII", "HydrusI", "LMC", "LeoI", "LeoII", "LeoIV", "LeoV", "PegasusIII", "PhoenixI", "PhoenixII", "PiscesII", "ReticulumII", "ReticulumIII", "SMC", "SagittariusII", "Sculptor", "Segue1", "Segue2", "Sextans", "Sgr", "TriangulumII", "TucanaII", "TucanaIII", "TucanaIV", "TucanaV", "UrsaMajorI", "UrsaMajorII", "UrsaMinor", "Willman1" ], "solarsystem": [ "Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune" ] }, "_synonyms": { "OmegaCen": "NGC5139", "47Tuc": "NGC104", "M13": "NGC6205", "AM1": "E1", "M79": "NGC1904", "ESO371": "E3", "Laevens1": "Crater", "M68": "NGC4590", "M53": "NGC5024", "M3": "NGC5272", "M5": "NGC5904", "ESO2248": "BH176", "Arp1": "Pal14", "Lynga7": "BH184", "M80": "NGC6093", "M4": "NGC6121", "M107": "NGC6171", "1636283": "ESO45211", "M12": "NGC6218", "2MASSGC03": "FSR1735", "M10": "NGC6254", "M62": "NGC6266", "M19": "NGC6273", "M92": "NGC6341", "M9": "NGC6333", "HP3": "Terzan2", "HP4": "Terzan4", "HP1": "BH229", "Ton1": "NGC6380", "HP2": "Terzan1", "Pismis26": "Ton2", "M14": "NGC6402", "Terzan11": "Terzan5", "HP5": "Terzan6", "Djorg3": "NGC6540", "IC1276": "Pal7", "ESO45678": "BH261", "M28": "NGC6626", "M69": "NGC6637", "M22": "NGC6656", "M70": "NGC6681", "M54": "NGC6715", "Pal9": "NGC6717", "M56": "NGC6779", "M55": "NGC6809", "M71": "NGC6838", "M75": "NGC6864", "M72": "NGC6981", "M15": "NGC7078", "M2": "NGC7089", "M30": "NGC7099", "Ryu059": "RLGC1-missingvlos", "Ryu879": "RLGC2-missingvlos" }, "GD-1": { "ra": 148.9363998668805, "dec": 36.15980426805254, "distance": 7.555339165941959, "pmra": -5.332929760383195, "pmdec": -12.198914465325117, "vlos": 6.944006091929623, "ro": 8.0, "vo": 220.0, "zo": 0.025, "solarmotion": [ -11.1, 24.0, 7.25 ], "source": "arXiv:1811.07022" }, "AntliaII": { "ra": 143.8868, "dec": -36.7673, "distance": 132.0, "pmra": -0.0928385269606818, "pmdec": 0.0999082937338185, "vlos": 290.7, "source": "arXiv:2205.05699", "pmcorr": 0.0004264068697118, "distance_e": 6.0, "vlos_e": 0.5, "pmra_e": 0.0081022301749938, "pmdec_e": 0.0088674701106851 }, "AquariusII": { "ra": 338.4813, "dec": -9.3274, "distance": 107.9, "pmra": -0.1702346528764635, "pmdec": -0.4655247639844811, "vlos": -71.1, "source": "arXiv:2205.05699", "dr2": { "ra": 338.481, "dec": -9.327, "distance": 107.6, "pmra": -0.252, "pmdec": 0.011, "vlos": -71.1, "source": "arXiv:1805.00908" }, "pmcorr": 0.0128719798176158, "distance_e": 3.3, "vlos_e": 2.5, "pmra_e": 0.1159443992559648, "pmdec_e": 0.0954053095851006 }, "BootesI": { "ra": 210.02, "dec": 14.5135, "distance": 66.0, "pmra": -0.385238, "pmdec": -1.06788, "vlos": 101.8, "source": "arXiv:2205.05699", "dr2": { "ra": 210.015, "dec": 14.512, "distance": 66.37, "pmra": -0.554, "pmdec": -1.111, "vlos": 99, "source": "arXiv:1805.00908" }, "pmcorr": -4.3815360600834166e-05, "distance_e": 3.0, "vlos_e": 0.7, "pmra_e": 0.0169285264244739, "pmdec_e": 0.0129833289626044 }, "BootesII": { "ra": 209.5141, "dec": 12.8553, "distance": 42.0, "pmra": -2.42629, "pmdec": -0.4136790144587254, "vlos": -117.0, "source": "arXiv:2205.05699", "dr2": { "ra": 209.521, "dec": 12.859, "distance": 41.88, "pmra": -2.686, "pmdec": -0.53, "vlos": -117, "source": "arXiv:1805.00908" }, "pmcorr": -0.0001832011894951, "distance_e": 2.0, "vlos_e": 5.2, "pmra_e": 0.0783926698419914, "pmdec_e": 0.0607948509478443 }, "BootesIII": { "ra": 209.3, "dec": 26.8, "distance": 46.5, "pmra": -1.1756284670234, "pmdec": -0.8903690086864998, "vlos": 197.5, "source": "arXiv:2205.05699", "pmcorr": 0.0003819472756655, "distance_e": 2.0, "vlos_e": 3.8, "pmra_e": 0.0190304553510767, "pmdec_e": 0.0150013968441832 }, "BootesIV-missingvlos": { "ra": 233.689, "dec": 43.726, "distance": 209.0, "pmra": 0.469013969031395, "pmdec": 0.4894436168586725, "vlos": NaN, "source": "arXiv:2205.05699" }, "CanesVenaticiI": { "ra": 202.0091, "dec": 33.5521, "distance": 210.0, "pmra": -0.0963782493563674, "pmdec": -0.1161690366244132, "vlos": 30.9, "source": "arXiv:2205.05699", "dr2": { "ra": 202.016, "dec": 33.559, "distance": 210.9, "pmra": -0.159, "pmdec": -0.067, "vlos": 30.9, "source": "arXiv:1805.00908" }, "pmcorr": 0.1314951413957209, "distance_e": 6.0, "vlos_e": 0.6, "pmra_e": 0.0305502191032151, "pmdec_e": 0.0199112961814761 }, "CanesVenaticiII": { "ra": 194.2927, "dec": 34.3226, "distance": 160.0, "pmra": -0.1242634341922089, "pmdec": -0.2541722655399443, "vlos": -128.9, "source": "arXiv:2205.05699", "dr2": { "ra": 194.292, "dec": 34.321, "distance": 160, "pmra": -0.342, "pmdec": -0.473, "vlos": -128.9, "source": "arXiv:1805.00908" }, "pmcorr": 0.1215164948526493, "distance_e": 4.5, "vlos_e": 1.2, "pmra_e": 0.1161556200321516, "pmdec_e": 0.0809204018973889 }, "Carina": { "ra": 100.4065, "dec": -50.9593, "distance": 105.6, "pmra": 0.532107, "pmdec": 0.1270525767834623, "vlos": 222.9, "source": "arXiv:2205.05699", "dr2": { "ra": 100.407, "dec": -50.966, "distance": 103.8, "pmra": 0.485, "pmdec": 0.131, "vlos": 229.1, "source": "arXiv:1805.00908" }, "pmcorr": -2.27869747555652e-05, "distance_e": 5.4, "vlos_e": 0.1, "pmra_e": 0.0064986729232124, "pmdec_e": 0.0063318387502804 }, "CarinaII": { "ra": 114.1066, "dec": -57.9991, "distance": 37.4, "pmra": 1.88539, "pmdec": 0.1332994733137613, "vlos": 477.2, "source": "arXiv:2205.05699", "dr2": { "ra": 114.107, "dec": -57.999, "distance": 36.14, "pmra": 1.867, "pmdec": 0.082, "vlos": 477.2, "source": "arXiv:1805.00908" }, "pmcorr": 4.3315081067251935e-06, "distance_e": 0.4, "vlos_e": 1.2, "pmra_e": 0.018745, "pmdec_e": 0.0191045877533837 }, "CarinaIII": { "ra": 114.6298, "dec": -57.8997, "distance": 27.8, "pmra": 3.09493, "pmdec": 1.39453, "vlos": 284.6, "source": "arXiv:2205.05699", "dr2": { "ra": 114.63, "dec": -57.9, "distance": 27.8, "pmra": 3.046, "pmdec": 1.565, "vlos": 284.6, "source": "arXiv:1805.00908" }, "pmcorr": 1.29200344989739e-05, "distance_e": 0.6, "vlos_e": 3.25, "pmra_e": 0.040685, "pmdec_e": 0.0449183066468054 }, "CentaurusI-missingvlos": { "ra": 189.585, "dec": -40.902, "distance": 116.3, "pmra": -0.0744723019505324, "pmdec": -0.1993701407138002, "vlos": NaN, "source": "arXiv:2205.05699" }, "CetusII-missingvlos": { "ra": 19.47, "dec": -17.42, "distance": 30.0, "pmra": 2.84424, "pmdec": 0.4736214040129863, "vlos": NaN, "source": "arXiv:2205.05699" }, "CetusIII-missingvlos": { "ra": 31.331, "dec": -4.27, "distance": 251.0, "pmra": 0.4767878711637915, "pmdec": -0.501269156723, "vlos": NaN, "source": "arXiv:2205.05699" }, "ColumbaI": { "ra": 82.85696, "dec": -28.04253, "distance": 183.0, "pmra": 0.1690657485999218, "pmdec": -0.3996494474064953, "vlos": 153.7, "source": "arXiv:2205.05699", "pmcorr": -0.0007032788582435, "distance_e": 10.0, "vlos_e": 4.9, "pmra_e": 0.0722182423741031, "pmdec_e": 0.0790787376433387 }, "ComaBerenices": { "ra": 186.7454, "dec": 23.9069, "distance": 42.0, "pmra": 0.423025, "pmdec": -1.72123, "vlos": 98.1, "source": "arXiv:2205.05699", "dr2": { "ra": 186.746, "dec": 23.908, "distance": 42.27, "pmra": 0.471, "pmdec": -1.716, "vlos": 98.1, "source": "arXiv:1805.00908" }, "pmcorr": -3.384959892091563e-05, "distance_e": 1.5, "vlos_e": 0.9, "pmra_e": 0.0261832769694625, "pmdec_e": 0.024065 }, "CraterII": { "ra": 177.31, "dec": -18.413, "distance": 117.5, "pmra": -0.0716845046347039, "pmdec": -0.111923, "vlos": 87.5, "source": "arXiv:2205.05699", "dr2": { "ra": 177.31, "dec": -18.413, "distance": 112.2, "pmra": -0.184, "pmdec": -0.106, "vlos": 87.5, "source": "arXiv:1805.00908" }, "pmcorr": -0.0015436028012196, "distance_e": 1.1, "vlos_e": 0.4, "pmra_e": 0.0201584643945918, "pmdec_e": 0.013407108503816 }, "Draco": { "ra": 260.0684, "dec": 57.9185, "distance": 75.8, "pmra": 0.0441852296388743, "pmdec": -0.1877441020409549, "vlos": -290.7, "source": "arXiv:2205.05699", "dr2": { "ra": 260.06, "dec": 57.965, "distance": 79.07, "pmra": -0.012, "pmdec": -0.158, "vlos": -291, "source": "arXiv:1805.00908" }, "pmcorr": 0.0001423887719577, "distance_e": 5.4, "vlos_e": 0.75, "pmra_e": 0.0055125692987297, "pmdec_e": 0.0061630141950566 }, "DracoII": { "ra": 238.1983333, "dec": 64.56527778, "distance": 21.5, "pmra": 1.0270557217157783, "pmdec": 0.8868501260687403, "vlos": -342.5, "source": "arXiv:2205.05699", "dr2": { "ra": 238.198, "dec": 64.565, "distance": 21.48, "pmra": 1.242, "pmdec": 0.845, "vlos": -347.6, "source": "arXiv:1805.00908" }, "pmcorr": 0.0051978285385124, "distance_e": 0.4, "vlos_e": 1.15, "pmra_e": 0.0657710456172892, "pmdec_e": 0.0722119400386259 }, "Fornax": { "ra": 39.9583, "dec": -34.4997, "distance": 147.2, "pmra": 0.380848, "pmdec": -0.358727, "vlos": 55.2, "source": "arXiv:2205.05699", "dr2": { "ra": 39.962, "dec": -34.511, "distance": 139.3, "pmra": 0.374, "pmdec": -0.401, "vlos": 55.3, "source": "arXiv:1805.00908" }, "pmcorr": -1.7008883700596204e-06, "distance_e": 8.4, "vlos_e": 0.1, "pmra_e": 0.0012304999999999, "pmdec_e": 0.0018724662203652 }, "EridanusII": { "ra": 56.0925, "dec": -43.5329, "distance": 366.0, "pmra": 0.1251736758478618, "pmdec": 0.0125224117276731, "vlos": 75.6, "source": "arXiv:2205.05699", "dr2": { "ra": 56.08375, "dec": -43.533806, "distance": 363.08, "pmra": 0.159, "pmdec": 0.372, "vlos": 75.6, "source": "arXiv:1805.00908" }, "pmcorr": -0.0997182220213643, "distance_e": 17.0, "vlos_e": 1.3, "pmra_e": 0.1002544845631449, "pmdec_e": 0.1252142040645504 }, "GrusI": { "ra": 344.166, "dec": -50.168, "distance": 127.0, "pmra": 0.0691698866710332, "pmdec": -0.2475444606489662, "vlos": -140.5, "source": "arXiv:2205.05699", "dr2": { "ra": 344.176, "dec": -50.163, "distance": 120, "pmra": -0.261, "pmdec": -0.437, "vlos": -140.5, "source": "arXiv:1805.00908" }, "pmcorr": 0.0023115197504675, "distance_e": 6.0, "vlos_e": 2.0, "pmra_e": 0.0502654456574199, "pmdec_e": 0.0717076283912978 }, "GrusII": { "ra": 331.025, "dec": -46.442, "distance": 55.0, "pmra": 0.383682408987787, "pmdec": -1.4838306788561713, "vlos": -110.0, "source": "arXiv:2205.05699", "pmcorr": 6.228549841157092e-05, "distance_e": 2.0, "vlos_e": 0.5, "pmra_e": 0.0330016530654147, "pmdec_e": 0.0393017155138466 }, "Hercules": { "ra": 247.7722, "dec": 12.7852, "distance": 130.6, "pmra": -0.0353337339906295, "pmdec": -0.339063594839842, "vlos": 45.0, "source": "arXiv:2205.05699", "dr2": { "ra": 247.763, "dec": 12.787, "distance": 134.3, "pmra": -0.297, "pmdec": -0.329, "vlos": 45, "source": "arXiv:1805.00908" }, "pmcorr": 0.0085313644147255, "distance_e": 6.1, "vlos_e": 1.1, "pmra_e": 0.0416390456725038, "pmdec_e": 0.0351475552496657 }, "HorologiumI": { "ra": 43.8813, "dec": -54.116, "distance": 79.0, "pmra": 0.8469509213125719, "pmdec": -0.606973353721251, "vlos": 112.8, "source": "arXiv:2205.05699", "dr2": { "ra": 43.882, "dec": -54.119, "distance": 83.2, "pmra": 0.891, "pmdec": -0.55, "vlos": 112.8, "source": "arXiv:1805.00908" }, "pmcorr": 3.4021961727708753e-05, "distance_e": 4.0, "vlos_e": 2.55, "pmra_e": 0.0342641159508456, "pmdec_e": 0.0350074161609038 }, "HorologiumII-missingvlos": { "ra": 49.1077, "dec": -50.0486, "distance": 78.0, "pmra": 0.9674514230795114, "pmdec": -0.7712314075203168, "vlos": NaN, "source": "arXiv:2205.05699" }, "HydraII": { "ra": 185.4251, "dec": -31.986, "distance": 151.0, "pmra": -0.3938523333252398, "pmdec": 0.000246682399745, "vlos": 303.1, "source": "arXiv:2205.05699", "dr2": { "ra": 185.425, "dec": -31.985, "distance": 150.7, "pmra": -0.416, "pmdec": 0.134, "vlos": 303.1, "source": "arXiv:1805.00908" }, "pmcorr": 0.004082359873383, "distance_e": 8.0, "vlos_e": 1.4, "pmra_e": 0.1396536717708021, "pmdec_e": 0.1038538825157545 }, "HydrusI": { "ra": 37.389, "dec": -79.3089, "distance": 27.6, "pmra": 3.78099, "pmdec": -1.49607718345734, "vlos": 80.4, "source": "arXiv:2205.05699", "dr2": { "ra": 37.389, "dec": -79.309, "distance": 27.54, "pmra": 3.733, "pmdec": -1.605, "vlos": 80.4, "source": "arXiv:1805.00908" }, "pmcorr": 3.1545767731463254e-07, "distance_e": 0.5, "vlos_e": 0.6, "pmra_e": 0.0156399999999998, "pmdec_e": 0.015 }, "LeoI": { "ra": 152.1146, "dec": 12.3059, "distance": 258.2, "pmra": -0.0503729254682685, "pmdec": -0.1198263702584443, "vlos": 282.9, "source": "arXiv:2205.05699", "dr2": { "ra": 152.122, "dec": 12.313, "distance": 269.2, "pmra": -0.086, "pmdec": -0.128, "vlos": 282.5, "source": "arXiv:1805.00908" }, "pmcorr": -0.0127132678738697, "distance_e": 9.5, "vlos_e": 0.5, "pmra_e": 0.0143805406476985, "pmdec_e": 0.0100589086817705 }, "LeoII": { "ra": 168.3627, "dec": 22.1529, "distance": 233.0, "pmra": -0.1089031322592522, "pmdec": -0.1497827449602166, "vlos": 78.5, "source": "arXiv:2205.05699", "dr2": { "ra": 168.37, "dec": 22.152, "distance": 224.9, "pmra": -0.025, "pmdec": -0.173, "vlos": 78, "source": "arXiv:1805.00908" }, "pmcorr": -0.0840375896874679, "distance_e": 15.0, "vlos_e": 0.6, "pmra_e": 0.0280770681140113, "pmdec_e": 0.026023481292112 }, "LeoIV": { "ra": 173.2405, "dec": -0.5453, "distance": 151.4, "pmra": -0.0085598670795916, "pmdec": -0.2788212881633587, "vlos": 132.3, "source": "arXiv:2205.05699", "dr2": { "ra": 173.233, "dec": -0.54, "distance": 154.2, "pmra": -0.59, "pmdec": -0.449, "vlos": 132.3, "source": "arXiv:1805.00908" }, "pmcorr": -0.035986453862924, "distance_e": 4.4, "vlos_e": 1.4, "pmra_e": 0.1519441002442846, "pmdec_e": 0.1139411371795591 }, "LeoV": { "ra": 172.7857, "dec": 2.2194, "distance": 169.0, "pmra": 0.1128552471692806, "pmdec": -0.391169300618627, "vlos": 173.0, "source": "arXiv:2205.05699", "dr2": { "ra": 172.784, "dec": 2.222, "distance": 173.0, "pmra": -0.097, "pmdec": -0.628, "vlos": 173.3, "source": "arXiv:1805.00908" }, "pmcorr": -0.0151952851618191, "distance_e": 4.4, "vlos_e": 0.9, "pmra_e": 0.217241189463712, "pmdec_e": 0.1537568970175684 }, "LMC": { "ra": 78.77, "dec": -69.01, "distance": 50.1, "pmra": 1.85, "pmdec": 0.234, "vlos": 262.2, "source": "arXiv:1804:09381" }, "PegasusIII": { "ra": 336.1074167, "dec": 5.4150472, "distance": 215.0, "pmra": -0.0299216582741334, "pmdec": -0.5797763323877697, "vlos": -222.9, "source": "arXiv:2205.05699", "pmcorr": 0.0736826822209776, "distance_e": 12.0, "vlos_e": 2.6, "pmra_e": 0.2102547999919144, "pmdec_e": 0.2107220618863989 }, "PhoenixI": { "ra": 27.77625, "dec": -44.44472, "distance": 418.8, "pmra": 0.079, "pmdec": -0.049, "vlos": -21.2, "source": "arXiv:1805.00908" }, "PhoenixII": { "ra": 354.996, "dec": -54.4115, "distance": 84.1, "pmra": 0.506528, "pmdec": -1.198659754663964, "vlos": 32.4, "source": "arXiv:2205.05699", "pmcorr": -0.0003781125959571, "distance_e": 8.0, "vlos_e": 3.75, "pmra_e": 0.0471227351125297, "pmdec_e": 0.0575818094398018 }, "PictorI-missingvlos": { "ra": 70.9475, "dec": -50.28305556, "distance": 125.9, "pmra": 0.1529285888833958, "pmdec": 0.0955046531774119, "vlos": NaN, "source": "arXiv:2205.05699" }, "PictorII-missingvlos": { "ra": 101.18, "dec": -59.897, "distance": 45.0, "pmra": 1.09069, "pmdec": 1.1787952857264017, "vlos": NaN, "source": "arXiv:2205.05699" }, "PiscesII": { "ra": 344.6345, "dec": 5.9526, "distance": 183.0, "pmra": 0.681327145296221, "pmdec": -0.6454465339692803, "vlos": -226.5, "source": "arXiv:2205.05699", "dr2": { "ra": 344.634, "dec": 5.955, "distance": 182.8, "pmra": -0.108, "pmdec": -0.586, "vlos": -226.5, "source": "arXiv:1805.00908" }, "pmcorr": -0.0010558387146335, "distance_e": 15.0, "vlos_e": 2.7, "pmra_e": 0.3079890210478, "pmdec_e": 0.211884860072492 }, "ReticulumII": { "ra": 53.9203, "dec": -54.0513, "distance": 31.4, "pmra": 2.37704444977278, "pmdec": -1.37892, "vlos": 64.3, "source": "arXiv:2205.05699", "dr2": { "ra": 53.949, "dec": -54.047, "distance": 31.6, "pmra": 2.398, "pmdec": -1.319, "vlos": 62.8, "source": "arXiv:1805.00908" }, "pmcorr": -5.381636022762169e-06, "distance_e": 1.4, "vlos_e": 1.2, "pmra_e": 0.02366, "pmdec_e": 0.0256549999999999 }, "ReticulumIII": { "ra": 56.36, "dec": -60.45, "distance": 92.0, "pmra": 0.2598007220980685, "pmdec": -0.5023048619674502, "vlos": 274.2, "source": "arXiv:2205.05699", "pmcorr": 0.0023869159360083, "distance_e": 13.0, "vlos_e": 7.45, "pmra_e": 0.1419198018680925, "pmdec_e": 0.2239221184763457 }, "SagittariusII": { "ra": 298.16875, "dec": -22.06805556, "distance": 70.2, "pmra": -0.7689199561451294, "pmdec": -0.9027957814765905, "vlos": -177.2, "source": "arXiv:2205.05699", "pmcorr": -0.0013610848372049, "distance_e": 5.0, "vlos_e": 0.55, "pmra_e": 0.035273118439512, "pmdec_e": 0.0225306688829601 }, "Sgr": { "ra": 283.8313, "dec": -30.5453, "distance": 26.67, "pmra": -2.692, "pmdec": -1.359, "vlos": 140, "source": "arXiv:1804.09381" }, "Sculptor": { "ra": 15.0183, "dec": -33.7186, "distance": 83.9, "pmra": 0.100221, "pmdec": -0.157803, "vlos": 111.4, "source": "arXiv:2205.05699", "dr2": { "ra": 15.039, "dec": -33.709, "distance": 84.72, "pmra": 0.084, "pmdec": -0.133, "vlos": 111.4, "source": "arXiv:1805.00908" }, "pmcorr": -2.506942895089131e-05, "distance_e": 1.5, "vlos_e": 0.1, "pmra_e": 0.0024635313816496, "pmdec_e": 0.0018515 }, "Segue1": { "ra": 151.7504, "dec": 16.0756, "distance": 23.0, "pmra": -2.10226, "pmdec": -3.374565167868929, "vlos": 208.5, "source": "arXiv:2205.05699", "dr2": { "ra": 151.763, "dec": 16.074, "distance": 22.9, "pmra": -1.697, "pmdec": -3.501, "vlos": 208.5, "source": "arXiv:1805.00908" }, "pmcorr": -0.0005328722470825, "distance_e": 2.0, "vlos_e": 0.9, "pmra_e": 0.0507242591657863, "pmdec_e": 0.0449548845362171 }, "Segue2": { "ra": 34.8226, "dec": 20.1624, "distance": 36.6, "pmra": 1.44644, "pmdec": -0.322212950467822, "vlos": -40.2, "source": "arXiv:2205.05699", "dr2": { "ra": 34.817, "dec": 20.175, "distance": 36.3, "pmra": 1.656, "pmdec": 0.135, "vlos": -39.2, "source": "arXiv:1805.00908" }, "pmcorr": 0.0002455702242824, "distance_e": 2.45, "vlos_e": 0.9, "pmra_e": 0.0590566724732428, "pmdec_e": 0.0494405394226662 }, "Sextans": { "ra": 153.2628, "dec": -1.6133, "distance": 92.5, "pmra": -0.4093681743933117, "pmdec": 0.0367510595063521, "vlos": 224.3, "source": "arXiv:2205.05699", "dr2": { "ra": 153.268, "dec": -1.62, "distance": 85.9, "pmra": -0.438, "pmdec": 0.055, "vlos": 224.2, "source": "arXiv:1805.00908" }, "pmcorr": -0.0001369637536294, "distance_e": 2.5, "vlos_e": 0.1, "pmra_e": 0.0085303946384054, "pmdec_e": 0.0088390953846637 }, "SMC": { "ra": 16.26, "dec": -72.42, "distance": 62.8, "pmra": 0.797, "pmdec": -1.22, "vlos": 145.6, "source": "arXiv:1804:09381" }, "TriangulumII": { "ra": 33.3252, "dec": 36.1702, "distance": 28.4, "pmra": 0.5747815907848864, "pmdec": 0.1118444365691554, "vlos": -381.7, "source": "arXiv:2205.05699", "dr2": { "ra": 33.322, "dec": 36.172, "distance": 28.44, "pmra": 0.588, "pmdec": 0.554, "vlos": -381.7, "source": "arXiv:1805.00908" }, "pmcorr": 0.0051017584856756, "distance_e": 1.6, "vlos_e": 1.1, "pmra_e": 0.0601940405253875, "pmdec_e": 0.0679926324304876 }, "TucanaII": { "ra": 342.9796, "dec": -58.5689, "distance": 58.0, "pmra": 0.9114636084464584, "pmdec": -1.27982, "vlos": -129.1, "source": "arXiv:2205.05699", "dr2": { "ra": 343.06, "dec": -58.57, "distance": 57.5, "pmra": 0.91, "pmdec": -1.159, "vlos": -129.1, "source": "arXiv:1805.00908" }, "pmcorr": -3.4640399051363e-05, "distance_e": 3.0, "vlos_e": 3.5, "pmra_e": 0.0253550200804748, "pmdec_e": 0.0291799999999999 }, "TucanaIII": { "ra": 359.1075, "dec": -59.58332, "distance": 22.9, "pmra": -0.0481093254537824, "pmdec": -1.6377335278547986, "vlos": -102.3, "source": "arXiv:2205.05699", "dr2": { "ra": 359.15, "dec": -59.6, "distance": 22.9, "pmra": -0.025, "pmdec": -1.661, "vlos": -102.3, "source": "arXiv:1805.00908" }, "pmcorr": -6.910549504775947e-05, "distance_e": 0.9, "vlos_e": 0.4, "pmra_e": 0.0353169096608266, "pmdec_e": 0.0393326448033661 }, "TucanaIV": { "ra": 0.717, "dec": -60.83, "distance": 47.0, "pmra": 0.5339771507040556, "pmdec": -1.7066854148206296, "vlos": 15.9, "source": "arXiv:2205.05699", "pmcorr": -3.4212209891647727e-05, "distance_e": 4.0, "vlos_e": 1.75, "pmra_e": 0.0516440212734232, "pmdec_e": 0.0549116595223267 }, "TucanaV": { "ra": 354.347, "dec": -63.266, "distance": 55.0, "pmra": -0.1605551328066419, "pmdec": -1.1567287513410291, "vlos": 36.2, "source": "arXiv:2205.05699", "pmcorr": -0.0086533085567915, "distance_e": 5.5, "vlos_e": 2.35, "pmra_e": 0.1312409433256793, "pmdec_e": 0.172337021524203 }, "UrsaMajorI": { "ra": 158.7706, "dec": 51.9479, "distance": 97.3, "pmra": -0.4006419988723494, "pmdec": -0.6127163118156805, "vlos": -55.3, "source": "arXiv:2205.05699", "dr2": { "ra": 158.685, "dec": 51.926, "distance": 97.27, "pmra": -0.683, "pmdec": -0.72, "vlos": -55.3, "source": "arXiv:1805.00908" }, "pmcorr": 0.0007372456000796, "distance_e": 5.85, "vlos_e": 1.4, "pmra_e": 0.036408337896791, "pmdec_e": 0.0410453212383609 }, "UrsaMajorII": { "ra": 132.8726, "dec": 63.1335, "distance": 34.7, "pmra": 1.73092, "pmdec": -1.90586, "vlos": -116.5, "source": "arXiv:2205.05699", "dr2": { "ra": 132.874, "dec": 63.133, "distance": 34.67, "pmra": 1.691, "pmdec": -1.902, "vlos": -116.5, "source": "arXiv:1805.00908" }, "pmcorr": 4.109661490242699e-06, "distance_e": 2.1, "vlos_e": 1.9, "pmra_e": 0.0211449999999999, "pmdec_e": 0.0245108604260261 }, "UrsaMinor": { "ra": 227.242, "dec": 67.2221, "distance": 76.2, "pmra": -0.119964, "pmdec": 0.0709743843037567, "vlos": -247.0, "source": "arXiv:2205.05699", "dr2": { "ra": 227.242, "dec": 67.222, "distance": 75.86, "pmra": -0.184, "pmdec": 0.082, "vlos": -246.9, "source": "arXiv:1805.00908" }, "pmcorr": -5.0277158885832734e-05, "distance_e": 4.2, "vlos_e": 0.4, "pmra_e": 0.0046979388502507, "pmdec_e": 0.0049882259666321 }, "VirgoI-missingvlos": { "ra": 180.038, "dec": -0.681, "distance": 91.0, "pmra": -0.1669269582743862, "pmdec": -0.0080203907748424, "vlos": NaN, "source": "arXiv:2205.05699" }, "Willman1": { "ra": 162.3436, "dec": 51.0501, "distance": 38.0, "pmra": 0.2551105365975281, "pmdec": -1.10975, "vlos": -12.8, "source": "arXiv:2205.05699", "dr2": { "ra": 162.341, "dec": 51.053, "distance": 38.02, "pmra": 0.199, "pmdec": -1.342, "vlos": -12.3, "source": "arXiv:1805.00908" }, "pmcorr": -5.3422082655748224e-05, "distance_e": 7.0, "vlos_e": 1.0, "pmra_e": 0.0819957480091583, "pmdec_e": 0.0928939033184242 }, "NGC104": { "ra": 6.024, "dec": -72.081, "distance": 4.52, "pmra": 5.2516, "pmdec": -2.5515, "vlos": -17.45, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.03, "vlos_e": 0.16, "pmra_e": 0.0214, "pmdec_e": 0.0214, "pmcorr": -0.0023, "dr2": { "ra": 6.024, "dec": -72.081, "distance": 4.5, "pmra": 5.237, "pmdec": -2.524, "vlos": -17.21, "source": "arXiv:1807.09775" } }, "NGC288": { "ra": 13.188, "dec": -26.583, "distance": 8.99, "pmra": 4.1641, "pmdec": -5.7053, "vlos": -44.45, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.09, "vlos_e": 0.13, "pmra_e": 0.0241, "pmdec_e": 0.0243, "pmcorr": 0.0094, "dr2": { "ra": 13.188, "dec": -26.583, "distance": 8.9, "pmra": 4.267, "pmdec": -5.636, "vlos": -44.83, "source": "arXiv:1807.09775" } }, "NGC362": { "ra": 15.809, "dec": -70.849, "distance": 8.83, "pmra": 6.6935, "pmdec": -2.5354, "vlos": 223.12, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.1, "vlos_e": 0.28, "pmra_e": 0.0245, "pmdec_e": 0.0242, "pmcorr": 0.0039, "dr2": { "ra": 15.809, "dec": -70.849, "distance": 8.6, "pmra": 6.73, "pmdec": -2.535, "vlos": 223.26, "source": "arXiv:1807.09775" } }, "Whiting1": { "ra": 30.737, "dec": -3.253, "distance": 30.59, "pmra": -0.2277, "pmdec": -2.0458, "vlos": -130.41, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 1.17, "vlos_e": 1.79, "pmra_e": 0.0653, "pmdec_e": 0.0561, "pmcorr": 0.0333, "dr2": { "ra": 30.738, "dec": -3.253, "distance": 30.1, "pmra": -0.234, "pmdec": -1.782, "vlos": -130.41, "source": "arXiv:1807.09775" } }, "NGC1261": { "ra": 48.068, "dec": -55.216, "distance": 16.4, "pmra": 1.5957, "pmdec": -2.0642, "vlos": 71.34, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.19, "vlos_e": 0.21, "pmra_e": 0.0249, "pmdec_e": 0.0251, "pmcorr": 0.0082, "dr2": { "ra": 48.068, "dec": -55.216, "distance": 16.3, "pmra": 1.632, "pmdec": -2.038, "vlos": 71.36, "source": "arXiv:1807.09775" } }, "Pal1": { "ra": 53.333, "dec": 79.581, "distance": 11.18, "pmra": -0.2515, "pmdec": 0.0075, "vlos": -75.72, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.32, "vlos_e": 0.29, "pmra_e": 0.0342, "pmdec_e": 0.0373, "pmcorr": 0.0768, "dr2": { "ra": 53.334, "dec": 79.581, "distance": 11.1, "pmra": -0.171, "pmdec": 0.07, "vlos": -75.41, "source": "arXiv:1807.09775" } }, "E1": { "ra": 58.76, "dec": -49.615, "distance": 118.91, "pmra": 0.2912, "pmdec": -0.1772, "vlos": 118.0, "distance_e": 3.4, "vlos_e": 14.14, "pmra_e": 0.1071, "pmdec_e": 0.0859, "pmcorr": -0.223, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "dr2": { "ra": 58.76, "dec": -49.615, "distance": 123.3, "pmra": 0.357, "pmdec": -0.424, "vlos": 118.0, "source": "arXiv:1807.09775" } }, "Eridanus": { "ra": 66.185, "dec": -21.187, "distance": 84.68, "pmra": 0.5096, "pmdec": -0.3011, "vlos": -23.15, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 2.89, "vlos_e": 0.73, "pmra_e": 0.0387, "pmdec_e": 0.0408, "pmcorr": -0.0858, "dr2": { "ra": 66.185, "dec": -21.187, "distance": 90.1, "pmra": 0.493, "pmdec": -0.402, "vlos": -23.79, "source": "arXiv:1807.09775" } }, "Pal2": { "ra": 71.525, "dec": 31.381, "distance": 26.17, "pmra": 1.0448, "pmdec": -1.5218, "vlos": -135.97, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 1.28, "vlos_e": 1.55, "pmra_e": 0.0341, "pmdec_e": 0.0312, "pmcorr": 0.044, "dr2": { "ra": 71.525, "dec": 31.381, "distance": 27.2, "pmra": 1.034, "pmdec": -1.557, "vlos": -135.97, "source": "arXiv:1807.09775" } }, "NGC1851": { "ra": 78.528, "dec": -40.047, "distance": 11.95, "pmra": 2.1452, "pmdec": -0.6496, "vlos": 321.4, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.13, "vlos_e": 1.55, "pmra_e": 0.024, "pmdec_e": 0.0242, "pmcorr": -0.0192, "dr2": { "ra": 78.528, "dec": -40.047, "distance": 12.1, "pmra": 2.12, "pmdec": -0.589, "vlos": 320.3, "source": "arXiv:1807.09775" } }, "NGC1904": { "ra": 81.044, "dec": -24.524, "distance": 13.08, "pmra": 2.469, "pmdec": -1.5938, "vlos": 205.76, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.18, "vlos_e": 0.2, "pmra_e": 0.0249, "pmdec_e": 0.0251, "pmcorr": -0.0008, "dr2": { "ra": 81.046, "dec": -24.525, "distance": 12.9, "pmra": 2.467, "pmdec": -1.573, "vlos": 205.84, "source": "arXiv:1807.09775" } }, "NGC2298": { "ra": 102.248, "dec": -36.005, "distance": 9.83, "pmra": 3.3195, "pmdec": -2.1755, "vlos": 147.15, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.17, "vlos_e": 0.57, "pmra_e": 0.0255, "pmdec_e": 0.0256, "pmcorr": 0.005, "dr2": { "ra": 102.248, "dec": -36.005, "distance": 10.8, "pmra": 3.316, "pmdec": -2.186, "vlos": 146.18, "source": "arXiv:1807.09775" } }, "NGC2419": { "ra": 114.535, "dec": 38.882, "distance": 88.47, "pmra": 0.0073, "pmdec": -0.5226, "vlos": -21.1, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 2.4, "vlos_e": 0.31, "pmra_e": 0.0278, "pmdec_e": 0.0264, "pmcorr": 0.0288, "dr2": { "ra": 114.535, "dec": 38.882, "distance": 82.6, "pmra": -0.011, "pmdec": -0.557, "vlos": -20.67, "source": "arXiv:1807.09775" } }, "Pyxis": { "ra": 136.991, "dec": -37.221, "distance": 36.53, "pmra": 1.0304, "pmdec": 0.1378, "vlos": 40.46, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.66, "vlos_e": 0.21, "pmra_e": 0.0318, "pmdec_e": 0.0347, "pmcorr": 0.0316, "dr2": { "ra": 136.991, "dec": -37.221, "distance": 39.4, "pmra": 1.078, "pmdec": 0.212, "vlos": 40.46, "source": "arXiv:1807.09775" } }, "NGC2808": { "ra": 138.013, "dec": -64.863, "distance": 10.06, "pmra": 0.9944, "pmdec": 0.2734, "vlos": 103.57, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.11, "vlos_e": 0.27, "pmra_e": 0.0238, "pmdec_e": 0.024, "pmcorr": -0.0145, "dr2": { "ra": 138.013, "dec": -64.864, "distance": 9.6, "pmra": 1.005, "pmdec": 0.274, "vlos": 103.9, "source": "arXiv:1807.09775" } }, "E3": { "ra": 140.238, "dec": -77.282, "distance": 7.88, "pmra": -2.7274, "pmdec": 7.0827, "vlos": 11.71, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.25, "vlos_e": 0.34, "pmra_e": 0.0272, "pmdec_e": 0.0271, "pmcorr": 0.0012, "dr2": { "ra": 140.238, "dec": -77.282, "distance": 8.1, "pmra": -2.695, "pmdec": 7.115, "vlos": 4.93, "source": "arXiv:1807.09775" } }, "Pal3": { "ra": 151.383, "dec": 0.072, "distance": 94.84, "pmra": 0.0861, "pmdec": -0.1484, "vlos": 94.04, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 3.23, "vlos_e": 0.8, "pmra_e": 0.0602, "pmdec_e": 0.0709, "pmcorr": -0.4099, "dr2": { "ra": 151.383, "dec": 0.072, "distance": 92.5, "pmra": 0.055, "pmdec": -0.085, "vlos": 94.04, "source": "arXiv:1807.09775" } }, "NGC3201": { "ra": 154.403, "dec": -46.412, "distance": 4.74, "pmra": 8.3476, "pmdec": -1.9575, "vlos": 493.65, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.04, "vlos_e": 0.21, "pmra_e": 0.022, "pmdec_e": 0.0221, "pmcorr": -0.0049, "dr2": { "ra": 154.403, "dec": -46.412, "distance": 4.9, "pmra": 8.324, "pmdec": -1.991, "vlos": 494.34, "source": "arXiv:1807.09775" } }, "Pal4": { "ra": 172.32, "dec": 28.974, "distance": 101.39, "pmra": -0.1877, "pmdec": -0.4761, "vlos": 72.4, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 2.57, "vlos_e": 0.24, "pmra_e": 0.0419, "pmdec_e": 0.0409, "pmcorr": -0.1436, "dr2": { "ra": 172.32, "dec": 28.974, "distance": 108.7, "pmra": -0.135, "pmdec": -0.518, "vlos": 72.4, "source": "arXiv:1807.09775" } }, "Crater": { "ra": 174.067, "dec": -10.877, "distance": 147.23, "pmra": -0.0585, "pmdec": -0.1157, "vlos": 148.1, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 4.27, "vlos_e": 0.65, "pmra_e": 0.1254, "pmdec_e": 0.1161, "pmcorr": -0.2253, "dr2": { "ra": 174.067, "dec": -10.877, "distance": 145.0, "pmra": 0.001, "pmdec": -0.13, "vlos": 148.3, "source": "arXiv:1807.09775" } }, "NGC4147": { "ra": 182.526, "dec": 18.543, "distance": 18.54, "pmra": -1.707, "pmdec": -2.0896, "vlos": 179.35, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.21, "vlos_e": 0.31, "pmra_e": 0.0273, "pmdec_e": 0.0274, "pmcorr": -0.0228, "dr2": { "ra": 182.526, "dec": 18.543, "distance": 19.3, "pmra": -1.705, "pmdec": -2.114, "vlos": 179.52, "source": "arXiv:1807.09775" } }, "NGC4372": { "ra": 186.439, "dec": -72.659, "distance": 5.71, "pmra": -6.409, "pmdec": 3.2975, "vlos": 75.59, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.21, "vlos_e": 0.3, "pmra_e": 0.0241, "pmdec_e": 0.0238, "pmcorr": 0.0037, "dr2": { "ra": 186.439, "dec": -72.659, "distance": 5.8, "pmra": -6.378, "pmdec": 3.358, "vlos": 75.59, "source": "arXiv:1807.09775" } }, "Rup106": { "ra": 189.667, "dec": -51.15, "distance": 20.71, "pmra": -1.2535, "pmdec": 0.4009, "vlos": -38.36, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.36, "vlos_e": 0.26, "pmra_e": 0.0264, "pmdec_e": 0.0261, "pmcorr": 0.0286, "dr2": { "ra": 189.667, "dec": -51.15, "distance": 21.2, "pmra": -1.263, "pmdec": 0.399, "vlos": -38.42, "source": "arXiv:1807.09775" } }, "NGC4590": { "ra": 189.867, "dec": -26.744, "distance": 10.4, "pmra": -2.739, "pmdec": 1.7786, "vlos": -93.11, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.1, "vlos_e": 0.18, "pmra_e": 0.024, "pmdec_e": 0.0239, "pmcorr": 0.0008, "dr2": { "ra": 189.867, "dec": -26.744, "distance": 10.3, "pmra": -2.752, "pmdec": 1.762, "vlos": -92.99, "source": "arXiv:1807.09775" } }, "NGC4833": { "ra": 194.891, "dec": -70.877, "distance": 6.48, "pmra": -8.3774, "pmdec": -0.9631, "vlos": 201.99, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.08, "vlos_e": 0.4, "pmra_e": 0.0249, "pmdec_e": 0.0245, "pmcorr": -0.0146, "dr2": { "ra": 194.891, "dec": -70.876, "distance": 6.6, "pmra": -8.361, "pmdec": -0.949, "vlos": 201.99, "source": "arXiv:1807.09775" } }, "NGC5024": { "ra": 198.23, "dec": 18.168, "distance": 18.5, "pmra": -0.1334, "pmdec": -1.3306, "vlos": -63.37, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.18, "vlos_e": 0.25, "pmra_e": 0.0241, "pmdec_e": 0.024, "pmcorr": 0.0008, "dr2": { "ra": 198.23, "dec": 18.168, "distance": 17.9, "pmra": -0.148, "pmdec": -1.355, "vlos": -62.85, "source": "arXiv:1807.09775" } }, "NGC5053": { "ra": 199.113, "dec": 17.7, "distance": 17.54, "pmra": -0.3293, "pmdec": -1.2129, "vlos": 42.82, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.23, "vlos_e": 0.25, "pmra_e": 0.0253, "pmdec_e": 0.0253, "pmcorr": -0.0219, "dr2": { "ra": 199.113, "dec": 17.7, "distance": 17.4, "pmra": -0.366, "pmdec": -1.248, "vlos": 42.77, "source": "arXiv:1807.09775" } }, "NGC5139": { "ra": 201.697, "dec": -47.48, "distance": 5.43, "pmra": -3.2499, "pmdec": -6.7463, "vlos": 232.78, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.05, "vlos_e": 0.21, "pmra_e": 0.0217, "pmdec_e": 0.0216, "pmcorr": 0.0138, "dr2": { "ra": 201.697, "dec": -47.48, "distance": 5.2, "pmra": -3.234, "pmdec": -6.719, "vlos": 234.28, "source": "arXiv:1807.09775" } }, "NGC5272": { "ra": 205.548, "dec": 28.377, "distance": 10.18, "pmra": -0.1519, "pmdec": -2.6696, "vlos": -147.2, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.08, "vlos_e": 0.27, "pmra_e": 0.0228, "pmdec_e": 0.0222, "pmcorr": 0.0035, "dr2": { "ra": 205.548, "dec": 28.377, "distance": 10.2, "pmra": -0.142, "pmdec": -2.647, "vlos": -147.28, "source": "arXiv:1807.09775" } }, "NGC5286": { "ra": 206.612, "dec": -51.374, "distance": 11.1, "pmra": 0.1984, "pmdec": -0.1533, "vlos": 62.38, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.14, "vlos_e": 0.4, "pmra_e": 0.0255, "pmdec_e": 0.0253, "pmcorr": -0.0021, "dr2": { "ra": 206.612, "dec": -51.374, "distance": 11.7, "pmra": 0.207, "pmdec": -0.111, "vlos": 62.38, "source": "arXiv:1807.09775" } }, "NGC5466": { "ra": 211.364, "dec": 28.534, "distance": 16.12, "pmra": -5.3423, "pmdec": -0.8223, "vlos": 106.82, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.16, "vlos_e": 0.2, "pmra_e": 0.0245, "pmdec_e": 0.0242, "pmcorr": 0.014, "dr2": { "ra": 211.364, "dec": 28.534, "distance": 16.0, "pmra": -5.412, "pmdec": -0.8, "vlos": 106.93, "source": "arXiv:1807.09775" } }, "NGC5634": { "ra": 217.405, "dec": -5.976, "distance": 25.96, "pmra": -1.6918, "pmdec": -1.4781, "vlos": -16.07, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.62, "vlos_e": 0.6, "pmra_e": 0.0269, "pmdec_e": 0.0263, "pmcorr": -0.0078, "dr2": { "ra": 217.405, "dec": -5.976, "distance": 25.2, "pmra": -1.724, "pmdec": -1.507, "vlos": -16.07, "source": "arXiv:1807.09775" } }, "NGC5694": { "ra": 219.901, "dec": -26.539, "distance": 34.84, "pmra": -0.4636, "pmdec": -1.1046, "vlos": -139.55, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.74, "vlos_e": 0.49, "pmra_e": 0.0293, "pmdec_e": 0.029, "pmcorr": -0.061, "dr2": { "ra": 219.901, "dec": -26.539, "distance": 35.0, "pmra": -0.486, "pmdec": -1.071, "vlos": -139.55, "source": "arXiv:1807.09775" } }, "IC4499": { "ra": 225.077, "dec": -82.214, "distance": 18.89, "pmra": 0.4658, "pmdec": -0.489, "vlos": 38.41, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.25, "vlos_e": 0.31, "pmra_e": 0.0253, "pmdec_e": 0.0253, "pmcorr": 0.0108, "dr2": { "ra": 225.077, "dec": -82.214, "distance": 18.8, "pmra": 0.491, "pmdec": -0.485, "vlos": 38.41, "source": "arXiv:1807.09775" } }, "NGC5824": { "ra": 225.994, "dec": -33.068, "distance": 31.71, "pmra": -1.1892, "pmdec": -2.2338, "vlos": -25.24, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.6, "vlos_e": 0.52, "pmra_e": 0.026, "pmdec_e": 0.0257, "pmcorr": -0.0179, "dr2": { "ra": 225.994, "dec": -33.068, "distance": 32.1, "pmra": -1.17, "pmdec": -2.226, "vlos": -25.24, "source": "arXiv:1807.09775" } }, "Pal5": { "ra": 229.019, "dec": -0.121, "distance": 21.94, "pmra": -2.7298, "pmdec": -2.654, "vlos": -58.61, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.51, "vlos_e": 0.15, "pmra_e": 0.0278, "pmdec_e": 0.0272, "pmcorr": 0.0027, "dr2": { "ra": 229.022, "dec": -0.112, "distance": 23.2, "pmra": -2.736, "pmdec": -2.646, "vlos": -58.6, "source": "arXiv:1807.09775" } }, "NGC5897": { "ra": 229.352, "dec": -21.01, "distance": 12.55, "pmra": -5.4217, "pmdec": -3.3926, "vlos": 101.31, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.24, "vlos_e": 0.22, "pmra_e": 0.0252, "pmdec_e": 0.0251, "pmcorr": -0.0057, "dr2": { "ra": 229.352, "dec": -21.01, "distance": 12.5, "pmra": -5.427, "pmdec": -3.438, "vlos": 101.31, "source": "arXiv:1807.09775" } }, "NGC5904": { "ra": 229.638, "dec": 2.081, "distance": 7.48, "pmra": 4.0856, "pmdec": -9.8696, "vlos": 53.5, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.06, "vlos_e": 0.25, "pmra_e": 0.023, "pmdec_e": 0.0231, "pmcorr": -0.0051, "dr2": { "ra": 229.638, "dec": 2.081, "distance": 7.5, "pmra": 4.078, "pmdec": -9.854, "vlos": 53.7, "source": "arXiv:1807.09775" } }, "NGC5927": { "ra": 232.003, "dec": -50.673, "distance": 8.27, "pmra": -5.0564, "pmdec": -3.2166, "vlos": -104.09, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.11, "vlos_e": 0.28, "pmra_e": 0.0246, "pmdec_e": 0.0249, "pmcorr": -0.0076, "dr2": { "ra": 232.003, "dec": -50.673, "distance": 7.7, "pmra": -5.049, "pmdec": -3.231, "vlos": -104.07, "source": "arXiv:1807.09775" } }, "NGC5946": { "ra": 233.869, "dec": -50.66, "distance": 9.64, "pmra": -5.331, "pmdec": -1.6572, "vlos": 137.6, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.51, "vlos_e": 0.94, "pmra_e": 0.0275, "pmdec_e": 0.0271, "pmcorr": -0.0134, "dr2": { "ra": 233.869, "dec": -50.66, "distance": 10.6, "pmra": -5.331, "pmdec": -1.614, "vlos": 137.41, "source": "arXiv:1807.09775" } }, "BH176": { "ra": 234.781, "dec": -50.05, "distance": 18.9, "pmra": -3.9889, "pmdec": -3.0568, "vlos": -6.0, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": NaN, "vlos_e": 14.0, "pmra_e": 0.0293, "pmdec_e": 0.0287, "pmcorr": -0.02, "dr2": { "ra": 234.781, "dec": -50.053, "distance": 18.9, "pmra": -4.002, "pmdec": -3.064, "vlos": -6.0, "source": "arXiv:1807.09775" } }, "NGC5986": { "ra": 236.512, "dec": -37.786, "distance": 10.54, "pmra": -4.1925, "pmdec": -4.568, "vlos": 101.18, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.13, "vlos_e": 0.43, "pmra_e": 0.0259, "pmdec_e": 0.0256, "pmcorr": -0.013, "dr2": { "ra": 236.512, "dec": -37.786, "distance": 10.4, "pmra": -4.186, "pmdec": -4.604, "vlos": 101.18, "source": "arXiv:1807.09775" } }, "FSR1716": { "ra": 242.625, "dec": -53.749, "distance": 7.43, "pmra": -4.3537, "pmdec": -8.8318, "vlos": -30.7, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.27, "vlos_e": 0.98, "pmra_e": 0.0334, "pmdec_e": 0.0312, "pmcorr": -0.0012, "dr2": { "ra": 242.625, "dec": -53.748, "distance": 9.5, "pmra": -4.527, "pmdec": -8.639, "vlos": -33.14, "source": "arXiv:1807.09775" } }, "Pal14": { "ra": 242.752, "dec": 14.958, "distance": 73.58, "pmra": -0.4626, "pmdec": -0.4132, "vlos": 72.3, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 1.63, "vlos_e": 0.14, "pmra_e": 0.0384, "pmdec_e": 0.0378, "pmcorr": 0.1208, "dr2": { "ra": 242.752, "dec": 14.958, "distance": 76.5, "pmra": -0.504, "pmdec": -0.461, "vlos": 72.3, "source": "arXiv:1807.09775" } }, "BH184": { "ra": 242.765, "dec": -55.318, "distance": 7.9, "pmra": -3.851, "pmdec": -7.0495, "vlos": 17.86, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.16, "vlos_e": 0.83, "pmra_e": 0.0269, "pmdec_e": 0.0271, "pmcorr": 0.0161, "dr2": { "ra": 242.765, "dec": -55.318, "distance": 8.0, "pmra": -3.844, "pmdec": -7.039, "vlos": 17.86, "source": "arXiv:1807.09775" } }, "NGC6093": { "ra": 244.26, "dec": -22.976, "distance": 10.34, "pmra": -2.9339, "pmdec": -5.5784, "vlos": 10.93, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.12, "vlos_e": 0.39, "pmra_e": 0.0267, "pmdec_e": 0.0261, "pmcorr": -0.0149, "dr2": { "ra": 244.26, "dec": -22.976, "distance": 10.0, "pmra": -2.931, "pmdec": -5.578, "vlos": 10.93, "source": "arXiv:1807.09775" } }, "NGC6121": { "ra": 245.897, "dec": -26.526, "distance": 1.85, "pmra": -12.514, "pmdec": -19.022, "vlos": 71.21, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.02, "vlos_e": 0.15, "pmra_e": 0.0231, "pmdec_e": 0.0227, "pmcorr": -0.0184, "dr2": { "ra": 245.897, "dec": -26.526, "distance": 2.2, "pmra": -12.49, "pmdec": -19.001, "vlos": 71.05, "source": "arXiv:1807.09775" } }, "NGC6101": { "ra": 246.45, "dec": -72.202, "distance": 14.45, "pmra": 1.7558, "pmdec": -0.2577, "vlos": 366.33, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.19, "vlos_e": 0.32, "pmra_e": 0.0241, "pmdec_e": 0.0245, "pmcorr": 0.0009, "dr2": { "ra": 246.451, "dec": -72.202, "distance": 15.4, "pmra": 1.757, "pmdec": -0.223, "vlos": 366.33, "source": "arXiv:1807.09775" } }, "NGC6144": { "ra": 246.808, "dec": -26.023, "distance": 8.15, "pmra": -1.7435, "pmdec": -2.6068, "vlos": 194.79, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.13, "vlos_e": 0.58, "pmra_e": 0.0263, "pmdec_e": 0.0258, "pmcorr": -0.0022, "dr2": { "ra": 246.808, "dec": -26.023, "distance": 8.9, "pmra": -1.772, "pmdec": -2.626, "vlos": 195.74, "source": "arXiv:1807.09775" } }, "NGC6139": { "ra": 246.918, "dec": -38.849, "distance": 10.04, "pmra": -6.0814, "pmdec": -2.7112, "vlos": 24.41, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.45, "vlos_e": 0.95, "pmra_e": 0.0267, "pmdec_e": 0.0262, "pmcorr": 0.006, "dr2": { "ra": 246.918, "dec": -38.849, "distance": 10.1, "pmra": -6.184, "pmdec": -2.648, "vlos": 24.41, "source": "arXiv:1807.09775" } }, "Terzan3": { "ra": 247.167, "dec": -35.353, "distance": 7.64, "pmra": -5.5765, "pmdec": -1.7597, "vlos": -135.76, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.31, "vlos_e": 0.57, "pmra_e": 0.0267, "pmdec_e": 0.0261, "pmcorr": 0.0084, "dr2": { "ra": 247.167, "dec": -35.353, "distance": 8.2, "pmra": -5.602, "pmdec": -1.69, "vlos": -135.76, "source": "arXiv:1807.09775" } }, "NGC6171": { "ra": 248.133, "dec": -13.054, "distance": 5.63, "pmra": -1.9392, "pmdec": -5.9789, "vlos": -34.71, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.08, "vlos_e": 0.18, "pmra_e": 0.0253, "pmdec_e": 0.025, "pmcorr": 0.0047, "dr2": { "ra": 248.133, "dec": -13.054, "distance": 6.4, "pmra": -1.924, "pmdec": -5.968, "vlos": -34.68, "source": "arXiv:1807.09775" } }, "ESO45211": { "ra": 249.854, "dec": -28.399, "distance": 7.39, "pmra": -1.4234, "pmdec": -6.472, "vlos": 16.37, "distance_e": 0.2, "vlos_e": 0.44, "pmra_e": 0.0314, "pmdec_e": 0.0303, "pmcorr": -0.0562, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "dr2": { "ra": 249.856, "dec": -28.399, "distance": 8.3, "pmra": -1.54, "pmdec": -6.418, "vlos": 16.27, "source": "arXiv:1807.09775" } }, "NGC6205": { "ra": 250.422, "dec": 36.46, "distance": 7.42, "pmra": -3.1493, "pmdec": -2.5735, "vlos": -244.9, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.08, "vlos_e": 0.3, "pmra_e": 0.0227, "pmdec_e": 0.0231, "pmcorr": -0.0092, "dr2": { "ra": 250.422, "dec": 36.46, "distance": 7.1, "pmra": -3.164, "pmdec": -2.588, "vlos": -244.49, "source": "arXiv:1807.09775" } }, "NGC6229": { "ra": 251.745, "dec": 47.528, "distance": 30.11, "pmra": -1.1706, "pmdec": -0.4665, "vlos": -137.89, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.47, "vlos_e": 0.71, "pmra_e": 0.0263, "pmdec_e": 0.0267, "pmcorr": 0.0141, "dr2": { "ra": 251.745, "dec": 47.528, "distance": 30.5, "pmra": -1.192, "pmdec": -0.44, "vlos": -138.64, "source": "arXiv:1807.09775" } }, "NGC6218": { "ra": 251.809, "dec": -1.949, "distance": 5.11, "pmra": -0.1914, "pmdec": -6.8019, "vlos": -41.67, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.05, "vlos_e": 0.14, "pmra_e": 0.0241, "pmdec_e": 0.024, "pmcorr": 0.0124, "dr2": { "ra": 251.809, "dec": -1.949, "distance": 4.8, "pmra": -0.141, "pmdec": -6.802, "vlos": -41.35, "source": "arXiv:1807.09775" } }, "FSR1735": { "ra": 253.044, "dec": -47.058, "distance": 9.08, "pmra": -4.4395, "pmdec": -1.5341, "vlos": -69.85, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.53, "vlos_e": 4.88, "pmra_e": 0.0537, "pmdec_e": 0.0476, "pmcorr": -0.0471, "dr2": { "ra": 253.044, "dec": -47.058, "distance": 10.8, "pmra": -5.015, "pmdec": -0.141, "vlos": 5.0, "source": "arXiv:1807.09775" } }, "NGC6235": { "ra": 253.355, "dec": -22.177, "distance": 11.94, "pmra": -3.9306, "pmdec": -7.5872, "vlos": 126.68, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.38, "vlos_e": 0.33, "pmra_e": 0.0272, "pmdec_e": 0.0266, "pmcorr": -0.0113, "dr2": { "ra": 253.355, "dec": -22.177, "distance": 11.5, "pmra": -3.973, "pmdec": -7.624, "vlos": 126.68, "source": "arXiv:1807.09775" } }, "NGC6254": { "ra": 254.288, "dec": -4.1, "distance": 5.07, "pmra": -4.7577, "pmdec": -6.5968, "vlos": 74.21, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.06, "vlos_e": 0.23, "pmra_e": 0.0235, "pmdec_e": 0.0236, "pmcorr": -0.0179, "dr2": { "ra": 254.288, "dec": -4.1, "distance": 4.4, "pmra": -4.759, "pmdec": -6.554, "vlos": 74.02, "source": "arXiv:1807.09775" } }, "NGC6256": { "ra": 254.886, "dec": -37.121, "distance": 7.24, "pmra": -3.7149, "pmdec": -1.6375, "vlos": -99.75, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.29, "vlos_e": 0.66, "pmra_e": 0.0312, "pmdec_e": 0.0304, "pmcorr": -0.0006, "dr2": { "ra": 254.886, "dec": -37.121, "distance": 10.3, "pmra": -3.664, "pmdec": -1.493, "vlos": -101.37, "source": "arXiv:1807.09775" } }, "Pal15": { "ra": 254.963, "dec": -0.539, "distance": 44.1, "pmra": -0.5924, "pmdec": -0.901, "vlos": 72.27, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 1.14, "vlos_e": 1.74, "pmra_e": 0.0369, "pmdec_e": 0.0343, "pmcorr": 0.0244, "dr2": { "ra": 254.963, "dec": -0.539, "distance": 45.1, "pmra": -0.58, "pmdec": -0.861, "vlos": 72.27, "source": "arXiv:1807.09775" } }, "NGC6266": { "ra": 255.303, "dec": -30.114, "distance": 6.41, "pmra": -4.9778, "pmdec": -2.9467, "vlos": -73.98, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.1, "vlos_e": 0.67, "pmra_e": 0.0265, "pmdec_e": 0.0263, "pmcorr": 0.0119, "dr2": { "ra": 255.303, "dec": -30.114, "distance": 6.8, "pmra": -5.041, "pmdec": -2.952, "vlos": -73.49, "source": "arXiv:1807.09775" } }, "NGC6273": { "ra": 255.657, "dec": -26.268, "distance": 8.34, "pmra": -3.2487, "pmdec": 1.6604, "vlos": 145.54, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.16, "vlos_e": 0.59, "pmra_e": 0.0256, "pmdec_e": 0.0251, "pmcorr": 0.0023, "dr2": { "ra": 255.657, "dec": -26.268, "distance": 8.8, "pmra": -3.232, "pmdec": 1.669, "vlos": 145.54, "source": "arXiv:1807.09775" } }, "NGC6284": { "ra": 256.119, "dec": -24.765, "distance": 14.21, "pmra": -3.1997, "pmdec": -2.0021, "vlos": 28.62, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.42, "vlos_e": 0.73, "pmra_e": 0.0286, "pmdec_e": 0.0278, "pmcorr": -0.0018, "dr2": { "ra": 256.119, "dec": -24.765, "distance": 15.3, "pmra": -3.196, "pmdec": -2.008, "vlos": 28.26, "source": "arXiv:1807.09775" } }, "NGC6287": { "ra": 256.288, "dec": -22.708, "distance": 7.93, "pmra": -5.0103, "pmdec": -1.883, "vlos": -294.74, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.37, "vlos_e": 1.65, "pmra_e": 0.0292, "pmdec_e": 0.0277, "pmcorr": 0.0067, "dr2": { "ra": 256.288, "dec": -22.708, "distance": 9.4, "pmra": -4.977, "pmdec": -1.882, "vlos": -294.74, "source": "arXiv:1807.09775" } }, "NGC6293": { "ra": 257.543, "dec": -26.582, "distance": 9.19, "pmra": 0.8703, "pmdec": -4.326, "vlos": -143.66, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.28, "vlos_e": 0.39, "pmra_e": 0.028, "pmdec_e": 0.0275, "pmcorr": -0.0059, "dr2": { "ra": 257.543, "dec": -26.582, "distance": 9.5, "pmra": 0.89, "pmdec": -4.338, "vlos": -143.66, "source": "arXiv:1807.09775" } }, "NGC6304": { "ra": 258.634, "dec": -29.462, "distance": 6.15, "pmra": -4.07, "pmdec": -1.0878, "vlos": -108.62, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.15, "vlos_e": 0.39, "pmra_e": 0.0285, "pmdec_e": 0.0283, "pmcorr": 0.0052, "dr2": { "ra": 258.634, "dec": -29.462, "distance": 5.9, "pmra": -4.051, "pmdec": -1.073, "vlos": -108.62, "source": "arXiv:1807.09775" } }, "NGC6316": { "ra": 259.155, "dec": -28.14, "distance": 11.15, "pmra": -4.9691, "pmdec": -4.5916, "vlos": 99.65, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.39, "vlos_e": 0.84, "pmra_e": 0.0313, "pmdec_e": 0.0301, "pmcorr": 0.0148, "dr2": { "ra": 259.155, "dec": -28.14, "distance": 10.4, "pmra": -4.945, "pmdec": -4.608, "vlos": 99.81, "source": "arXiv:1807.09775" } }, "NGC6341": { "ra": 259.281, "dec": 43.136, "distance": 8.5, "pmra": -4.9349, "pmdec": -0.6251, "vlos": -120.55, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.07, "vlos_e": 0.27, "pmra_e": 0.0243, "pmdec_e": 0.0239, "pmcorr": 0.0, "dr2": { "ra": 259.281, "dec": 43.136, "distance": 8.3, "pmra": -4.925, "pmdec": -0.536, "vlos": -120.48, "source": "arXiv:1807.09775" } }, "NGC6325": { "ra": 259.497, "dec": -23.766, "distance": 7.53, "pmra": -8.2893, "pmdec": -9.0004, "vlos": 29.54, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.32, "vlos_e": 0.58, "pmra_e": 0.0302, "pmdec_e": 0.0291, "pmcorr": 0.0204, "dr2": { "ra": 259.497, "dec": -23.766, "distance": 7.8, "pmra": -8.426, "pmdec": -9.011, "vlos": 29.54, "source": "arXiv:1807.09775" } }, "NGC6333": { "ra": 259.797, "dec": -18.516, "distance": 8.3, "pmra": -2.1801, "pmdec": -3.2218, "vlos": 310.75, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.14, "vlos_e": 2.12, "pmra_e": 0.0262, "pmdec_e": 0.0259, "pmcorr": 0.0172, "dr2": { "ra": 259.797, "dec": -18.516, "distance": 7.9, "pmra": -2.228, "pmdec": -3.214, "vlos": 310.75, "source": "arXiv:1807.09775" } }, "NGC6342": { "ra": 260.292, "dec": -19.587, "distance": 8.01, "pmra": -2.9026, "pmdec": -7.1156, "vlos": 115.75, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.23, "vlos_e": 0.9, "pmra_e": 0.0272, "pmdec_e": 0.0265, "pmcorr": -0.0037, "dr2": { "ra": 260.292, "dec": -19.587, "distance": 8.5, "pmra": -2.932, "pmdec": -7.101, "vlos": 116.56, "source": "arXiv:1807.09775" } }, "NGC6356": { "ra": 260.896, "dec": -17.813, "distance": 15.66, "pmra": -3.7501, "pmdec": -3.3923, "vlos": 48.18, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.92, "vlos_e": 1.82, "pmra_e": 0.0264, "pmdec_e": 0.026, "pmcorr": -0.0007, "dr2": { "ra": 260.896, "dec": -17.813, "distance": 15.1, "pmra": -3.814, "pmdec": -3.381, "vlos": 38.93, "source": "arXiv:1807.09775" } }, "NGC6355": { "ra": 260.994, "dec": -26.353, "distance": 8.65, "pmra": -4.7384, "pmdec": -0.5721, "vlos": -195.85, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.22, "vlos_e": 0.55, "pmra_e": 0.0312, "pmdec_e": 0.0301, "pmcorr": 0.021, "dr2": { "ra": 260.994, "dec": -26.353, "distance": 9.2, "pmra": -4.657, "pmdec": -0.522, "vlos": -194.13, "source": "arXiv:1807.09775" } }, "NGC6352": { "ra": 261.371, "dec": -48.422, "distance": 5.54, "pmra": -2.1575, "pmdec": -4.4465, "vlos": -125.63, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.07, "vlos_e": 1.01, "pmra_e": 0.0251, "pmdec_e": 0.0247, "pmcorr": -0.0095, "dr2": { "ra": 261.371, "dec": -48.422, "distance": 5.6, "pmra": -2.172, "pmdec": -4.398, "vlos": -125.63, "source": "arXiv:1807.09775" } }, "IC1257": { "ra": 261.785, "dec": -7.093, "distance": 26.59, "pmra": -1.0069, "pmdec": -1.4916, "vlos": -137.97, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 1.43, "vlos_e": 2.04, "pmra_e": 0.04, "pmdec_e": 0.0321, "pmcorr": 0.0998, "dr2": { "ra": 261.785, "dec": -7.093, "distance": 25.0, "pmra": -0.928, "pmdec": -1.407, "vlos": -137.97, "source": "arXiv:1807.09775" } }, "Terzan2": { "ra": 261.888, "dec": -30.802, "distance": 7.75, "pmra": -2.1699, "pmdec": -6.2626, "vlos": 134.56, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.33, "vlos_e": 0.96, "pmra_e": 0.041, "pmdec_e": 0.0382, "pmcorr": 0.0495, "dr2": { "ra": 261.888, "dec": -30.802, "distance": 7.5, "pmra": -2.237, "pmdec": -6.21, "vlos": 128.96, "source": "arXiv:1807.09775" } }, "NGC6366": { "ra": 261.934, "dec": -5.08, "distance": 3.44, "pmra": -0.3324, "pmdec": -5.1595, "vlos": -120.65, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.05, "vlos_e": 0.19, "pmra_e": 0.0246, "pmdec_e": 0.0244, "pmcorr": 0.0057, "dr2": { "ra": 261.934, "dec": -5.08, "distance": 3.5, "pmra": -0.363, "pmdec": -5.115, "vlos": -120.65, "source": "arXiv:1807.09775" } }, "Terzan4": { "ra": 262.663, "dec": -31.596, "distance": 7.59, "pmra": -5.4616, "pmdec": -3.711, "vlos": -48.96, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.31, "vlos_e": 1.57, "pmra_e": 0.06, "pmdec_e": 0.0481, "pmcorr": 0.1929, "dr2": { "ra": 262.663, "dec": -31.596, "distance": 7.2, "pmra": -5.386, "pmdec": -3.361, "vlos": -39.93, "source": "arXiv:1807.09775" } }, "BH229": { "ra": 262.772, "dec": -29.982, "distance": 7.0, "pmra": 2.5228, "pmdec": -10.093, "vlos": 39.76, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.14, "vlos_e": 1.22, "pmra_e": 0.0391, "pmdec_e": 0.0374, "pmcorr": 0.0381, "dr2": { "ra": 262.772, "dec": -29.982, "distance": 8.2, "pmra": 2.462, "pmdec": -10.142, "vlos": 40.61, "source": "arXiv:1807.09775" } }, "NGC6362": { "ra": 262.979, "dec": -67.048, "distance": 7.65, "pmra": -5.5058, "pmdec": -4.7629, "vlos": -14.58, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.07, "vlos_e": 0.18, "pmra_e": 0.0238, "pmdec_e": 0.024, "pmcorr": -0.0033, "dr2": { "ra": 262.979, "dec": -67.048, "distance": 7.6, "pmra": -5.51, "pmdec": -4.75, "vlos": -14.58, "source": "arXiv:1807.09775" } }, "NGC6380": { "ra": 263.617, "dec": -39.069, "distance": 9.61, "pmra": -2.1832, "pmdec": -3.2326, "vlos": -1.48, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.3, "vlos_e": 0.73, "pmra_e": 0.031, "pmdec_e": 0.0302, "pmcorr": 0.0115, "dr2": { "ra": 263.617, "dec": -39.069, "distance": 10.9, "pmra": -2.142, "pmdec": -3.107, "vlos": -6.54, "source": "arXiv:1807.09775" } }, "Terzan1": { "ra": 263.946, "dec": -30.481, "distance": 5.67, "pmra": -2.8062, "pmdec": -4.8613, "vlos": 56.75, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.17, "vlos_e": 1.61, "pmra_e": 0.0553, "pmdec_e": 0.0548, "pmcorr": 0.0114, "dr2": { "ra": 263.949, "dec": -30.481, "distance": 6.7, "pmra": -2.967, "pmdec": -4.811, "vlos": 57.55, "source": "arXiv:1807.09775" } }, "Ton2": { "ra": 264.044, "dec": -38.553, "distance": 6.99, "pmra": -5.9041, "pmdec": -0.7551, "vlos": -184.72, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.34, "vlos_e": 1.12, "pmra_e": 0.0308, "pmdec_e": 0.0294, "pmcorr": 0.0335, "dr2": { "ra": 264.044, "dec": -38.553, "distance": 8.2, "pmra": -5.912, "pmdec": -0.548, "vlos": -184.72, "source": "arXiv:1807.09775" } }, "NGC6388": { "ra": 264.072, "dec": -44.736, "distance": 11.17, "pmra": -1.3159, "pmdec": -2.7088, "vlos": 83.11, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.16, "vlos_e": 0.45, "pmra_e": 0.0262, "pmdec_e": 0.0259, "pmcorr": -0.0089, "dr2": { "ra": 264.072, "dec": -44.736, "distance": 9.9, "pmra": -1.331, "pmdec": -2.672, "vlos": 82.85, "source": "arXiv:1807.09775" } }, "NGC6402": { "ra": 264.4, "dec": -3.246, "distance": 9.14, "pmra": -3.5899, "pmdec": -5.0588, "vlos": -60.71, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.25, "vlos_e": 0.45, "pmra_e": 0.0254, "pmdec_e": 0.0255, "pmcorr": 0.0132, "dr2": { "ra": 264.4, "dec": -3.246, "distance": 9.3, "pmra": -3.64, "pmdec": -5.035, "vlos": -60.71, "source": "arXiv:1807.09775" } }, "NGC6401": { "ra": 264.652, "dec": -23.91, "distance": 8.06, "pmra": -2.7477, "pmdec": 1.4441, "vlos": -105.44, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.24, "vlos_e": 2.5, "pmra_e": 0.0353, "pmdec_e": 0.0342, "pmcorr": 0.014, "dr2": { "ra": 264.652, "dec": -23.91, "distance": 10.6, "pmra": -2.849, "pmdec": 1.476, "vlos": -99.26, "source": "arXiv:1807.09775" } }, "NGC6397": { "ra": 265.175, "dec": -53.674, "distance": 2.48, "pmra": 3.2603, "pmdec": -17.664, "vlos": 18.51, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.02, "vlos_e": 0.08, "pmra_e": 0.0227, "pmdec_e": 0.0225, "pmcorr": -0.0022, "dr2": { "ra": 265.175, "dec": -53.674, "distance": 2.3, "pmra": 3.285, "pmdec": -17.621, "vlos": 18.39, "source": "arXiv:1807.09775" } }, "Pal6": { "ra": 265.926, "dec": -26.223, "distance": 7.05, "pmra": -9.2219, "pmdec": -5.3466, "vlos": 177.0, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.45, "vlos_e": 1.35, "pmra_e": 0.0384, "pmdec_e": 0.0361, "pmcorr": 0.0284, "dr2": { "ra": 265.926, "dec": -26.223, "distance": 5.8, "pmra": -9.256, "pmdec": -5.33, "vlos": 176.28, "source": "arXiv:1807.09775" } }, "NGC6426": { "ra": 266.228, "dec": 3.17, "distance": 20.71, "pmra": -1.828, "pmdec": -2.9992, "vlos": -210.51, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.35, "vlos_e": 0.51, "pmra_e": 0.0265, "pmdec_e": 0.0265, "pmcorr": 0.0223, "dr2": { "ra": 266.228, "dec": 3.17, "distance": 20.6, "pmra": -1.862, "pmdec": -2.994, "vlos": -210.51, "source": "arXiv:1807.09775" } }, "Djorg1": { "ra": 266.868, "dec": -33.066, "distance": 9.88, "pmra": -4.6934, "pmdec": -8.4678, "vlos": -359.18, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.65, "vlos_e": 1.64, "pmra_e": 0.0462, "pmdec_e": 0.0411, "pmcorr": 0.0187, "dr2": { "ra": 266.868, "dec": -33.066, "distance": 9.3, "pmra": -5.158, "pmdec": -8.323, "vlos": -359.81, "source": "arXiv:1807.09775" } }, "Terzan5": { "ra": 267.02, "dec": -24.779, "distance": 6.62, "pmra": -1.9891, "pmdec": -5.2433, "vlos": -82.57, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.15, "vlos_e": 0.73, "pmra_e": 0.0681, "pmdec_e": 0.0661, "pmcorr": 0.0551, "dr2": { "ra": 267.02, "dec": -24.779, "distance": 6.9, "pmra": -1.56, "pmdec": -4.724, "vlos": -81.4, "source": "arXiv:1807.09775" } }, "NGC6440": { "ra": 267.22, "dec": -20.36, "distance": 8.25, "pmra": -1.187, "pmdec": -4.0196, "vlos": -69.39, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.24, "vlos_e": 0.93, "pmra_e": 0.0365, "pmdec_e": 0.0351, "pmcorr": 0.008, "dr2": { "ra": 267.22, "dec": -20.36, "distance": 8.5, "pmra": -1.07, "pmdec": -3.828, "vlos": -69.39, "source": "arXiv:1807.09775" } }, "NGC6441": { "ra": 267.554, "dec": -37.051, "distance": 12.73, "pmra": -2.5514, "pmdec": -5.348, "vlos": 18.47, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.16, "vlos_e": 0.56, "pmra_e": 0.0283, "pmdec_e": 0.0276, "pmcorr": 0.0293, "dr2": { "ra": 267.554, "dec": -37.051, "distance": 11.6, "pmra": -2.568, "pmdec": -5.322, "vlos": 17.27, "source": "arXiv:1807.09775" } }, "Terzan6": { "ra": 267.693, "dec": -31.275, "distance": 7.27, "pmra": -4.979, "pmdec": -7.4313, "vlos": 136.45, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.35, "vlos_e": 1.5, "pmra_e": 0.0482, "pmdec_e": 0.0392, "pmcorr": 0.137, "dr2": { "ra": 267.693, "dec": -31.275, "distance": 6.8, "pmra": -5.634, "pmdec": -7.042, "vlos": 137.15, "source": "arXiv:1807.09775" } }, "NGC6453": { "ra": 267.715, "dec": -34.599, "distance": 10.07, "pmra": 0.2026, "pmdec": -5.9336, "vlos": -99.23, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.22, "vlos_e": 1.24, "pmra_e": 0.0356, "pmdec_e": 0.0368, "pmcorr": -0.039, "dr2": { "ra": 267.715, "dec": -34.599, "distance": 11.6, "pmra": 0.165, "pmdec": -5.895, "vlos": -91.16, "source": "arXiv:1807.09775" } }, "NGC6496": { "ra": 269.765, "dec": -44.266, "distance": 9.64, "pmra": -3.06, "pmdec": -9.2711, "vlos": -134.72, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.15, "vlos_e": 0.26, "pmra_e": 0.0267, "pmdec_e": 0.0261, "pmcorr": 0.0077, "dr2": { "ra": 269.765, "dec": -44.266, "distance": 11.3, "pmra": -3.037, "pmdec": -9.239, "vlos": -134.72, "source": "arXiv:1807.09775" } }, "Terzan9": { "ra": 270.412, "dec": -26.84, "distance": 5.77, "pmra": -2.1211, "pmdec": -7.7633, "vlos": 68.49, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.34, "vlos_e": 0.56, "pmra_e": 0.0517, "pmdec_e": 0.0485, "pmcorr": 0.033, "dr2": { "ra": 270.412, "dec": -26.84, "distance": 7.1, "pmra": -2.197, "pmdec": -7.451, "vlos": 29.31, "source": "arXiv:1807.09775" } }, "Djorg2": { "ra": 270.455, "dec": -27.826, "distance": 8.76, "pmra": 0.6619, "pmdec": -2.9828, "vlos": -149.75, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.18, "vlos_e": 1.1, "pmra_e": 0.0415, "pmdec_e": 0.0371, "pmcorr": 0.0031, "dr2": { "ra": 270.455, "dec": -27.826, "distance": 6.3, "pmra": 0.515, "pmdec": -3.052, "vlos": -148.05, "source": "arXiv:1807.09775" } }, "NGC6517": { "ra": 270.461, "dec": -8.959, "distance": 9.23, "pmra": -1.5508, "pmdec": -4.4698, "vlos": -35.06, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.56, "vlos_e": 1.65, "pmra_e": 0.0288, "pmdec_e": 0.0282, "pmcorr": 0.0305, "dr2": { "ra": 270.461, "dec": -8.959, "distance": 10.6, "pmra": -1.498, "pmdec": -4.221, "vlos": -37.07, "source": "arXiv:1807.09775" } }, "Terzan10": { "ra": 270.742, "dec": -26.073, "distance": 10.21, "pmra": -6.8266, "pmdec": -2.5885, "vlos": 211.37, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.4, "vlos_e": 2.27, "pmra_e": 0.0594, "pmdec_e": 0.0496, "pmcorr": 0.0135, "dr2": { "ra": 270.742, "dec": -26.067, "distance": 10.4, "pmra": -6.912, "pmdec": -2.409, "vlos": 208.0, "source": "arXiv:1807.09775" } }, "NGC6522": { "ra": 270.892, "dec": -30.034, "distance": 7.29, "pmra": 2.5655, "pmdec": -6.4384, "vlos": -15.23, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.21, "vlos_e": 0.49, "pmra_e": 0.039, "pmdec_e": 0.0358, "pmcorr": -0.0208, "dr2": { "ra": 270.892, "dec": -30.034, "distance": 7.7, "pmra": 2.618, "pmdec": -6.431, "vlos": -13.9, "source": "arXiv:1807.09775" } }, "NGC6535": { "ra": 270.96, "dec": -0.298, "distance": 6.36, "pmra": -4.214, "pmdec": -2.9387, "vlos": -214.85, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.12, "vlos_e": 0.46, "pmra_e": 0.0268, "pmdec_e": 0.0265, "pmcorr": 0.0105, "dr2": { "ra": 270.96, "dec": -0.298, "distance": 6.8, "pmra": -4.249, "pmdec": -2.9, "vlos": -214.85, "source": "arXiv:1807.09775" } }, "NGC6528": { "ra": 271.207, "dec": -30.056, "distance": 7.83, "pmra": -2.1571, "pmdec": -5.649, "vlos": 211.86, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.24, "vlos_e": 0.43, "pmra_e": 0.0432, "pmdec_e": 0.0392, "pmcorr": -0.0319, "dr2": { "ra": 271.207, "dec": -30.056, "distance": 7.9, "pmra": -2.327, "pmdec": -5.527, "vlos": 210.31, "source": "arXiv:1807.09775" } }, "NGC6539": { "ra": 271.207, "dec": -7.586, "distance": 8.16, "pmra": -6.8958, "pmdec": -3.5374, "vlos": 35.19, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.39, "vlos_e": 0.5, "pmra_e": 0.0264, "pmdec_e": 0.026, "pmcorr": 0.0067, "dr2": { "ra": 271.207, "dec": -7.586, "distance": 7.8, "pmra": -6.865, "pmdec": -3.477, "vlos": 35.69, "source": "arXiv:1807.09775" } }, "NGC6540": { "ra": 271.536, "dec": -27.765, "distance": 5.91, "pmra": -3.7019, "pmdec": -2.7911, "vlos": -16.5, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.27, "vlos_e": 0.78, "pmra_e": 0.032, "pmdec_e": 0.0318, "pmcorr": -0.0231, "dr2": { "ra": 271.536, "dec": -27.765, "distance": 5.3, "pmra": -3.76, "pmdec": -2.799, "vlos": -17.98, "source": "arXiv:1807.09775" } }, "NGC6544": { "ra": 271.836, "dec": -24.997, "distance": 2.58, "pmra": -2.3041, "pmdec": -18.604, "vlos": -38.46, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.06, "vlos_e": 0.67, "pmra_e": 0.0308, "pmdec_e": 0.0305, "pmcorr": 0.0068, "dr2": { "ra": 271.836, "dec": -24.997, "distance": 3.0, "pmra": -2.349, "pmdec": -18.557, "vlos": -38.12, "source": "arXiv:1807.09775" } }, "NGC6541": { "ra": 272.01, "dec": -43.715, "distance": 7.61, "pmra": 0.2866, "pmdec": -8.8472, "vlos": -163.97, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.1, "vlos_e": 0.46, "pmra_e": 0.0249, "pmdec_e": 0.0245, "pmcorr": 0.0046, "dr2": { "ra": 272.01, "dec": -43.715, "distance": 7.5, "pmra": 0.349, "pmdec": -8.843, "vlos": -163.97, "source": "arXiv:1807.09775" } }, "ESO28006": { "ra": 272.275, "dec": -46.423, "distance": 20.95, "pmra": -0.6878, "pmdec": -2.7775, "vlos": 93.2, "distance_e": 0.65, "vlos_e": 0.34, "pmra_e": 0.0392, "pmdec_e": 0.0332, "pmcorr": 0.0916, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "dr2": { "ra": 272.275, "dec": -46.423, "distance": 21.4, "pmra": -0.552, "pmdec": -2.724, "vlos": 93.2, "source": "arXiv:1807.09775" } }, "NGC6553": { "ra": 272.323, "dec": -25.909, "distance": 5.33, "pmra": 0.3445, "pmdec": -0.4538, "vlos": -0.27, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.13, "vlos_e": 0.34, "pmra_e": 0.0301, "pmdec_e": 0.0295, "pmcorr": 0.011, "dr2": { "ra": 272.323, "dec": -25.909, "distance": 6.0, "pmra": 0.246, "pmdec": -0.409, "vlos": 0.72, "source": "arXiv:1807.09775" } }, "NGC6558": { "ra": 272.573, "dec": -31.764, "distance": 7.47, "pmra": -1.7199, "pmdec": -4.1438, "vlos": -195.12, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.29, "vlos_e": 0.73, "pmra_e": 0.0361, "pmdec_e": 0.0337, "pmcorr": 0.0406, "dr2": { "ra": 272.573, "dec": -31.764, "distance": 7.4, "pmra": -1.81, "pmdec": -4.133, "vlos": -195.7, "source": "arXiv:1807.09775" } }, "Pal7": { "ra": 272.684, "dec": -7.208, "distance": 4.55, "pmra": -2.5531, "pmdec": -4.5676, "vlos": 155.06, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.25, "vlos_e": 0.69, "pmra_e": 0.0258, "pmdec_e": 0.0257, "pmcorr": 0.0073, "dr2": { "ra": 272.684, "dec": -7.208, "distance": 5.4, "pmra": -2.577, "pmdec": -4.374, "vlos": 155.06, "source": "arXiv:1807.09775" } }, "Terzan12": { "ra": 273.066, "dec": -22.742, "distance": 5.17, "pmra": -6.2223, "pmdec": -3.0524, "vlos": 95.61, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.38, "vlos_e": 1.21, "pmra_e": 0.037, "pmdec_e": 0.0336, "pmcorr": 0.1281, "dr2": { "ra": 273.066, "dec": -22.742, "distance": 4.8, "pmra": -6.151, "pmdec": -2.679, "vlos": 94.77, "source": "arXiv:1807.09775" } }, "NGC6569": { "ra": 273.412, "dec": -31.827, "distance": 10.53, "pmra": -4.1247, "pmdec": -7.3543, "vlos": -49.83, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.26, "vlos_e": 0.5, "pmra_e": 0.0283, "pmdec_e": 0.0279, "pmcorr": 0.0059, "dr2": { "ra": 273.412, "dec": -31.827, "distance": 10.9, "pmra": -4.109, "pmdec": -7.267, "vlos": -49.83, "source": "arXiv:1807.09775" } }, "BH261": { "ra": 273.527, "dec": -28.635, "distance": 6.12, "pmra": 3.5656, "pmdec": -3.5897, "vlos": -45.0, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.26, "vlos_e": 15.0, "pmra_e": 0.0434, "pmdec_e": 0.0375, "pmcorr": 0.096, "dr2": { "ra": 273.527, "dec": -28.635, "distance": 6.5, "pmra": 3.59, "pmdec": -3.573, "vlos": -29.38, "source": "arXiv:1807.09775" } }, "NGC6584": { "ra": 274.657, "dec": -52.216, "distance": 13.61, "pmra": -0.0898, "pmdec": -7.2021, "vlos": 260.64, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.17, "vlos_e": 1.58, "pmra_e": 0.0258, "pmdec_e": 0.0254, "pmcorr": -0.005, "dr2": { "ra": 274.657, "dec": -52.216, "distance": 13.5, "pmra": -0.053, "pmdec": -7.185, "vlos": 260.64, "source": "arXiv:1807.09775" } }, "NGC6624": { "ra": 275.919, "dec": -30.361, "distance": 8.02, "pmra": 0.1245, "pmdec": -6.9357, "vlos": 54.79, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.11, "vlos_e": 0.4, "pmra_e": 0.0292, "pmdec_e": 0.0285, "pmcorr": -0.0012, "dr2": { "ra": 275.919, "dec": -30.361, "distance": 7.9, "pmra": 0.099, "pmdec": -6.904, "vlos": 54.26, "source": "arXiv:1807.09775" } }, "NGC6626": { "ra": 276.137, "dec": -24.87, "distance": 5.37, "pmra": -0.278, "pmdec": -8.9222, "vlos": 11.11, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.1, "vlos_e": 0.6, "pmra_e": 0.0283, "pmdec_e": 0.028, "pmcorr": -0.0095, "dr2": { "ra": 276.137, "dec": -24.87, "distance": 5.5, "pmra": -0.301, "pmdec": -8.913, "vlos": 11.11, "source": "arXiv:1807.09775" } }, "NGC6638": { "ra": 277.734, "dec": -25.497, "distance": 9.78, "pmra": -2.5185, "pmdec": -4.0756, "vlos": 8.63, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.34, "vlos_e": 2.0, "pmra_e": 0.0287, "pmdec_e": 0.0287, "pmcorr": 0.0126, "dr2": { "ra": 277.734, "dec": -25.497, "distance": 9.4, "pmra": -2.55, "pmdec": -4.075, "vlos": 8.63, "source": "arXiv:1807.09775" } }, "NGC6637": { "ra": 277.846, "dec": -32.348, "distance": 8.9, "pmra": -5.0339, "pmdec": -5.8322, "vlos": 47.48, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.1, "vlos_e": 1.0, "pmra_e": 0.0276, "pmdec_e": 0.0275, "pmcorr": 0.0378, "dr2": { "ra": 277.846, "dec": -32.348, "distance": 8.8, "pmra": -5.113, "pmdec": -5.813, "vlos": 46.63, "source": "arXiv:1807.09775" } }, "NGC6642": { "ra": 277.975, "dec": -23.475, "distance": 8.05, "pmra": -0.173, "pmdec": -3.8922, "vlos": -60.61, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.2, "vlos_e": 1.35, "pmra_e": 0.0303, "pmdec_e": 0.03, "pmcorr": 0.0029, "dr2": { "ra": 277.975, "dec": -23.475, "distance": 8.1, "pmra": -0.189, "pmdec": -3.898, "vlos": -33.23, "source": "arXiv:1807.09775" } }, "NGC6652": { "ra": 278.94, "dec": -32.991, "distance": 9.46, "pmra": -5.4844, "pmdec": -4.2739, "vlos": -95.37, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.14, "vlos_e": 0.86, "pmra_e": 0.0274, "pmdec_e": 0.0268, "pmcorr": 0.0096, "dr2": { "ra": 278.94, "dec": -32.991, "distance": 10.0, "pmra": -5.506, "pmdec": -4.204, "vlos": -99.04, "source": "arXiv:1807.09775" } }, "NGC6656": { "ra": 279.1, "dec": -23.905, "distance": 3.3, "pmra": 9.8506, "pmdec": -5.6175, "vlos": -148.72, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.04, "vlos_e": 0.78, "pmra_e": 0.0234, "pmdec_e": 0.0232, "pmcorr": -0.0029, "dr2": { "ra": 279.1, "dec": -23.905, "distance": 3.2, "pmra": 9.833, "pmdec": -5.557, "vlos": -147.76, "source": "arXiv:1807.09775" } }, "Pal8": { "ra": 280.375, "dec": -19.826, "distance": 11.32, "pmra": -1.9871, "pmdec": -5.6935, "vlos": -31.54, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.63, "vlos_e": 0.21, "pmra_e": 0.027, "pmdec_e": 0.0267, "pmcorr": 0.0097, "dr2": { "ra": 280.375, "dec": -19.826, "distance": 12.8, "pmra": -2.031, "pmdec": -5.634, "vlos": -41.14, "source": "arXiv:1807.09775" } }, "NGC6681": { "ra": 280.803, "dec": -32.292, "distance": 9.36, "pmra": 1.4307, "pmdec": -4.7438, "vlos": 216.62, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.11, "vlos_e": 0.84, "pmra_e": 0.0267, "pmdec_e": 0.0261, "pmcorr": 0.0055, "dr2": { "ra": 280.803, "dec": -32.292, "distance": 9.0, "pmra": 1.458, "pmdec": -4.688, "vlos": 216.62, "source": "arXiv:1807.09775" } }, "NGC6712": { "ra": 283.268, "dec": -8.706, "distance": 7.38, "pmra": 3.3629, "pmdec": -4.4362, "vlos": -107.45, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.24, "vlos_e": 0.29, "pmra_e": 0.027, "pmdec_e": 0.0266, "pmcorr": 0.0165, "dr2": { "ra": 283.268, "dec": -8.706, "distance": 6.9, "pmra": 3.341, "pmdec": -4.384, "vlos": -107.45, "source": "arXiv:1807.09775" } }, "NGC6715": { "ra": 283.764, "dec": -30.48, "distance": 26.28, "pmra": -2.6792, "pmdec": -1.3865, "vlos": 143.13, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.33, "vlos_e": 0.43, "pmra_e": 0.0253, "pmdec_e": 0.0254, "pmcorr": 0.0025, "dr2": { "ra": 283.764, "dec": -30.48, "distance": 26.5, "pmra": -2.711, "pmdec": -1.355, "vlos": 143.06, "source": "arXiv:1807.09775" } }, "NGC6717": { "ra": 283.775, "dec": -22.701, "distance": 7.52, "pmra": -3.1253, "pmdec": -5.0082, "vlos": 30.25, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.13, "vlos_e": 0.9, "pmra_e": 0.0273, "pmdec_e": 0.0271, "pmcorr": 0.0314, "dr2": { "ra": 283.775, "dec": -22.701, "distance": 7.1, "pmra": -3.106, "pmdec": -4.951, "vlos": 32.45, "source": "arXiv:1807.09775" } }, "NGC6723": { "ra": 284.888, "dec": -36.632, "distance": 8.27, "pmra": 1.0285, "pmdec": -2.4185, "vlos": -94.39, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.1, "vlos_e": 0.26, "pmra_e": 0.0255, "pmdec_e": 0.0254, "pmcorr": 0.0045, "dr2": { "ra": 284.888, "dec": -36.632, "distance": 8.7, "pmra": 1.033, "pmdec": -2.445, "vlos": -94.18, "source": "arXiv:1807.09775" } }, "NGC6749": { "ra": 286.314, "dec": 1.901, "distance": 7.59, "pmra": -2.8293, "pmdec": -6.0057, "vlos": -58.44, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.21, "vlos_e": 0.96, "pmra_e": 0.0276, "pmdec_e": 0.0274, "pmcorr": 0.0073, "dr2": { "ra": 286.314, "dec": 1.901, "distance": 7.9, "pmra": -2.865, "pmdec": -5.987, "vlos": -58.44, "source": "arXiv:1807.09775" } }, "NGC6752": { "ra": 287.717, "dec": -59.985, "distance": 4.12, "pmra": -3.1611, "pmdec": -4.0272, "vlos": -26.01, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.04, "vlos_e": 0.12, "pmra_e": 0.0225, "pmdec_e": 0.0224, "pmcorr": -0.0129, "dr2": { "ra": 287.717, "dec": -59.985, "distance": 4.0, "pmra": -3.17, "pmdec": -4.043, "vlos": -26.28, "source": "arXiv:1807.09775" } }, "NGC6760": { "ra": 287.8, "dec": 1.03, "distance": 8.41, "pmra": -1.107, "pmdec": -3.615, "vlos": -2.37, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.43, "vlos_e": 1.27, "pmra_e": 0.026, "pmdec_e": 0.0262, "pmcorr": 0.0153, "dr2": { "ra": 287.8, "dec": 1.03, "distance": 7.4, "pmra": -1.129, "pmdec": -3.561, "vlos": -0.42, "source": "arXiv:1807.09775" } }, "NGC6779": { "ra": 289.148, "dec": 30.183, "distance": 10.43, "pmra": -2.0179, "pmdec": 1.6176, "vlos": -136.97, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.14, "vlos_e": 0.45, "pmra_e": 0.0251, "pmdec_e": 0.0252, "pmcorr": 0.0094, "dr2": { "ra": 289.148, "dec": 30.183, "distance": 9.4, "pmra": -2.02, "pmdec": 1.644, "vlos": -136.97, "source": "arXiv:1807.09775" } }, "Terzan7": { "ra": 289.433, "dec": -34.658, "distance": 24.28, "pmra": -3.0018, "pmdec": -1.651, "vlos": 159.85, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.49, "vlos_e": 0.14, "pmra_e": 0.0286, "pmdec_e": 0.0287, "pmcorr": 0.0488, "dr2": { "ra": 289.433, "dec": -34.658, "distance": 22.8, "pmra": -2.999, "pmdec": -1.586, "vlos": 159.45, "source": "arXiv:1807.09775" } }, "Pal10": { "ra": 289.509, "dec": 18.572, "distance": 8.94, "pmra": -4.3225, "pmdec": -7.1728, "vlos": -31.7, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 1.18, "vlos_e": 0.23, "pmra_e": 0.0285, "pmdec_e": 0.0286, "pmcorr": 0.0151, "dr2": { "ra": 289.509, "dec": 18.572, "distance": 5.9, "pmra": -4.25, "pmdec": -6.924, "vlos": -31.7, "source": "arXiv:1807.09775" } }, "Arp2": { "ra": 292.184, "dec": -30.356, "distance": 28.73, "pmra": -2.3307, "pmdec": -1.4753, "vlos": 122.64, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.34, "vlos_e": 0.29, "pmra_e": 0.0313, "pmdec_e": 0.0294, "pmcorr": 0.0528, "dr2": { "ra": 292.184, "dec": -30.356, "distance": 28.6, "pmra": -2.381, "pmdec": -1.51, "vlos": 123.01, "source": "arXiv:1807.09775" } }, "NGC6809": { "ra": 294.999, "dec": -30.965, "distance": 5.35, "pmra": -3.4316, "pmdec": -9.311, "vlos": 174.7, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.05, "vlos_e": 0.17, "pmra_e": 0.0237, "pmdec_e": 0.0237, "pmcorr": 0.0093, "dr2": { "ra": 294.999, "dec": -30.965, "distance": 5.4, "pmra": -3.42, "pmdec": -9.269, "vlos": 174.4, "source": "arXiv:1807.09775" } }, "Terzan8": { "ra": 295.435, "dec": -33.999, "distance": 27.54, "pmra": -2.4958, "pmdec": -1.5807, "vlos": 148.43, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.42, "vlos_e": 0.17, "pmra_e": 0.027, "pmdec_e": 0.026, "pmcorr": -0.0216, "dr2": { "ra": 295.435, "dec": -33.999, "distance": 26.3, "pmra": -2.472, "pmdec": -1.556, "vlos": 148.53, "source": "arXiv:1807.09775" } }, "Pal11": { "ra": 296.31, "dec": -8.007, "distance": 14.02, "pmra": -1.7661, "pmdec": -4.9709, "vlos": -67.64, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.51, "vlos_e": 0.76, "pmra_e": 0.03, "pmdec_e": 0.0284, "pmcorr": 0.042, "dr2": { "ra": 296.31, "dec": -8.007, "distance": 13.4, "pmra": -1.821, "pmdec": -4.934, "vlos": -67.64, "source": "arXiv:1807.09775" } }, "NGC6838": { "ra": 298.444, "dec": 18.779, "distance": 4.0, "pmra": -3.4161, "pmdec": -2.6559, "vlos": -22.72, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.05, "vlos_e": 0.2, "pmra_e": 0.0245, "pmdec_e": 0.0244, "pmcorr": 0.002, "dr2": { "ra": 298.444, "dec": 18.779, "distance": 4.0, "pmra": -3.415, "pmdec": -2.614, "vlos": -22.27, "source": "arXiv:1807.09775" } }, "NGC6864": { "ra": 301.52, "dec": -21.921, "distance": 20.52, "pmra": -0.5975, "pmdec": -2.8099, "vlos": -189.08, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.45, "vlos_e": 1.12, "pmra_e": 0.0262, "pmdec_e": 0.0258, "pmcorr": 0.0155, "dr2": { "ra": 301.52, "dec": -21.921, "distance": 20.9, "pmra": -0.559, "pmdec": -2.798, "vlos": -189.08, "source": "arXiv:1807.09775" } }, "NGC6934": { "ra": 308.547, "dec": 7.404, "distance": 15.72, "pmra": -2.6545, "pmdec": -4.6887, "vlos": -406.22, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.17, "vlos_e": 0.73, "pmra_e": 0.026, "pmdec_e": 0.0259, "pmcorr": 0.0074, "dr2": { "ra": 308.547, "dec": 7.404, "distance": 15.6, "pmra": -2.636, "pmdec": -4.667, "vlos": -406.22, "source": "arXiv:1807.09775" } }, "NGC6981": { "ra": 313.365, "dec": -12.537, "distance": 16.66, "pmra": -1.2736, "pmdec": -3.3608, "vlos": -331.39, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.18, "vlos_e": 1.47, "pmra_e": 0.0262, "pmdec_e": 0.0257, "pmcorr": 0.0049, "dr2": { "ra": 313.365, "dec": -12.537, "distance": 17.0, "pmra": -1.233, "pmdec": -3.29, "vlos": -331.39, "source": "arXiv:1807.09775" } }, "NGC7006": { "ra": 315.372, "dec": 16.187, "distance": 39.32, "pmra": -0.1275, "pmdec": -0.6329, "vlos": -383.47, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.56, "vlos_e": 0.73, "pmra_e": 0.0274, "pmdec_e": 0.027, "pmcorr": 0.0017, "dr2": { "ra": 315.372, "dec": 16.187, "distance": 41.2, "pmra": -0.102, "pmdec": -0.569, "vlos": -383.47, "source": "arXiv:1807.09775" } }, "NGC7078": { "ra": 322.493, "dec": 12.167, "distance": 10.71, "pmra": -0.6588, "pmdec": -3.8025, "vlos": -106.84, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.1, "vlos_e": 0.3, "pmra_e": 0.0239, "pmdec_e": 0.0236, "pmcorr": 0.0138, "dr2": { "ra": 322.493, "dec": 12.167, "distance": 10.4, "pmra": -0.643, "pmdec": -3.763, "vlos": -106.76, "source": "arXiv:1807.09775" } }, "NGC7089": { "ra": 323.363, "dec": -0.823, "distance": 11.69, "pmra": 3.4346, "pmdec": -2.1588, "vlos": -3.78, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.11, "vlos_e": 0.3, "pmra_e": 0.0247, "pmdec_e": 0.0244, "pmcorr": 0.0136, "dr2": { "ra": 323.363, "dec": -0.823, "distance": 11.5, "pmra": 3.518, "pmdec": -2.145, "vlos": -3.72, "source": "arXiv:1807.09775" } }, "NGC7099": { "ra": 325.092, "dec": -23.18, "distance": 8.46, "pmra": -0.7374, "pmdec": -7.2987, "vlos": -185.19, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.09, "vlos_e": 0.17, "pmra_e": 0.0246, "pmdec_e": 0.0244, "pmcorr": 0.0125, "dr2": { "ra": 325.092, "dec": -23.18, "distance": 8.1, "pmra": -0.694, "pmdec": -7.271, "vlos": -185.19, "source": "arXiv:1807.09775" } }, "Pal12": { "ra": 326.662, "dec": -21.253, "distance": 18.49, "pmra": -3.2197, "pmdec": -3.3335, "vlos": 27.91, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.3, "vlos_e": 0.28, "pmra_e": 0.0291, "pmdec_e": 0.028, "pmcorr": 0.0767, "dr2": { "ra": 326.662, "dec": -21.253, "distance": 19.0, "pmra": -3.249, "pmdec": -3.303, "vlos": 27.91, "source": "arXiv:1807.09775" } }, "Pal13": { "ra": 346.685, "dec": 12.772, "distance": 23.48, "pmra": 1.7475, "pmdec": 0.1038, "vlos": 25.3, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.4, "vlos_e": 0.22, "pmra_e": 0.0487, "pmdec_e": 0.0473, "pmcorr": -0.0163, "dr2": { "ra": 346.685, "dec": 12.772, "distance": 26.0, "pmra": 1.615, "pmdec": 0.142, "vlos": 25.87, "source": "arXiv:1807.09775" } }, "NGC7492": { "ra": 347.111, "dec": -15.611, "distance": 24.39, "pmra": 0.7558, "pmdec": -2.32, "vlos": -176.7, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526", "distance_e": 0.57, "vlos_e": 0.27, "pmra_e": 0.0279, "pmdec_e": 0.0276, "pmcorr": 0.0286, "dr2": { "ra": 347.111, "dec": -15.611, "distance": 26.3, "pmra": 0.799, "pmdec": -2.273, "vlos": -176.7, "source": "arXiv:1807.09775" } }, "Ko2-missingvlos": { "ra": 119.571, "dec": 26.255, "distance": 34.7, "pmra": -0.53, "pmdec": -0.0491, "vlos": NaN, "distance_e": NaN, "vlos_e": NaN, "pmra_e": 0.1573, "pmdec_e": 0.1367, "pmcorr": -0.1271, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "ESO93-8": { "ra": 169.925, "dec": -65.22, "distance": 13.7, "pmra": -4.0679, "pmdec": 1.4002, "vlos": 86.0, "distance_e": NaN, "vlos_e": 10.0, "pmra_e": 0.0329, "pmdec_e": 0.0338, "pmcorr": -0.009, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "Bliss1-missingvlos": { "ra": 177.511, "dec": -41.772, "distance": 23.7, "pmra": -2.3402, "pmdec": 0.1378, "vlos": NaN, "distance_e": NaN, "vlos_e": NaN, "pmra_e": 0.0417, "pmdec_e": 0.0382, "pmcorr": -0.1603, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "Ko1-missingvlos": { "ra": 179.827, "dec": 12.26, "distance": 48.3, "pmra": -1.5133, "pmdec": -0.8135, "vlos": NaN, "distance_e": NaN, "vlos_e": NaN, "pmra_e": 0.1349, "pmdec_e": 0.1054, "pmcorr": -0.215, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "BH140": { "ra": 193.473, "dec": -67.177, "distance": 4.81, "pmra": -14.848, "pmdec": 1.2241, "vlos": 90.3, "distance_e": 0.25, "vlos_e": 0.35, "pmra_e": 0.0244, "pmdec_e": 0.0243, "pmcorr": 0.0018, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "Kim3-missingvlos": { "ra": 200.688, "dec": -30.601, "distance": 15.1, "pmra": -0.862, "pmdec": 3.3412, "vlos": NaN, "distance_e": NaN, "vlos_e": NaN, "pmra_e": 0.1686, "pmdec_e": 0.1305, "pmcorr": -0.2738, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "AM4": { "ra": 209.09, "dec": -27.167, "distance": 29.01, "pmra": -0.2911, "pmdec": -2.5119, "vlos": 151.19, "distance_e": 0.94, "vlos_e": 2.85, "pmra_e": 0.4448, "pmdec_e": 0.3441, "pmcorr": -0.3645, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "Munoz1": { "ra": 225.45, "dec": 66.969, "distance": 45.0, "pmra": -0.1031, "pmdec": -0.0591, "vlos": -137.0, "distance_e": NaN, "vlos_e": 4.0, "pmra_e": 0.192, "pmdec_e": 0.1792, "pmcorr": -0.2434, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "RLGC1-missingvlos": { "ra": 244.286, "dec": -44.593, "distance": 28.8, "pmra": 1.0223, "pmdec": 0.7697, "vlos": NaN, "distance_e": NaN, "vlos_e": NaN, "pmra_e": 0.0553, "pmdec_e": 0.047, "pmcorr": 0.0104, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "FSR1758": { "ra": 262.8, "dec": -39.808, "distance": 11.09, "pmra": -2.8815, "pmdec": 2.5186, "vlos": 227.31, "distance_e": 0.74, "vlos_e": 0.59, "pmra_e": 0.0259, "pmdec_e": 0.0255, "pmcorr": -0.0113, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "Liller1": { "ra": 263.352, "dec": -33.39, "distance": 8.06, "pmra": -5.4028, "pmdec": -7.4313, "vlos": 60.36, "distance_e": 0.34, "vlos_e": 2.44, "pmra_e": 0.1087, "pmdec_e": 0.0769, "pmcorr": 0.2974, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "VVVCL002-missingvlos": { "ra": 265.276, "dec": -28.845, "distance": 8.0, "pmra": -8.8674, "pmdec": 2.39, "vlos": NaN, "distance_e": NaN, "vlos_e": NaN, "pmra_e": 0.1425, "pmdec_e": 0.0852, "pmcorr": 0.1745, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "UKS1": { "ra": 268.613, "dec": -24.145, "distance": 15.58, "pmra": -2.0401, "pmdec": -2.7541, "vlos": 59.38, "distance_e": 0.56, "vlos_e": 2.63, "pmra_e": 0.0948, "pmdec_e": 0.0633, "pmcorr": -0.0518, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "VVVCL001": { "ra": 268.677, "dec": -24.015, "distance": 8.08, "pmra": -3.4875, "pmdec": -1.6523, "vlos": -327.28, "distance_e": 1.48, "vlos_e": 0.9, "pmra_e": 0.1437, "pmdec_e": 0.1075, "pmcorr": 0.2621, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "Gran1-missingvlos": { "ra": 269.653, "dec": -32.02, "distance": 8.8, "pmra": -8.1628, "pmdec": -8.0453, "vlos": NaN, "distance_e": NaN, "vlos_e": NaN, "pmra_e": 0.0377, "pmdec_e": 0.0364, "pmcorr": 0.0019, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "Pfleiderer2-missingvlos": { "ra": 269.664, "dec": -5.07, "distance": 16.0, "pmra": -2.7837, "pmdec": -4.1576, "vlos": NaN, "distance_e": NaN, "vlos_e": NaN, "pmra_e": 0.034, "pmdec_e": 0.0313, "pmcorr": 0.0889, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "Mercer5": { "ra": 275.832, "dec": -13.669, "distance": 5.5, "pmra": -3.9647, "pmdec": -7.2199, "vlos": 185.5, "distance_e": 0.5, "vlos_e": 3.75, "pmra_e": 0.114, "pmdec_e": 0.1106, "pmcorr": 0.3086, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "RLGC2-missingvlos": { "ra": 281.367, "dec": -5.192, "distance": 15.8, "pmra": -2.3957, "pmdec": -1.794, "vlos": NaN, "distance_e": NaN, "vlos_e": NaN, "pmra_e": 0.0771, "pmdec_e": 0.0692, "pmcorr": 0.3894, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "Laevens3": { "ra": 316.729, "dec": 14.984, "distance": 61.77, "pmra": 0.1722, "pmdec": -0.6658, "vlos": -70.3, "distance_e": 1.65, "vlos_e": 0.82, "pmra_e": 0.1013, "pmdec_e": 0.0797, "pmcorr": -0.0186, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "Segue3": { "ra": 320.379, "dec": 19.117, "distance": 29.5, "pmra": -0.9806, "pmdec": -1.6668, "vlos": -167.0, "distance_e": 1.1, "vlos_e": 1.5, "pmra_e": 0.1213, "pmdec_e": 0.0807, "pmcorr": -0.313, "source": "arXiv:1811.01507;arXiv:2102.09568;arXiv:2105.09526" }, "Mercury": { "ra": 272.53694669839314, "dec": -23.264707107603133, "distance": 3.30782629431e-09, "pmra": 114048573.58893394, "pmdec": 1113250730.9213943, "vlos": 24.09379083329671, "ro": 4.84813681113e-09, "vo": 30.0, "zo": 0.0, "solarmotion": [ 0.0, 0.0, 0.0 ], "source": "arXiv:0903.5308" }, "Venus": { "ra": 261.5911299067298, "dec": -33.022168008543325, "distance": 8.29181155636e-09, "pmra": -899463102.5785071, "pmdec": -1377271614.5834432, "vlos": -1.8240077203994514, "ro": 4.84813681113e-09, "vo": 30.0, "zo": 0.0, "solarmotion": [ 0.0, 0.0, 0.0 ], "source": "arXiv:0903.5308" }, "Earth": { "ra": 262.9641214706733, "dec": -33.593148459313646, "distance": 9.65631218281e-09, "pmra": -712425950.9163029, "pmdec": -1089092311.4748962, "vlos": 0.377358898819384, "ro": 4.84813681113e-09, "vo": 30.0, "zo": 0.0, "solarmotion": [ 0.0, 0.0, 0.0 ], "source": "arXiv:0903.5308" }, "Mars": { "ra": 127.89885319623308, "dec": -44.74750104086732, "distance": 4.04254311334e-09, "pmra": -412616788.46222305, "pmdec": 620326241.2420622, "vlos": 10.399320182892934, "ro": 4.84813681113e-09, "vo": 30.0, "zo": 0.0, "solarmotion": [ 0.0, 0.0, 0.0 ], "source": "arXiv:0903.5308" }, "Jupiter": { "ra": 115.15711593351926, "dec": -24.04306151473348, "distance": 2.182038447526e-08, "pmra": 10122386.407639211, "pmdec": -17153149.63312022, "vlos": 23.077939746070587, "ro": 4.84813681113e-09, "vo": 30.0, "zo": 0.0, "solarmotion": [ 0.0, 0.0, 0.0 ], "source": "arXiv:0903.5308" }, "Saturn": { "ra": 270.18477455320834, "dec": -19.4445953028556, "distance": 5.03213842936e-08, "pmra": -83101997.58440553, "pmdec": -142355056.17142785, "vlos": -4.480931566688518, "ro": 4.84813681113e-09, "vo": 30.0, "zo": 0.0, "solarmotion": [ 0.0, 0.0, 0.0 ], "source": "arXiv:0903.5308" }, "Uranus": { "ra": 89.97246477323895, "dec": 21.909381464992038, "distance": 9.262040283203e-08, "pmra": 26453654.021113504, "pmdec": -45851111.81130144, "vlos": 3.960384099783045, "ro": 4.84813681113e-09, "vo": 30.0, "zo": 0.0, "solarmotion": [ 0.0, 0.0, 0.0 ], "source": "arXiv:0903.5308" }, "Neptune": { "ra": 104.39324539298492, "dec": -3.8403976205388903, "distance": 1.4167718752406e-07, "pmra": 12522815.916762747, "pmdec": -24543456.29389769, "vlos": 17.919062691908074, "ro": 4.84813681113e-09, "vo": 30.0, "zo": 0.0, "solarmotion": [ 0.0, 0.0, 0.0 ], "source": "arXiv:0903.5308" } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667233486.7054672 galpy-1.8.1/galpy/orbit/orbit_c_ext/0000755000175100001710000000000014327773317017006 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/orbit/orbit_c_ext/integrateFullOrbit.c0000644000175100001710000011731214327773303022757 0ustar00runnerdocker/* Wrappers around the C integration code for Full Orbits */ #ifdef _WIN32 #include #endif #include #include #include #include #include #include #include #include #include #include #include //Potentials #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif #ifndef ORBITS_CHUNKSIZE #define ORBITS_CHUNKSIZE 1 #endif //Macros to export functions in DLL on different OS #if defined(_WIN32) #define EXPORT __declspec(dllexport) #elif defined(__GNUC__) #define EXPORT __attribute__((visibility("default"))) #else // Just do nothing? #define EXPORT #endif #ifdef _WIN32 // On Windows, *need* to define this function to allow the package to be imported #if PY_MAJOR_VERSION >= 3 PyMODINIT_FUNC PyInit_libgalpy(void) { // Python 3 return NULL; } #else PyMODINIT_FUNC initlibgalpy(void) {} // Python 2 #endif #endif /* Function Declarations */ void evalRectForce(double, double *, double *, int, struct potentialArg *); void evalRectDeriv(double, double *, double *, int, struct potentialArg *); void evalRectDeriv_dxdv(double,double *, double *, int, struct potentialArg *); void initMovingObjectSplines(struct potentialArg *, double ** pot_args); void initChandrasekharDynamicalFrictionSplines(struct potentialArg *, double ** pot_args); /* Actual functions */ void parse_leapFuncArgs_Full(int npot, struct potentialArg * potentialArgs, int ** pot_type, double ** pot_args, tfuncs_type_arr * pot_tfuncs){ int ii,jj,kk; int nR, nz, nr; double * Rgrid, * zgrid, * potGrid_splinecoeffs; init_potentialArgs(npot,potentialArgs); for (ii=0; ii < npot; ii++){ switch ( *(*pot_type)++ ) { case 0: //LogarithmicHaloPotential, 4 arguments potentialArgs->potentialEval= &LogarithmicHaloPotentialEval; potentialArgs->Rforce= &LogarithmicHaloPotentialRforce; potentialArgs->zforce= &LogarithmicHaloPotentialzforce; potentialArgs->phitorque= &LogarithmicHaloPotentialphitorque; potentialArgs->dens= &LogarithmicHaloPotentialDens; //potentialArgs->R2deriv= &LogarithmicHaloPotentialR2deriv; //potentialArgs->planarphi2deriv= &ZeroForce; //potentialArgs->planarRphideriv= &ZeroForce; potentialArgs->nargs= 4; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 1: //DehnenBarPotential, 6 arguments potentialArgs->Rforce= &DehnenBarPotentialRforce; potentialArgs->phitorque= &DehnenBarPotentialphitorque; potentialArgs->zforce= &DehnenBarPotentialzforce; potentialArgs->nargs= 6; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 5: //MiyamotoNagaiPotential, 3 arguments potentialArgs->potentialEval= &MiyamotoNagaiPotentialEval; potentialArgs->Rforce= &MiyamotoNagaiPotentialRforce; potentialArgs->zforce= &MiyamotoNagaiPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &MiyamotoNagaiPotentialDens; //potentialArgs->R2deriv= &MiyamotoNagaiPotentialR2deriv; //potentialArgs->planarphi2deriv= &ZeroForce; //potentialArgs->planarRphideriv= &ZeroForce; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 7: //PowerSphericalPotential, 2 arguments potentialArgs->potentialEval= &PowerSphericalPotentialEval; potentialArgs->Rforce= &PowerSphericalPotentialRforce; potentialArgs->zforce= &PowerSphericalPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &PowerSphericalPotentialDens; //potentialArgs->R2deriv= &PowerSphericalPotentialR2deriv; //potentialArgs->planarphi2deriv= &ZeroForce; //potentialArgs->planarRphideriv= &ZeroForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 8: //HernquistPotential, 2 arguments potentialArgs->potentialEval= &HernquistPotentialEval; potentialArgs->Rforce= &HernquistPotentialRforce; potentialArgs->zforce= &HernquistPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &HernquistPotentialDens; //potentialArgs->R2deriv= &HernquistPotentialR2deriv; //potentialArgs->planarphi2deriv= &ZeroForce; //potentialArgs->planarRphideriv= &ZeroForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 9: //NFWPotential, 2 arguments potentialArgs->potentialEval= &NFWPotentialEval; potentialArgs->Rforce= &NFWPotentialRforce; potentialArgs->zforce= &NFWPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &NFWPotentialDens; //potentialArgs->R2deriv= &NFWPotentialR2deriv; //potentialArgs->planarphi2deriv= &ZeroForce; //potentialArgs->planarRphideriv= &ZeroForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 10: //JaffePotential, 2 arguments potentialArgs->potentialEval= &JaffePotentialEval; potentialArgs->Rforce= &JaffePotentialRforce; potentialArgs->zforce= &JaffePotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &JaffePotentialDens; //potentialArgs->R2deriv= &JaffePotentialR2deriv; //potentialArgs->planarphi2deriv= &ZeroForce; //potentialArgs->planarRphideriv= &ZeroForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 11: //DoubleExponentialDiskPotential, XX arguments potentialArgs->potentialEval= &DoubleExponentialDiskPotentialEval; potentialArgs->Rforce= &DoubleExponentialDiskPotentialRforce; potentialArgs->zforce= &DoubleExponentialDiskPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &DoubleExponentialDiskPotentialDens; //Look at pot_args to figure out the number of arguments potentialArgs->nargs= (int) (5 + 4 * *(*pot_args+4) ); potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 12: //FlattenedPowerPotential, 4 arguments potentialArgs->potentialEval= &FlattenedPowerPotentialEval; potentialArgs->Rforce= &FlattenedPowerPotentialRforce; potentialArgs->zforce= &FlattenedPowerPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &FlattenedPowerPotentialDens; potentialArgs->nargs= 4; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 13: //interpRZPotential, XX arguments //Grab the grids and the coefficients nR= (int) *(*pot_args)++; nz= (int) *(*pot_args)++; Rgrid= (double *) malloc ( nR * sizeof ( double ) ); zgrid= (double *) malloc ( nz * sizeof ( double ) ); potGrid_splinecoeffs= (double *) malloc ( nR * nz * sizeof ( double ) ); for (kk=0; kk < nR; kk++) *(Rgrid+kk)= *(*pot_args)++; for (kk=0; kk < nz; kk++) *(zgrid+kk)= *(*pot_args)++; for (kk=0; kk < nR; kk++) put_row(potGrid_splinecoeffs,kk,*pot_args+kk*nz,nz); *pot_args+= nR*nz; potentialArgs->i2d= interp_2d_alloc(nR,nz); interp_2d_init(potentialArgs->i2d,Rgrid,zgrid,potGrid_splinecoeffs, INTERP_2D_LINEAR); //latter bc we already calculated the coeffs potentialArgs->accx= gsl_interp_accel_alloc (); potentialArgs->accy= gsl_interp_accel_alloc (); for (kk=0; kk < nR; kk++) put_row(potGrid_splinecoeffs,kk,*pot_args+kk*nz,nz); *pot_args+= nR*nz; potentialArgs->i2drforce= interp_2d_alloc(nR,nz); interp_2d_init(potentialArgs->i2drforce,Rgrid,zgrid,potGrid_splinecoeffs, INTERP_2D_LINEAR); //latter bc we already calculated the coeffs potentialArgs->accxrforce= gsl_interp_accel_alloc (); potentialArgs->accyrforce= gsl_interp_accel_alloc (); for (kk=0; kk < nR; kk++) put_row(potGrid_splinecoeffs,kk,*pot_args+kk*nz,nz); *pot_args+= nR*nz; potentialArgs->i2dzforce= interp_2d_alloc(nR,nz); interp_2d_init(potentialArgs->i2dzforce,Rgrid,zgrid,potGrid_splinecoeffs, INTERP_2D_LINEAR); //latter bc we already calculated the coeffs potentialArgs->accxzforce= gsl_interp_accel_alloc (); potentialArgs->accyzforce= gsl_interp_accel_alloc (); potentialArgs->potentialEval= &interpRZPotentialEval; potentialArgs->Rforce= &interpRZPotentialRforce; potentialArgs->zforce= &interpRZPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; //clean up free(Rgrid); free(zgrid); free(potGrid_splinecoeffs); potentialArgs->requiresVelocity= false; break; case 14: //IsochronePotential, 2 arguments potentialArgs->potentialEval= &IsochronePotentialEval; potentialArgs->Rforce= &IsochronePotentialRforce; potentialArgs->zforce= &IsochronePotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &IsochronePotentialDens; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 15: //PowerSphericalwCutoffPotential, 3 arguments potentialArgs->potentialEval= &PowerSphericalPotentialwCutoffEval; potentialArgs->Rforce= &PowerSphericalPotentialwCutoffRforce; potentialArgs->zforce= &PowerSphericalPotentialwCutoffzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &PowerSphericalPotentialwCutoffDens; //potentialArgs->R2deriv= &PowerSphericalPotentialR2deriv; //potentialArgs->planarphi2deriv= &ZeroForce; //potentialArgs->planarRphideriv= &ZeroForce; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 16: //KuzminKutuzovStaeckelPotential, 3 arguments potentialArgs->potentialEval= &KuzminKutuzovStaeckelPotentialEval; potentialArgs->Rforce= &KuzminKutuzovStaeckelPotentialRforce; potentialArgs->zforce= &KuzminKutuzovStaeckelPotentialzforce; potentialArgs->phitorque= &ZeroForce; //potentialArgs->R2deriv= &KuzminKutuzovStaeckelPotentialR2deriv; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 17: //PlummerPotential, 2 arguments potentialArgs->potentialEval= &PlummerPotentialEval; potentialArgs->Rforce= &PlummerPotentialRforce; potentialArgs->zforce= &PlummerPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &PlummerPotentialDens; //potentialArgs->R2deriv= &PlummerPotentialR2deriv; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 18: //PseudoIsothermalPotential, 2 arguments potentialArgs->potentialEval= &PseudoIsothermalPotentialEval; potentialArgs->Rforce= &PseudoIsothermalPotentialRforce; potentialArgs->zforce= &PseudoIsothermalPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &PseudoIsothermalPotentialDens; //potentialArgs->R2deriv= &PseudoIsothermalPotentialR2deriv; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 19: //KuzminDiskPotential, 2 arguments potentialArgs->potentialEval= &KuzminDiskPotentialEval; potentialArgs->Rforce= &KuzminDiskPotentialRforce; potentialArgs->zforce= &KuzminDiskPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 20: //BurkertPotential, 2 arguments potentialArgs->potentialEval= &BurkertPotentialEval; potentialArgs->Rforce= &BurkertPotentialRforce; potentialArgs->zforce= &BurkertPotentialzforce; potentialArgs->dens= &BurkertPotentialDens; potentialArgs->phitorque= &ZeroForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 21: //TriaxialHernquistPotential, lots of arguments potentialArgs->potentialEval= &EllipsoidalPotentialEval; potentialArgs->Rforce = &EllipsoidalPotentialRforce; potentialArgs->zforce = &EllipsoidalPotentialzforce; potentialArgs->phitorque = &EllipsoidalPotentialphitorque; potentialArgs->dens= &EllipsoidalPotentialDens; // Also assign functions specific to EllipsoidalPotential potentialArgs->psi= &TriaxialHernquistPotentialpsi; potentialArgs->mdens= &TriaxialHernquistPotentialmdens; potentialArgs->mdensDeriv= &TriaxialHernquistPotentialmdensDeriv; potentialArgs->nargs = (int) (21 + *(*pot_args+7) + 2 * *(*pot_args + (int) (*(*pot_args+7) + 20))); potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 22: //TriaxialNFWPotential, lots of arguments potentialArgs->potentialEval= &EllipsoidalPotentialEval; potentialArgs->Rforce = &EllipsoidalPotentialRforce; potentialArgs->zforce = &EllipsoidalPotentialzforce; potentialArgs->phitorque = &EllipsoidalPotentialphitorque; potentialArgs->dens= &EllipsoidalPotentialDens; // Also assign functions specific to EllipsoidalPotential potentialArgs->psi= &TriaxialNFWPotentialpsi; potentialArgs->mdens= &TriaxialNFWPotentialmdens; potentialArgs->mdensDeriv= &TriaxialNFWPotentialmdensDeriv; potentialArgs->nargs = (int) (21 + *(*pot_args+7) + 2 * *(*pot_args + (int) (*(*pot_args+7) + 20))); potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 23: //TriaxialJaffePotential, lots of arguments potentialArgs->potentialEval= &EllipsoidalPotentialEval; potentialArgs->Rforce = &EllipsoidalPotentialRforce; potentialArgs->zforce = &EllipsoidalPotentialzforce; potentialArgs->phitorque = &EllipsoidalPotentialphitorque; potentialArgs->dens= &EllipsoidalPotentialDens; // Also assign functions specific to EllipsoidalPotential potentialArgs->psi= &TriaxialJaffePotentialpsi; potentialArgs->mdens= &TriaxialJaffePotentialmdens; potentialArgs->mdensDeriv= &TriaxialJaffePotentialmdensDeriv; potentialArgs->nargs = (int) (21 + *(*pot_args+7) + 2 * *(*pot_args + (int) (*(*pot_args+7) + 20))); potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 24: //SCFPotential, many arguments potentialArgs->potentialEval= &SCFPotentialEval; potentialArgs->Rforce= &SCFPotentialRforce; potentialArgs->zforce= &SCFPotentialzforce; potentialArgs->phitorque= &SCFPotentialphitorque; potentialArgs->dens= &SCFPotentialDens; potentialArgs->nargs= (int) (5 + (1 + *(*pot_args + 1)) * *(*pot_args+2) * *(*pot_args+3)* *(*pot_args+4) + 7); potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 25: //SoftenedNeedleBarPotential, 13 arguments potentialArgs->potentialEval= &SoftenedNeedleBarPotentialEval; potentialArgs->Rforce= &SoftenedNeedleBarPotentialRforce; potentialArgs->zforce= &SoftenedNeedleBarPotentialzforce; potentialArgs->phitorque= &SoftenedNeedleBarPotentialphitorque; potentialArgs->nargs= 13; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 26: //DiskSCFPotential, nsigma+3 arguments potentialArgs->potentialEval= &DiskSCFPotentialEval; potentialArgs->Rforce= &DiskSCFPotentialRforce; potentialArgs->zforce= &DiskSCFPotentialzforce; potentialArgs->dens= &DiskSCFPotentialDens; potentialArgs->phitorque= &ZeroForce; potentialArgs->nargs= (int) **pot_args + 3; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 27: // SpiralArmsPotential, 10 arguments + array of Cs potentialArgs->Rforce = &SpiralArmsPotentialRforce; potentialArgs->zforce = &SpiralArmsPotentialzforce; potentialArgs->phitorque = &SpiralArmsPotentialphitorque; //potentialArgs->R2deriv = &SpiralArmsPotentialR2deriv; //potentialArgs->z2deriv = &SpiralArmsPotentialz2deriv; potentialArgs->phi2deriv = &SpiralArmsPotentialphi2deriv; //potentialArgs->Rzderiv = &SpiralArmsPotentialRzderiv; potentialArgs->Rphideriv = &SpiralArmsPotentialRphideriv; potentialArgs->nargs = (int) 10 + **pot_args; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 30: // PerfectEllipsoidPotential, lots of arguments potentialArgs->potentialEval= &EllipsoidalPotentialEval; potentialArgs->Rforce = &EllipsoidalPotentialRforce; potentialArgs->zforce = &EllipsoidalPotentialzforce; potentialArgs->phitorque = &EllipsoidalPotentialphitorque; potentialArgs->dens= &EllipsoidalPotentialDens; //potentialArgs->R2deriv = &EllipsoidalPotentialR2deriv; //potentialArgs->z2deriv = &EllipsoidalPotentialz2deriv; //potentialArgs->phi2deriv = &EllipsoidalPotentialphi2deriv; //potentialArgs->Rzderiv = &EllipsoidalPotentialRzderiv; //potentialArgs->Rphideriv = &EllipsoidalPotentialRphideriv; // Also assign functions specific to EllipsoidalPotential potentialArgs->psi= &PerfectEllipsoidPotentialpsi; potentialArgs->mdens= &PerfectEllipsoidPotentialmdens; potentialArgs->mdensDeriv= &PerfectEllipsoidPotentialmdensDeriv; potentialArgs->nargs = (int) (21 + *(*pot_args+7) + 2 * *(*pot_args + (int) (*(*pot_args+7) + 20))); potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; // 31: KGPotential // 32: IsothermalDiskPotential case 33: //DehnenCoreSphericalPotential, 2 arguments potentialArgs->potentialEval= &DehnenCoreSphericalPotentialEval; potentialArgs->Rforce= &DehnenCoreSphericalPotentialRforce; potentialArgs->zforce= &DehnenCoreSphericalPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &DehnenCoreSphericalPotentialDens; //potentialArgs->R2deriv= &DehnenCoreSphericalPotentialR2deriv; //potentialArgs->planarphi2deriv= &ZeroForce; //potentialArgs->planarRphideriv= &ZeroForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 34: //DehnenSphericalPotential, 3 arguments potentialArgs->potentialEval= &DehnenSphericalPotentialEval; potentialArgs->Rforce= &DehnenSphericalPotentialRforce; potentialArgs->zforce= &DehnenSphericalPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &DehnenSphericalPotentialDens; //potentialArgs->R2deriv= &DehnenSphericalPotentialR2deriv; //potentialArgs->planarphi2deriv= &ZeroForce; //potentialArgs->planarRphideriv= &ZeroForce; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 35: //HomogeneousSpherePotential, 3 arguments potentialArgs->potentialEval= &HomogeneousSpherePotentialEval; potentialArgs->Rforce= &HomogeneousSpherePotentialRforce; potentialArgs->zforce= &HomogeneousSpherePotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &HomogeneousSpherePotentialDens; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 36: //interpSphericalPotential, XX arguments // Set up 1 spline in potentialArgs potentialArgs->nspline1d= 1; potentialArgs->spline1d= (gsl_spline **) \ malloc ( potentialArgs->nspline1d*sizeof ( gsl_spline *) ); potentialArgs->acc1d= (gsl_interp_accel **) \ malloc ( potentialArgs->nspline1d * sizeof ( gsl_interp_accel * ) ); // allocate accelerator *potentialArgs->acc1d= gsl_interp_accel_alloc(); // Set up interpolater nr= (int) **pot_args; *potentialArgs->spline1d= gsl_spline_alloc(gsl_interp_cspline,nr); gsl_spline_init(*potentialArgs->spline1d,*pot_args+1,*pot_args+1+nr,nr); *pot_args+= 2*nr+1; // Bind forces potentialArgs->potentialEval= &SphericalPotentialEval; potentialArgs->Rforce = &SphericalPotentialRforce; potentialArgs->zforce = &SphericalPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &SphericalPotentialDens; // Also assign functions specific to SphericalPotential potentialArgs->revaluate= &interpSphericalPotentialrevaluate; potentialArgs->rforce= &interpSphericalPotentialrforce; potentialArgs->r2deriv= &interpSphericalPotentialr2deriv; potentialArgs->rdens= &interpSphericalPotentialrdens; potentialArgs->nargs = 6; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 37: // TriaxialGaussianPotential, lots of arguments potentialArgs->potentialEval= &EllipsoidalPotentialEval; potentialArgs->Rforce = &EllipsoidalPotentialRforce; potentialArgs->zforce = &EllipsoidalPotentialzforce; potentialArgs->phitorque = &EllipsoidalPotentialphitorque; potentialArgs->dens= &EllipsoidalPotentialDens; //potentialArgs->R2deriv = &EllipsoidalPotentialR2deriv; //potentialArgs->z2deriv = &EllipsoidalPotentialz2deriv; //potentialArgs->phi2deriv = &EllipsoidalPotentialphi2deriv; //potentialArgs->Rzderiv = &EllipsoidalPotentialRzderiv; //potentialArgs->Rphideriv = &EllipsoidalPotentialRphideriv; // Also assign functions specific to EllipsoidalPotential potentialArgs->psi= &TriaxialGaussianPotentialpsi; potentialArgs->mdens= &TriaxialGaussianPotentialmdens; potentialArgs->mdensDeriv= &TriaxialGaussianPotentialmdensDeriv; potentialArgs->nargs = (int) (21 + *(*pot_args+7) + 2 * *(*pot_args + (int) (*(*pot_args+7) + 20))); potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 38: // PowerTriaxialPotential, lots of arguments potentialArgs->potentialEval= &EllipsoidalPotentialEval; potentialArgs->Rforce = &EllipsoidalPotentialRforce; potentialArgs->zforce = &EllipsoidalPotentialzforce; potentialArgs->phitorque = &EllipsoidalPotentialphitorque; potentialArgs->dens= &EllipsoidalPotentialDens; //potentialArgs->R2deriv = &EllipsoidalPotentialR2deriv; //potentialArgs->z2deriv = &EllipsoidalPotentialz2deriv; //potentialArgs->phi2deriv = &EllipsoidalPotentialphi2deriv; //potentialArgs->Rzderiv = &EllipsoidalPotentialRzderiv; //potentialArgs->Rphideriv = &EllipsoidalPotentialRphideriv; // Also assign functions specific to EllipsoidalPotential potentialArgs->psi= &PowerTriaxialPotentialpsi; potentialArgs->mdens= &PowerTriaxialPotentialmdens; potentialArgs->mdensDeriv= &PowerTriaxialPotentialmdensDeriv; potentialArgs->nargs = (int) (21 + *(*pot_args+7) + 2 * *(*pot_args + (int) (*(*pot_args+7) + 20))); potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 39: //NonInertialFrameForce, 22 arguments (10 caching ones) potentialArgs->RforceVelocity= &NonInertialFrameForceRforce; potentialArgs->zforceVelocity= &NonInertialFrameForcezforce; potentialArgs->phitorqueVelocity= &NonInertialFrameForcephitorque; potentialArgs->nargs= 23; potentialArgs->ntfuncs= (int) ( 3 * *(*pot_args + 12) * ( 1 + 2 * *(*pot_args + 11) ) \ + ( 6 - 4 * ( *(*pot_args + 13) ) ) * *(*pot_args + 15) ); potentialArgs->requiresVelocity= true; break; case 40: //NullPotential, no arguments (only supported for orbit int) potentialArgs->Rforce= &ZeroForce; potentialArgs->zforce= &ZeroForce; potentialArgs->phitorque= &ZeroForce; potentialArgs->dens= &ZeroForce; potentialArgs->nargs= 0; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; //////////////////////////////// WRAPPERS ///////////////////////////////////// case -1: //DehnenSmoothWrapperPotential potentialArgs->potentialEval= &DehnenSmoothWrapperPotentialEval; potentialArgs->Rforce= &DehnenSmoothWrapperPotentialRforce; potentialArgs->zforce= &DehnenSmoothWrapperPotentialzforce; potentialArgs->phitorque= &DehnenSmoothWrapperPotentialphitorque; potentialArgs->nargs= 4; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case -2: //SolidBodyRotationWrapperPotential potentialArgs->Rforce= &SolidBodyRotationWrapperPotentialRforce; potentialArgs->zforce= &SolidBodyRotationWrapperPotentialzforce; potentialArgs->phitorque= &SolidBodyRotationWrapperPotentialphitorque; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case -4: //CorotatingRotationWrapperPotential potentialArgs->Rforce= &CorotatingRotationWrapperPotentialRforce; potentialArgs->zforce= &CorotatingRotationWrapperPotentialzforce; potentialArgs->phitorque= &CorotatingRotationWrapperPotentialphitorque; potentialArgs->nargs= 5; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case -5: //GaussianAmplitudeWrapperPotential potentialArgs->potentialEval= &GaussianAmplitudeWrapperPotentialEval; potentialArgs->Rforce= &GaussianAmplitudeWrapperPotentialRforce; potentialArgs->zforce= &GaussianAmplitudeWrapperPotentialzforce; potentialArgs->phitorque= &GaussianAmplitudeWrapperPotentialphitorque; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case -6: //MovingObjectPotential potentialArgs->Rforce= &MovingObjectPotentialRforce; potentialArgs->zforce= &MovingObjectPotentialzforce; potentialArgs->phitorque= &MovingObjectPotentialphitorque; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case -7: //ChandrasekharDynamicalFrictionForce potentialArgs->RforceVelocity= &ChandrasekharDynamicalFrictionForceRforce; potentialArgs->zforceVelocity= &ChandrasekharDynamicalFrictionForcezforce; potentialArgs->phitorqueVelocity= &ChandrasekharDynamicalFrictionForcephitorque; potentialArgs->nargs= 16; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= true; break; case -8: //RotateAndTiltWrapperPotential potentialArgs->Rforce= &RotateAndTiltWrapperPotentialRforce; potentialArgs->zforce= &RotateAndTiltWrapperPotentialzforce; potentialArgs->phitorque= &RotateAndTiltWrapperPotentialphitorque; potentialArgs->nargs= 21; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case -9: //TimeDependentAmplitudeWrapperPotential potentialArgs->potentialEval= &TimeDependentAmplitudeWrapperPotentialEval; potentialArgs->Rforce= &TimeDependentAmplitudeWrapperPotentialRforce; potentialArgs->zforce= &TimeDependentAmplitudeWrapperPotentialzforce; potentialArgs->phitorque= &TimeDependentAmplitudeWrapperPotentialphitorque; potentialArgs->nargs= 1; potentialArgs->ntfuncs= 1; potentialArgs->requiresVelocity= false; break; } int setupMovingObjectSplines = *(*pot_type-1) == -6 ? 1 : 0; int setupChandrasekharDynamicalFrictionSplines = *(*pot_type-1) == -7 ? 1 : 0; if ( *(*pot_type-1) < 0 ) { // Parse wrapped potential for wrappers potentialArgs->nwrapped= (int) *(*pot_args)++; potentialArgs->wrappedPotentialArg= \ (struct potentialArg *) malloc ( potentialArgs->nwrapped \ * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg, pot_type,pot_args,pot_tfuncs); } if (setupMovingObjectSplines) initMovingObjectSplines(potentialArgs, pot_args); if (setupChandrasekharDynamicalFrictionSplines) initChandrasekharDynamicalFrictionSplines(potentialArgs,pot_args); // Now load each potential's parameters potentialArgs->args= (double *) malloc( potentialArgs->nargs * sizeof(double)); for (jj=0; jj < potentialArgs->nargs; jj++){ *(potentialArgs->args)= *(*pot_args)++; potentialArgs->args++; } potentialArgs->args-= potentialArgs->nargs; // and load each potential's time functions if ( potentialArgs->ntfuncs > 0 ) { potentialArgs->tfuncs= (*pot_tfuncs); (*pot_tfuncs)+= potentialArgs->ntfuncs; } potentialArgs++; } potentialArgs-= npot; } EXPORT void integrateFullOrbit(int nobj, double *yo, int nt, double *t, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double dt, double rtol, double atol, double *result, int * err, int odeint_type, orbint_callback_type cb){ //Set up the forces, first count int ii,jj; int dim; int max_threads; int * thread_pot_type; double * thread_pot_args; tfuncs_type_arr thread_pot_tfuncs; max_threads= ( nobj < omp_get_max_threads() ) ? nobj : omp_get_max_threads(); // Because potentialArgs may cache, safest to have one / thread struct potentialArg * potentialArgs= (struct potentialArg *) malloc ( max_threads * npot * sizeof (struct potentialArg) ); #pragma omp parallel for schedule(static,1) private(ii,thread_pot_type,thread_pot_args,thread_pot_tfuncs) num_threads(max_threads) for (ii=0; ii < max_threads; ii++) { thread_pot_type= pot_type; // need to make thread-private pointers, bc thread_pot_args= pot_args; // these pointers are changed in parse_... thread_pot_tfuncs= pot_tfuncs; // ... parse_leapFuncArgs_Full(npot,potentialArgs+ii*npot, &thread_pot_type,&thread_pot_args,&thread_pot_tfuncs); } //Integrate void (*odeint_func)(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *,int *); void (*odeint_deriv_func)(double, double *, double *, int,struct potentialArg *); switch ( odeint_type ) { case 0: //leapfrog odeint_func= &leapfrog; odeint_deriv_func= &evalRectForce; dim= 3; break; case 1: //RK4 odeint_func= &bovy_rk4; odeint_deriv_func= &evalRectDeriv; dim= 6; break; case 2: //RK6 odeint_func= &bovy_rk6; odeint_deriv_func= &evalRectDeriv; dim= 6; break; case 3: //symplec4 odeint_func= &symplec4; odeint_deriv_func= &evalRectForce; dim= 3; break; case 4: //symplec6 odeint_func= &symplec6; odeint_deriv_func= &evalRectForce; dim= 3; break; case 5: //DOPR54 odeint_func= &bovy_dopr54; odeint_deriv_func= &evalRectDeriv; dim= 6; break; case 6: //DOP853 odeint_func= &dop853; odeint_deriv_func= &evalRectDeriv; dim= 6; break; } #pragma omp parallel for schedule(dynamic,ORBITS_CHUNKSIZE) private(ii,jj) num_threads(max_threads) for (ii=0; ii < nobj; ii++) { cyl_to_rect_galpy(yo+6*ii); odeint_func(odeint_deriv_func,dim,yo+6*ii,nt,dt,t, npot,potentialArgs+omp_get_thread_num()*npot,rtol,atol, result+6*nt*ii,err+ii); for (jj=0; jj < nt; jj++) rect_to_cyl_galpy(result+6*jj+6*nt*ii); if ( cb ) // Callback if not void cb(); } //Free allocated memory #pragma omp parallel for schedule(static,1) private(ii) num_threads(max_threads) for (ii=0; ii < max_threads; ii++) free_potentialArgs(npot,potentialArgs+ii*npot); free(potentialArgs); //Done! } // LCOV_EXCL_START void integrateOrbit_dxdv(double *yo, int nt, double *t, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double rtol, double atol, double *result, int * err, int odeint_type){ //Set up the forces, first count int dim; struct potentialArg * potentialArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,potentialArgs,&pot_type,&pot_args,&pot_tfuncs); //Integrate void (*odeint_func)(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *,int *); void (*odeint_deriv_func)(double, double *, double *, int,struct potentialArg *); switch ( odeint_type ) { case 0: //leapfrog odeint_func= &leapfrog; odeint_deriv_func= &evalRectForce; dim= 6; break; case 1: //RK4 odeint_func= &bovy_rk4; odeint_deriv_func= &evalRectDeriv_dxdv; dim= 12; break; case 2: //RK6 odeint_func= &bovy_rk6; odeint_deriv_func= &evalRectDeriv_dxdv; dim= 12; break; case 3: //symplec4 odeint_func= &symplec4; odeint_deriv_func= &evalRectForce; dim= 6; break; case 4: //symplec6 odeint_func= &symplec6; odeint_deriv_func= &evalRectForce; dim= 6; break; case 5: //DOPR54 odeint_func= &bovy_dopr54; odeint_deriv_func= &evalRectDeriv_dxdv; dim= 12; break; case 6: //DOP853 odeint_func= &dop853; odeint_deriv_func= &evalRectDeriv_dxdv; dim= 12; break; } odeint_func(odeint_deriv_func,dim,yo,nt,-9999.99,t,npot,potentialArgs, rtol,atol,result,err); //Free allocated memory free_potentialArgs(npot,potentialArgs); free(potentialArgs); //Done! } // LCOV_EXCL_STOP void evalRectForce(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs){ double sinphi, cosphi, x, y, phi,R,Rforce,phitorque, z, zforce; //q is rectangular so calculate R and phi x= *q; y= *(q+1); z= *(q+2); R= sqrt(x*x+y*y); phi= acos(x/R); sinphi= y/R; cosphi= x/R; if ( y < 0. ) phi= 2.*M_PI-phi; //Calculate the forces Rforce= calcRforce(R,z,phi,t,nargs,potentialArgs); zforce= calczforce(R,z,phi,t,nargs,potentialArgs); phitorque= calcphitorque(R,z,phi,t,nargs,potentialArgs); *a++= cosphi*Rforce-1./R*sinphi*phitorque; *a++= sinphi*Rforce+1./R*cosphi*phitorque; *a= zforce; } void evalRectDeriv(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs){ double sinphi, cosphi, x, y, phi,R,Rforce,phitorque,z,zforce,vR,vT; //first three derivatives are just the velocities *a++= *(q+3); *a++= *(q+4); *a++= *(q+5); //Rest is force //q is rectangular so calculate R and phi, vR and vT (for dissipative) x= *q; y= *(q+1); z= *(q+2); R= sqrt(x*x+y*y); phi= acos(x/R); sinphi= y/R; cosphi= x/R; if ( y < 0. ) phi= 2.*M_PI-phi; vR= *(q+3) * cosphi + *(q+4) * sinphi; vT= -*(q+3) * sinphi + *(q+4) * cosphi; //Calculate the forces Rforce= calcRforce(R,z,phi,t,nargs,potentialArgs,vR,vT,*(q+5)); zforce= calczforce(R,z,phi,t,nargs,potentialArgs,vR,vT,*(q+5)); phitorque= calcphitorque(R,z,phi,t,nargs,potentialArgs,vR,vT,*(q+5)); *a++= cosphi*Rforce-1./R*sinphi*phitorque; *a++= sinphi*Rforce+1./R*cosphi*phitorque; *a= zforce; } void initMovingObjectSplines(struct potentialArg * potentialArgs, double ** pot_args){ gsl_interp_accel *x_accel_ptr = gsl_interp_accel_alloc(); gsl_interp_accel *y_accel_ptr = gsl_interp_accel_alloc(); gsl_interp_accel *z_accel_ptr = gsl_interp_accel_alloc(); int nPts = (int) **pot_args; gsl_spline *x_spline = gsl_spline_alloc(gsl_interp_cspline, nPts); gsl_spline *y_spline = gsl_spline_alloc(gsl_interp_cspline, nPts); gsl_spline *z_spline = gsl_spline_alloc(gsl_interp_cspline, nPts); double * t_arr = *pot_args+1; double * x_arr = t_arr+1*nPts; double * y_arr = t_arr+2*nPts; double * z_arr = t_arr+3*nPts; double * t= (double *) malloc ( nPts * sizeof (double) ); double tf = *(t_arr+4*nPts+2); double to = *(t_arr+4*nPts+1); int ii; for (ii=0; ii < nPts; ii++) *(t+ii) = (t_arr[ii]-to)/(tf-to); gsl_spline_init(x_spline, t, x_arr, nPts); gsl_spline_init(y_spline, t, y_arr, nPts); gsl_spline_init(z_spline, t, z_arr, nPts); potentialArgs->nspline1d= 3; potentialArgs->spline1d= (gsl_spline **) malloc ( 3*sizeof ( gsl_spline *) ); potentialArgs->acc1d= (gsl_interp_accel **) \ malloc ( 3 * sizeof ( gsl_interp_accel * ) ); *potentialArgs->spline1d = x_spline; *potentialArgs->acc1d = x_accel_ptr; *(potentialArgs->spline1d+1)= y_spline; *(potentialArgs->acc1d+1)= y_accel_ptr; *(potentialArgs->spline1d+2)= z_spline; *(potentialArgs->acc1d+2)= z_accel_ptr; *pot_args = *pot_args + (int) (1+4*nPts); free(t); } void initChandrasekharDynamicalFrictionSplines(struct potentialArg * potentialArgs, double ** pot_args){ gsl_interp_accel *sr_accel_ptr = gsl_interp_accel_alloc(); int nPts = (int) **pot_args; gsl_spline *sr_spline = gsl_spline_alloc(gsl_interp_cspline,nPts); double * r_arr = *pot_args+1; double * sr_arr = r_arr+1*nPts; double * r= (double *) malloc ( nPts * sizeof (double) ); double ro = *(r_arr+2*nPts+14); double rf = *(r_arr+2*nPts+15); int ii; for (ii=0; ii < nPts; ii++) *(r+ii) = (r_arr[ii]-ro)/(rf-ro); gsl_spline_init(sr_spline,r,sr_arr,nPts); potentialArgs->nspline1d= 1; potentialArgs->spline1d= (gsl_spline **) \ malloc ( potentialArgs->nspline1d*sizeof ( gsl_spline *) ); potentialArgs->acc1d= (gsl_interp_accel **) \ malloc ( potentialArgs->nspline1d * sizeof ( gsl_interp_accel * ) ); *potentialArgs->spline1d = sr_spline; *potentialArgs->acc1d = sr_accel_ptr; *pot_args = *pot_args + (int) (1+(1+potentialArgs->nspline1d)*nPts); free(r); } // LCOV_EXCL_START void evalRectDeriv_dxdv(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs){ double sinphi, cosphi, x, y, phi,R,Rforce,phitorque,z,zforce; double R2deriv, phi2deriv, Rphideriv, dFxdx, dFxdy, dFydx, dFydy; //first three derivatives are just the velocities *a++= *(q+3); *a++= *(q+4); *a++= *(q+5); //Rest is force //q is rectangular so calculate R and phi x= *q; y= *(q+1); z= *(q+2); R= sqrt(x*x+y*y); phi= acos(x/R); sinphi= y/R; cosphi= x/R; if ( y < 0. ) phi= 2.*M_PI-phi; //Calculate the forces Rforce= calcRforce(R,z,phi,t,nargs,potentialArgs); zforce= calczforce(R,z,phi,t,nargs,potentialArgs); phitorque= calcphitorque(R,z,phi,t,nargs,potentialArgs); *a++= cosphi*Rforce-1./R*sinphi*phitorque; *a++= sinphi*Rforce+1./R*cosphi*phitorque; *a++= zforce; //dx derivatives are just dv *a++= *(q+9); *a++= *(q+10); *a++= *(q+11); //for the dv derivatives we need also R2deriv, phi2deriv, and Rphideriv R2deriv= calcR2deriv(R,z,phi,t,nargs,potentialArgs); phi2deriv= calcphi2deriv(R,z,phi,t,nargs,potentialArgs); Rphideriv= calcRphideriv(R,z,phi,t,nargs,potentialArgs); //..and dFxdx, dFxdy, dFydx, dFydy dFxdx= -cosphi*cosphi*R2deriv +2.*cosphi*sinphi/R/R*phitorque +sinphi*sinphi/R*Rforce +2.*sinphi*cosphi/R*Rphideriv -sinphi*sinphi/R/R*phi2deriv; dFxdy= -sinphi*cosphi*R2deriv +(sinphi*sinphi-cosphi*cosphi)/R/R*phitorque -cosphi*sinphi/R*Rforce -(cosphi*cosphi-sinphi*sinphi)/R*Rphideriv +cosphi*sinphi/R/R*phi2deriv; dFydx= -cosphi*sinphi*R2deriv +(sinphi*sinphi-cosphi*cosphi)/R/R*phitorque +(sinphi*sinphi-cosphi*cosphi)/R*Rphideriv -sinphi*cosphi/R*Rforce +sinphi*cosphi/R/R*phi2deriv; dFydy= -sinphi*sinphi*R2deriv -2.*sinphi*cosphi/R/R*phitorque -2.*sinphi*cosphi/R*Rphideriv +cosphi*cosphi/R*Rforce -cosphi*cosphi/R/R*phi2deriv; *a++= dFxdx * *(q+4) + dFxdy * *(q+5); *a++= dFydx * *(q+4) + dFydy * *(q+5); *a= 0; //BOVY: PUT IN Z2DERIVS } // LCOV_EXCL_STOP ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/orbit/orbit_c_ext/integrateFullOrbit.h0000644000175100001710000000151314327773303022757 0ustar00runnerdocker#ifndef __INTEGRATEFULLORBIT_H__ #define __INTEGRATEFULLORBIT_H__ #ifdef __cplusplus extern "C" { #endif #ifdef _WIN32 #include #endif #include typedef void (*orbint_callback_type)(); // Callback function void parse_leapFuncArgs_Full(int, struct potentialArg *,int **,double **,tfuncs_type_arr *); #ifdef _WIN32 // On Windows, *need* to define this function to allow the package to be imported #if PY_MAJOR_VERSION >= 3 PyMODINIT_FUNC PyInit_libgalpy(void); // Python 3 #else PyMODINIT_FUNC initlibgalpy(void); // Python 2 #endif #endif //OpenMP #if defined(_OPENMP) #include #else typedef int omp_int_t; static inline omp_int_t omp_get_thread_num(void) { return 0;} static inline omp_int_t omp_get_max_threads(void) { return 1;} #endif #ifdef __cplusplus } #endif #endif /* integrateFullOrbit.h */ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/orbit/orbit_c_ext/integrateLinearOrbit.c0000644000175100001710000001573114327773303023271 0ustar00runnerdocker/* Wrappers around the C integration code for linear Orbits */ #include #include #include #include #include #include #include #include //Potentials #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif #ifndef ORBITS_CHUNKSIZE #define ORBITS_CHUNKSIZE 1 #endif //Macros to export functions in DLL on different OS #if defined(_WIN32) #define EXPORT __declspec(dllexport) #elif defined(__GNUC__) #define EXPORT __attribute__((visibility("default"))) #else // Just do nothing? #define EXPORT #endif /* Function Declarations */ void evalLinearForce(double, double *, double *, int, struct potentialArg *); void evalLinearDeriv(double, double *, double *, int, struct potentialArg *); /* Actual functions */ void parse_leapFuncArgs_Linear(int npot,struct potentialArg * potentialArgs, int ** pot_type, double ** pot_args, tfuncs_type_arr * pot_tfuncs){ int ii,jj; init_potentialArgs(npot,potentialArgs); for (ii=0; ii < npot; ii++){ switch ( *(*pot_type)++ ) { default: //verticalPotential potentialArgs->linearForce= &verticalPotentialLinearForce; break; case 31: // KGPotential potentialArgs->linearForce= &KGPotentialLinearForce; potentialArgs->nargs= 4; potentialArgs->ntfuncs= 0; break; case 32: // IsothermalDiskPotential potentialArgs->linearForce= &IsothermalDiskPotentialLinearForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; break; //////////////////////////////// WRAPPERS ///////////////////////////////////// // NOT CURRENTLY SUPPORTED /* case -1: //DehnenSmoothWrapperPotential potentialArgs->linearForce= &DehnenSmoothWrapperPotentialPlanarRforce; potentialArgs->nargs= (int) 3; break; case -2: //SolidBodyRotationWrapperPotential potentialArgs->linearForce= &SolidBodyRotationWrapperPotentialPlanarRforce; potentialArgs->nargs= (int) 3; break; case -4: //CorotatingRotationWrapperPotential potentialArgs->linearForce= &CorotatingRotationWrapperPotentialPlanarRforce; potentialArgs->nargs= (int) 5; break; case -5: //GaussianAmplitudeWrapperPotential potentialArgs->linearForce= &GaussianAmplitudeWrapperPotentialPlanarRforce; potentialArgs->nargs= (int) 3; break; */ } /* if ( *(*pot_type-1) < 0) { // Parse wrapped potential for wrappers potentialArgs->nwrapped= (int) *(*pot_args)++; potentialArgs->wrappedPotentialArg= \ (struct potentialArg *) malloc ( potentialArgs->nwrapped \ * sizeof (struct potentialArg) ); parse_leapFuncArgs_Linear(potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg, pot_type,pot_args); } */ // linear from 3D: assign R location parameter as the only one, rest // of potential as wrapped if ( potentialArgs->linearForce == &verticalPotentialLinearForce ) { potentialArgs->nwrapped= (int) 1; potentialArgs->wrappedPotentialArg= \ (struct potentialArg *) malloc ( potentialArgs->nwrapped \ * sizeof (struct potentialArg) ); *(pot_type)-= 1; // Do FullOrbit processing for same potential parse_leapFuncArgs_Full(potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg, pot_type,pot_args,pot_tfuncs); potentialArgs->nargs= 2; // R, phi } potentialArgs->args= (double *) malloc( potentialArgs->nargs * sizeof(double)); for (jj=0; jj < potentialArgs->nargs; jj++){ *(potentialArgs->args)= *(*pot_args)++; potentialArgs->args++; } potentialArgs->args-= potentialArgs->nargs; potentialArgs++; } potentialArgs-= npot; } EXPORT void integrateLinearOrbit(int nobj, double *yo, int nt, double *t, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double dt, double rtol, double atol, double *result, int * err, int odeint_type, orbint_callback_type cb){ //Set up the forces, first count int dim; int ii; int max_threads; int * thread_pot_type; double * thread_pot_args; tfuncs_type_arr thread_pot_tfuncs; max_threads= ( nobj < omp_get_max_threads() ) ? nobj : omp_get_max_threads(); // Because potentialArgs may cache, safest to have one / thread struct potentialArg * potentialArgs= (struct potentialArg *) malloc ( max_threads * npot * sizeof (struct potentialArg) ); #pragma omp parallel for schedule(static,1) private(ii,thread_pot_type,thread_pot_args,thread_pot_tfuncs) num_threads(max_threads) for (ii=0; ii < max_threads; ii++) { thread_pot_type= pot_type; // need to make thread-private pointers, bc thread_pot_args= pot_args; // these pointers are changed in parse_... thread_pot_tfuncs= pot_tfuncs; // ... parse_leapFuncArgs_Linear(npot,potentialArgs+ii*npot, &thread_pot_type,&thread_pot_args,&thread_pot_tfuncs); } //Integrate void (*odeint_func)(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *,int *); void (*odeint_deriv_func)(double, double *, double *, int,struct potentialArg *); switch ( odeint_type ) { case 0: //leapfrog odeint_func= &leapfrog; odeint_deriv_func= &evalLinearForce; dim= 1; break; case 1: //RK4 odeint_func= &bovy_rk4; odeint_deriv_func= &evalLinearDeriv; dim= 2; break; case 2: //RK6 odeint_func= &bovy_rk6; odeint_deriv_func= &evalLinearDeriv; dim= 2; break; case 3: //symplec4 odeint_func= &symplec4; odeint_deriv_func= &evalLinearForce; dim= 1; break; case 4: //symplec6 odeint_func= &symplec6; odeint_deriv_func= &evalLinearForce; dim= 1; break; case 5: //DOPR54 odeint_func= &bovy_dopr54; odeint_deriv_func= &evalLinearDeriv; dim= 2; break; case 6: //DOP853 odeint_func= &dop853; odeint_deriv_func= &evalLinearDeriv; dim= 2; break; } #pragma omp parallel for schedule(dynamic,ORBITS_CHUNKSIZE) private(ii) num_threads(max_threads) for (ii=0; ii < nobj; ii++) { odeint_func(odeint_deriv_func,dim,yo+2*ii,nt,dt,t, npot,potentialArgs+omp_get_thread_num()*npot,rtol,atol, result+2*nt*ii,err+ii); if ( cb ) // Callback if not void cb(); } //Free allocated memory #pragma omp parallel for schedule(static,1) private(ii) num_threads(max_threads) for (ii=0; ii < max_threads; ii++) free_potentialArgs(npot,potentialArgs+ii*npot); free(potentialArgs); //Done! } void evalLinearForce(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs){ *a= calcLinearForce(*q,t,nargs,potentialArgs); } void evalLinearDeriv(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs){ *a++= *(q+1); *a= calcLinearForce(*q,t,nargs,potentialArgs); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/orbit/orbit_c_ext/integratePlanarOrbit.c0000644000175100001710000010637214327773303023276 0ustar00runnerdocker/* Wrappers around the C integration code for planar Orbits */ #include #include #include #include #include #include #include #include #include //Potentials #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif #ifndef ORBITS_CHUNKSIZE #define ORBITS_CHUNKSIZE 1 #endif //Macros to export functions in DLL on different OS #if defined(_WIN32) #define EXPORT __declspec(dllexport) #elif defined(__GNUC__) #define EXPORT __attribute__((visibility("default"))) #else // Just do nothing? #define EXPORT #endif /* Function Declarations */ void evalPlanarRectForce(double, double *, double *, int, struct potentialArg *); void evalPlanarRectDeriv(double, double *, double *, int, struct potentialArg *); void evalPlanarRectDeriv_dxdv(double, double *, double *, int, struct potentialArg *); void initPlanarMovingObjectSplines(struct potentialArg *, double ** pot_args); /* Actual functions */ void parse_leapFuncArgs(int npot,struct potentialArg * potentialArgs, int ** pot_type, double ** pot_args, tfuncs_type_arr * pot_tfuncs){ int ii,jj; int nr; init_potentialArgs(npot,potentialArgs); for (ii=0; ii < npot; ii++){ switch ( *(*pot_type)++ ) { case 0: //LogarithmicHaloPotential, 4 arguments potentialArgs->potentialEval= &LogarithmicHaloPotentialEval; potentialArgs->planarRforce= &LogarithmicHaloPotentialPlanarRforce; potentialArgs->planarphitorque= &LogarithmicHaloPotentialPlanarphitorque; potentialArgs->planarR2deriv= &LogarithmicHaloPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &LogarithmicHaloPotentialPlanarphi2deriv; potentialArgs->planarRphideriv= &LogarithmicHaloPotentialPlanarRphideriv; potentialArgs->nargs= 4; potentialArgs->ntfuncs= 0; break; case 1: //DehnenBarPotential, 6 arguments potentialArgs->planarRforce= &DehnenBarPotentialPlanarRforce; potentialArgs->planarphitorque= &DehnenBarPotentialPlanarphitorque; potentialArgs->planarR2deriv= &DehnenBarPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &DehnenBarPotentialPlanarphi2deriv; potentialArgs->planarRphideriv= &DehnenBarPotentialPlanarRphideriv; potentialArgs->nargs= 6; potentialArgs->ntfuncs= 0; break; case 2: //TransientLogSpiralPotential, 8 arguments potentialArgs->planarRforce= &TransientLogSpiralPotentialRforce; potentialArgs->planarphitorque= &TransientLogSpiralPotentialphitorque; potentialArgs->nargs= 8; potentialArgs->ntfuncs= 0; break; case 3: //SteadyLogSpiralPotential, 8 arguments potentialArgs->planarRforce= &SteadyLogSpiralPotentialRforce; potentialArgs->planarphitorque= &SteadyLogSpiralPotentialphitorque; potentialArgs->nargs= 8; potentialArgs->ntfuncs= 0; break; case 4: //EllipticalDiskPotential, 6 arguments potentialArgs->planarRforce= &EllipticalDiskPotentialRforce; potentialArgs->planarphitorque= &EllipticalDiskPotentialphitorque; potentialArgs->planarR2deriv= &EllipticalDiskPotentialR2deriv; potentialArgs->planarphi2deriv= &EllipticalDiskPotentialphi2deriv; potentialArgs->planarRphideriv= &EllipticalDiskPotentialRphideriv; potentialArgs->nargs= 6; potentialArgs->ntfuncs= 0; break; case 5: //MiyamotoNagaiPotential, 3 arguments potentialArgs->potentialEval= &MiyamotoNagaiPotentialEval; potentialArgs->planarRforce= &MiyamotoNagaiPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &MiyamotoNagaiPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; break; case 6: //LopsidedDiskPotential, 4 arguments potentialArgs->planarRforce= &LopsidedDiskPotentialRforce; potentialArgs->planarphitorque= &LopsidedDiskPotentialphitorque; potentialArgs->planarR2deriv= &LopsidedDiskPotentialR2deriv; potentialArgs->planarphi2deriv= &LopsidedDiskPotentialphi2deriv; potentialArgs->planarRphideriv= &LopsidedDiskPotentialRphideriv; potentialArgs->nargs= 4; potentialArgs->ntfuncs= 0; break; case 7: //PowerSphericalPotential, 2 arguments potentialArgs->potentialEval= &PowerSphericalPotentialEval; potentialArgs->planarRforce= &PowerSphericalPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &PowerSphericalPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; break; case 8: //HernquistPotential, 2 arguments potentialArgs->potentialEval= &HernquistPotentialEval; potentialArgs->planarRforce= &HernquistPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &HernquistPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; break; case 9: //NFWPotential, 2 arguments potentialArgs->potentialEval= &NFWPotentialEval; potentialArgs->planarRforce= &NFWPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &NFWPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; break; case 10: //JaffePotential, 2 arguments potentialArgs->potentialEval= &JaffePotentialEval; potentialArgs->planarRforce= &JaffePotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &JaffePotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; break; case 11: //DoubleExponentialDiskPotential, XX arguments potentialArgs->potentialEval= &DoubleExponentialDiskPotentialEval; potentialArgs->planarRforce= &DoubleExponentialDiskPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; //potentialArgs->planarR2deriv= &DoubleExponentialDiskPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; //Look at pot_args to figure out the number of arguments potentialArgs->nargs= (int) (5 + 4 * *(*pot_args+4) ); potentialArgs->ntfuncs= 0; break; case 12: //FlattenedPowerPotential, 4 arguments potentialArgs->potentialEval= &FlattenedPowerPotentialEval; potentialArgs->planarRforce= &FlattenedPowerPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &FlattenedPowerPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; break; case 14: //IsochronePotential, 2 arguments potentialArgs->potentialEval= &IsochronePotentialEval; potentialArgs->planarRforce= &IsochronePotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &IsochronePotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; break; case 15: //PowerSphericalPotentialwCutoff, 3 arguments potentialArgs->potentialEval= &PowerSphericalPotentialwCutoffEval; potentialArgs->planarRforce= &PowerSphericalPotentialwCutoffPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &PowerSphericalPotentialwCutoffPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; break; case 16: //KuzminKutuzovStaeckelPotential, 3 arguments potentialArgs->potentialEval= &KuzminKutuzovStaeckelPotentialEval; potentialArgs->planarRforce= &KuzminKutuzovStaeckelPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &KuzminKutuzovStaeckelPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; break; case 17: //PlummerPotential, 2 arguments potentialArgs->potentialEval= &PlummerPotentialEval; potentialArgs->planarRforce= &PlummerPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &PlummerPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; break; case 18: //PseudoIsothermalPotential, 2 arguments potentialArgs->potentialEval= &PseudoIsothermalPotentialEval; potentialArgs->planarRforce= &PseudoIsothermalPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &PseudoIsothermalPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; break; case 19: //KuzminDiskPotential, 2 arguments potentialArgs->potentialEval= &KuzminDiskPotentialEval; potentialArgs->planarRforce= &KuzminDiskPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &KuzminDiskPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; break; case 20: //BurkertPotential, 2 arguments potentialArgs->potentialEval= &BurkertPotentialEval; potentialArgs->planarRforce= &BurkertPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &BurkertPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; break; case 21: // TriaxialHernquistPotential, lots of arguments potentialArgs->planarRforce = &EllipsoidalPotentialPlanarRforce; potentialArgs->planarphitorque = &EllipsoidalPotentialPlanarphitorque; //potentialArgs->planarR2deriv = &EllipsoidalPotentialPlanarR2deriv; //potentialArgs->planarphi2deriv = &EllipsoidalPotentialPlanarphi2deriv; //potentialArgs->planarRphideriv = &EllipsoidalPotentialPlanarRphideriv; // Also assign functions specific to EllipsoidalPotential potentialArgs->psi= &TriaxialHernquistPotentialpsi; potentialArgs->mdens= &TriaxialHernquistPotentialmdens; potentialArgs->mdensDeriv= &TriaxialHernquistPotentialmdensDeriv; potentialArgs->nargs = (int) (21 + *(*pot_args+7) + 2 * *(*pot_args + (int) (*(*pot_args+7) + 20))); potentialArgs->ntfuncs= 0; break; case 22: // TriaxialNFWPotential, lots of arguments potentialArgs->planarRforce = &EllipsoidalPotentialPlanarRforce; potentialArgs->planarphitorque = &EllipsoidalPotentialPlanarphitorque; //potentialArgs->planarR2deriv = &EllipsoidalPotentialPlanarR2deriv; //potentialArgs->planarphi2deriv = &EllipsoidalPotentialPlanarphi2deriv; //potentialArgs->planarRphideriv = &EllipsoidalPotentialPlanarRphideriv; // Also assign functions specific to EllipsoidalPotential potentialArgs->psi= &TriaxialNFWPotentialpsi; potentialArgs->mdens= &TriaxialNFWPotentialmdens; potentialArgs->mdensDeriv= &TriaxialNFWPotentialmdensDeriv; potentialArgs->nargs = (int) (21 + *(*pot_args+7) + 2 * *(*pot_args + (int) (*(*pot_args+7) + 20))); potentialArgs->ntfuncs= 0; break; case 23: // TriaxialJaffePotential, lots of arguments potentialArgs->planarRforce = &EllipsoidalPotentialPlanarRforce; potentialArgs->planarphitorque = &EllipsoidalPotentialPlanarphitorque; //potentialArgs->planarR2deriv = &EllipsoidalPotentialPlanarR2deriv; //potentialArgs->planarphi2deriv = &EllipsoidalPotentialPlanarphi2deriv; //potentialArgs->planarRphideriv = &EllipsoidalPotentialPlanarRphideriv; // Also assign functions specific to EllipsoidalPotential potentialArgs->psi= &TriaxialJaffePotentialpsi; potentialArgs->mdens= &TriaxialJaffePotentialmdens; potentialArgs->mdensDeriv= &TriaxialJaffePotentialmdensDeriv; potentialArgs->nargs = (int) (21 + *(*pot_args+7) + 2 * *(*pot_args + (int) (*(*pot_args+7) + 20))); potentialArgs->ntfuncs= 0; break; case 24: //SCFPotential, many arguments potentialArgs->potentialEval= &SCFPotentialEval; potentialArgs->planarRforce= &SCFPotentialPlanarRforce; potentialArgs->planarphitorque= &SCFPotentialPlanarphitorque; potentialArgs->planarR2deriv= &SCFPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &SCFPotentialPlanarphi2deriv; potentialArgs->planarRphideriv= &SCFPotentialPlanarRphideriv; potentialArgs->nargs= (int) (5 + (1 + *(*pot_args + 1)) * *(*pot_args+2) * *(*pot_args+3)* *(*pot_args+4) + 7); potentialArgs->ntfuncs= 0; break; case 25: //SoftenedNeedleBarPotential, 13 arguments potentialArgs->potentialEval= &SoftenedNeedleBarPotentialEval; potentialArgs->planarRforce= &SoftenedNeedleBarPotentialPlanarRforce; potentialArgs->planarphitorque= &SoftenedNeedleBarPotentialPlanarphitorque; potentialArgs->nargs= 13; potentialArgs->ntfuncs= 0; break; case 26: //DiskSCFPotential, nsigma+3 arguments potentialArgs->potentialEval= &DiskSCFPotentialEval; potentialArgs->planarRforce= &DiskSCFPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->nargs= (int) **pot_args + 3; potentialArgs->ntfuncs= 0; break; case 27: // SpiralArmsPotential, 10 arguments + array of Cs potentialArgs->planarRforce = &SpiralArmsPotentialPlanarRforce; potentialArgs->planarphitorque = &SpiralArmsPotentialPlanarphitorque; potentialArgs->planarR2deriv = &SpiralArmsPotentialPlanarR2deriv; potentialArgs->planarphi2deriv = &SpiralArmsPotentialPlanarphi2deriv; potentialArgs->planarRphideriv = &SpiralArmsPotentialPlanarRphideriv; potentialArgs->nargs = (int) 10 + **pot_args; potentialArgs->ntfuncs= 0; break; case 28: //CosmphiDiskPotential, 9 arguments potentialArgs->planarRforce= &CosmphiDiskPotentialRforce; potentialArgs->planarphitorque= &CosmphiDiskPotentialphitorque; potentialArgs->planarR2deriv= &CosmphiDiskPotentialR2deriv; potentialArgs->planarphi2deriv= &CosmphiDiskPotentialphi2deriv; potentialArgs->planarRphideriv= &CosmphiDiskPotentialRphideriv; potentialArgs->nargs= 9; potentialArgs->ntfuncs= 0; break; case 29: //HenonHeilesPotential, 1 argument potentialArgs->planarRforce= &HenonHeilesPotentialRforce; potentialArgs->planarphitorque= &HenonHeilesPotentialphitorque; potentialArgs->planarR2deriv= &HenonHeilesPotentialR2deriv; potentialArgs->planarphi2deriv= &HenonHeilesPotentialphi2deriv; potentialArgs->planarRphideriv= &HenonHeilesPotentialRphideriv; potentialArgs->nargs= 1; potentialArgs->ntfuncs= 0; break; case 30: // PerfectEllipsoidPotential, lots of arguments potentialArgs->planarRforce = &EllipsoidalPotentialPlanarRforce; potentialArgs->planarphitorque = &EllipsoidalPotentialPlanarphitorque; //potentialArgs->planarR2deriv = &EllipsoidalPotentialPlanarR2deriv; //potentialArgs->planarphi2deriv = &EllipsoidalPotentialPlanarphi2deriv; //potentialArgs->planarRphideriv = &EllipsoidalPotentialPlanarRphideriv; // Also assign functions specific to EllipsoidalPotential potentialArgs->psi= &PerfectEllipsoidPotentialpsi; potentialArgs->mdens= &PerfectEllipsoidPotentialmdens; potentialArgs->mdensDeriv= &PerfectEllipsoidPotentialmdensDeriv; potentialArgs->nargs = (int) (21 + *(*pot_args+7) + 2 * *(*pot_args + (int) (*(*pot_args+7) + 20))); potentialArgs->ntfuncs= 0; break; // 31: KGPotential // 32: IsothermalDiskPotential case 33: //DehnenCoreSphericalpotential potentialArgs->potentialEval= &DehnenCoreSphericalPotentialEval; potentialArgs->planarRforce= &DehnenCoreSphericalPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &DehnenCoreSphericalPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 2; potentialArgs->ntfuncs= 0; break; case 34: //DehnenSphericalpotential potentialArgs->potentialEval= &DehnenSphericalPotentialEval; potentialArgs->planarRforce= &DehnenSphericalPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &DehnenSphericalPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; break; case 35: //HomogeneousSpherePotential, 3 arguments potentialArgs->potentialEval= &HomogeneousSpherePotentialEval; potentialArgs->planarRforce= &HomogeneousSpherePotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &HomogeneousSpherePotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; break; case 36: //interpSphericalPotential, XX arguments // Set up 1 spline in potentialArgs potentialArgs->nspline1d= 1; potentialArgs->spline1d= (gsl_spline **) \ malloc ( potentialArgs->nspline1d*sizeof ( gsl_spline *) ); potentialArgs->acc1d= (gsl_interp_accel **) \ malloc ( potentialArgs->nspline1d * sizeof ( gsl_interp_accel * ) ); // allocate accelerator *potentialArgs->acc1d= gsl_interp_accel_alloc(); // Set up interpolater nr= (int) **pot_args; *potentialArgs->spline1d= gsl_spline_alloc(gsl_interp_cspline,nr); gsl_spline_init(*potentialArgs->spline1d,*pot_args+1,*pot_args+1+nr,nr); *pot_args+= 2*nr+1; // Bind forces potentialArgs->potentialEval= &SphericalPotentialEval; potentialArgs->planarRforce = &SphericalPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &SphericalPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; // Also assign functions specific to SphericalPotential potentialArgs->revaluate= &interpSphericalPotentialrevaluate; potentialArgs->rforce= &interpSphericalPotentialrforce; potentialArgs->r2deriv= &interpSphericalPotentialr2deriv; potentialArgs->nargs = 6; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 37: // TriaxialGaussianPotential, lots of arguments potentialArgs->planarRforce = &EllipsoidalPotentialPlanarRforce; potentialArgs->planarphitorque = &EllipsoidalPotentialPlanarphitorque; //potentialArgs->planarR2deriv = &EllipsoidalPotentialPlanarR2deriv; //potentialArgs->planarphi2deriv = &EllipsoidalPotentialPlanarphi2deriv; //potentialArgs->planarRphideriv = &EllipsoidalPotentialPlanarRphideriv; // Also assign functions specific to EllipsoidalPotential potentialArgs->psi= &TriaxialGaussianPotentialpsi; potentialArgs->mdens= &TriaxialGaussianPotentialmdens; potentialArgs->mdensDeriv= &TriaxialGaussianPotentialmdensDeriv; potentialArgs->nargs = (int) (21 + *(*pot_args+7) + 2 * *(*pot_args + (int) (*(*pot_args+7) + 20))); potentialArgs->ntfuncs= 0; break; case 38: // PowerTriaxialPotential, lots of arguments potentialArgs->planarRforce = &EllipsoidalPotentialPlanarRforce; potentialArgs->planarphitorque = &EllipsoidalPotentialPlanarphitorque; //potentialArgs->planarR2deriv = &EllipsoidalPotentialPlanarR2deriv; //potentialArgs->planarphi2deriv = &EllipsoidalPotentialPlanarphi2deriv; //potentialArgs->planarRphideriv = &EllipsoidalPotentialPlanarRphideriv; // Also assign functions specific to EllipsoidalPotential potentialArgs->psi= &PowerTriaxialPotentialpsi; potentialArgs->mdens= &PowerTriaxialPotentialmdens; potentialArgs->mdensDeriv= &PowerTriaxialPotentialmdensDeriv; potentialArgs->nargs = (int) (21 + *(*pot_args+7) + 2 * *(*pot_args + (int) (*(*pot_args+7) + 20))); potentialArgs->ntfuncs= 0; break; case 40: //NullPotential, no arguments (only supported for orbit int) potentialArgs->potentialEval= &ZeroForce; potentialArgs->planarRforce= &ZeroPlanarForce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &ZeroPlanarForce; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 0; potentialArgs->ntfuncs= 0; break; //////////////////////////////// WRAPPERS ///////////////////////////////////// case -1: //DehnenSmoothWrapperPotential potentialArgs->potentialEval= &DehnenSmoothWrapperPotentialEval; potentialArgs->planarRforce= &DehnenSmoothWrapperPotentialPlanarRforce; potentialArgs->planarphitorque= &DehnenSmoothWrapperPotentialPlanarphitorque; potentialArgs->planarR2deriv= &DehnenSmoothWrapperPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &DehnenSmoothWrapperPotentialPlanarphi2deriv; potentialArgs->planarRphideriv= &DehnenSmoothWrapperPotentialPlanarRphideriv; potentialArgs->nargs= 4; potentialArgs->ntfuncs= 0; break; case -2: //SolidBodyRotationWrapperPotential potentialArgs->planarRforce= &SolidBodyRotationWrapperPotentialPlanarRforce; potentialArgs->planarphitorque= &SolidBodyRotationWrapperPotentialPlanarphitorque; potentialArgs->planarR2deriv= &SolidBodyRotationWrapperPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &SolidBodyRotationWrapperPotentialPlanarphi2deriv; potentialArgs->planarRphideriv= &SolidBodyRotationWrapperPotentialPlanarRphideriv; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; break; case -4: //CorotatingRotationWrapperPotential potentialArgs->planarRforce= &CorotatingRotationWrapperPotentialPlanarRforce; potentialArgs->planarphitorque= &CorotatingRotationWrapperPotentialPlanarphitorque; potentialArgs->planarR2deriv= &CorotatingRotationWrapperPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &CorotatingRotationWrapperPotentialPlanarphi2deriv; potentialArgs->planarRphideriv= &CorotatingRotationWrapperPotentialPlanarRphideriv; potentialArgs->nargs= 5; potentialArgs->ntfuncs= 0; break; case -5: //GaussianAmplitudeWrapperPotential potentialArgs->planarRforce= &GaussianAmplitudeWrapperPotentialPlanarRforce; potentialArgs->planarphitorque= &GaussianAmplitudeWrapperPotentialPlanarphitorque; potentialArgs->planarR2deriv= &GaussianAmplitudeWrapperPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &GaussianAmplitudeWrapperPotentialPlanarphi2deriv; potentialArgs->planarRphideriv= &GaussianAmplitudeWrapperPotentialPlanarRphideriv; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; break; case -6: //MovingObjectPotential potentialArgs->planarRforce= &MovingObjectPotentialPlanarRforce; potentialArgs->planarphitorque= &MovingObjectPotentialPlanarphitorque; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; break; //ChandrasekharDynamicalFrictionForce omitted, bc no planar version //RotateAndTiltWrapperPotential omitted, bc no planar version case -9: //TimeDependentAmplitudeWrapperPotential potentialArgs->potentialEval= &TimeDependentAmplitudeWrapperPotentialEval; potentialArgs->planarRforce= &TimeDependentAmplitudeWrapperPotentialPlanarRforce; potentialArgs->planarphitorque= &TimeDependentAmplitudeWrapperPotentialPlanarphitorque; potentialArgs->planarR2deriv= &TimeDependentAmplitudeWrapperPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &TimeDependentAmplitudeWrapperPotentialPlanarphi2deriv; potentialArgs->planarRphideriv= &TimeDependentAmplitudeWrapperPotentialPlanarRphideriv; potentialArgs->nargs= 4; potentialArgs->ntfuncs= 1; break; } int setupSplines = *(*pot_type-1) == -6 ? 1 : 0; if ( *(*pot_type-1) < 0) { // Parse wrapped potential for wrappers potentialArgs->nwrapped= (int) *(*pot_args)++; potentialArgs->wrappedPotentialArg= \ (struct potentialArg *) malloc ( potentialArgs->nwrapped \ * sizeof (struct potentialArg) ); parse_leapFuncArgs(potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg, pot_type,pot_args,pot_tfuncs); } if (setupSplines) initPlanarMovingObjectSplines(potentialArgs, pot_args); // Now load each potential's parameters potentialArgs->args= (double *) malloc( potentialArgs->nargs * sizeof(double)); for (jj=0; jj < potentialArgs->nargs; jj++){ *(potentialArgs->args)= *(*pot_args)++; potentialArgs->args++; } potentialArgs->args-= potentialArgs->nargs; // and load each potential's time functions if ( potentialArgs->ntfuncs > 0 ) { potentialArgs->tfuncs= (*pot_tfuncs); (*pot_tfuncs)+= potentialArgs->ntfuncs; } potentialArgs++; } potentialArgs-= npot; } EXPORT void integratePlanarOrbit(int nobj, double *yo, int nt, double *t, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double dt, double rtol, double atol, double *result, int * err, int odeint_type, orbint_callback_type cb){ //Set up the forces, first count int ii,jj; int dim; int max_threads; int * thread_pot_type; double * thread_pot_args; tfuncs_type_arr thread_pot_tfuncs; max_threads= ( nobj < omp_get_max_threads() ) ? nobj : omp_get_max_threads(); // Because potentialArgs may cache, safest to have one / thread struct potentialArg * potentialArgs= (struct potentialArg *) malloc ( max_threads * npot * sizeof (struct potentialArg) ); #pragma omp parallel for schedule(static,1) private(ii,thread_pot_type,thread_pot_args,thread_pot_tfuncs) num_threads(max_threads) for (ii=0; ii < max_threads; ii++) { thread_pot_type= pot_type; // need to make thread-private pointers, bc thread_pot_args= pot_args; // these pointers are changed in parse_... thread_pot_tfuncs= pot_tfuncs; // ... parse_leapFuncArgs(npot,potentialArgs+ii*npot, &thread_pot_type,&thread_pot_args,&thread_pot_tfuncs); } //Integrate void (*odeint_func)(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *,int *); void (*odeint_deriv_func)(double, double *, double *, int,struct potentialArg *); switch ( odeint_type ) { case 0: //leapfrog odeint_func= &leapfrog; odeint_deriv_func= &evalPlanarRectForce; dim= 2; break; case 1: //RK4 odeint_func= &bovy_rk4; odeint_deriv_func= &evalPlanarRectDeriv; dim= 4; break; case 2: //RK6 odeint_func= &bovy_rk6; odeint_deriv_func= &evalPlanarRectDeriv; dim= 4; break; case 3: //symplec4 odeint_func= &symplec4; odeint_deriv_func= &evalPlanarRectForce; dim= 2; break; case 4: //symplec6 odeint_func= &symplec6; odeint_deriv_func= &evalPlanarRectForce; dim= 2; break; case 5: //DOPR54 odeint_func= &bovy_dopr54; odeint_deriv_func= &evalPlanarRectDeriv; dim= 4; break; case 6: //DOP853 odeint_func= &dop853; odeint_deriv_func= &evalPlanarRectDeriv; dim= 4; break; } #pragma omp parallel for schedule(dynamic,ORBITS_CHUNKSIZE) private(ii,jj) num_threads(max_threads) for (ii=0; ii < nobj; ii++) { polar_to_rect_galpy(yo+4*ii); odeint_func(odeint_deriv_func,dim,yo+4*ii,nt,dt,t, npot,potentialArgs+omp_get_thread_num()*npot,rtol,atol, result+4*nt*ii,err+ii); for (jj= 0; jj < nt; jj++) rect_to_polar_galpy(result+4*jj+4*nt*ii); if ( cb ) // Callback if not void cb(); } //Free allocated memory #pragma omp parallel for schedule(static,1) private(ii) num_threads(max_threads) for (ii=0; ii < max_threads; ii++) free_potentialArgs(npot,potentialArgs+ii*npot); free(potentialArgs); //Done! } EXPORT void integratePlanarOrbit_dxdv(double *yo, int nt, double *t, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double dt, double rtol, double atol, double *result, int * err, int odeint_type, orbint_callback_type cb){ //Set up the forces, first count int dim; struct potentialArg * potentialArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs(npot,potentialArgs,&pot_type,&pot_args,&pot_tfuncs); //Integrate void (*odeint_func)(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *,int *); void (*odeint_deriv_func)(double, double *, double *, int,struct potentialArg *); switch ( odeint_type ) { case 1: //RK4 odeint_func= &bovy_rk4; odeint_deriv_func= &evalPlanarRectDeriv_dxdv; dim= 8; break; case 2: //RK6 odeint_func= &bovy_rk6; odeint_deriv_func= &evalPlanarRectDeriv_dxdv; dim= 8; break; case 5: //DOPR54 odeint_func= &bovy_dopr54; odeint_deriv_func= &evalPlanarRectDeriv_dxdv; dim= 8; break; case 6: //DOP853 odeint_func= &dop853; odeint_deriv_func= &evalPlanarRectDeriv_dxdv; dim= 8; break; } odeint_func(odeint_deriv_func,dim,yo,nt,dt,t,npot,potentialArgs,rtol,atol, result,err); //Free allocated memory free_potentialArgs(npot,potentialArgs); free(potentialArgs); //Done! } void evalPlanarRectForce(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs){ double sinphi, cosphi, x, y, phi,R,Rforce,phitorque; //q is rectangular so calculate R and phi x= *q; y= *(q+1); R= sqrt(x*x+y*y); phi= acos(x/R); sinphi= y/R; cosphi= x/R; if ( y < 0. ) phi= 2.*M_PI-phi; //Calculate the forces Rforce= calcPlanarRforce(R,phi,t,nargs,potentialArgs); phitorque= calcPlanarphitorque(R,phi,t,nargs,potentialArgs); *a++= cosphi*Rforce-1./R*sinphi*phitorque; *a--= sinphi*Rforce+1./R*cosphi*phitorque; } void evalPlanarRectDeriv(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs){ double sinphi, cosphi, x, y, phi,R,Rforce,phitorque; //first two derivatives are just the velocities *a++= *(q+2); *a++= *(q+3); //Rest is force //q is rectangular so calculate R and phi x= *q; y= *(q+1); R= sqrt(x*x+y*y); phi= acos(x/R); sinphi= y/R; cosphi= x/R; if ( y < 0. ) phi= 2.*M_PI-phi; //Calculate the forces Rforce= calcPlanarRforce(R,phi,t,nargs,potentialArgs); phitorque= calcPlanarphitorque(R,phi,t,nargs,potentialArgs); *a++= cosphi*Rforce-1./R*sinphi*phitorque; *a= sinphi*Rforce+1./R*cosphi*phitorque; } void evalPlanarRectDeriv_dxdv(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs){ double sinphi, cosphi, x, y, phi,R,Rforce,phitorque; double R2deriv, phi2deriv, Rphideriv, dFxdx, dFxdy, dFydx, dFydy; //first two derivatives are just the velocities *a++= *(q+2); *a++= *(q+3); //Rest is force //q is rectangular so calculate R and phi x= *q; y= *(q+1); R= sqrt(x*x+y*y); phi= acos(x/R); sinphi= y/R; cosphi= x/R; if ( y < 0. ) phi= 2.*M_PI-phi; //Calculate the forces Rforce= calcPlanarRforce(R,phi,t,nargs,potentialArgs); phitorque= calcPlanarphitorque(R,phi,t,nargs,potentialArgs); *a++= cosphi*Rforce-1./R*sinphi*phitorque; *a++= sinphi*Rforce+1./R*cosphi*phitorque; //dx derivatives are just dv *a++= *(q+6); *a++= *(q+7); //for the dv derivatives we need also R2deriv, phi2deriv, and Rphideriv R2deriv= calcPlanarR2deriv(R,phi,t,nargs,potentialArgs); phi2deriv= calcPlanarphi2deriv(R,phi,t,nargs,potentialArgs); Rphideriv= calcPlanarRphideriv(R,phi,t,nargs,potentialArgs); //..and dFxdx, dFxdy, dFydx, dFydy dFxdx= -cosphi*cosphi*R2deriv +2.*cosphi*sinphi/R/R*phitorque +sinphi*sinphi/R*Rforce +2.*sinphi*cosphi/R*Rphideriv -sinphi*sinphi/R/R*phi2deriv; dFxdy= -sinphi*cosphi*R2deriv +(sinphi*sinphi-cosphi*cosphi)/R/R*phitorque -cosphi*sinphi/R*Rforce -(cosphi*cosphi-sinphi*sinphi)/R*Rphideriv +cosphi*sinphi/R/R*phi2deriv; dFydx= -cosphi*sinphi*R2deriv +(sinphi*sinphi-cosphi*cosphi)/R/R*phitorque +(sinphi*sinphi-cosphi*cosphi)/R*Rphideriv -sinphi*cosphi/R*Rforce +sinphi*cosphi/R/R*phi2deriv; dFydy= -sinphi*sinphi*R2deriv -2.*sinphi*cosphi/R/R*phitorque -2.*sinphi*cosphi/R*Rphideriv +cosphi*cosphi/R*Rforce -cosphi*cosphi/R/R*phi2deriv; *a++= dFxdx * *(q+4) + dFxdy * *(q+5); *a= dFydx * *(q+4) + dFydy * *(q+5); } void initPlanarMovingObjectSplines(struct potentialArg * potentialArgs, double ** pot_args){ gsl_interp_accel *x_accel_ptr = gsl_interp_accel_alloc(); gsl_interp_accel *y_accel_ptr = gsl_interp_accel_alloc(); int nPts = (int) **pot_args; gsl_spline *x_spline = gsl_spline_alloc(gsl_interp_cspline, nPts); gsl_spline *y_spline = gsl_spline_alloc(gsl_interp_cspline, nPts); double * t_arr = *pot_args+1; double * x_arr = t_arr+1*nPts; double * y_arr = t_arr+2*nPts; double * t= (double *) malloc ( nPts * sizeof (double) ); double tf = *(t_arr+3*nPts+2); double to = *(t_arr+3*nPts+1); int ii; for (ii=0; ii < nPts; ii++) *(t+ii) = (t_arr[ii]-to)/(tf-to); gsl_spline_init(x_spline, t, x_arr, nPts); gsl_spline_init(y_spline, t, y_arr, nPts); potentialArgs->nspline1d= 2; potentialArgs->spline1d= (gsl_spline **) malloc ( 2*sizeof ( gsl_spline *) ); potentialArgs->acc1d= (gsl_interp_accel **) \ malloc ( 2 * sizeof ( gsl_interp_accel * ) ); *potentialArgs->spline1d = x_spline; *potentialArgs->acc1d = x_accel_ptr; *(potentialArgs->spline1d+1)= y_spline; *(potentialArgs->acc1d+1)= y_accel_ptr; *pot_args = *pot_args+ (int) (1+3*nPts); free(t); } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667233486.7134671 galpy-1.8.1/galpy/potential/0000755000175100001710000000000014327773317015365 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/AdiabaticContractionWrapperPotential.py0000644000175100001710000001567314327773303025234 0ustar00runnerdocker############################################################################### # AdiabaticContractionWrapperPotential.py: Wrapper to adiabatically # contract a DM halo in response # to the growth of a baryonic # component ############################################################################### import numpy from scipy import integrate from scipy.interpolate import interp1d from scipy.optimize import fixed_point from ..util import conversion from .Force import Force from .interpSphericalPotential import interpSphericalPotential # Note: not actually implemented as a WrapperPotential! class AdiabaticContractionWrapperPotential(interpSphericalPotential): """AdiabaticContractionWrapperPotential: Wrapper to adiabatically contract a DM halo in response to the growth of a baryonic component. Use for example as:: dm= AdiabaticContractionWrapperPotential(pot=MWPotential2014[2],baryonpot=MWPotential2014[:2]) to contract the dark-matter halo in MWPotential2014 according to the baryon distribution within it. The basic physics of the adiabatic contraction is that a fraction `f_bar` of the mass in the original potential `pot` cools adiabatically to form a baryonic component `baryonpot`; this wrapper computes the resulting dark-matter potential using different approximations in the literature. """ def __init__(self,amp=1.,pot=None,baryonpot=None, method='cautun',f_bar=0.157,rmin=None,rmax=50., ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a AdiabaticContractionWrapper Potential INPUT: amp - amplitude to be applied to the potential (default: 1.) pot - Potential instance or list thereof representing the density that is adiabatically contracted baryonpot - Potential instance or list thereof representing the density of baryons whose growth causes the contraction method= ('cautun') Type of adiabatic-contraction formula: * 'cautun' for that from Cautun et al. 2020 (`2020MNRAS.494.4291C `__), * 'blumenthal' for that from Blumenthal et al. 1986 (`1986ApJ...301...27B 1986ApJ...301...27B `__) * 'gnedin' for that from Gnedin et al. 2004 (`2004ApJ...616...16G `__) f_bar= (0.157) universal baryon fraction; if None, calculated from pot and baryonpot assuming that at rmax the halo contains the universal baryon fraction; leave this at the default value unless you know what you are doing rmin= (None) minimum radius to consider (default: rmax/2500; don't set this to zero) rmax= (50.) maximum radius to consider (can be Quantity) ro, vo= standard unit-conversion parameters OUTPUT: (none) HISTORY: 2021-03-21 - Started based on Marius Cautun's code - Bovy (UofT) """ # Initialize with Force just to parse (ro,vo) Force.__init__(self,ro=ro,vo=vo) rmax= conversion.parse_length(rmax,ro=self._ro) rmin= conversion.parse_length(rmin,ro=self._ro) if not rmin is None \ else rmax/2500. # Compute baryon and DM enclosed masses on radial grid from ..potential import mass rgrid= numpy.geomspace(rmin,rmax,301) baryon_mass= numpy.array([mass(baryonpot,r,use_physical=False) for r in rgrid]) dm_mass= numpy.array([mass(pot,r,use_physical=False) for r in rgrid]) # Adiabatic contraction if f_bar is None: f_bar= baryon_mass[-1]/(baryon_mass[-1]+dm_mass[-1]) if method.lower() == 'cautun': new_rforce= _contraction_Cautun2020(rgrid,dm_mass,baryon_mass, f_bar) elif method.lower() == 'gnedin': new_rforce= \ _contraction_Gnedin2004(rgrid,dm_mass,baryon_mass, pot.rvir(overdens=180., wrtcrit=False), f_bar) elif method.lower() == 'blumenthal': new_rforce= _contraction_Blumenthal1986(rgrid,dm_mass, baryon_mass,f_bar) else: # pragma: no cover raise ValueError(f"Adiabatic contraction method '{method}' not recognized") # Add central point rgrid= numpy.concatenate(([0.],rgrid)) new_rforce= numpy.concatenate(([0.],new_rforce)) new_rforce_func= lambda r: -numpy.interp(r,rgrid,new_rforce) # Potential at zero = int_0^inf dr rforce, and enc. mass constant # outside of last rgrid point Phi0= integrate.quad(new_rforce_func,rgrid[0],rgrid[-1])[0]\ -new_rforce[-1]*rgrid[-1] interpSphericalPotential.__init__(self, rforce=new_rforce_func, rgrid=rgrid, Phi0=Phi0, ro=ro,vo=vo) def _contraction_Cautun2020(r,M_DMO,Mbar,fbar): # solve for the contracted enclosed DM mass func_M_DM_contract= lambda M: M_DMO*1.023*(M_DMO/(1.-fbar)/(M+Mbar))**-0.54 M_DM= fixed_point(func_M_DM_contract,M_DMO) return M_DM/M_DMO*M_DMO/r**2. def _contraction_Blumenthal1986(r,M_DMO,Mbar,fbar): # solve for the contracted radius 'rf' containing the same DM mass # as enclosed for r func_M_bar= interp1d(r,Mbar,bounds_error=False, fill_value=(Mbar[0],Mbar[-1]) ) func_r_contract= lambda rf: r*(M_DMO/(1.-fbar))/(M_DMO+func_M_bar(rf)) rf= fixed_point(func_r_contract,r) # now find how much the enclosed mass increased at r func_M_DM= interp1d(rf,M_DMO,bounds_error=False, fill_value=(M_DMO[0],M_DMO[-1])) return func_M_DM(r)/r**2. def _contraction_Gnedin2004(r,M_DMO,M_bar,Rvir,fbar): # solve for the contracted radius 'rf' containing the same DM mass # as enclosed for r func_M_bar= interp1d(r,M_bar,bounds_error=False, fill_value=(M_bar[0],M_bar[-1])) func_M_DMO= interp1d(r,M_DMO,bounds_error=False, fill_value=(M_DMO[0],M_DMO[-1])) A, w= 0.85, 0.8 func_r_mean= lambda ri: A*Rvir*(ri/Rvir)**w M_DMO_rmean= func_M_DMO(func_r_mean(r)) func_r_contract= lambda rf: r*(M_DMO_rmean/(1.-fbar))\ /(M_DMO_rmean+func_M_bar(func_r_mean(rf))) rf= fixed_point(func_r_contract,r) # now find how much the enclosed mass increased at r func_M_DM = interp1d(rf,M_DMO,bounds_error=False, fill_value=(M_DMO[0],M_DMO[-1])) return func_M_DM(r)/r**2. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/AnyAxisymmetricRazorThinDiskPotential.py0000644000175100001710000002314314327773303025417 0ustar00runnerdocker############################################################################### # AnyAxisymmetricRazorThinDiskPotential.py: class that implements the # potential of an arbitrary # axisymmetric, razor-thin disk ############################################################################### import numpy from scipy import integrate, special from ..util import conversion from ..util._optional_deps import _APY_LOADED from .Potential import Potential, check_potential_inputs_not_arrays if _APY_LOADED: from astropy import units class AnyAxisymmetricRazorThinDiskPotential(Potential): r"""Class that implements the potential of an arbitrary axisymmetric, razor-thin disk with surface density :math:`\Sigma(R)`""" def __init__(self,surfdens=lambda R: 1.5*numpy.exp(-3.*R),amp=1., normalize=False,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize the potential of an arbitrary axisymmetric disk INPUT: surfdens= (1.5 e^[-R/0.3]) function of a single variable that gives the surface density as a function of radius (can return a Quantity) amp= (1.) amplitude to be applied to the potential normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: AnyAxisymmetricRazorThinDiskPotential object HISTORY: 2021-01-04 - Written - Bovy (UofT) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo) # Parse surface density: does it have units? does it expect them? if _APY_LOADED: _sdens_unit_input= False try: surfdens(1) except (units.UnitConversionError,units.UnitTypeError): _sdens_unit_input= True _sdens_unit_output= False if _sdens_unit_input: try: surfdens(1.*units.kpc).to(units.Msun/units.pc**2) except (AttributeError,units.UnitConversionError): pass else: _sdens_unit_output= True else: try: surfdens(1.).to(units.Msun/units.pc**2) except (AttributeError,units.UnitConversionError): pass else: _sdens_unit_output= True if _sdens_unit_input and _sdens_unit_output: self._sdens= lambda R: conversion.parse_surfdens(\ surfdens(R*self._ro*units.kpc), ro=self._ro,vo=self._vo) elif _sdens_unit_input: self._sdens= lambda R: surfdens(R*self._ro*units.kpc) elif _sdens_unit_output: self._sdens= lambda R: conversion.parse_surfdens(surfdens(R), ro=self._ro, vo=self._vo) if not hasattr(self,'_sdens'): # unitless self._sdens= surfdens # The potential at zero, in case it's asked for self._pot_zero= -2.*numpy.pi*integrate.quad(lambda a: self._sdens(a), 0,numpy.inf)[0] if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) @check_potential_inputs_not_arrays def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: potential at (R,z) HISTORY: 2021-01-04 - Written - Bovy (UofT) """ if R == 0 and z == 0: return self._pot_zero elif numpy.isinf(R**2+z**2): return 0. potint= lambda a: a*self._sdens(a)\ /numpy.sqrt((R+a)**2.+z**2.)*special.ellipk(4*R*a/((R+a)**2.+z**2.)) return -4*(integrate.quad(potint,0,2*R,points=[R])[0] +integrate.quad(potint,2*R,numpy.inf)[0]) @check_potential_inputs_not_arrays def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force at (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: F_R at (R,z) HISTORY: 2021-01-04 - Written - Bovy (UofT) """ R2= R**2 z2= z**2 def rforceint(a): a2= a**2 aRz= (a+R)**2.+z2 faRoveraRz= 4*a*R/aRz return a*self._sdens(a)\ *((a2-R2+z2)*special.ellipe(faRoveraRz) -((a-R)**2+z2)*special.ellipk(faRoveraRz))\ /R/((a-R)**2+z2)/numpy.sqrt(aRz) return 2*(integrate.quad(rforceint,0,2*R,points=[R])[0] +integrate.quad(rforceint,2*R,numpy.inf)[0]) @check_potential_inputs_not_arrays def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force at (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: F_z at (R,z) HISTORY: 2021-01-04 - Written - Bovy (UofT) """ if z == 0: return 0. z2= z**2 def zforceint(a): aRz= (a+R)**2.+z2 faRoveraRz= 4*a*R/aRz return a*self._sdens(a)\ *special.ellipe(faRoveraRz)/((a-R)**2+z2)/numpy.sqrt(aRz) return -4*z*(integrate.quad(zforceint,0,2*R,points=[R])[0] +integrate.quad(zforceint,2*R,numpy.inf)[0]) @check_potential_inputs_not_arrays def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the 2nd radial derivative at (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: d2 Phi / dR2 at (R,z) HISTORY: 2021-01-04 - Written - Bovy (UofT) """ R2= R**2 z2= z**2 def r2derivint(a): a2= a**2 aRz= (a+R)**2.+z2 faRoveraRz= 4*a*R/aRz return a*self._sdens(a)\ *(-(((a2-3.*R2)*(a2-R2)**2+(3.*a2**2+2.*a2*R2+3.*R2**2)*z2 +(3.*a2+7.*R2)*z**4+z**6)*special.ellipe(faRoveraRz)) +((a-R)**2+z2)*((a2-R2)**2+2.*(a2+2.*R2)*z2+z**4) *special.ellipk(faRoveraRz))\ /(2.*R2*((a-R)**2+z2)**2*((a+R)**2+z2)**1.5) return -4*(integrate.quad(r2derivint,0,2*R,points=[R])[0] +integrate.quad(r2derivint,2*R,numpy.inf)[0]) @check_potential_inputs_not_arrays def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the 2nd vertical derivative at (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: d2 Phi / dz2 at (R,z) HISTORY: 2021-01-04 - Written - Bovy (UofT) """ R2= R**2 z2= z**2 def z2derivint(a): a2= a**2 aRz= (a+R)**2.+z2 faRoveraRz= 4*a*R/aRz return a*self._sdens(a)\ *(-(((a2-R2)**2-2.*(a2+R2)*z2-3.*z**4)*special.ellipe(faRoveraRz)) -z2*((a-R)**2+z2)*special.ellipk(faRoveraRz))\ /(((a-R)**2+z2)**2*((a+R)**2+z2)**1.5) return -4*(integrate.quad(z2derivint,0,2*R,points=[R])[0] +integrate.quad(z2derivint,2*R,numpy.inf)[0]) @check_potential_inputs_not_arrays def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed radial, vertical derivative at (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: d2 Phi / dRdz at (R,z) HISTORY: 2021-01-04 - Written - Bovy (UofT) """ R2= R**2 z2= z**2 def rzderivint(a): a2= a**2 aRz= (a+R)**2.+z2 faRoveraRz= 4*a*R/aRz return a*self._sdens(a)\ *(-((a**4-7.*R**4-6.*R2*z2+z**4+2.*a2*(3.*R2+z2)) *special.ellipe(faRoveraRz)) +((a-R)**2+z**2)*(a2-R2+z2)*special.ellipk(faRoveraRz))\ /R/((a-R)**2+z2)**2/((a+R)**2+z2)**1.5 return -2*z*(integrate.quad(rzderivint,0,2*R,points=[R])[0] +integrate.quad(rzderivint,2*R,numpy.inf)[0]) def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: Sigma (R,z) HISTORY: 2021-01-04 - Written - Bovy (UofT) """ return self._sdens(R) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/AnySphericalPotential.py0000644000175100001710000001117714327773303022203 0ustar00runnerdocker############################################################################### # AnySphericalPotential: Potential of an arbitrary spherical density ############################################################################### import numpy from scipy import integrate from ..util import conversion from ..util._optional_deps import _APY_LOADED from .SphericalPotential import SphericalPotential if _APY_LOADED: from astropy import units class AnySphericalPotential(SphericalPotential): """Class that implements the potential of an arbitrary spherical density distribution :math:`\\rho(r)`""" def __init__(self,dens=lambda r: 0.64/r/(1+r)**3,amp=1.,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize the potential of an arbitrary spherical density distribution INPUT: dens= (0.64/r/(1+r)**3) function of a single variable that gives the density as a function of radius (can return a Quantity) amp= (1.) amplitude to be applied to the potential normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2021-01-05 - Written - Bovy (UofT) """ SphericalPotential.__init__(self,amp=amp,ro=ro,vo=vo) # Parse density: does it have units? does it expect them? if _APY_LOADED: _dens_unit_input= False try: dens(1) except (units.UnitConversionError,units.UnitTypeError): _dens_unit_input= True _dens_unit_output= False if _dens_unit_input: try: dens(1.*units.kpc).to(units.Msun/units.pc**3) except (AttributeError,units.UnitConversionError): pass else: _dens_unit_output= True else: try: dens(1.).to(units.Msun/units.pc**3) except (AttributeError,units.UnitConversionError): pass else: _dens_unit_output= True if _dens_unit_input and _dens_unit_output: self._rawdens= lambda R: conversion.parse_dens(\ dens(R*self._ro*units.kpc), ro=self._ro,vo=self._vo) elif _dens_unit_input: self._rawdens= lambda R: dens(R*self._ro*units.kpc) elif _dens_unit_output: self._rawdens= lambda R: conversion.parse_dens(dens(R), ro=self._ro, vo=self._vo) if not hasattr(self,'_rawdens'): # unitless self._rawdens= dens self._rawmass= lambda r: 4.*numpy.pi\ *integrate.quad(lambda a: a**2*self._rawdens(a),0,r)[0] # The potential at zero, try to figure out whether it's finite _zero_msg= integrate.quad(lambda a: a*self._rawdens(a), 0,numpy.inf,full_output=True)[-1] _infpotzero= 'divergent' in _zero_msg or 'maximum number' in _zero_msg self._pot_zero= -numpy.inf if _infpotzero \ else -4.*numpy.pi\ *integrate.quad(lambda a: a*self._rawdens(a),0,numpy.inf)[0] # The potential at infinity _infmass= 'divergent' \ in integrate.quad(lambda a: a**2.*self._rawdens(a), 0,numpy.inf,full_output=True)[-1] self._pot_inf= 0. if not _infmass else numpy.inf # Normalize? if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) return None def _revaluate(self,r,t=0.): """Potential as a function of r and time""" if r == 0: return self._pot_zero elif numpy.isinf(r): return self._pot_inf else: return -self._rawmass(r)/r\ -4.*numpy.pi*integrate.quad(lambda a: self._rawdens(a)*a, r,numpy.inf)[0] def _rforce(self,r,t=0.): return -self._rawmass(r)/r**2 def _r2deriv(self,r,t=0.): return -2*self._rawmass(r)/r**3.+4.*numpy.pi*self._rawdens(r) def _rdens(self,r,t=0.): return self._rawdens(r) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/BurkertPotential.py0000644000175100001710000001022014327773303021223 0ustar00runnerdocker############################################################################### # BurkertPotential.py: Potential with a Burkert density ############################################################################### import numpy from scipy import special from ..util import conversion from .SphericalPotential import SphericalPotential class BurkertPotential(SphericalPotential): """BurkertPotential.py: Potential with a Burkert density .. math:: \\rho(r) = \\frac{\\mathrm{amp}}{(1+r/a)\\,(1+[r/a]^2)} """ def __init__(self,amp=1.,a=2.,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a Burkert-density potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass density or Gxmass density a = scale radius (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2013-04-10 - Written - Bovy (IAS) 2020-03-30 - Re-implemented using SphericalPotential - Bovy (UofT) """ SphericalPotential.__init__(self,amp=amp,ro=ro,vo=vo, amp_units='density') a= conversion.parse_length(a,ro=self._ro,vo=self._vo) self.a=a self._scale= self.a if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) self.hasC= True self.hasC_dxdv= True self.hasC_dens= True return None def _revaluate(self,r,t=0.): """Potential as a function of r and time""" x= r/self.a return -self.a**2.*numpy.pi*(-numpy.pi/x+2.*(1./x+1)*numpy.arctan(1/x) +(1./x+1)*numpy.log((1.+1./x)**2./(1.+1/x**2.)) +special.xlogy(2./x,1.+x**2.)) #Previous way, not stable as r -> infty #return -self.a**2.*numpy.pi/x*(-numpy.pi+2.*(1.+x)*numpy.arctan(1/x) # +2.*(1.+x)*numpy.log(1.+x) # +(1.-x)*numpy.log(1.+x**2.)) def _rforce(self,r,t=0.): x= r/self.a return self.a*numpy.pi/x**2.*(numpy.pi-2.*numpy.arctan(1./x) -2.*numpy.log(1.+x)-numpy.log(1.+x**2.)) def _r2deriv(self,r,t=0.): x= r/self.a return 4.*numpy.pi/(1.+x**2.)/(1.+x)+2.*self._rforce(r)/x/self.a def _rdens(self,r,t=0.): x= r/self.a return 1./(1.+x)/(1.+x**2.) def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the surface density HISTORY: 2018-08-19 - Written - Bovy (UofT) """ r= numpy.sqrt(R**2.+z**2.) x= r/self.a Rpa= numpy.sqrt(R**2.+self.a**2.) Rma= numpy.sqrt(R**2.-self.a**2.+0j) if Rma == 0: za= z/self.a return self.a**2./2.*((2.-2.*numpy.sqrt(za**2.+1) +numpy.sqrt(2.)*za\ *numpy.arctan(za/numpy.sqrt(2.)))/z +numpy.sqrt(2*za**2.+2.)\ *numpy.arctanh(za/numpy.sqrt(2.*(za**2.+1))) /numpy.sqrt(self.a**2.+z**2.)) else: return self.a**2.*(numpy.arctan(z/x/Rma)/Rma +numpy.arctanh(z/x/Rpa)/Rpa -numpy.arctan(z/Rma)/Rma +numpy.arctan(z/Rpa)/Rpa).real ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/Cautun20.py0000644000175100001710000000637614327773303017347 0ustar00runnerdocker# Cautun (2020) potential # Thanks to Thomas Callingham (Durham University, UK) which implemented the potential within galpy import numpy from ..potential import (AdiabaticContractionWrapperPotential, DiskSCFPotential, NFWPotential, PowerSphericalPotentialwCutoff, SCFPotential, mwpot_helpers, scf_compute_coeffs_axi) from ..util import conversion # Suppress the numpy floating-point warnings that this code generates... old_error_settings= numpy.seterr(all='ignore') # Unit normalizations ro= 8.122 vo= 229 sigo = conversion.surfdens_in_msolpc2(vo=vo,ro=ro) rhoo = conversion.dens_in_msolpc3(vo=vo,ro=ro) #Cautun DM halo fb = 4.825 / 30.7 # Planck 1 baryon fraction m200 = 0.969e12 # the DM halo mass conc = 8.76 #Cautun Bulge r0_bulge = 0.075/ro rcut_bulge= 2.1/ro rho0_bulge= 103/rhoo #Cautun Stellar Discs zd_thin = 0.3/ro Rd_thin =2.63/ro Sigma0_thin= 731./sigo zd_thick = 0.9/ro Rd_thick = 3.80/ro Sigma0_thick= 101./sigo #Cautun Gas Discs Rd_HI= 7./ro Rm_HI= 4./ro zd_HI= 0.085/ro Sigma0_HI= 53/sigo Rd_H2= 1.5/ro Rm_H2= 12./ro zd_H2= 0.045/ro Sigma0_H2= 2200/sigo # Cautun CGM A = 0.19 Beta = -1.46 critz0 = 127.5e-9/rhoo R200 = 219/ro #R200 for cgm cgm_amp = 200 * critz0 * A * fb def gas_dens(R,z): return \ mwpot_helpers.expsech2_dens_with_hole(R,z,Rd_HI,Rm_HI,zd_HI,Sigma0_HI)\ +mwpot_helpers.expsech2_dens_with_hole(R,z,Rd_H2,Rm_H2,zd_H2,Sigma0_H2) def stellar_dens(R,z): return mwpot_helpers.expexp_dens(R,z,Rd_thin,zd_thin,Sigma0_thin)\ +mwpot_helpers.expexp_dens(R,z,Rd_thick,zd_thick,Sigma0_thick) def bulge_dens(R,z): return mwpot_helpers.core_pow_dens_with_cut(R,z,1.8,r0_bulge,rcut_bulge, rho0_bulge,0.5) #dicts used in DiskSCFPotential sigmadict = [{'type':'exp','h':Rd_HI,'amp':Sigma0_HI, 'Rhole':Rm_HI}, {'type':'exp','h':Rd_H2,'amp':Sigma0_H2, 'Rhole':Rm_H2}, {'type':'exp','h':Rd_thin,'amp':Sigma0_thin, 'Rhole':0.}, {'type':'exp','h':Rd_thick,'amp':Sigma0_thick, 'Rhole':0.}] hzdict = [{'type':'sech2', 'h':zd_HI}, {'type':'sech2', 'h':zd_H2}, {'type':'exp', 'h':zd_thin}, {'type':'exp', 'h':zd_thick}] #generate separate disk and halo potential - and combined potential Cautun_bulge= SCFPotential(\ Acos=scf_compute_coeffs_axi(bulge_dens,20,10,a=0.1)[0], a=0.1,ro=ro,vo=vo) Cautun_cgm= PowerSphericalPotentialwCutoff(amp=cgm_amp,alpha=-Beta, r1=R200,rc=2.*R200,ro=ro,vo=vo) Cautun_disk= DiskSCFPotential(\ dens=lambda R,z: gas_dens(R,z) + stellar_dens(R,z), Sigma=sigmadict,hz=hzdict,a=2.5,N=30,L=30,ro=ro,vo=vo) Cautun_halo= AdiabaticContractionWrapperPotential(\ pot=NFWPotential(conc=conc,mvir=m200/1.e12, vo=vo,ro=ro,H=67.77,Om=0.307, overdens=200.0*(1.-fb),wrtcrit=True), baryonpot=Cautun_bulge+Cautun_cgm+Cautun_disk, f_bar=fb,method='cautun',ro=ro,vo=vo) Cautun20= Cautun_halo+Cautun_disk+Cautun_bulge+Cautun_cgm # Go back to old floating-point warnings settings numpy.seterr(**old_error_settings) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/ChandrasekharDynamicalFrictionForce.py0000644000175100001710000003015314327773303024771 0ustar00runnerdocker############################################################################### # ChandrasekharDynamicalFrictionForce: Class that implements the # Chandrasekhar dynamical friction ############################################################################### import copy import hashlib import numpy from scipy import interpolate, special from ..util import conversion from .DissipativeForce import DissipativeForce from .Potential import _check_c, evaluateDensities from .Potential import flatten as flatten_pot _INVSQRTTWO= 1./numpy.sqrt(2.) _INVSQRTPI= 1./numpy.sqrt(numpy.pi) class ChandrasekharDynamicalFrictionForce(DissipativeForce): """Class that implements the Chandrasekhar dynamical friction force .. math:: \\mathbf{F}(\\mathbf{x},\\mathbf{v}) = -2\\pi\\,[G\\,M]\\,[G\\,\\rho(\\mathbf{x})]\\,\\ln[1+\\Lambda^2] \\,\\left[\\mathrm{erf}(X)-\\frac{2X}{\\sqrt{\\pi}}\\exp\\left(-X^2\\right)\\right]\\,\\frac{\\mathbf{v}}{|\\mathbf{v}|^3}\\, on a mass (e.g., a satellite galaxy or a black hole) :math:`M` at position :math:`\\mathbf{x}` moving at velocity :math:`\\mathbf{v}` through a background density :math:`\\rho`. The quantity :math:`X` is the usual :math:`X=|\\mathbf{v}|/[\\sqrt{2}\\sigma_r(r)`. The factor :math:`\\Lambda` that goes into the Coulomb logarithm is taken to be .. math:: \\Lambda = \\frac{r/\\gamma}{\\mathrm{max}\\left(r_{\\mathrm{hm}},GM/|\\mathbf{v}|^2\\right)}\\,, where :math:`\\gamma` is a constant. This :math:`\\gamma` should be the absolute value of the logarithmic slope of the density :math:`\\gamma = |\\mathrm{d} \\ln \\rho / \\mathrm{d} \\ln r|`, although for :math:`\\gamma<1` it is advisable to set :math:`\\gamma=1`. Implementation here roughly follows `2016MNRAS.463..858P `__ and earlier work. """ def __init__(self,amp=1.,GMs=.1,gamma=1.,rhm=0., dens=None,sigmar=None, const_lnLambda=False,minr=0.0001,maxr=25.,nr=501, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a Chandrasekhar Dynamical Friction force INPUT: amp - amplitude to be applied to the potential (default: 1) GMs - satellite mass; can be a Quantity with units of mass or Gxmass; can be adjusted after initialization by setting obj.GMs= where obj is your ChandrasekharDynamicalFrictionForce instance (note that the mass of the satellite can *not* be changed simply by multiplying the instance by a number, because he mass is not only used as an amplitude) rhm - half-mass radius of the satellite (set to zero for a black hole; can be a Quantity); can be adjusted after initialization by setting obj.rhm= where obj is your ChandrasekharDynamicalFrictionForce instance gamma - Free-parameter in :math:`\\Lambda` dens - Potential instance or list thereof that represents the density [default: LogarithmicHaloPotential(normalize=1.,q=1.)] sigmar= (None) function that gives the velocity dispersion as a function of r (has to be in natural units!); if None, computed from the dens potential using the spherical Jeans equation (in galpy.df.jeans) assuming zero anisotropy; if set to a lambda function, *the object cannot be pickled* (so set it to a real function) cont_lnLambda= (False) if set to a number, use a constant ln(Lambda) instead with this value minr= (0.0001) minimum r at which to apply dynamical friction: at r < minr, friction is set to zero (can be a Quantity) Interpolation: maxr= (25) maximum r for which sigmar gets interpolated; for best performance set this to the maximum r you will consider (can be a Quantity) nr= (501) number of radii to use in the interpolation of sigmar You can check that sigmar is interpolated correctly by comparing the methods sigmar [the interpolated version] and sigmar_orig [the original or directly computed version] OUTPUT: (none) HISTORY: 2011-12-26 - Started - Bovy (NYU) 2018-03-18 - Re-started: updated to r dependent Lambda form and integrated into galpy framework - Bovy (UofT) 2018-07-23 - Calculate sigmar from the Jeans equation and interpolate it; allow GMs and rhm to be set on the fly - Bovy (UofT) """ DissipativeForce.__init__(self,amp=amp*GMs,ro=ro,vo=vo, amp_units='mass') rhm= conversion.parse_length(rhm,ro=self._ro) minr= conversion.parse_length(minr,ro=self._ro) maxr= conversion.parse_length(maxr,ro=self._ro) self._gamma= gamma self._ms= self._amp/amp # from handling in __init__ above, should be ms in galpy units self._rhm= rhm self._minr= minr self._maxr= maxr self._dens_kwarg= dens # for pickling self._sigmar_kwarg= sigmar # for pickling # Parse density if dens is None: from .LogarithmicHaloPotential import LogarithmicHaloPotential dens= LogarithmicHaloPotential(normalize=1.,q=1.) if sigmar is None: # we know this solution! sigmar= lambda x: _INVSQRTTWO dens= flatten_pot(dens) self._dens_pot= dens self._dens=\ lambda R,z,phi=0.,t=0.: evaluateDensities(self._dens_pot, R,z,phi=phi,t=t, use_physical=False) if sigmar is None: from ..df import jeans sigmar= lambda x: jeans.sigmar(self._dens_pot,x,beta=0., use_physical=False) self._sigmar_rs_4interp= numpy.linspace(self._minr,self._maxr,nr) self._sigmars_4interp= numpy.array([sigmar(x) for x in self._sigmar_rs_4interp]) if numpy.any(numpy.isnan(self._sigmars_4interp)): # Check for case where density is zero, in that case, just # paint in the nearest neighbor for the interpolation # (doesn't matter in the end, because force = 0 when dens = 0) nanrs_indx= numpy.isnan(self._sigmars_4interp) if numpy.all(numpy.array([self._dens(r*_INVSQRTTWO,r*_INVSQRTTWO) for r in self._sigmar_rs_4interp[nanrs_indx]]) == 0.): self._sigmars_4interp[nanrs_indx]= interpolate.interp1d(\ self._sigmar_rs_4interp[True^nanrs_indx], self._sigmars_4interp[True^nanrs_indx], kind="nearest",fill_value="extrapolate")\ (self._sigmar_rs_4interp[nanrs_indx]) self.sigmar_orig= sigmar self.sigmar= interpolate.InterpolatedUnivariateSpline(\ self._sigmar_rs_4interp,self._sigmars_4interp,k=3) if const_lnLambda: self._lnLambda= const_lnLambda else: self._lnLambda= False self._amp*= 4.*numpy.pi self._force_hash= None self.hasC= _check_c(self._dens_pot,dens=True) return None def GMs(self,gms): gms= conversion.parse_mass(gms,ro=self._ro,vo=self._vo) self._amp*= gms/self._ms self._ms= gms # Reset the hash self._force_hash= None return None GMs= property(None,GMs) def rhm(self,new_rhm): self._rhm= conversion.parse_length(new_rhm,ro=self._ro) # Reset the hash self._force_hash= None return None rhm= property(None,rhm) def lnLambda(self,r,v): """ NAME: lnLambda PURPOSE: evaluate the Coulomb logarithm :math:`\\ln \\Lambda` INPUT: r - spherical radius (natural units) v - current velocity in cylindrical coordinates (natural units) OUTPUT: Coulomb logarithm HISTORY: 2018-03-18 - Started - Bovy (UofT) """ if self._lnLambda: lnLambda= self._lnLambda else: GMvs= self._ms/v**2. if GMvs < self._rhm: Lambda= r/self._gamma/self._rhm else: Lambda= r/self._gamma/GMvs lnLambda= 0.5*numpy.log(1.+Lambda**2.) return lnLambda def _calc_force(self,R,phi,z,v,t): r= numpy.sqrt(R**2.+z**2.) if r < self._minr: self._cached_force= 0. else: vs= numpy.sqrt(v[0]**2.+v[1]**2.+v[2]**2.) if r > self._maxr: sr= self.sigmar_orig(r) else: sr= self.sigmar(r) X= vs*_INVSQRTTWO/sr Xfactor= special.erf(X)-2.*X*_INVSQRTPI*numpy.exp(-X**2.) lnLambda= self.lnLambda(r,vs) self._cached_force=\ -self._dens(R,z,phi=phi,t=t)/vs**3.\ *Xfactor*lnLambda def _Rforce(self,R,z,phi=0.,t=0.,v=None): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time v= current velocity in cylindrical coordinates OUTPUT: the radial force HISTORY: 2018-03-18 - Started - Bovy (UofT) """ new_hash= hashlib.md5(numpy.array([R,phi,z,v[0],v[1],v[2],t]))\ .hexdigest() if new_hash != self._force_hash: self._calc_force(R,phi,z,v,t) return self._cached_force*v[0] def _phitorque(self,R,z,phi=0.,t=0.,v=None): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time v= current velocity in cylindrical coordinates OUTPUT: the azimuthal torque HISTORY: 2018-03-18 - Started - Bovy (UofT) """ new_hash= hashlib.md5(numpy.array([R,phi,z,v[0],v[1],v[2],t]))\ .hexdigest() if new_hash != self._force_hash: self._calc_force(R,phi,z,v,t) return self._cached_force*v[1]*R def _zforce(self,R,z,phi=0.,t=0.,v=None): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time v= current velocity in cylindrical coordinates OUTPUT: the vertical force HISTORY: 2018-03-18 - Started - Bovy (UofT) """ new_hash= hashlib.md5(numpy.array([R,phi,z,v[0],v[1],v[2],t]))\ .hexdigest() if new_hash != self._force_hash: self._calc_force(R,phi,z,v,t) return self._cached_force*v[2] # Pickling functions def __getstate__(self): pdict= copy.copy(self.__dict__) # rm lambda function del pdict['_dens'] if self._sigmar_kwarg is None: # because an object set up with sigmar = user-provided function # cannot typically be picked, disallow this explicitly # (so if it can, everything should be fine; if not, pickling error) del pdict['sigmar_orig'] return pdict def __setstate__(self,pdict): self.__dict__= pdict # Re-setup _dens self._dens=\ lambda R,z,phi=0.,t=0.: evaluateDensities(self._dens_pot, R,z,phi=phi,t=t, use_physical=False) # Re-setup sigmar_orig if self._dens_kwarg is None and self._sigmar_kwarg is None: self.sigmar_orig= lambda x: _INVSQRTTWO else: from ..df import jeans self.sigmar_orig= lambda x: jeans.sigmar(self._dens_pot,x,beta=0., use_physical=False) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/CorotatingRotationWrapperPotential.py0000644000175100001710000001043614327773303025010 0ustar00runnerdocker############################################################################### # CorotatingRotationWrapperPotential.py: Wrapper to make a potential rotate # with a fixed R x pattern speed, # around the z axis ############################################################################### from ..util import conversion from .WrapperPotential import parentWrapperPotential class CorotatingRotationWrapperPotential(parentWrapperPotential): """Potential wrapper class that implements rotation with fixed R x pattern-speed around the z-axis. Can be used to make spiral structure that is everywhere co-rotating. The potential is rotated by replacing .. math:: \\phi \\rightarrow \\phi + \\frac{V_p(R)}{R} \\times \\left(t-t_0\\right) + \\mathrm{pa} with :math:`V_p(R)` the circular velocity curve, :math:`t_0` a reference time---time at which the potential is unchanged by the wrapper---and :math:`\\mathrm{pa}` the position angle at :math:`t=0`. The circular velocity is parameterized as .. math:: V_p(R) = V_{p,0}\\,\\left(\\frac{R}{R_0}\\right)^\\beta\\,. """ def __init__(self,amp=1.,pot=None,vpo=1.,beta=0.,to=0.,pa=0., ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a CorotatingRotationWrapper Potential INPUT: amp - amplitude to be applied to the potential (default: 1.) pot - Potential instance or list thereof; this potential is made to rotate around the z axis by the wrapper vpo= (1.) amplitude of the circular-velocity curve (can be a Quantity) beta= (0.) power-law amplitude of the circular-velocity curve to= (0.) reference time at which the potential == pot pa= (0.) the position angle (can be a Quantity) OUTPUT: (none) HISTORY: 2018-02-21 - Started - Bovy (UofT) """ vpo= conversion.parse_velocity(vpo,vo=self._vo) to= conversion.parse_time(to,ro=self._ro,vo=self._vo) pa= conversion.parse_angle(pa) self._vpo= vpo self._beta= beta self._pa= pa self._to= to self.hasC= True self.hasC_dxdv= True def _wrap(self,attribute,*args,**kwargs): kwargs['phi']= kwargs.get('phi',0.)\ -self._vpo*args[0]**(self._beta-1.)*(kwargs.get('t',0.)-self._to)\ -self._pa return self._wrap_pot_func(attribute)(self._pot,*args,**kwargs) # Derivatives that involve R need to be adjusted, bc they require also # the R dependence of phi to be taken into account def _Rforce(self,*args,**kwargs): kwargs['phi']= kwargs.get('phi',0.)\ -self._vpo*args[0]**(self._beta-1.)*(kwargs.get('t',0.)-self._to)\ -self._pa return self._wrap_pot_func('_Rforce')(self._pot,*args,**kwargs)\ -self._wrap_pot_func('_phitorque')(self._pot,*args,**kwargs)\ *(self._vpo*(self._beta-1.)*args[0]**(self._beta-2.) *(kwargs.get('t',0.)-self._to)) def _R2deriv(self,*args,**kwargs): kwargs['phi']= kwargs.get('phi',0.)\ -self._vpo*args[0]**(self._beta-1.)*(kwargs.get('t',0.)-self._to)\ -self._pa phiRderiv= -self._vpo*(self._beta-1.)*args[0]**(self._beta-2.)\ *(kwargs.get('t',0.)-self._to) return self._wrap_pot_func('_R2deriv')(self._pot,*args,**kwargs)\ +2.*self._wrap_pot_func('_Rphideriv')(self._pot,*args,**kwargs)\ *phiRderiv\ +self._wrap_pot_func('_phi2deriv')(self._pot,*args,**kwargs)\ *phiRderiv**2.\ +self._wrap_pot_func('_phitorque')(self._pot,*args,**kwargs)\ *(self._vpo*(self._beta-1.)*(self._beta-2.) *args[0]**(self._beta-3.)*(kwargs.get('t',0.)-self._to)) def _Rphideriv(self,*args,**kwargs): kwargs['phi']= kwargs.get('phi',0.)\ -self._vpo*args[0]**(self._beta-1.)*(kwargs.get('t',0.)-self._to)\ -self._pa return self._wrap_pot_func('_Rphideriv')(self._pot,*args,**kwargs)\ -self._wrap_pot_func('_phi2deriv')(self._pot,*args,**kwargs)\ *self._vpo*(self._beta-1.)*args[0]**(self._beta-2.)\ *(kwargs.get('t',0.)-self._to) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/CosmphiDiskPotential.py0000644000175100001710000001627014327773303022035 0ustar00runnerdocker############################################################################### # CosmphiDiskPotential: cos(mphi) potential ############################################################################### import numpy from ..util import conversion from .planarPotential import planarPotential _degtorad= numpy.pi/180. class CosmphiDiskPotential(planarPotential): """Class that implements the disk potential .. math:: \\Phi(R,\\phi) = \\mathrm{amp}\\,\\phi_0\\,\\,\\cos\\left[m\\,(\\phi-\\phi_b)\\right]\\times \\begin{cases} \\left(\\frac{R}{R_1}\\right)^p\\,, & \\text{for}\\ R \\geq R_b\\\\ \\left[2-\\left(\\frac{R_b}{R}\\right)^p\\right]\\times\\left(\\frac{R_b}{R_1}\\right)^p\\,, & \\text{for}\\ R\\leq R_b. \\end{cases} This potential can be grown between :math:`t_{\\mathrm{form}}` and :math:`t_{\\mathrm{form}}+T_{\\mathrm{steady}}` in a similar way as DehnenBarPotential by wrapping it with a DehnenSmoothWrapperPotential """ def __init__(self,amp=1.,phib=25.*_degtorad, p=1.,phio=0.01,m=4,r1=1.,rb=None, cp=None,sp=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize an cosmphi disk potential INPUT: amp= amplitude to be applied to the potential (default: 1.), degenerate with phio below, but kept for overall consistency with potentials m= cos( m * (phi - phib) ), integer p= power-law index of the phi(R) = (R/Ro)^p part r1= (1.) normalization radius for the amplitude (can be Quantity); amp x phio is only the potential at (R,phi) = (r1,pib) when r1 > rb; otherwise more complicated rb= (None) if set, break radius for power-law: potential R^p at R > Rb, R^-p at R < Rb, potential and force continuous at Rb Either: a) phib= angle (in rad; default=25 degree; or can be Quantity) phio= potential perturbation (in terms of phio/vo^2 if vo=1 at Ro=1; or can be Quantity with units of velocity-squared) b) cp, sp= m * phio * cos(m * phib), m * phio * sin(m * phib); can be Quantity with units of velocity-squared) OUTPUT: (none) HISTORY: 2011-10-27 - Started - Bovy (IAS) 2017-09-16 - Added break radius rb - Bovy (UofT) """ planarPotential.__init__(self,amp=amp,ro=ro,vo=vo) phib= conversion.parse_angle(phib) r1= conversion.parse_length(r1,ro=self._ro) rb= conversion.parse_length(rb,ro=self._ro) phio= conversion.parse_energy(phio,vo=self._vo) cp= conversion.parse_energy(cp,vo=self._vo) sp= conversion.parse_energy(sp,vo=self._vo) # Back to old definition self._r1p= r1**p self._amp/= self._r1p self.hasC= False self._m= int(m) # make sure this is an int if cp is None or sp is None: self._phib= phib self._mphio= phio*self._m else: self._mphio= numpy.sqrt(cp*cp+sp*sp) self._phib= numpy.arctan(sp/cp)/self._m if m < 2. and cp < 0.: self._phib= numpy.pi+self._phib self._p= p if rb is None: self._rb= 0. self._rbp= 1. # never used, but for p < 0 general expr fails self._rb2p= 1. else: self._rb= rb self._rbp= self._rb**self._p self._rb2p= self._rbp**2. self._mphib= self._m*self._phib self.hasC= True self.hasC_dxdv= True def _evaluate(self,R,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,phi,t INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: Phi(R,phi,t) HISTORY: 2011-10-19 - Started - Bovy (IAS) """ if R < self._rb: return self._mphio/self._m*numpy.cos(self._m*phi-self._mphib)\ *self._rbp*(2.*self._r1p-self._rbp/R**self._p) else: return self._mphio/self._m*R**self._p\ *numpy.cos(self._m*phi-self._mphib) def _Rforce(self,R,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the radial force HISTORY: 2011-10-19 - Written - Bovy (IAS) """ if R < self._rb: return -self._p*self._mphio/self._m*self._rb2p/R**(self._p+1.)\ *numpy.cos(self._m*phi-self._mphib) else: return -self._p*self._mphio/self._m*R**(self._p-1.)\ *numpy.cos(self._m*phi-self._mphib) def _phitorque(self,R,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the azimuthal torque HISTORY: 2011-10-19 - Written - Bovy (IAS) """ if R < self._rb: return self._mphio*numpy.sin(self._m*phi-self._mphib)\ *self._rbp*(2.*self._r1p-self._rbp/R**self._p) else: return self._mphio*R**self._p*numpy.sin(self._m*phi-self._mphib) def _R2deriv(self,R,phi=0.,t=0.): if R < self._rb: return -self._p*(self._p+1.)*self._mphio/self._m\ *self._rb2p/R**(self._p+2.)*numpy.cos(self._m*phi-self._mphib) else: return self._p*(self._p-1.)/self._m*self._mphio*R**(self._p-2.)\ *numpy.cos(self._m*phi-self._mphib) def _phi2deriv(self,R,phi=0.,t=0.): if R < self._rb: return -self._m*self._mphio*numpy.cos(self._m*phi-self._mphib)\ *self._rbp*(2.*self._r1p-self._rbp/R**self._p) else: return -self._m*self._mphio*R**self._p\ *numpy.cos(self._m*phi-self._mphib) def _Rphideriv(self,R,phi=0.,t=0.): if R < self._rb: return -self._p*self._mphio/self._m*self._rb2p/R**(self._p+1.)\ *numpy.sin(self._m*phi-self._mphib) else: return -self._p*self._mphio*R**(self._p-1.)*\ numpy.sin(self._m*phi-self._mphib) class LopsidedDiskPotential(CosmphiDiskPotential): """Class that implements the disk potential .. math:: \\Phi(R,\\phi) = \\mathrm{amp}\\,\\phi_0\\,\\left(\\frac{R}{R_1}\\right)^p\\,\\cos\\left(\\phi-\\phi_b\\right) Special case of CosmphiDiskPotential with m=1; see documentation for CosmphiDiskPotential """ def __init__(self,amp=1.,phib=25.*_degtorad, p=1.,phio=0.01,r1=1., cp=None,sp=None,ro=None,vo=None): CosmphiDiskPotential.__init__(self, amp=amp, phib=phib, p=p,phio=phio,m=1., cp=cp,sp=sp,ro=ro,vo=vo) self.hasC= True self.hasC_dxdv= True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/DehnenBarPotential.py0000644000175100001710000005145314327773303021450 0ustar00runnerdocker############################################################################### # DehnenBarPotential: Dehnen (2000)'s bar potential ############################################################################### import numpy from ..util import conversion from .Potential import Potential _degtorad= numpy.pi/180. class DehnenBarPotential(Potential): """Class that implements the Dehnen bar potential (`Dehnen 2000 `__), generalized to 3D following `Monari et al. (2016) `__ .. math:: \\Phi(R,z,\\phi) = A_b(t)\\,\\cos\\left(2\\,(\\phi-\\Omega_b\\,t)\\right))\\,\\left(\\frac{R}{r}\\right)^2\\,\\times \\begin{cases} -(R_b/r)^3\\,, & \\text{for}\\ r \\geq R_b\\\\ (r/R_b)^3-2\\,, & \\text{for}\\ r\\leq R_b. \\end{cases} where :math:`r^2 = R^2+z^2` is the spherical radius and .. math:: A_b(t) = A_f\\,\\left(\\frac{3}{16}\\xi^5-\\frac{5}{8}\\xi^3+\\frac{15}{16}\\xi+\\frac{1}{2}\\right)\\,, \\xi = 2\\frac{t/T_b-t_\\mathrm{form}}{T_\\mathrm{steady}}-1\\,,\\ \\mathrm{if}\\ t_\\mathrm{form} \\leq \\frac{t}{T_b} \\leq t_\\mathrm{form}+T_\\mathrm{steady} and .. math:: A_b(t) = \\begin{cases} 0\\,, & \\frac{t}{T_b} < t_\\mathrm{form}\\\\ A_f\\,, & \\frac{t}{T_b} > t_\\mathrm{form}+T_\\mathrm{steady} \\end{cases} where .. math:: T_b = \\frac{2\\pi}{\\Omega_b} is the bar period and the strength can also be specified using :math:`\\alpha` .. math:: \\alpha = 3\\,\\frac{A_f}{v_0^2}\\,\\left(\\frac{R_b}{r_0}\\right)^3\\,. """ normalize= property() # turn off normalize def __init__(self,amp=1.,omegab=None,rb=None,chi=0.8, rolr=0.9,barphi=25.*_degtorad, tform=-4.,tsteady=None,beta=0., alpha=0.01,Af=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a Dehnen bar potential INPUT: amp - amplitude to be applied to the potential (default: 1., see alpha or Ab below) barphi - angle between sun-GC line and the bar's major axis (in rad; default=25 degree; or can be Quantity)) tform - start of bar growth / bar period (default: -4) tsteady - time from tform at which the bar is fully grown / bar period (default: -tform/2, st the perturbation is fully grown at tform/2) Either provide: a) rolr - radius of the Outer Lindblad Resonance for a circular orbit (can be Quantity) chi - fraction R_bar / R_CR (corotation radius of bar) alpha - relative bar strength (default: 0.01) beta - power law index of rotation curve (to calculate OLR, etc.) b) omegab - rotation speed of the bar (can be Quantity) rb - bar radius (can be Quantity) Af - bar strength (can be Quantity) OUTPUT: (none) HISTORY: 2010-11-24 - Started - Bovy (NYU) 2017-06-23 - Converted to 3D following Monari et al. (2016) - Bovy (UofT/CCA) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo) barphi= conversion.parse_angle(barphi) rolr= conversion.parse_length(rolr,ro=self._ro) rb= conversion.parse_length(rb,ro=self._ro) omegab= conversion.parse_frequency(omegab,ro=self._ro,vo=self._vo) Af= conversion.parse_energy(Af,vo=self._vo) self.hasC= True self.hasC_dxdv= True self.isNonAxi= True self._barphi= barphi if omegab is None: self._rolr= rolr self._chi= chi self._beta= beta #Calculate omegab and rb self._omegab= 1./((self._rolr**(1.-self._beta))/(1.+numpy.sqrt((1.+self._beta)/2.))) self._rb= self._chi*self._omegab**(1./(self._beta-1.)) self._alpha= alpha self._af= self._alpha/3./self._rb**3. else: self._omegab= omegab self._rb= rb self._af= Af self._tb= 2.*numpy.pi/self._omegab self._tform= tform*self._tb if tsteady is None: self._tsteady= self._tform/2. else: self._tsteady= self._tform+tsteady*self._tb def _smooth(self,t): if isinstance(t,numpy.ndarray) and not numpy.ndim(t) == 0: smooth=numpy.ones(len(t)) indx=(t < self._tform) smooth[indx]=0. indx=(t < self._tsteady) * (t >= self._tform) deltat=t[indx]-self._tform xi= 2.*deltat/(self._tsteady-self._tform)-1. smooth[indx]= (3./16.*xi**5.-5./8*xi**3.+15./16.*xi+.5) else: if t < self._tform: smooth= 0. elif t < self._tsteady: deltat= t-self._tform xi= 2.*deltat/(self._tsteady-self._tform)-1. smooth= (3./16.*xi**5.-5./8*xi**3.+15./16.*xi+.5) else: #bar is fully on smooth= 1. return smooth def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,phi,t INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z,phi,t) HISTORY: 2010-11-24 - Started - Bovy (NYU) """ #Calculate relevant time smooth=self._smooth(t) r2= R**2.+z**2. r= numpy.sqrt(r2) if isinstance(r,numpy.ndarray): if not isinstance(R,numpy.ndarray): R=numpy.repeat(R,len(r)) if not isinstance(z,numpy.ndarray): z=numpy.repeat(z,len(r)) out=numpy.empty(len(r)) indx= r <= self._rb out[indx]= ((r[indx]/self._rb)**3.-2.)\ *numpy.divide(R[indx]**2.,r2[indx],numpy.ones_like(R[indx]), where=R[indx]!=0) indx=numpy.invert(indx) out[indx]= -(self._rb/r[indx])**3.*1./(1.+z[indx]**2./R[indx]**2.) out*=self._af*smooth*numpy.cos(2.*(phi-self._omegab*t-self._barphi)) return out else: if r == 0: return -2.*self._af*smooth*numpy.cos(2.*(phi-self._omegab*t-self._barphi)) elif r <= self._rb: return self._af*smooth*numpy.cos(2.*(phi-self._omegab*t-self._barphi))\ *((r/self._rb)**3.-2.)*R**2./r2 else: return -self._af*smooth*numpy.cos(2.*(phi-self._omegab*t- self._barphi))\ *(self._rb/r)**3.\ *1./(1.+z**2./R**2.) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2010-11-24 - Written - Bovy (NYU) """ #Calculate relevant time smooth=self._smooth(t) r= numpy.sqrt(R**2.+z**2.) if isinstance(r,numpy.ndarray): if not isinstance(R,numpy.ndarray): R=numpy.repeat(R,len(r)) if not isinstance(z,numpy.ndarray): z=numpy.repeat(z,len(r)) out=numpy.empty(len(r)) indx= r <= self._rb out[indx]= -((r[indx]/self._rb)**3.*R[indx]*(3.*R[indx]**2.+2.*z[indx]**2.)-4.*R[indx]*z[indx]**2.)/r[indx]**4. indx= numpy.invert(indx) out[indx]= -(self._rb/r[indx])**3.*R[indx]/r[indx]**4.*(3.*R[indx]**2.-2.*z[indx]**2.) out*=self._af*smooth*numpy.cos(2.*(phi-self._omegab*t-self._barphi)) return out else: if r <= self._rb: return -self._af*smooth*numpy.cos(2.*(phi-self._omegab*t -self._barphi))\ *((r/self._rb)**3.*R*(3.*R**2.+2.*z**2.)-4.*R*z**2.)/r**4. else: return -self._af*smooth*numpy.cos(2.*(phi-self._omegab*t- self._barphi))\ *(self._rb/r)**3.*R/r**4.*(3.*R**2.-2.*z**2.) def _phitorque(self,R,z,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the azimuthal torque HISTORY: 2010-11-24 - Written - Bovy (NYU) """ #Calculate relevant time smooth=self._smooth(t) r2= R**2.+z**2. r= numpy.sqrt(r2) if isinstance(r,numpy.ndarray): if not isinstance(R,numpy.ndarray): R=numpy.repeat(R,len(r)) if not isinstance(z,numpy.ndarray): z=numpy.repeat(z,len(r)) out=numpy.empty(len(r)) indx= r <= self._rb out[indx]= ((r[indx]/self._rb)**3.-2.)*R[indx]**2./r2[indx] indx=numpy.invert(indx) out[indx]= -(self._rb/r[indx])**3.*R[indx]**2./r2[indx] out*=2.*self._af*smooth*numpy.sin(2.*(phi-self._omegab*t-self._barphi)) return out else: if r <= self._rb: return 2.*self._af*smooth*numpy.sin(2.*(phi-self._omegab*t- self._barphi))\ *((r/self._rb)**3.-2.)*R**2./r2 else: return -2.*self._af*smooth*numpy.sin(2.*(phi-self._omegab*t- self._barphi))\ *(self._rb/r)**3.*R**2./r2 def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2017-06-23 - Written - Bovy (NYU) """ #Calculate relevant time smooth=self._smooth(t) r= numpy.sqrt(R**2.+z**2.) if isinstance(r,numpy.ndarray): if not isinstance(R,numpy.ndarray): R=numpy.repeat(R,len(r)) if not isinstance(z,numpy.ndarray): z=numpy.repeat(z,len(r)) out=numpy.empty(len(r)) indx= r <= self._rb out[indx]= -((r[indx]/self._rb)**3.+4.)*R[indx]**2.*z[indx]/r[indx]**4. indx=numpy.invert(indx) out[indx]= -5.*(self._rb/r[indx])**3.*R[indx]**2.*z[indx]/r[indx]**4. out*=self._af*smooth*numpy.cos(2.*(phi-self._omegab*t-self._barphi)) return out else: if r <= self._rb: return -self._af*smooth*numpy.cos(2.*(phi-self._omegab*t -self._barphi))\ *((r/self._rb)**3.+4.)*R**2.*z/r**4. else: return -5.*self._af*smooth*numpy.cos(2.*(phi-self._omegab*t- self._barphi))\ *(self._rb/r)**3.*R**2.*z/r**4. def _R2deriv(self,R,z,phi=0.,t=0.): #Calculate relevant time smooth=self._smooth(t) r= numpy.sqrt(R**2.+z**2.) if isinstance(r,numpy.ndarray): if not isinstance(R,numpy.ndarray): R=numpy.repeat(R,len(r)) if not isinstance(z,numpy.ndarray): z=numpy.repeat(z,len(r)) out=numpy.empty(len(r)) indx= r <= self._rb out[indx]= ((r[indx]/self._rb)**3.*((9.*R[indx]**2.+2.*z[indx]**2.)/r[indx]**4. -R[indx]**2./r[indx]**6.*(3.*R[indx]**2.+2.*z[indx]**2.))\ +4.*z[indx]**2./r[indx]**6.*(4.*R[indx]**2.-r[indx]**2.)) indx=numpy.invert(indx) out[indx]= (self._rb/r[indx])**3./r[indx]**6.*((r[indx]**2.-7.*R[indx]**2.)*(3.*R[indx]**2.-2.*z[indx]**2.)\ +6.*R[indx]**2.*r[indx]**2.) out*=self._af*smooth*numpy.cos(2.*(phi-self._omegab*t-self._barphi)) return out else: if r <= self._rb: return self._af*smooth*numpy.cos(2.*(phi-self._omegab*t -self._barphi))\ *((r/self._rb)**3.*((9.*R**2.+2.*z**2.)/r**4. -R**2./r**6.*(3.*R**2.+2.*z**2.))\ +4.*z**2./r**6.*(4.*R**2.-r**2.)) else: return self._af*smooth*numpy.cos(2.*(phi-self._omegab*t- self._barphi))\ *(self._rb/r)**3./r**6.*((r**2.-7.*R**2.)*(3.*R**2.-2.*z**2.)\ +6.*R**2.*r**2.) def _phi2deriv(self,R,z,phi=0.,t=0.): #Calculate relevant time smooth=self._smooth(t) r= numpy.sqrt(R**2.+z**2.) if isinstance(r,numpy.ndarray): if not isinstance(R,numpy.ndarray): R=numpy.repeat(R,len(r)) if not isinstance(z,numpy.ndarray): z=numpy.repeat(z,len(r)) out=numpy.empty(len(r)) indx= r <= self._rb out[indx]= -((r[indx]/self._rb)**3.-2.)*R[indx]**2./r[indx]**2. indx=numpy.invert(indx) out[indx]= (self._rb/r[indx])**3.*R[indx]**2./r[indx]**2. out*=4.*self._af*smooth*numpy.cos(2.*(phi-self._omegab*t-self._barphi)) return out else: if r <= self._rb: return -4.*self._af*smooth*numpy.cos(2.*(phi-self._omegab*t- self._barphi))\ *((r/self._rb)**3.-2.)*R**2./r**2. else: return 4.*self._af*smooth*numpy.cos(2.*(phi-self._omegab*t- self._barphi))\ *(self._rb/r)**3.*R**2./r**2. def _Rphideriv(self,R,z,phi=0.,t=0.): #Calculate relevant time smooth=self._smooth(t) r= numpy.sqrt(R**2.+z**2.) if isinstance(r,numpy.ndarray): if not isinstance(R,numpy.ndarray): R=numpy.repeat(R,len(r)) if not isinstance(z,numpy.ndarray): z=numpy.repeat(z,len(r)) out=numpy.empty(len(r)) indx= r <= self._rb out[indx]= ((r[indx]/self._rb)**3.*R[indx]*(3.*R[indx]**2.+2.*z[indx]**2.)-4.*R[indx]*z[indx]**2.)/r[indx]**4. indx=numpy.invert(indx) out[indx]= (self._rb/r[indx])**3.*R[indx]/r[indx]**4.*(3.*R[indx]**2.-2.*z[indx]**2.) out*=-2.*self._af*smooth*numpy.sin(2.*(phi-self._omegab*t-self._barphi)) return out else: if r <= self._rb: return -2.*self._af*smooth*numpy.sin(2.*(phi-self._omegab*t -self._barphi))\ *((r/self._rb)**3.*R*(3.*R**2.+2.*z**2.)-4.*R*z**2.)/r**4. else: return -2.*self._af*smooth*numpy.sin(2.*(phi-self._omegab*t- self._barphi))\ *(self._rb/r)**3.*R/r**4.*(3.*R**2.-2.*z**2.) def _z2deriv(self,R,z,phi=0.,t=0.): #Calculate relevant time smooth=self._smooth(t) r= numpy.sqrt(R**2.+z**2.) if isinstance(r,numpy.ndarray): if not isinstance(R,numpy.ndarray): R=numpy.repeat(R,len(r)) if not isinstance(z,numpy.ndarray): z=numpy.repeat(z,len(r)) out=numpy.empty(len(r)) indx= r <= self._rb out[indx]= R[indx]**2./r[indx]**6.*((r[indx]/self._rb)**3.*(r[indx]**2.-z[indx]**2.) +4.*(r[indx]**2.-4.*z[indx]**2.)) indx=numpy.invert(indx) out[indx]=5.*(self._rb/r[indx])**3.*R[indx]**2./r[indx]**6.*(r[indx]**2.-7.*z[indx]**2.) out*=self._af*smooth*numpy.cos(2.*(phi-self._omegab*t-self._barphi)) return out else: if r <= self._rb: return self._af*smooth*numpy.cos(2.*(phi-self._omegab*t -self._barphi))\ *R**2./r**6.*((r/self._rb)**3.*(r**2.-z**2.) +4.*(r**2.-4.*z**2.)) else: return 5.*self._af*smooth*numpy.cos(2.*(phi-self._omegab*t- self._barphi))\ *(self._rb/r)**3.*R**2./r**6.*(r**2.-7.*z**2.) def _Rzderiv(self,R,z,phi=0.,t=0.): #Calculate relevant time smooth=self._smooth(t) r= numpy.sqrt(R**2.+z**2.) if isinstance(r,numpy.ndarray): if not isinstance(R,numpy.ndarray): R=numpy.repeat(R,len(r)) if not isinstance(z,numpy.ndarray): z=numpy.repeat(z,len(r)) out=numpy.empty(len(r)) indx= r <= self._rb out[indx]= R[indx]*z[indx]/r[indx]**6.*((r[indx]/self._rb)**3.*(2.*r[indx]**2.-R[indx]**2.) +8.*(r[indx]**2.-2.*R[indx]**2.)) indx=numpy.invert(indx) out[indx]= 5.*(self._rb/r[indx])**3.*R[indx]*z[indx]/r[indx]**6.*(2.*r[indx]**2.-7.*R[indx]**2.) out*=self._af*smooth*numpy.cos(2.*(phi-self._omegab*t-self._barphi)) return out else: if r <= self._rb: return self._af*smooth*numpy.cos(2.*(phi-self._omegab*t -self._barphi))\ *R*z/r**6.*((r/self._rb)**3.*(2.*r**2.-R**2.) +8.*(r**2.-2.*R**2.)) else: return 5.*self._af*smooth*numpy.cos(2.*(phi-self._omegab*t- self._barphi))\ *(self._rb/r)**3.*R*z/r**6.*(2.*r**2.-7.*R**2.) def _phizderiv(self,R,z,phi=0.,t=0.): """ NAME: _phizderiv PURPOSE: evaluate the mixed azimuthal, vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed azimuthal, vertical derivative HISTORY: 2021-04-30 - Written - Bovy (UofT) """ #Calculate relevant time smooth=self._smooth(t) r= numpy.sqrt(R**2.+z**2.) if isinstance(r,numpy.ndarray): if not isinstance(R,numpy.ndarray): R=numpy.repeat(R,len(r)) if not isinstance(z,numpy.ndarray): z=numpy.repeat(z,len(r)) out=numpy.empty(len(r)) indx= r <= self._rb out[indx]= -((r[indx]/self._rb)**3.+4.)*R[indx]**2.*z[indx]/r[indx]**4. indx=numpy.invert(indx) out[indx]= -5.*(self._rb/r[indx])**3.*R[indx]**2.*z[indx]/r[indx]**4. out*=self._af*smooth*numpy.sin(2.*(phi-self._omegab*t-self._barphi)) return 2.*out else: if r <= self._rb: return -2*self._af*smooth*numpy.sin(2.*(phi-self._omegab*t -self._barphi))\ *((r/self._rb)**3.+4.)*R**2.*z/r**4. else: return -10.*self._af*smooth*numpy.sin(2.*(phi-self._omegab*t- self._barphi))\ *(self._rb/r)**3.*R**2.*z/r**4. def tform(self): #pragma: no cover """ NAME: tform PURPOSE: return formation time of the bar INPUT: (none) OUTPUT: tform in normalized units HISTORY: 2011-03-08 - Written - Bovy (NYU) """ return self._tform def OmegaP(self): """ NAME: OmegaP PURPOSE: return the pattern speed INPUT: (none) OUTPUT: pattern speed HISTORY: 2011-10-10 - Written - Bovy (IAS) """ return self._omegab ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/DehnenBinney98.py0000644000175100001710000001130714327773303020463 0ustar00runnerdocker# Dehnen & Binney (1998) potentials: models 1 through 4 import numpy from ..potential import (DiskSCFPotential, SCFPotential, TwoPowerTriaxialPotential, mwpot_helpers, scf_compute_coeffs_axi) from ..util import conversion # Unit normalization, all models have R0 = 8 kpc ro= 8.0 # Parameters in common between all models Rm_ISM= 4./ro zd_ISM= 0.040/ro zd_thin= 0.180/ro zd_thick= 1./ro Rd_ISM_over_Rd= 2. Rd_thin_over_Rd= 1. Rd_thick_over_Rd= 1. SigmaR0_ISM_over_SigmaR0= 0.25 SigmaR0_thin_over_SigmaR0= 0.70 SigmaR0_thick_over_SigmaR0= 0.05 alpha_bulge= 1.8 q_bulge= 0.6 r0_bulge= 1./ro rc_bulge= 1.9/ro q_halo= 0.8 def define_dehnenbinney98_models(model=1): # Suppress the numpy floating-point warnings that this code generates... old_error_settings= numpy.seterr(all='ignore') if model == 1: vo= 222. Rd_star= 0.25 Sigma0= 1905. rho0_bulge= 0.4271 rho0_halo= 0.7110 alpha_halo= -2. beta_halo= 2.959 r0_halo= 3.83/ro elif model == 2: vo= 217. Rd_star= 0.30 Sigma0= 1208. rho0_bulge= 0.7561 rho0_halo= 1.263 alpha_halo= -2. beta_halo= 2.207 r0_halo= 1.09/ro elif model == 3: vo= 217. Rd_star= 0.35 Sigma0= 778.4 rho0_bulge= 0.3 rho0_halo= 0.1179 alpha_halo= 1.8 beta_halo= 2.002 r0_halo= 2.29/ro elif model == 4: vo= 220. Rd_star= 0.40 Sigma0= 536. rho0_bulge= 0.3 rho0_halo= 0.2659 alpha_halo= 1.629 beta_halo= 2.167 r0_halo= 1.899/ro sigo= conversion.surfdens_in_msolpc2(vo=vo,ro=ro) rhoo= conversion.dens_in_msolpc3(vo=vo,ro=ro) Sigma0/= sigo rho0_bulge/= rhoo rho0_halo/= rhoo Rd_ISM= Rd_star*Rd_ISM_over_Rd Rd_thin= Rd_star*Rd_thin_over_Rd Rd_thick= Rd_star*Rd_thick_over_Rd SigmaR0= Sigma0/(SigmaR0_ISM_over_SigmaR0\ /(2.*zd_ISM) /mwpot_helpers.expexp_dens_with_hole(1.,0.,Rd_ISM,Rm_ISM,zd_ISM,1.) +SigmaR0_thin_over_SigmaR0\ *mwpot_helpers.expexp_dens(0.,0.,Rd_thin,zd_thin,1.)\ /mwpot_helpers.expexp_dens(1.,0.,Rd_thin,zd_thin,1.) +SigmaR0_thick_over_SigmaR0\ *mwpot_helpers.expexp_dens(0.,0.,Rd_thick,zd_thick,1.)\ /mwpot_helpers.expexp_dens(1.,0.,Rd_thick,zd_thick,1.)) Sigma0_ISM= SigmaR0_ISM_over_SigmaR0*SigmaR0/(2.*zd_ISM)\ /mwpot_helpers.expexp_dens_with_hole(1.,0.,Rd_ISM,Rm_ISM,zd_ISM,1.) Sigma0_thin= SigmaR0_thin_over_SigmaR0*SigmaR0\ *mwpot_helpers.expexp_dens(0.,0.,Rd_thin,zd_thin,1.)\ /mwpot_helpers.expexp_dens(1.,0.,Rd_thin,zd_thin,1.) Sigma0_thick= SigmaR0_thick_over_SigmaR0*SigmaR0\ *mwpot_helpers.expexp_dens(0.,0.,Rd_thick,zd_thick,1.)\ /mwpot_helpers.expexp_dens(1.,0.,Rd_thick,zd_thick,1.) # now define gas and disk functions for DiskSCF gas_dens= lambda R,z: mwpot_helpers.expexp_dens_with_hole(R,z,Rd_ISM, Rm_ISM,zd_ISM, Sigma0_ISM) disk_dens= lambda R,z: mwpot_helpers.expexp_dens(R,z,Rd_thin, zd_thin,Sigma0_thin)\ +mwpot_helpers.expexp_dens(R,z,Rd_thick, zd_thick,Sigma0_thick) bulge_dens= lambda R,z: mwpot_helpers.pow_dens_with_cut(R,z,alpha_bulge, r0_bulge,rc_bulge, rho0_bulge,q_bulge) #dicts used in DiskSCFPotential sigmadict = [{'type':'expwhole','h':Rd_ISM,'amp':Sigma0_ISM, 'Rhole':Rm_ISM}, {'type':'exp','h':Rd_thin,'amp':Sigma0_thin}, {'type':'exp','h':Rd_thick,'amp':Sigma0_thick}] hzdict = [{'type':'exp', 'h':zd_ISM}, {'type':'exp', 'h':zd_thin}, {'type':'exp', 'h':zd_thick}] # Now put together the potential DB98_bulge= SCFPotential(\ Acos=scf_compute_coeffs_axi(bulge_dens,30,10,a=0.025)[0], a=0.025,ro=ro,vo=vo) DB98_disk= DiskSCFPotential(\ dens=lambda R,z: gas_dens(R,z)+disk_dens(R,z), Sigma=sigmadict,hz=hzdict,a=2.5,N=30,L=30,ro=ro,vo=vo) DB98_halo= TwoPowerTriaxialPotential(amp=rho0_halo*(4*numpy.pi*r0_halo**3), alpha=alpha_halo,beta=beta_halo, a=r0_halo,ro=ro,vo=vo,c=q_halo) # Go back to old floating-point warnings settings numpy.seterr(**old_error_settings) return DB98_bulge+DB98_disk+DB98_halo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/DehnenSmoothWrapperPotential.py0000644000175100001710000000572414327773303023556 0ustar00runnerdocker############################################################################### # DehnenSmoothWrapperPotential.py: Wrapper to smoothly grow a potential ############################################################################### from ..util import conversion from .WrapperPotential import parentWrapperPotential class DehnenSmoothWrapperPotential(parentWrapperPotential): """Potential wrapper class that implements the growth of a gravitational potential following `Dehnen (2000) `__. The amplitude A applied to a potential wrapped by an instance of this class is changed as .. math:: A(t) = amp\\,\\left(\\frac{3}{16}\\xi^5-\\frac{5}{8}\\xi^3+\\frac{15}{16}\\xi+\\frac{1}{2}\\right) where .. math:: \\xi = \\begin{cases} -1 & t < t_\\mathrm{form}\\\\ 2\\left(\\frac{t-t_\\mathrm{form}}{t_\\mathrm{steady}}\\right)-1\\,, & t_\\mathrm{form} \\leq t \\leq t_\\mathrm{form}+t_\\mathrm{steady}\\\\ 1 & t > t_\\mathrm{form}+t_\\mathrm{steady} \\end{cases} if ``decay=True``, the amplitude decays rather than grows as decay = 1 - grow. """ def __init__(self,amp=1.,pot=None,tform=-4.,tsteady=None,decay=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a DehnenSmoothWrapper Potential INPUT: amp - amplitude to be applied to the potential (default: 1.) pot - Potential instance or list thereof; the amplitude of this will be grown by this wrapper tform - start of growth (can be a Quantity) tsteady - time from tform at which the potential is fully grown (default: -tform/2, st the perturbation is fully grown at tform/2; can be a Quantity) decay= (False) if True, decay the amplitude instead of growing it (as 1-grow) OUTPUT: (none) HISTORY: 2017-06-26 - Started - Bovy (UofT) 2018-10-07 - Added 'decay' option - Bovy (UofT) """ tform= conversion.parse_time(tform,ro=self._ro,vo=self._vo) tsteady= conversion.parse_time(tsteady,ro=self._ro,vo=self._vo) self._tform= tform if tsteady is None: self._tsteady= self._tform/2. else: self._tsteady= self._tform+tsteady self._grow= not decay self.hasC= True self.hasC_dxdv= True def _smooth(self,t): #Calculate relevant time if t < self._tform: smooth= 0. elif t < self._tsteady: deltat= t-self._tform xi= 2.*deltat/(self._tsteady-self._tform)-1. smooth= (3./16.*xi**5.-5./8*xi**3.+15./16.*xi+.5) else: #bar is fully on smooth= 1. return smooth if self._grow else 1.-smooth def _wrap(self,attribute,*args,**kwargs): return self._smooth(kwargs.get('t',0.))\ *self._wrap_pot_func(attribute)(self._pot,*args,**kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/DiskSCFPotential.py0000644000175100001710000005022614327773303021045 0ustar00runnerdocker############################################################################### # DiskSCFPotential.py: Potential expansion for disk+halo potentials ############################################################################### import copy import numpy import scipy from packaging.version import parse as parse_version _SCIPY_VERSION= parse_version(scipy.__version__) if _SCIPY_VERSION < parse_version('0.10'): #pragma: no cover from scipy.maxentropy import logsumexp elif _SCIPY_VERSION < parse_version('0.19'): #pragma: no cover from scipy.misc import logsumexp else: from scipy.special import logsumexp from scipy import integrate from ..util import conversion from .Potential import Potential from .SCFPotential import (SCFPotential, scf_compute_coeffs, scf_compute_coeffs_axi) class DiskSCFPotential(Potential): """Class that implements a basis-function-expansion technique for solving the Poisson equation for disk (+halo) systems. We solve the Poisson equation for a given density :math:`\\rho(R,\\phi,z)` by introducing *K* helper function pairs :math:`[\\Sigma_i(R),h_i(z)]`, with :math:`h_i(z) = \\mathrm{d}^2 H(z) / \\mathrm{d} z^2` and search for solutions of the form .. math:: \\Phi(R,\\phi,z = \\Phi_{\\mathrm{ME}}(R,\\phi,z) + 4\\pi G\\sum_i \\Sigma_i(r)\\,H_i(z)\\,, where :math:`r` is the spherical radius :math:`r^2 = R^2+z^2`. We can solve for :math:`\\Phi_{\\mathrm{ME}}(R,\\phi,z)` by solving .. math:: \\frac{\\Delta \\Phi_{\\mathrm{ME}}(R,\\phi,z)}{4\\pi G} = \\rho(R,\\phi,z) - \\sum_i\\left\\{ \\Sigma_i(r)\\,h_i(z) + \\frac{\\mathrm{d}^2 \\Sigma_i(r)}{\\mathrm{d} r^2}\\,H_i(z)+\\frac{2}{r}\\,\\frac{\\mathrm{d} \\Sigma_i(r)}{\\mathrm{d} r}\\left[H_i(z)+z\\,\\frac{\\mathrm{d}H_i(z)}{\\mathrm{d} z}\\right]\\right\\}\\,. We solve this equation by using the :ref:`SCFPotential ` class and methods (:ref:`scf_compute_coeffs_axi ` or :ref:`scf_compute_coeffs ` depending on whether :math:`\\rho(R,\\phi,z)` is axisymmetric or not). This technique works very well if the disk portion of the potential can be exactly written as :math:`\\rho_{\\mathrm{disk}} = \\sum_i \\Sigma_i(R)\\,h_i(z)`, because the effective density on the right-hand side of this new Poisson equation is then not 'disky' and can be well represented using spherical harmonics. But the technique is general and can be used to compute the potential of any disk+halo potential; the closer the disk is to :math:`\\rho_{\\mathrm{disk}} \\approx \\sum_i \\Sigma_i(R)\\,h_i(z)`, the better the technique works. This technique was introduced by `Kuijken & Dubinski (1995) `__ and was popularized by `Dehnen & Binney (1998) `__. The current implementation is a slight generalization of the technique in those papers and uses the SCF approach of `Hernquist & Ostriker (1992) `__ to solve the Poisson equation for :math:`\\Phi_{\\mathrm{ME}}(R,\\phi,z)` rather than solving it on a grid using spherical harmonics and interpolating the solution (as done in `Dehnen & Binney 1998 `__). """ def __init__(self,amp=1.,normalize=False, dens= lambda R,z: 13.5*numpy.exp(-3.*R)\ *numpy.exp(-27.*numpy.fabs(z)), Sigma={'type':'exp','h':1./3.,'amp':1.}, hz={'type':'exp','h':1./27.}, Sigma_amp=None,dSigmadR=None,d2SigmadR2=None, Hz=None,dHzdz=None, N=10,L=10,a=1.,radial_order=None,costheta_order=None, phi_order=None, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a DiskSCF Potential INPUT: amp - amplitude to be applied to the potential (default: 1); cannot have units currently normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) dens= function of R,z[,phi optional] that gives the density [in natural units, cannot return a Quantity currently] N=, L=, a=, radial_order=, costheta_order=, phi_order= keywords setting parameters for SCF solution for Phi_ME (see :ref:`scf_compute_coeffs_axi ` or :ref:`scf_compute_coeffs ` depending on whether :math:`\\rho(R,\\phi,z)` is axisymmetric or not) Either: (a) Sigma= Dictionary of surface density (example: {'type':'exp','h':1./3.,'amp':1.,'Rhole':0.} for amp x exp(-Rhole/R-R/h) ) hz= Dictionary of vertical profile, either 'exp' or 'sech2' (example {'type':'exp','h':1./27.} for exp(-|z|/h)/[2h], sech2 is sech^2(z/[2h])/[4h]) (b) Sigma= function of R that gives the surface density dSigmadR= function that gives d Sigma / d R d2SigmadR2= function that gives d^2 Sigma / d R^2 Sigma_amp= amplitude to apply to all Sigma functions hz= function of z that gives the vertical profile Hz= function of z such that d^2 Hz(z) / d z^2 = hz dHzdz= function of z that gives d Hz(z) / d z In both of these cases lists of arguments can be given for multiple disk components; can't mix (a) and (b) in these lists; if hz is a single item the same vertical profile is assumed for all Sigma OUTPUT: DiskSCFPotential object HISTORY: 2016-12-26 - Written - Bovy (UofT) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units=None) a= conversion.parse_length(a,ro=self._ro) # Parse and store given functions self.isNonAxi= dens.__code__.co_argcount == 3 self._parse_Sigma(Sigma_amp,Sigma,dSigmadR,d2SigmadR2) self._parse_hz(hz,Hz,dHzdz) if self.isNonAxi: self._inputdens= dens else: self._inputdens= lambda R,z,phi: dens(R,z) # Solve Poisson equation for Phi_ME if not self.isNonAxi: dens_func= lambda R,z: phiME_dens(R,z,0.,self._inputdens, self._Sigma,self._dSigmadR, self._d2SigmadR2, self._hz,self._Hz, self._dHzdz,self._Sigma_amp) Acos, Asin= scf_compute_coeffs_axi(dens_func,N,L,a=a, radial_order=radial_order, costheta_order=costheta_order) else: dens_func= lambda R,z,phi: phiME_dens(R,z,phi,self._inputdens, self._Sigma,self._dSigmadR, self._d2SigmadR2, self._hz,self._Hz, self._dHzdz,self._Sigma_amp) Acos, Asin= scf_compute_coeffs(dens_func,N,L,a=a, radial_order=radial_order, costheta_order=costheta_order, phi_order=phi_order) self._phiME_dens_func= dens_func self._scf= SCFPotential(amp=1.,Acos=Acos,Asin=Asin,a=a,ro=None,vo=None) if not self._Sigma_dict is None and not self._hz_dict is None: self.hasC= True self.hasC_dens= True if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): self.normalize(normalize) return None def _parse_Sigma(self,Sigma_amp,Sigma,dSigmadR,d2SigmadR2): """ NAME: _parse_Sigma PURPOSE: Parse the various input options for Sigma* functions HISTORY: 2016-12-27 - Written - Bovy (UofT/CCA) """ if isinstance(Sigma,dict): Sigma= [Sigma] try: nsigma= len(Sigma) except TypeError: Sigma_amp= [Sigma_amp] Sigma= [Sigma] dSigmadR= [dSigmadR] d2SigmadR2= [d2SigmadR2] nsigma= 1 self._nsigma= nsigma self._Sigma_amp= Sigma_amp self._Sigma= Sigma self._dSigmadR= dSigmadR self._d2SigmadR2= d2SigmadR2 if isinstance(Sigma[0],dict): self._Sigma_dict= copy.copy(Sigma) self._parse_Sigma_dict() else: self._Sigma_dict= None return None def _parse_Sigma_dict(self): Sigma_amp, Sigma, dSigmadR, d2SigmadR2= [], [], [], [] for ii in range(self._nsigma): ta, ts, tds, td2s= self._parse_Sigma_dict_indiv(self._Sigma[ii]) Sigma_amp.append(ta) Sigma.append(ts) dSigmadR.append(tds) d2SigmadR2.append(td2s) self._Sigma_amp= Sigma_amp self._Sigma= Sigma self._dSigmadR= dSigmadR self._d2SigmadR2= d2SigmadR2 return None def _parse_Sigma_dict_indiv(self,Sigma): stype= Sigma.get('type','exp') if stype == 'exp' and not 'Rhole' in Sigma: rd= Sigma.get('h',1./3.) ta= Sigma.get('amp',1.) ts= lambda R, trd=rd: numpy.exp(-R/trd) tds= lambda R, trd=rd: -numpy.exp(-R/trd)/trd td2s= lambda R, trd=rd: numpy.exp(-R/trd)/trd**2. elif stype == 'expwhole' or (stype == 'exp' and 'Rhole' in Sigma): rd= Sigma.get('h',1./3.) rm= Sigma.get('Rhole',0.5) ta= Sigma.get('amp',1.) ts= lambda R, trd=rd, trm=rm: numpy.exp(-trm/R-R/trd) tds= lambda R, trd=rd, trm=rm: \ (trm/R**2.-1./trd)*numpy.exp(-trm/R-R/trd) td2s= lambda R, trd=rd,trm=rm: \ ((trm/R**2.-1./trd)**2.-2.*trm/R**3.)*numpy.exp(-trm/R-R/trd) return (ta,ts,tds,td2s) def _parse_hz(self,hz,Hz,dHzdz): """ NAME: _parse_hz PURPOSE: Parse the various input options for Sigma* functions HISTORY: 2016-12-27 - Written - Bovy (UofT/CCA) """ if isinstance(hz,dict): hz= [hz] try: nhz= len(hz) except TypeError: hz= [hz] Hz= [Hz] dHzdz= [dHzdz] nhz= 1 if nhz != self._nsigma and nhz != 1: raise ValueError('Number of hz functions needs to be equal to the number of Sigma functions or to 1') if nhz == 1 and self._nsigma > 1: hz= [hz[0] for ii in range(self._nsigma)] if not isinstance(hz[0],dict): Hz= [Hz[0] for ii in range(self._nsigma)] dHzdz= [dHzdz[0] for ii in range(self._nsigma)] self._Hz= Hz self._hz= hz self._dHzdz= dHzdz self._nhz= len(self._hz) if isinstance(hz[0],dict): self._hz_dict= copy.copy(hz) self._parse_hz_dict() else: self._hz_dict= None return None def _parse_hz_dict(self): hz, Hz, dHzdz= [], [], [] for ii in range(self._nhz): th, tH, tdH= self._parse_hz_dict_indiv(self._hz[ii]) hz.append(th) Hz.append(tH) dHzdz.append(tdH) self._hz= hz self._Hz= Hz self._dHzdz= dHzdz return None def _parse_hz_dict_indiv(self,hz): htype= hz.get('type','exp') if htype == 'exp': zd= hz.get('h',0.0375) th= lambda z, tzd=zd: 1./2./tzd*numpy.exp(-numpy.fabs(z)/tzd) tH= lambda z, tzd= zd: (numpy.exp(-numpy.fabs(z)/tzd)-1. +numpy.fabs(z)/tzd)*tzd/2. tdH= lambda z, tzd= zd: 0.5*numpy.sign(z)\ *(1.-numpy.exp(-numpy.fabs(z)/tzd)) elif htype == 'sech2': zd= hz.get('h',0.0375) # th/tH written so as to avoid overflow in cosh th= lambda z, tzd=zd: numpy.exp(\ -logsumexp(numpy.array([z/tzd,-z/tzd, numpy.log(2.)*numpy.ones_like(z)]),axis=0))/tzd tH= lambda z, tzd= zd: \ tzd*(logsumexp(numpy.array([z/2./tzd,-z/2./tzd]),axis=0)\ -numpy.log(2.)) tdH= lambda z, tzd= zd: numpy.tanh(z/2./tzd)/2. return (th,tH,tdH) def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at (R,z, phi) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: potential at (R,z, phi) HISTORY: 2016-12-26 - Written - Bovy (UofT/CCA) """ r= numpy.sqrt(R**2.+z**2.) out= self._scf(R,z,phi=phi,use_physical=False) for a,s,H in zip(self._Sigma_amp,self._Sigma,self._Hz): out+= 4.*numpy.pi*a*s(r)*H(z) return out def _Rforce(self,R,z,phi=0, t=0): """ NAME: _Rforce PURPOSE: evaluate the radial force at (R,z, phi) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: radial force at (R,z, phi) HISTORY: 2016-12-26 - Written - Bovy (UofT/CCA) """ r= numpy.sqrt(R**2.+z**2.) out= self._scf.Rforce(R,z,phi=phi,use_physical=False) for a,ds,H in zip(self._Sigma_amp,self._dSigmadR,self._Hz): out-= 4.*numpy.pi*a*ds(r)*H(z)*R/r return out def _zforce(self,R,z,phi=0,t=0): """ NAME: _zforce PURPOSE: evaluate the vertical force at (R,z, phi) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: vertical force at (R,z, phi) HISTORY: 2016-12-26 - Written - Bovy (UofT/CCA) """ r= numpy.sqrt(R**2.+z**2.) out= self._scf.zforce(R,z,phi=phi,use_physical=False) for a,s,ds,H,dH in zip(self._Sigma_amp,self._Sigma,self._dSigmadR, self._Hz,self._dHzdz): out-= 4.*numpy.pi*a*(ds(r)*H(z)*z/r+s(r)*dH(z)) return out def _phitorque(self,R,z,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the azimuthal torque HISTORY: 2016-12-26 - Written - Bovy (UofT) """ return self._scf.phitorque(R,z,phi=phi,use_physical=False) def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2016-12-26 - Written - Bovy (UofT/CCA) """ r= numpy.sqrt(R**2.+z**2.) out= self._scf.R2deriv(R,z,phi=phi,use_physical=False) for a,ds,d2s,H in zip(self._Sigma_amp,self._dSigmadR,self._d2SigmadR2, self._Hz): out+= 4.*numpy.pi*a*H(z)/r**2.*(d2s(r)*R**2.+z**2./r*ds(r)) return out def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second vertical derivative HISTORY: 2016-12-26 - Written - Bovy (UofT/CCA) """ r= numpy.sqrt(R**2.+z**2.) out= self._scf.z2deriv(R,z,phi=phi,use_physical=False) for a,s,ds,d2s,h,H,dH in zip(self._Sigma_amp, self._Sigma,self._dSigmadR,self._d2SigmadR2, self._hz,self._Hz,self._dHzdz): out+= 4.*numpy.pi*a*(H(z)/r**2.*(d2s(r)*z**2.+ds(r)*R**2./r) +2.*ds(r)*dH(z)*z/r+s(r)*h(z)) return out def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2phi/dR/dz HISTORY: 2016-12-26 - Written - Bovy (UofT/CCA) """ r= numpy.sqrt(R**2.+z**2.) out= self._scf.Rzderiv(R,z,phi=phi,use_physical=False) for a,ds,d2s,H,dH in zip(self._Sigma_amp,self._dSigmadR, self._d2SigmadR2,self._Hz,self._dHzdz): out+= 4.*numpy.pi*a*(H(z)*R*z/r**2.*(d2s(r)-ds(r)/r) +ds(r)*dH(z)*R/r) return out def _phi2deriv(self,R,z,phi=0.,t=0.): """ NAME: _phi2deriv PURPOSE: evaluate the second azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second azimuthal derivative HISTORY: 2016-12-26 - Written - Bovy (UofT/CCA) """ return self._scf.phi2deriv(R,z,phi=phi,use_physical=False) def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density at (R,z, phi) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: density at (R,z, phi) HISTORY: 2016-12-26 - Written - Bovy (UofT/CCA) """ r= numpy.sqrt(R**2.+z**2.) out= self._scf.dens(R,z,phi=phi,use_physical=False) for a,s,ds,d2s,h,H,dH in zip(self._Sigma_amp,self._Sigma, self._dSigmadR,self._d2SigmadR2, self._hz,self._Hz,self._dHzdz): out+= a*(s(r)*h(z)+d2s(r)*H(z)+2./r*ds(r)*(H(z)+z*dH(z))) return out def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R (and z) for this potential; if z=None, integrate spherical INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2021-03-09 - Written - Bovy (UofT) """ if not z is None: raise AttributeError # Hack to fall back to general out= self._scf.mass(R,z=None,use_physical=False) r= R def _integrand(theta): # ~ rforce tz= r*numpy.cos(theta) tR= r*numpy.sin(theta) out= 0. for a,s,ds,H,dH in zip(self._Sigma_amp,self._Sigma,self._dSigmadR, self._Hz,self._dHzdz): out+= a*ds(r)*H(tz)*tR**2 out+= a*(ds(r)*H(tz)*tz/r+s(r)*dH(tz))*tz*r return out*numpy.sin(theta) return out+2.*numpy.pi*integrate.quad(_integrand,0.,numpy.pi)[0] def phiME_dens(R,z,phi,dens,Sigma,dSigmadR,d2SigmadR2,hz,Hz,dHzdz,Sigma_amp): """The density corresponding to phi_ME""" r= numpy.sqrt(R**2.+z**2.) out= dens(R,z,phi) for a,s,ds,d2s,h,H,dH \ in zip(Sigma_amp,Sigma,dSigmadR,d2SigmadR2,hz,Hz,dHzdz): out-= a*(s(r)*h(z)+d2s(r)*H(z)+2./r*ds(r)*(H(z)+z*dH(z))) return out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/DissipativeForce.py0000644000175100001710000001205214327773303021175 0ustar00runnerdocker############################################################################### # DissipativeForce.py: top-level class for non-conservative forces ############################################################################### import numpy from ..util.conversion import physical_conversion, potential_physical_input from .Force import Force class DissipativeForce(Force): """Top-level class for non-conservative forces (cannot be derived from a potential function)""" def __init__(self,amp=1.,ro=None,vo=None,amp_units=None): """ NAME: __init__ PURPOSE: INPUT: amp - amplitude to be applied when evaluating the potential and its forces OUTPUT: HISTORY: 2018-03-16 - Started - Bovy (UofT) """ Force.__init__(self,amp=amp,ro=ro,vo=vo,amp_units=amp_units) self.dim= 3 self.isNonAxi= True # Default: are non-axisymmetric self.hasC= False self.hasC_dxdv= False self.hasC_dens= False @potential_physical_input @physical_conversion('force',pop=True) def Rforce(self,R,z,phi=0.,t=0.,v=None): """ NAME: Rforce PURPOSE: evaluate cylindrical radial force F_R (R,z) INPUT: R - Cylindrical Galactocentric radius (can be Quantity) z - vertical height (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) v - 3d velocity (optional; can be Quantity) OUTPUT: F_R (R,z,phi,t,v) HISTORY: 2018-03-18 - Written - Bovy (UofT) """ return self._Rforce_nodecorator(R,z,phi=phi,t=t,v=v) def _Rforce_nodecorator(self,R,z,phi=0.,t=0.,v=None): # Separate, so it can be used during orbit integration try: return self._amp*self._Rforce(R,z,phi=phi,t=t,v=v) except AttributeError: #pragma: no cover raise from .Potential import PotentialError raise PotentialError("'_Rforce' function not implemented for this DissipativeForce") @potential_physical_input @physical_conversion('force',pop=True) def zforce(self,R,z,phi=0.,t=0.,v=None): """ NAME: zforce PURPOSE: evaluate the vertical force F_z (R,z,t) INPUT: R - Cylindrical Galactocentric radius (can be Quantity) z - vertical height (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) v - 3d velocity (optional; can be Quantity) OUTPUT: F_z (R,z,phi,t,v) HISTORY: 2018-03-16 - Written - Bovy (UofT) """ return self._zforce_nodecorator(R,z,phi=phi,t=t,v=v) def _zforce_nodecorator(self,R,z,phi=0.,t=0.,v=None): # Separate, so it can be used during orbit integration try: return self._amp*self._zforce(R,z,phi=phi,t=t,v=v) except AttributeError: #pragma: no cover from .Potential import PotentialError raise PotentialError("'_zforce' function not implemented for this DissipativeForce") @potential_physical_input @physical_conversion('force',pop=True) def phitorque(self,R,z,phi=0.,t=0.,v=None): """ NAME: phitorque PURPOSE: evaluate the azimuthal torque F_phi (R,z,phi,t,v) INPUT: R - Cylindrical Galactocentric radius (can be Quantity) z - vertical height (can be Quantity) phi - azimuth (rad; can be Quantity) t - time (optional; can be Quantity) v - 3d velocity (optional; can be Quantity) OUTPUT: F_phi (R,z,phi,t,v) HISTORY: 2018-03-16 - Written - Bovy (UofT) """ return self._phitorque_nodecorator(R,z,phi=phi,t=t,v=v) def _phitorque_nodecorator(self,R,z,phi=0.,t=0.,v=None): # Separate, so it can be used during orbit integration try: return self._amp*self._phitorque(R,z,phi=phi,t=t,v=v) except AttributeError: #pragma: no cover if self.isNonAxi: from .Potential import PotentialError raise PotentialError("'_phitorque' function not implemented for this DissipativeForce") return 0. def _isDissipative(obj): """ NAME: _isDissipative PURPOSE: Determine whether this combination of potentials and forces is Dissipative INPUT: obj - Potential/DissipativeForce instance or list of such instances OUTPUT: True or False depending on whether the object is dissipative HISTORY: 2018-03-16 - Written - Bovy (UofT) """ from .Potential import flatten obj= flatten(obj) isList= isinstance(obj,list) if isList: isCons= [not isinstance(p,DissipativeForce) for p in obj] nonCons= not numpy.prod(numpy.array(isCons)) else: nonCons= isinstance(obj,DissipativeForce) return nonCons ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/DoubleExponentialDiskPotential.py0000644000175100001710000002661614327773303024061 0ustar00runnerdocker############################################################################### # DoubleExponentialDiskPotential.py: class that implements the double # exponential disk potential # # rho(R,z) = rho_0 e^-R/h_R e^-|z|/h_z ############################################################################### import numpy from scipy import special from ..util import conversion from .Potential import Potential, check_potential_inputs_not_arrays def _de_psi(t): return t*numpy.tanh(numpy.pi/2.*numpy.sinh(t)) def _de_psiprime(t): return (numpy.sinh(numpy.pi*numpy.sinh(t)) +numpy.pi*t*numpy.cosh(t))/(numpy.cosh(numpy.pi*numpy.sinh(t))+1) class DoubleExponentialDiskPotential(Potential): """Class that implements the double exponential disk potential .. math:: \\rho(R,z) = \\mathrm{amp}\\,\\exp\\left(-R/h_R-|z|/h_z\\right) """ def __init__(self,amp=1.,hr=1./3.,hz=1./16.,normalize=False, ro=None,vo=None, de_h=1e-3,de_n=10000): """ NAME: __init__ PURPOSE: initialize a double-exponential disk potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass density or Gxmass density hr - disk scale-length (can be Quantity) hz - scale-height (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. de_h= (1e-3) step used in numerical integration (use 1000 for a lower accuracy version that is typically still high accuracy enough, but faster) de_b= (10000) number of points used in numerical integration ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: DoubleExponentialDiskPotential object HISTORY: 2010-04-16 - Written - Bovy (NYU) 2013-01-01 - Re-implemented using faster integration techniques - Bovy (IAS) 2020-12-24 - Re-implemented again using more accurate integration techniques for Bessel integrals - Bovy (UofT) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='density') hr= conversion.parse_length(hr,ro=self._ro) hz= conversion.parse_length(hz,ro=self._ro) self.hasC= True self.hasC_dens= True self._hr= hr self._scale= self._hr self._hz= hz self._alpha= 1./self._hr self._beta= 1./self._hz self._zforceNotSetUp= True #We have not calculated a typical Kz yet # For double-exponential formula self._de_h= de_h self._de_n= de_n self._de_j0zeros= special.jn_zeros(0,self._de_n)/numpy.pi self._de_j1zeros= special.jn_zeros(1,self._de_n)/numpy.pi self._de_j0_xs= numpy.pi/self._de_h\ *_de_psi(self._de_h*self._de_j0zeros) self._de_j0_weights= 2./(numpy.pi*self._de_j0zeros\ *special.j1(numpy.pi*self._de_j0zeros)**2.)\ *special.j0(self._de_j0_xs)\ *_de_psiprime(self._de_h*self._de_j0zeros) self._de_j1_xs= numpy.pi/self._de_h\ *_de_psi(self._de_h*self._de_j1zeros) self._de_j1_weights= 2./(numpy.pi*self._de_j1zeros\ *special.jv(2,numpy.pi*self._de_j1zeros)**2.)\ *special.j1(self._de_j1_xs)\ *_de_psiprime(self._de_h*self._de_j1zeros) # Potential at zero in case we want that _gamma= self._beta/self._alpha _gamma2= _gamma**2. self._pot_zero= (2.*(_gamma-1.)*numpy.sqrt(1.+_gamma2) +2.*numpy.arctanh(1./numpy.sqrt(1.+_gamma2)) -numpy.log(1.-_gamma/numpy.sqrt(1.+_gamma2)) +numpy.log(1.+_gamma/numpy.sqrt(1.+_gamma2)))\ /(2.*(1.+_gamma2)**1.5) self._pot_zero*= -4.*numpy.pi/self._alpha**2. # Normalize? if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) def _evaluate(self,R,z,phi=0.,t=0.,dR=0,dphi=0): """ NAME: _evaluate PURPOSE: evaluate the potential at (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: potential at (R,z) HISTORY: 2010-04-16 - Written - Bovy (NYU) 2012-12-26 - New method using Gaussian quadrature between zeros - Bovy (IAS) 2020-12-24 - New method using Ogata's Bessel integral formula - Bovy (UofT) """ if isinstance(R,(float,int)): floatIn= True R= numpy.array([R]) z= numpy.array([z]) else: if isinstance(z,float): z= z*numpy.ones_like(R) floatIn= False outShape= R.shape # this code can't do arbitrary shapes R= R.flatten() z= z.flatten() fun= lambda x: (self._alpha**2.+(x/R[:,numpy.newaxis])**2.)**-1.5\ *(self._beta*numpy.exp(-x/R[:,numpy.newaxis]*numpy.fabs(z[:,numpy.newaxis])) -x/R[:,numpy.newaxis]*numpy.exp(-self._beta*numpy.fabs(z[:,numpy.newaxis])))\ /(self._beta**2.-(x/R[:,numpy.newaxis])**2.) out= -4.*numpy.pi*self._alpha/R*\ numpy.nansum(fun(self._de_j0_xs)*self._de_j0_weights, axis=1) out[(R == 0)*(z == 0)]= self._pot_zero if floatIn: return out[0] else: return numpy.reshape(out,outShape) @check_potential_inputs_not_arrays def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: Rforce PURPOSE: evaluate radial force K_R (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: K_R (R,z) HISTORY: 2010-04-16 - Written - Bovy (NYU) 2012-12-26 - New method using Gaussian quadrature between zeros - Bovy (IAS) 2020-12-24 - New method using Ogata's Bessel integral formula - Bovy (UofT) """ fun= lambda x: x*(self._alpha**2.+(x/R)**2.)**-1.5\ *(self._beta*numpy.exp(-x/R*numpy.fabs(z)) -x/R*numpy.exp(-self._beta*numpy.fabs(z)))\ /(self._beta**2.-(x/R)**2.) return -4.*numpy.pi*self._alpha/R**2.\ *numpy.nansum(fun(self._de_j1_xs)*self._de_j1_weights) @check_potential_inputs_not_arrays def _zforce(self,R,z,phi=0.,t=0.): """ NAME: zforce PURPOSE: evaluate vertical force K_z (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: K_z (R,z) HISTORY: 2010-04-16 - Written - Bovy (NYU) 2012-12-26 - New method using Gaussian quadrature between zeros - Bovy (IAS) 2020-12-24 - New method using Ogata's Bessel integral formula - Bovy (UofT) """ fun= lambda x: (self._alpha**2.+(x/R)**2.)**-1.5*x/R\ *(numpy.exp(-x/R*numpy.fabs(z)) -numpy.exp(-self._beta*numpy.fabs(z)))\ /(self._beta**2.-(x/R)**2.) out= -4.*numpy.pi*self._alpha*self._beta/R*\ numpy.nansum(fun(self._de_j0_xs)*self._de_j0_weights) if z > 0.: return out else: return -out @check_potential_inputs_not_arrays def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: R2deriv PURPOSE: evaluate R2 derivative INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: -d K_R (R,z) d R HISTORY: 2012-12-27 - Written - Bovy (IAS) 2020-12-24 - New method using Ogata's Bessel integral formula - Bovy (UofT) """ fun= lambda x: x**2*(self._alpha**2.+(x/R)**2.)**-1.5\ *(self._beta*numpy.exp(-x/R*numpy.fabs(z)) -x/R*numpy.exp(-self._beta*numpy.fabs(z)))\ /(self._beta**2.-(x/R)**2.) return 4.*numpy.pi*self._alpha/R**3.\ *numpy.nansum(fun(self._de_j0_xs)*self._de_j0_weights -fun(self._de_j1_xs)/self._de_j1_xs\ *self._de_j1_weights) @check_potential_inputs_not_arrays def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: z2deriv PURPOSE: evaluate z2 derivative INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: -d K_Z (R,z) d Z HISTORY: 2012-12-26 - Written - Bovy (IAS) 2020-12-24 - New method using Ogata's Bessel integral formula - Bovy (UofT) """ fun= lambda x: (self._alpha**2.+(x/R)**2.)**-1.5*x/R\ *(x/R*numpy.exp(-x/R*numpy.fabs(z)) -self._beta*numpy.exp(-self._beta*numpy.fabs(z)))\ /(self._beta**2.-(x/R)**2.) return -4.*numpy.pi*self._alpha*self._beta/R*\ numpy.nansum(fun(self._de_j0_xs)*self._de_j0_weights) @check_potential_inputs_not_arrays def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: Rzderiv PURPOSE: evaluate the mixed R,z derivative INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: d2phi/dR/dz HISTORY: 2013-08-28 - Written - Bovy (IAS) 2020-12-24 - New method using Ogata's Bessel integral formula - Bovy (UofT) """ fun= lambda x: (self._alpha**2.+(x/R)**2.)**-1.5*(x/R)**2.\ *(numpy.exp(-x/R*numpy.fabs(z)) -numpy.exp(-self._beta*numpy.fabs(z)))\ /(self._beta**2.-(x/R)**2.) out= -4.*numpy.pi*self._alpha*self._beta/R*\ numpy.nansum(fun(self._de_j1_xs)*self._de_j1_weights) if z > 0.: return out else: return -out def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: rho (R,z) HISTORY: 2010-08-08 - Written - Bovy (NYU) """ return numpy.exp(-self._alpha*R-self._beta*numpy.fabs(z)) def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: Sigma (R,z) HISTORY: 2018-08-19 - Written - Bovy (UofT) """ return 2.*numpy.exp(-self._alpha*R)/self._beta\ *(1.-numpy.exp(-self._beta*numpy.fabs(z))) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/EllipsoidalPotential.py0000644000175100001710000004777714327773303022101 0ustar00runnerdocker############################################################################### # EllipsoidalPotential.py: base class for potentials corresponding to # density profiles that are stratified on # ellipsoids: # # \rho(x,y,z) ~ \rho(m) # # with m^2 = x^2+y^2/b^2+z^2/c^2 # ############################################################################### import hashlib import numpy from scipy import integrate from ..util import _rotate_to_arbitrary_vector, conversion, coords from .Potential import Potential, check_potential_inputs_not_arrays class EllipsoidalPotential(Potential): """Base class for potentials corresponding to density profiles that are stratified on ellipsoids: .. math:: \\rho(x,y,z) \\equiv \\rho(m^2) where :math:`m^2 = x^2+y^2/b^2+z^2/c^2`. Note that :math:`b` and :math:`c` are defined to be the axis ratios (rather than using :math:`m^2 = x^2/a^2+y^2/b^2+z^2/c^2` as is common). Implement a specific density distribution with this form by inheriting from this class and defining functions ``_mdens(self,m)`` (the density as a function of ``m``), ``_mdens_deriv(self,m)`` (the derivative of the density as a function of ``m``), and ``_psi(self,m)``, which is: .. math:: \\psi(m) = -\\int_{m^2}^\\infty d m^2 \\rho(m^2) See PerfectEllipsoidPotential for an example and `Merritt & Fridman (1996) `_ for the formalism. """ def __init__(self,amp=1., b=1.,c=1., zvec=None,pa=None,glorder=50, ro=None,vo=None,amp_units=None): """ NAME: __init__ PURPOSE: initialize a ellipsoidal potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units that depend on the specific spheroidal potential b - y-to-x axis ratio of the density c - z-to-x axis ratio of the density zvec= (None) If set, a unit vector that corresponds to the z axis pa= (None) If set, the position angle of the x axis (rad or Quantity) glorder= (50) if set, compute the relevant force and potential integrals with Gaussian quadrature of this order amp_units - ('mass', 'velocity2', 'density') type of units that amp should have if it has units (passed to Potential.__init__) ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2018-08-06 - Started - Bovy (UofT) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units=amp_units) # Setup axis ratios self._b= b self._c= c self._b2= self._b**2. self._c2= self._c**2. self._force_hash= None # Setup rotation self._setup_zvec_pa(zvec,pa) # Setup integration self._setup_gl(glorder) if not self._aligned or numpy.fabs(self._b-1.) > 10.**-10.: self.isNonAxi= True return None def _setup_zvec_pa(self,zvec,pa): if not pa is None: pa= conversion.parse_angle(pa) if zvec is None and (pa is None or numpy.fabs(pa) < 10.**-10.): self._aligned= True else: self._aligned= False if not pa is None: pa_rot= numpy.array([[numpy.cos(pa),numpy.sin(pa),0.], [-numpy.sin(pa),numpy.cos(pa),0.], [0.,0.,1.]]) else: pa_rot= numpy.eye(3) if not zvec is None: if not isinstance(zvec,numpy.ndarray): zvec= numpy.array(zvec) zvec/= numpy.sqrt(numpy.sum(zvec**2.)) zvec_rot= _rotate_to_arbitrary_vector(\ numpy.array([[0.,0.,1.]]),zvec,inv=True)[0] else: zvec_rot= numpy.eye(3) self._rot= numpy.dot(pa_rot,zvec_rot) return None def _setup_gl(self,glorder): self._glorder= glorder if self._glorder is None: self._glx, self._glw= None, None else: self._glx, self._glw=\ numpy.polynomial.legendre.leggauss(self._glorder) # Interval change self._glx= 0.5*self._glx+0.5 self._glw*= 0.5 return None @check_potential_inputs_not_arrays def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2016-05-30 - Started - Bovy (UofT) """ if not self.isNonAxi: phi= 0. x,y,z= coords.cyl_to_rect(R,phi,z) if numpy.isinf(R): y= 0. if self._aligned: return self._evaluate_xyz(x,y,z) else: xyzp= numpy.dot(self._rot,numpy.array([x,y,z])) return self._evaluate_xyz(xyzp[0],xyzp[1],xyzp[2]) def _evaluate_xyz(self,x,y,z): """Evaluation of the potential as a function of (x,y,z) in the aligned coordinate frame""" return 2.*numpy.pi*self._b*self._c\ *_potInt(x,y,z,self._psi, self._b2,self._c2,glx=self._glx,glw=self._glw) @check_potential_inputs_not_arrays def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2016-06-09 - Written - Bovy (UofT) """ if not self.isNonAxi: phi= 0. x,y,z= coords.cyl_to_rect(R,phi,z) # Compute all rectangular forces new_hash= hashlib.md5(numpy.array([x,y,z])).hexdigest() if new_hash == self._force_hash: Fx= self._cached_Fx Fy= self._cached_Fy Fz= self._cached_Fz else: if self._aligned: xp, yp, zp= x, y, z else: xyzp= numpy.dot(self._rot,numpy.array([x,y,z])) xp, yp, zp= xyzp[0], xyzp[1], xyzp[2] Fx= self._force_xyz(xp,yp,zp,0) Fy= self._force_xyz(xp,yp,zp,1) Fz= self._force_xyz(xp,yp,zp,2) self._force_hash= new_hash self._cached_Fx= Fx self._cached_Fy= Fy self._cached_Fz= Fz if not self._aligned: Fxyz= numpy.dot(self._rot.T,numpy.array([Fx,Fy,Fz])) Fx, Fy= Fxyz[0], Fxyz[1] return numpy.cos(phi)*Fx+numpy.sin(phi)*Fy @check_potential_inputs_not_arrays def _phitorque(self,R,z,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the azimuthal torque HISTORY: 2016-06-09 - Written - Bovy (UofT) """ if not self.isNonAxi: phi= 0. x,y,z= coords.cyl_to_rect(R,phi,z) # Compute all rectangular forces new_hash= hashlib.md5(numpy.array([x,y,z])).hexdigest() if new_hash == self._force_hash: Fx= self._cached_Fx Fy= self._cached_Fy Fz= self._cached_Fz else: if self._aligned: xp, yp, zp= x, y, z else: xyzp= numpy.dot(self._rot,numpy.array([x,y,z])) xp, yp, zp= xyzp[0], xyzp[1], xyzp[2] Fx= self._force_xyz(xp,yp,zp,0) Fy= self._force_xyz(xp,yp,zp,1) Fz= self._force_xyz(xp,yp,zp,2) self._force_hash= new_hash self._cached_Fx= Fx self._cached_Fy= Fy self._cached_Fz= Fz if not self._aligned: Fxyz= numpy.dot(self._rot.T,numpy.array([Fx,Fy,Fz])) Fx, Fy= Fxyz[0], Fxyz[1] return R*(-numpy.sin(phi)*Fx+numpy.cos(phi)*Fy) @check_potential_inputs_not_arrays def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2016-06-09 - Written - Bovy (UofT) """ if not self.isNonAxi: phi= 0. x,y,z= coords.cyl_to_rect(R,phi,z) # Compute all rectangular forces new_hash= hashlib.md5(numpy.array([x,y,z])).hexdigest() if new_hash == self._force_hash: Fx= self._cached_Fx Fy= self._cached_Fy Fz= self._cached_Fz else: if self._aligned: xp, yp, zp= x, y, z else: xyzp= numpy.dot(self._rot,numpy.array([x,y,z])) xp, yp, zp= xyzp[0], xyzp[1], xyzp[2] Fx= self._force_xyz(xp,yp,zp,0) Fy= self._force_xyz(xp,yp,zp,1) Fz= self._force_xyz(xp,yp,zp,2) self._force_hash= new_hash self._cached_Fx= Fx self._cached_Fy= Fy self._cached_Fz= Fz if not self._aligned: Fxyz= numpy.dot(self._rot.T,numpy.array([Fx,Fy,Fz])) Fz= Fxyz[2] return Fz def _force_xyz(self,x,y,z,i): """Evaluation of the i-th force component as a function of (x,y,z)""" return -4.*numpy.pi*self._b*self._c\ *_forceInt(x,y,z, lambda m: self._mdens(m), self._b2,self._c2,i,glx=self._glx,glw=self._glw) @check_potential_inputs_not_arrays def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2016-06-15 - Written - Bovy (UofT) """ if not self.isNonAxi: phi= 0. x,y,z= coords.cyl_to_rect(R,phi,z) if not self._aligned: raise NotImplementedError("2nd potential derivatives of TwoPowerTriaxialPotential not implemented for rotated coordinated frames (non-trivial zvec and pa); use RotateAndTiltWrapperPotential for this functionality instead") phixx= self._2ndderiv_xyz(x,y,z,0,0) phixy= self._2ndderiv_xyz(x,y,z,0,1) phiyy= self._2ndderiv_xyz(x,y,z,1,1) return numpy.cos(phi)**2.*phixx+numpy.sin(phi)**2.*phiyy\ +2.*numpy.cos(phi)*numpy.sin(phi)*phixy @check_potential_inputs_not_arrays def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed radial, vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed radial, vertical derivative HISTORY: 2016-06-15 - Written - Bovy (UofT) """ if not self.isNonAxi: phi= 0. x,y,z= coords.cyl_to_rect(R,phi,z) if not self._aligned: raise NotImplementedError("2nd potential derivatives of TwoPowerTriaxialPotential not implemented for rotated coordinated frames (non-trivial zvec and pa; use RotateAndTiltWrapperPotential for this functionality instead)") phixz= self._2ndderiv_xyz(x,y,z,0,2) phiyz= self._2ndderiv_xyz(x,y,z,1,2) return numpy.cos(phi)*phixz+numpy.sin(phi)*phiyz @check_potential_inputs_not_arrays def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second vertical derivative HISTORY: 2016-06-15 - Written - Bovy (UofT) """ if not self.isNonAxi: phi= 0. x,y,z= coords.cyl_to_rect(R,phi,z) if not self._aligned: raise NotImplementedError("2nd potential derivatives of TwoPowerTriaxialPotential not implemented for rotated coordinated frames (non-trivial zvec and pa; use RotateAndTiltWrapperPotential for this functionality instead)") return self._2ndderiv_xyz(x,y,z,2,2) @check_potential_inputs_not_arrays def _phi2deriv(self,R,z,phi=0.,t=0.): """ NAME: _phi2deriv PURPOSE: evaluate the second azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second azimuthal derivative HISTORY: 2016-06-15 - Written - Bovy (UofT) """ if not self.isNonAxi: phi= 0. x,y,z= coords.cyl_to_rect(R,phi,z) if not self._aligned: raise NotImplementedError("2nd potential derivatives of TwoPowerTriaxialPotential not implemented for rotated coordinated frames (non-trivial zvec and pa; use RotateAndTiltWrapperPotential for this functionality instead)") Fx= self._force_xyz(x,y,z,0) Fy= self._force_xyz(x,y,z,1) phixx= self._2ndderiv_xyz(x,y,z,0,0) phixy= self._2ndderiv_xyz(x,y,z,0,1) phiyy= self._2ndderiv_xyz(x,y,z,1,1) return R**2.*(numpy.sin(phi)**2.*phixx+numpy.cos(phi)**2.*phiyy\ -2.*numpy.cos(phi)*numpy.sin(phi)*phixy)\ +R*(numpy.cos(phi)*Fx+numpy.sin(phi)*Fy) @check_potential_inputs_not_arrays def _Rphideriv(self,R,z,phi=0.,t=0.): """ NAME: _Rphideriv PURPOSE: evaluate the mixed radial, azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed radial, azimuthal derivative HISTORY: 2016-06-15 - Written - Bovy (UofT) """ if not self.isNonAxi: phi= 0. x,y,z= coords.cyl_to_rect(R,phi,z) if not self._aligned: raise NotImplementedError("2nd potential derivatives of TwoPowerTriaxialPotential not implemented for rotated coordinated frames (non-trivial zvec and pa; use RotateAndTiltWrapperPotential for this functionality instead)") Fx= self._force_xyz(x,y,z,0) Fy= self._force_xyz(x,y,z,1) phixx= self._2ndderiv_xyz(x,y,z,0,0) phixy= self._2ndderiv_xyz(x,y,z,0,1) phiyy= self._2ndderiv_xyz(x,y,z,1,1) return R*numpy.cos(phi)*numpy.sin(phi)*\ (phiyy-phixx)+R*numpy.cos(2.*phi)*phixy\ +numpy.sin(phi)*Fx-numpy.cos(phi)*Fy @check_potential_inputs_not_arrays def _phizderiv(self,R,z,phi=0.,t=0.): """ NAME: _phizderiv PURPOSE: evaluate the mixed azimuthal, vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed radial, azimuthal derivative HISTORY: 2021-04-30 - Written - Bovy (UofT) """ if not self.isNonAxi: phi= 0. x,y,z= coords.cyl_to_rect(R,phi,z) if not self._aligned: raise NotImplementedError("2nd potential derivatives of TwoPowerTriaxialPotential not implemented for rotated coordinated frames (non-trivial zvec and pa; use RotateAndTiltWrapperPotential for this functionality instead)") phixz= self._2ndderiv_xyz(x,y,z,0,2) phiyz= self._2ndderiv_xyz(x,y,z,1,2) return R*(numpy.cos(phi)*phiyz-numpy.sin(phi)*phixz) def _2ndderiv_xyz(self,x,y,z,i,j): """General 2nd derivative of the potential as a function of (x,y,z) in the aligned coordinate frame""" return 4.*numpy.pi*self._b*self._c\ *_2ndDerivInt(x,y,z, lambda m: self._mdens(m), lambda m: self._mdens_deriv(m), self._b2,self._c2,i,j,glx=self._glx,glw=self._glw) @check_potential_inputs_not_arrays def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2018-08-06 - Written - Bovy (UofT) """ x,y,z= coords.cyl_to_rect(R,phi,z) if self._aligned: xp, yp, zp= x, y, z else: xyzp= numpy.dot(self._rot,numpy.array([x,y,z])) xp, yp, zp= xyzp[0], xyzp[1], xyzp[2] m= numpy.sqrt(xp**2.+yp**2./self._b2+zp**2./self._c2) return self._mdens(m) def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R (and z) for this potential; if z=None, integrate to z=inf INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2021-03-08 - Written - Bovy (UofT) """ if not z is None: raise AttributeError # Hack to fall back to general return 4.*numpy.pi*self._b*self._c\ *integrate.quad(lambda m: m**2.*self._mdens(m),0,R)[0] def OmegaP(self): """ NAME: OmegaP PURPOSE: return the pattern speed INPUT: (none) OUTPUT: pattern speed HISTORY: 2016-05-31 - Written - Bovy (UofT) """ return 0. def _potInt(x,y,z,psi,b2,c2,glx=None,glw=None): r"""int_0^\infty [psi(m)-psi(\infy)]/sqrt([1+tau]x[b^2+tau]x[c^2+tau])dtau""" def integrand(s): t= 1/s**2.-1. return psi(numpy.sqrt(x**2./(1.+t)+y**2./(b2+t)+z**2./(c2+t)))\ /numpy.sqrt((1.+(b2-1.)*s**2.)*(1.+(c2-1.)*s**2.)) if glx is None: return integrate.quad(integrand,0.,1.)[0] else: return numpy.sum(glw*integrand(glx)) def _forceInt(x,y,z,dens,b2,c2,i,glx=None,glw=None): """Integral that gives the force in x,y,z""" def integrand(s): t= 1/s**2.-1. return dens(numpy.sqrt(x**2./(1.+t)+y**2./(b2+t)+z**2./(c2+t)))\ *(x/(1.+t)*(i==0)+y/(b2+t)*(i==1)+z/(c2+t)*(i==2))\ /numpy.sqrt((1.+(b2-1.)*s**2.)*(1.+(c2-1.)*s**2.)) if glx is None: return integrate.quad(integrand,0.,1.)[0] else: return numpy.sum(glw*integrand(glx)) def _2ndDerivInt(x,y,z,dens,densDeriv,b2,c2,i,j,glx=None,glw=None): """Integral that gives the 2nd derivative of the potential in x,y,z""" def integrand(s): t= 1/s**2.-1. m= numpy.sqrt(x**2./(1.+t)+y**2./(b2+t)+z**2./(c2+t)) return (densDeriv(m) *(x/(1.+t)*(i==0)+y/(b2+t)*(i==1)+z/(c2+t)*(i==2)) *(x/(1.+t)*(j==0)+y/(b2+t)*(j==1)+z/(c2+t)*(j==2))/m\ +dens(m)*(i==j)*(1./(1.+t)*(i==0)+1./(b2+t)*(i==1)+1./(c2+t)*(i==2)))\ /numpy.sqrt((1.+(b2-1.)*s**2.)*(1.+(c2-1.)*s**2.)) if glx is None: return integrate.quad(integrand,0.,1.)[0] else: return numpy.sum(glw*integrand(glx)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/EllipticalDiskPotential.py0000644000175100001710000001747414327773303022524 0ustar00runnerdocker############################################################################### # EllipticalDiskPotential: Kuijken & Tremaine (1994)'s elliptical disk # potential ############################################################################### import numpy from ..util import conversion from .planarPotential import planarPotential _degtorad= numpy.pi/180. class EllipticalDiskPotential(planarPotential): """Class that implements the Elliptical disk potential of Kuijken & Tremaine (1994) .. math:: \\Phi(R,\\phi) = \\mathrm{amp}\\,\\phi_0\\,\\left(\\frac{R}{R_1}\\right)^p\\,\\cos\\left(2\\,(\\phi-\\phi_b)\\right) This potential can be grown between :math:`t_{\\mathrm{form}}` and :math:`t_{\\mathrm{form}}+T_{\\mathrm{steady}}` in a similar way as DehnenBarPotential, but times are given directly in galpy time units """ def __init__(self,amp=1.,phib=25.*_degtorad, p=1.,twophio=0.01,r1=1., tform=None,tsteady=None, cp=None,sp=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize an Elliptical disk potential phi(R,phi) = phio (R/Ro)^p cos[2(phi-phib)] INPUT: amp= amplitude to be applied to the potential (default: 1.), see twophio below tform= start of growth (to smoothly grow this potential (can be Quantity) tsteady= time delay at which the perturbation is fully grown (default: 2.; can be Quantity) p= power-law index of the phi(R) = (R/Ro)^p part r1= (1.) normalization radius for the amplitude (can be Quantity) Either: a) phib= angle (in rad; default=25 degree; or can be Quantity) twophio= potential perturbation (in terms of 2phio/vo^2 if vo=1 at Ro=1; can be Quantity with units of velocity-squared) b) cp, sp= twophio * cos(2phib), twophio * sin(2phib) (can be Quantity with units of velocity-squared) OUTPUT: (none) HISTORY: 2011-10-19 - Started - Bovy (IAS) """ planarPotential.__init__(self,amp=amp,ro=ro,vo=vo) phib= conversion.parse_angle(phib) r1= conversion.parse_length(r1,ro=self._ro) tform= conversion.parse_time(tform,ro=self._ro,vo=self._vo) tsteady= conversion.parse_time(tsteady,ro=self._ro,vo=self._vo) twophio= conversion.parse_energy(twophio,vo=self._vo) cp= conversion.parse_energy(cp,vo=self._vo) sp= conversion.parse_energy(sp,vo=self._vo) # Back to old definition self._amp/= r1**p self.hasC= True self.hasC_dxdv= True if cp is None or sp is None: self._phib= phib self._twophio= twophio else: self._twophio= numpy.sqrt(cp*cp+sp*sp) self._phib= numpy.arctan2(sp,cp)/2. self._p= p if not tform is None: self._tform= tform else: self._tform= None if not tsteady is None: self._tsteady= self._tform+tsteady else: if self._tform is None: self._tsteady= None else: self._tsteady= self._tform+2. def _evaluate(self,R,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,phi,t INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: Phi(R,phi,t) HISTORY: 2011-10-19 - Started - Bovy (IAS) """ #Calculate relevant time if not self._tform is None: if t < self._tform: smooth= 0. elif t < self._tsteady: deltat= t-self._tform xi= 2.*deltat/(self._tsteady-self._tform)-1. smooth= (3./16.*xi**5.-5./8*xi**3.+15./16.*xi+.5) else: #fully on smooth= 1. else: smooth= 1. return smooth*self._twophio/2.*R**self._p\ *numpy.cos(2.*(phi-self._phib)) def _Rforce(self,R,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the radial force HISTORY: 2011-10-19 - Written - Bovy (IAS) """ #Calculate relevant time if not self._tform is None: if t < self._tform: smooth= 0. elif t < self._tsteady: deltat= t-self._tform xi= 2.*deltat/(self._tsteady-self._tform)-1. smooth= (3./16.*xi**5.-5./8*xi**3.+15./16.*xi+.5) else: #fully on smooth= 1. else: smooth= 1. return -smooth*self._p*self._twophio/2.*R**(self._p-1.)\ *numpy.cos(2.*(phi-self._phib)) def _phitorque(self,R,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the azimuthal torque HISTORY: 2011-10-19 - Written - Bovy (IAS) """ #Calculate relevant time if not self._tform is None: if t < self._tform: smooth= 0. elif t < self._tsteady: deltat= t-self._tform xi= 2.*deltat/(self._tsteady-self._tform)-1. smooth= (3./16.*xi**5.-5./8*xi**3.+15./16.*xi+.5) else: #fully on smooth= 1. else: smooth= 1. return smooth*self._twophio*R**self._p*numpy.sin(2.*(phi-self._phib)) def _R2deriv(self,R,phi=0.,t=0.): #Calculate relevant time if not self._tform is None: if t < self._tform: smooth= 0. elif t < self._tsteady: deltat= t-self._tform xi= 2.*deltat/(self._tsteady-self._tform)-1. smooth= (3./16.*xi**5.-5./8*xi**3.+15./16.*xi+.5) else: #fully on smooth= 1. else: smooth= 1. return smooth*self._p*(self._p-1.)/2.*self._twophio*R**(self._p-2.)\ *numpy.cos(2.*(phi-self._phib)) def _phi2deriv(self,R,phi=0.,t=0.): #Calculate relevant time if not self._tform is None: if t < self._tform: smooth= 0. elif t < self._tsteady: deltat= t-self._tform xi= 2.*deltat/(self._tsteady-self._tform)-1. smooth= (3./16.*xi**5.-5./8*xi**3.+15./16.*xi+.5) else: #perturbation is fully on smooth= 1. else: smooth= 1. return -2.*smooth*self._twophio*R**self._p*numpy.cos(2.*(phi-self._phib)) def _Rphideriv(self,R,phi=0.,t=0.): #Calculate relevant time if not self._tform is None: if t < self._tform: smooth= 0. elif t < self._tsteady: deltat= t-self._tform xi= 2.*deltat/(self._tsteady-self._tform)-1. smooth= (3./16.*xi**5.-5./8*xi**3.+15./16.*xi+.5) else: #perturbation is fully on smooth= 1. else: smooth= 1. return -smooth*self._p*self._twophio*R**(self._p-1.)*numpy.sin(2.*(phi-self._phib)) def tform(self): #pragma: no cover """ NAME: tform PURPOSE: return formation time of the perturbation INPUT: (none) OUTPUT: tform in normalized units HISTORY: 2011-10-19 - Written - Bovy (IAS) """ return self._tform ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/FerrersPotential.py0000644000175100001710000004242214327773303021226 0ustar00runnerdocker############################################################################### # FerrersPotential.py: General class for triaxial Ferrers Potential # # rho(r) = amp/[a^3 b c pi^1.5] Gamma(n+5/2)/Gamma(n+1) (1 - (m/a)^2)^n # # with # # m^2 = x^2 + y^2/b^2 + z^2/c^2 ######################################################################## import hashlib import numpy from scipy import integrate from scipy.special import gamma from ..util import conversion, coords from .Potential import Potential class FerrersPotential(Potential): """Class that implements triaxial Ferrers potential for the ellipsoidal density profile with the short axis along the z-direction .. math:: \\rho(x,y,z) = \\frac{\\mathrm{amp}}{\\pi^{1.5} a^3 b c} \\frac{\\Gamma(n+\\frac{5}{2})}{\\Gamma(n+1)}\\,(1-(m/a)^2)^n with .. math:: m^2 = x'^2 + \\frac{y'^2}{b^2}+\\frac{z'^2}{c^2} and :math:`(x',y',z')` is a rotated frame wrt :math:`(x,y,z)` so that the major axis is aligned with :math:`x'`. Note that this potential has not yet been optimized for speed and has no C implementation, so orbit integration is currently slow. """ def __init__(self,amp=1.,a=1.,n=2,b=0.35,c=0.2375,omegab=0., pa=0.,normalize=False,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a Ferrers potential INPUT: amp - total mass of the ellipsoid determines the amplitude of the potential; can be a Quantity with units of mass or Gxmass a - scale radius (can be Quantity) n - power of Ferrers density (n > 0) b - y-to-x axis ratio of the density c - z-to-x axis ratio of the density omegab - rotation speed of the ellipsoid (can be Quantity) pa= (None) If set, the position angle of the x axis (rad or Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) omegab= conversion.parse_frequency(omegab,ro=self._ro,vo=self._vo) pa= conversion.parse_angle(pa) self.a= a self._scale= self.a if n <= 0: raise ValueError('FerrersPotential requires n > 0') self.n= n self._b= b self._c= c self._omegab= omegab self._a2= self.a**2 self._b2= self._b**2. self._c2= self._c**2. self._force_hash= None self._pa = pa self._rhoc_M = gamma(n+2.5)/gamma(n+1) / numpy.pi**1.5/a**3/b/c if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) if numpy.fabs(self._b-1.) > 10.**-10.: self.isNonAxi= True return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) """ if not self.isNonAxi: phi= 0. x,y,z= coords.cyl_to_rect(R,phi,z) xy= numpy.dot(self.rot(t),numpy.array([x,y])) x,y= xy[0],xy[1] return self._evaluate_xyz(x,y,z) def _evaluate_xyz(self,x,y,z=0.): """Evaluation of the potential as a function of (x,y,z) in the aligned coordinate frame""" return -numpy.pi * self._rhoc_M /(self.n+1.)*self.a**3*self._b*self._c * \ _potInt(x, y, z, self._a2, self._b2*self._a2, self._c2*self._a2, self.n) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE:T evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force """ if not self.isNonAxi: phi= 0. self._compute_xyzforces(R,z,phi,t) return numpy.cos(phi)*self._cached_Fx+numpy.sin(phi)*self._cached_Fy def _phitorque(self,R,z,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the azimuthal torque """ if not self.isNonAxi: phi= 0. self._compute_xyzforces(R,z,phi,t) return R*(-numpy.sin(phi)*self._cached_Fx\ +numpy.cos(phi)*self._cached_Fy) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force """ if not self.isNonAxi: phi= 0. self._compute_xyzforces(R,z,phi,t) return self._cached_Fz def _compute_xyz(self,R,phi,z,t): return coords.cyl_to_rect(R,phi-self._pa-self._omegab*t,z) def _compute_xyzforces(self,R,z,phi,t): # Compute all rectangular forces new_hash= hashlib.md5(numpy.array([R,phi,z,t])).hexdigest() if new_hash == self._force_hash: Fx= self._cached_Fx Fy= self._cached_Fy Fz= self._cached_Fz else: x,y,z= self._compute_xyz(R,phi,z,t) Fx= self._xforce_xyz(x,y,z) Fy= self._yforce_xyz(x,y,z) Fz= self._zforce_xyz(x,y,z) self._force_hash= new_hash tp= self._pa+self._omegab*t cp, sp= numpy.cos(tp), numpy.sin(tp) self._cached_Fx= cp*Fx-sp*Fy self._cached_Fy= sp*Fx+cp*Fy self._cached_Fz= Fz def _xforce_xyz(self,x,y,z): """Evaluation of the x force as a function of (x,y,z) in the aligned coordinate frame""" return -2.*numpy.pi*self._rhoc_M * self.a**3*self._b*self._c * \ _forceInt(x, y, z, self._a2, self._b2*self._a2, self._c2*self._a2, self.n, 0) def _yforce_xyz(self,x,y,z): """Evaluation of the y force as a function of (x,y,z) in the aligned coordinate frame""" return -2.*numpy.pi*self._rhoc_M * self.a**3*self._b*self._c * \ _forceInt(x, y, z, self._a2, self._b2*self._a2, self._c2*self._a2, self.n, 1) def _zforce_xyz(self,x,y,z): """Evaluation of the z force as a function of (x,y,z) in the aligned coordinate frame""" return -2.*numpy.pi*self._rhoc_M * self.a**3*self._b*self._c * \ _forceInt(x, y, z, self._a2, self._b2*self._a2, self._c2*self._a2, self.n, 2) def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative """ if not self.isNonAxi: phi= 0. x,y,z= self._compute_xyz(R,phi,z,t) phixxa= self._2ndderiv_xyz(x,y,z,0,0) phixya= self._2ndderiv_xyz(x,y,z,0,1) phiyya= self._2ndderiv_xyz(x,y,z,1,1) ang = self._omegab*t + self._pa c, s = numpy.cos(ang), numpy.sin(ang) phixx = c**2*phixxa + 2.*c*s*phixya + s**2*phiyya phixy = (c**2-s**2)*phixya + c*s*(phiyya - phixxa) phiyy = s**2*phixxa - 2.*c*s*phixya + c**2*phiyya return numpy.cos(phi)**2.*phixx + numpy.sin(phi)**2.*phiyy + \ 2.*numpy.cos(phi)*numpy.sin(phi)*phixy def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed radial, vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed radial, vertical derivative """ if not self.isNonAxi: phi= 0. x,y,z= self._compute_xyz(R,phi,z,t) phixza= self._2ndderiv_xyz(x,y,z,0,2) phiyza= self._2ndderiv_xyz(x,y,z,1,2) ang = self._omegab*t + self._pa c, s = numpy.cos(ang), numpy.sin(ang) phixz = c*phixza + s*phiyza phiyz = -s*phixza + c*phiyza return numpy.cos(phi)*phixz + numpy.sin(phi)*phiyz def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second vertical derivative """ if not self.isNonAxi: phi= 0. x,y,z= self._compute_xyz(R,phi,z,t) return self._2ndderiv_xyz(x,y,z,2,2) def _phi2deriv(self,R,z,phi=0.,t=0.): """ NAME: _phi2deriv PURPOSE: evaluate the second azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second azimuthal derivative """ if not self.isNonAxi: phi= 0. x,y,z= self._compute_xyz(R,phi,z,t) Fx= self._xforce_xyz(x,y,z) Fy= self._yforce_xyz(x,y,z) Fxy= numpy.dot(self.rot(t, transposed = True),numpy.array([Fx,Fy])) Fx, Fy= Fxy[0], Fxy[1] phixxa= self._2ndderiv_xyz(x,y,z,0,0) phixya= self._2ndderiv_xyz(x,y,z,0,1) phiyya= self._2ndderiv_xyz(x,y,z,1,1) ang = self._omegab*t + self._pa c, s = numpy.cos(ang), numpy.sin(ang) phixx = c**2*phixxa + 2.*c*s*phixya + s**2*phiyya phixy = (c**2-s**2)*phixya + c*s*(phiyya - phixxa) phiyy = s**2*phixxa - 2.*c*s*phixya + c**2*phiyya return R**2.*(numpy.sin(phi)**2.*phixx+numpy.cos(phi)**2.*phiyy\ -2.*numpy.cos(phi)*numpy.sin(phi)*phixy)\ +R*(numpy.cos(phi)*Fx+numpy.sin(phi)*Fy) def _Rphideriv(self,R,z,phi=0.,t=0.): """ NAME: _Rphideriv PURPOSE: evaluate the mixed radial, azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed radial, azimuthal derivative """ if not self.isNonAxi: phi= 0. x,y,z= self._compute_xyz(R,phi,z,t) Fx= self._xforce_xyz(x,y,z) Fy= self._yforce_xyz(x,y,z) Fxy= numpy.dot(self.rot(t, transposed = True),numpy.array([Fx,Fy])) Fx, Fy= Fxy[0], Fxy[1] phixxa= self._2ndderiv_xyz(x,y,z,0,0) phixya= self._2ndderiv_xyz(x,y,z,0,1) phiyya= self._2ndderiv_xyz(x,y,z,1,1) ang = self._omegab*t + self._pa c, s = numpy.cos(ang), numpy.sin(ang) phixx = c**2*phixxa + 2.*c*s*phixya + s**2*phiyya phixy = (c**2-s**2)*phixya + c*s*(phiyya - phixxa) phiyy = s**2*phixxa - 2.*c*s*phixya + c**2*phiyya return R*numpy.cos(phi)*numpy.sin(phi)*\ (phiyy-phixx)+R*numpy.cos(2.*(phi))*phixy\ +numpy.sin(phi)*Fx-numpy.cos(phi)*Fy def _phizderiv(self,R,z,phi=0.,t=0.): """ NAME: _phizderiv PURPOSE: evaluate the mixed azimuthal, vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed azimuthal, vertical derivative HISTORY: 2021-04-30 - Written - Bovy (UofT) """ if not self.isNonAxi: phi= 0. x,y,z= self._compute_xyz(R,phi,z,t) phixza= self._2ndderiv_xyz(x,y,z,0,2) phiyza= self._2ndderiv_xyz(x,y,z,1,2) phixz, phiyz= numpy.dot(self.rot(t,transposed=True), numpy.array([phixza,phiyza])) return R*(numpy.cos(phi)*phiyz-numpy.sin(phi)*phixz) def _2ndderiv_xyz(self,x,y,z,i,j): r"""General 2nd derivative of the potential as a function of (x,y,z) in the aligned coordinate frame, d^2\Phi/dx_i/dx_j""" return -numpy.pi*self._rhoc_M*self.a**3*self._b*self._c *\ _2ndDerivInt(x,y,z,self._a2,self._b2*self._a2,self._c2*self._a2,self.n,i,j) def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density """ x,y,z= self._compute_xyz(R,phi,z,t) m2 = x**2/self._a2+y**2/self._b2+z**2/self._c2 if m2 < 1: return self._rhoc_M*(1.-m2/self.a**2)**self.n else: return 0. def OmegaP(self): """ NAME: OmegaP PURPOSE: return the pattern speed INPUT: (none) OUTPUT: pattern speed """ return self._omegab def rot(self, t=0., transposed=False): """2D Rotation matrix for non-zero pa or pattern speed to goto the aligned coordinates """ rotmat = numpy.array( [[numpy.cos(self._pa+self._omegab*t),numpy.sin(self._pa+self._omegab*t)], [-numpy.sin(self._pa+self._omegab*t),numpy.cos(self._pa+self._omegab*t)]]) if transposed: return rotmat.T else: return rotmat def _potInt(x,y,z,a2,b2,c2,n): """Integral involed in the potential at (x,y,z) integrates 1/A B^(n+1) where A = sqrt((tau+a)(tau+b)(tau+c)) and B = (1-x^2/(tau+a)-y^2/(tau+b)-z^2/(tau+c)) from lambda to infty with respect to tau. The lower limit lambda is given by lowerlim function. """ def integrand(tau): return _FracInt(x, y, z, a2, b2, c2, tau, n+1) return integrate.quad(integrand, lowerlim(x**2,y**2,z**2,a2,b2,c2), numpy.inf)[0] def _forceInt(x,y,z,a2,b2,c2,n,i): """Integral involved in the force at (x,y,z) integrates 1/A B^n (x_i/(tau+a_i)) where A = sqrt((tau+a)(tau+b)(tau+c)) and B = (1-x^2/(tau+a)-y^2/(tau+b)-z^2/(tau+c)) from lambda to infty with respect to tau. The lower limit lambda is given by lowerlim function. """ def integrand(tau): return (x*(i==0) + y*(i==1) + z*(i==2))/(a2*(i==0) + b2*(i==1) + c2*(i==2) + tau) * \ _FracInt(x, y, z, a2, b2, c2, tau, n) return integrate.quad(integrand, lowerlim(x**2, y**2, z**2, a2, b2, c2), numpy.inf, epsabs=1e-12)[0] def _2ndDerivInt(x,y,z,a2,b2,c2,n,i,j): r"""Integral involved in second derivatives d^\Phi/(dx_i dx_j) integrate 1/A B^(n-1) (-2 x_i/(tau+a_i)) (-2 x_j/(tau+a_j)) when i /= j or 1/A [ B^(n-1) 4n x_i^2 / (a_i+t)^2 + B^n -(-2/(a_i+t)) ] when i == j where A = sqrt((tau+a)(tau+b)(tau+c)) and B = (1-x^2/(tau+a)-y^2/(tau+b)-z^2/(tau+c)) from lambda to infty with respect to tau The lower limit lambda is given by lowerlim function. This is a second derivative of _potInt. """ def integrand(tau): if i!=j: return _FracInt(x,y,z,a2,b2,c2,tau,n-1)*n*\ (1.+(-1.-2.*x/(tau+a2))*(i==0 or j==0))*\ (1.+(-1.-2.*y/(tau+b2))*(i==1 or j==1))*\ (1.+(-1.-2.*z/(tau+c2))*(i==2 or j==2)) else: var2 = x**2*(i==0) + y**2*(i==1) + z**2*(i==2) coef2 = a2*(i==0) + b2*(i==1) + c2*(i==2) return _FracInt(x,y,z,a2,b2,c2,tau,n-1)*n*(4.*var2)/(tau+coef2)**2 +\ _FracInt(x,y,z,a2,b2,c2,tau,n)*(-2./(tau+coef2)) return integrate.quad(integrand, lowerlim(x**2,y**2,z**2,a2,b2,c2), numpy.inf)[0] def _FracInt(x,y,z,a,b,c,tau,n): """Returns 1 x^2 y^2 z^2 -------------------------- (1 - ------- - ------- - -------)^n sqrt(tau+a)(tau+b)(tau+c)) tau+a tau+b tau+c """ denom = numpy.sqrt((a + tau)*(b + tau)*(c + tau)) return (1. - x**2/(a + tau) - y**2/(b + tau) - z**2/(c + tau))**n / denom def lowerlim(x, y, z, a, b, c): """Returns the real positive root of x/(a+t) + y/(b+t) + z/(c+t) = 1 when x/a + y/b + z/c > 1 else zero """ if x/a + y/b + z/c > 1: B = a + b + c - x - y - z C = a*b + a*c + b*c - a*y - a*z - b*x - b*z - c*x - c*y D = a*b*c - a*b*z - a*c*y - b*c*x r = numpy.roots([1,B,C,D]) ll = r[~numpy.iscomplex(r) & (r>0.)] return ll[0].real else: return 0. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/FlattenedPowerPotential.py0000644000175100001710000001521214327773303022536 0ustar00runnerdocker############################################################################### # FlattenedPowerPotential.py: Power-law potential that is flattened in the # potential (NOT the density) # # amp # phi(R,z)= --------- ; m^2 = R^2 + z^2/q^2 # m^\alpha ############################################################################### import numpy from ..util import conversion from .Potential import Potential _CORE=10**-8 class FlattenedPowerPotential(Potential): """Class that implements a power-law potential that is flattened in the potential (NOT the density) .. math:: \\Phi(R,z) = -\\frac{\\mathrm{amp}\\,r_1^\\alpha}{\\alpha\\,\\left(R^2+(z/q)^2+\\mathrm{core}^2\\right)^{\\alpha/2}} and the same as LogarithmicHaloPotential for :math:`\\alpha=0` See Figure 1 in `Evans (1994) `_ for combinations of alpha and q that correspond to positive densities """ def __init__(self,amp=1.,alpha=0.5,q=0.9,core=_CORE,normalize=False,r1=1., ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a flattened power-law potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of velocity-squared alpha - power q - flattening core - core radius (can be Quantity) r1= (1.) reference radius for amplitude (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2013-01-09 - Written - Bovy (IAS) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='velocity2') core= conversion.parse_length(core,ro=self._ro) r1= conversion.parse_length(r1,ro=self._ro) self.alpha= alpha self.q2= q**2. self.core2= core**2. # Back to old definition self._amp*= r1**self.alpha if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) self.hasC= True self.hasC_dxdv= True self.hasC_dens= True def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2013-01-09 - Started - Bovy (IAS) """ if self.alpha == 0.: return 1./2.*numpy.log(R**2.+z**2./self.q2+self.core2) else: m2= self.core2+R**2.+z**2./self.q2 return -m2**(-self.alpha/2.)/self.alpha def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2010-07-10 - Written - Bovy (NYU) """ if self.alpha == 0.: return -R/(R**2.+z**2./self.q2+self.core2) else: m2= self.core2+R**2.+z**2./self.q2 return -m2**(-self.alpha/2.-1.)*R def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2010-07-10 - Written - Bovy (NYU) """ if self.alpha == 0.: return -z/self.q2/(R**2.+z**2./self.q2+self.core2) else: m2= self.core2+R**2.+z**2./self.q2 return -m2**(-self.alpha/2.-1.)*z/self.q2 def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _Rderiv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2011-10-09 - Written - Bovy (NYU) """ if self.alpha == 0.: denom= 1./(R**2.+z**2./self.q2+self.core2) return denom-2.*R**2.*denom**2. else: m2= self.core2+R**2.+z**2./self.q2 return -m2**(-self.alpha/2.-1.)*((self.alpha+2)*R**2./m2-1.) def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second vertical derivative HISTORY: 2012-07-26 - Written - Bovy (IAS@MPIA) """ if self.alpha == 0.: denom= 1./(R**2.+z**2./self.q2+self.core2) return denom/self.q2-2.*z**2.*denom**2./self.q2**2. else: m2= self.core2+R**2.+z**2./self.q2 return -1./self.q2*m2**(-self.alpha/2.-1.)*((self.alpha+2)*z**2./m2/self.q2-1.) def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2013-01-09 - Written - Bovy (IAS) """ if self.alpha == 0.: return 1./4./numpy.pi/self.q2*((2.*self.q2+1.)*self.core2+R**2.\ +(2.-1./self.q2)*z**2.)/\ (R**2.+z**2./self.q2+self.core2)**2. else: m2= self.core2+R**2.+z**2./self.q2 return 1./self.q2*(self.core2*(1.+2.*self.q2)+R**2.*(1.-self.alpha*self.q2)+z**2.*(2.-(1.+self.alpha)/self.q2))*m2**(-self.alpha/2.-2.)/4./numpy.pi ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/Force.py0000644000175100001710000002124614327773303016775 0ustar00runnerdocker############################################################################### # Force.py: top-level class for a 3D force, conservative (Potential) or # not (DissipativeForce) # ############################################################################### import copy import numpy from ..util import config, conversion from ..util._optional_deps import _APY_LOADED from ..util.conversion import (physical_compatible, physical_conversion, potential_physical_input) if _APY_LOADED: from astropy import units class Force: """Top-level class for any force, conservative or dissipative""" def __init__(self,amp=1.,ro=None,vo=None,amp_units=None): """ NAME: __init__ PURPOSE: Initialize Force INPUT: amp - amplitude to be applied when evaluating the potential and its forces ro - physical distance scale (in kpc or as Quantity) vo - physical velocity scale (in km/s or as Quantity) amp_units - ('mass', 'velocity2', 'density') type of units that amp should have if it has units OUTPUT: HISTORY: 2018-03-18 - Written to generalize Potential to force that may or may not be conservative - Bovy (UofT) """ self._amp= amp # Parse ro and vo if ro is None: self._ro= config.__config__.getfloat('normalization','ro') self._roSet= False else: self._ro= conversion.parse_length_kpc(ro) self._roSet= True if vo is None: self._vo= config.__config__.getfloat('normalization','vo') self._voSet= False else: self._vo= conversion.parse_velocity_kms(vo) self._voSet= True # Parse amp if it has units if _APY_LOADED and isinstance(self._amp,units.Quantity): # Try a bunch of possible units unitFound= False # velocity^2 try: self._amp= conversion.parse_energy(self._amp,vo=self._vo) except units.UnitConversionError: pass else: unitFound= True if not amp_units == 'velocity2': raise units.UnitConversionError(f'amp= parameter of {type(self).__name__} should have units of {amp_units}, but has units of velocity2 instead') if not unitFound: # mass try: self._amp= conversion.parse_mass(self._amp,ro=self._ro,vo=self._vo) except units.UnitConversionError: pass else: unitFound= True if not amp_units == 'mass': raise units.UnitConversionError(f'amp= parameter of {type(self).__name__} should have units of {amp_units}, but has units of mass instead') if not unitFound: # density try: self._amp= conversion.parse_dens(self._amp,ro=self._ro,vo=self._vo) except units.UnitConversionError: pass else: unitFound= True if not amp_units == 'density': raise units.UnitConversionError(f'amp= parameter of {type(self).__name__} should have units of {amp_units}, but has units of density instead') if not unitFound: # surface density try: self._amp= conversion.parse_surfdens(self._amp,ro=self._ro,vo=self._vo) except units.UnitConversionError: pass else: unitFound= True if not amp_units == 'surfacedensity': raise units.UnitConversionError(f'amp= parameter of {type(self).__name__} should have units of {amp_units}, but has units of surface density instead') if not unitFound: raise units.UnitConversionError(f'amp= parameter of {type(self).__name__} should have units of {amp_units}; given units are not understood') else: # When amplitude is given with units, turn on physical output self._roSet= True self._voSet= True return None def __mul__(self,b): """ NAME: __mul__ PURPOSE: Multiply a Force or Potential's amplitude by a number INPUT: b - number OUTPUT: New instance with amplitude = (old amplitude) x b HISTORY: 2019-01-27 - Written - Bovy (UofT) """ if not isinstance(b,(int,float)): raise TypeError("Can only multiply a Force or Potential instance with a number") out= copy.deepcopy(self) out._amp*= b return out # Similar functions __rmul__= __mul__ def __div__(self,b): return self.__mul__(1./b) __truediv__= __div__ def __add__(self,b): """ NAME: __add__ PURPOSE: Add Force or Potential instances together to create a multi-component potential (e.g., pot= pot1+pot2+pot3) INPUT: b - Force or Potential instance or a list thereof OUTPUT: List of Force or Potential instances that represents the combined potential HISTORY: 2019-01-27 - Written - Bovy (UofT) 2020-04-22 - Added check that unit systems of combined potentials are compatible - Bovy (UofT) """ from ..potential import flatten as flatten_pot from ..potential import planarPotential if not isinstance(flatten_pot([b])[0],(Force,planarPotential)): raise TypeError("""Can only combine galpy Force objects with """ """other Force objects or lists thereof""") assert physical_compatible(self,b), \ """Physical unit conversion parameters (ro,vo) are not """\ """compatible between potentials to be combined""" if isinstance(b,list): return [self]+b else: return [self,b] # Define separately to keep order def __radd__(self,b): from ..potential import flatten as flatten_pot from ..potential import planarPotential if not isinstance(flatten_pot([b])[0],(Force,planarPotential)): raise TypeError("""Can only combine galpy Force objects with """ """other Force objects or lists thereof""") assert physical_compatible(self,b), \ """Physical unit conversion parameters (ro,vo) are not """\ """compatible between potentials to be combined""" # If we get here, b has to be a list return b+[self] def turn_physical_off(self): """ NAME: turn_physical_off PURPOSE: turn off automatic returning of outputs in physical units INPUT: (none) OUTPUT: (none) HISTORY: 2016-01-30 - Written - Bovy (UofT) """ self._roSet= False self._voSet= False return None def turn_physical_on(self,ro=None,vo=None): """ NAME: turn_physical_on PURPOSE: turn on automatic returning of outputs in physical units INPUT: ro= reference distance (kpc; can be Quantity) vo= reference velocity (km/s; can be Quantity) OUTPUT: (none) HISTORY: 2016-01-30 - Written - Bovy (UofT) 2020-04-22 - Don't turn on a parameter when it is False - Bovy (UofT) """ if not ro is False: self._roSet= True if not vo is False: self._voSet= True if not ro is None and ro: self._ro= conversion.parse_length_kpc(ro) if not vo is None and vo: self._vo= conversion.parse_velocity_kms(vo) return None @potential_physical_input @physical_conversion('force',pop=True) def rforce(self,R,z,**kwargs): """ NAME: rforce PURPOSE: evaluate spherical radial force F_r (R,z) INPUT: R - Cylindrical Galactocentric radius (can be Quantity) z - vertical height (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) v - current velocity in cylindrical coordinates (optional, but required when including dissipative forces; can be a Quantity) OUTPUT: F_r (R,z,phi,t) HISTORY: 2016-06-20 - Written - Bovy (UofT) """ r= numpy.sqrt(R**2.+z**2.) kwargs['use_physical']= False return self.Rforce(R,z,**kwargs)*R/r+self.zforce(R,z,**kwargs)*z/r ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/GaussianAmplitudeWrapperPotential.py0000644000175100001710000000357514327773303024604 0ustar00runnerdocker############################################################################### # GaussianAmplitudeWrapperPotential.py: Wrapper to modulate the amplitude # of a potential with a Gaussian ############################################################################### import numpy from ..util import conversion from .WrapperPotential import parentWrapperPotential class GaussianAmplitudeWrapperPotential(parentWrapperPotential): """Potential wrapper class that allows the amplitude of a Potential object to be modulated as a Gaussian. The amplitude A applied to a potential wrapped by an instance of this class is changed as .. math:: A(t) = amp\\,\\exp\\left(-\\frac{[t-t_0]^2}{2\\,\\sigma^2}\\right) """ def __init__(self,amp=1.,pot=None,to=0.,sigma=1.,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a GaussianAmplitudeWrapper Potential INPUT: amp - amplitude to be applied to the potential (default: 1.) pot - Potential instance or list thereof; this potential is made to rotate around the z axis by the wrapper to= (0.) time at which the Gaussian peaks sigma= (1.) standard deviation of the Gaussian (can be a Quantity) OUTPUT: (none) HISTORY: 2018-02-21 - Started - Bovy (UofT) """ to= conversion.parse_time(to,ro=self._ro,vo=self._vo) sigma= conversion.parse_time(sigma,ro=self._ro,vo=self._vo) self._to= to self._sigma2= sigma**2. self.hasC= True self.hasC_dxdv= True def _smooth(self,t): return numpy.exp(-0.5*(t-self._to)**2./self._sigma2) def _wrap(self,attribute,*args,**kwargs): return self._smooth(kwargs.get('t',0.))\ *self._wrap_pot_func(attribute)(self._pot,*args,**kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/HenonHeilesPotential.py0000644000175100001710000000725314327773303022022 0ustar00runnerdocker############################################################################### # HenonHeilesPotential: the Henon-Heiles (1964) potential ############################################################################### import numpy from .planarPotential import planarPotential class HenonHeilesPotential(planarPotential): """Class that implements a the `Henon & Heiles (1964) `__ potential .. math:: \\Phi(R,\\phi) = \\frac{\\mathrm{amp}}{2}\\,\\left[R^2 + \\frac{2\\,R^3}{3}\\,\\sin\\left(3\\,\\phi\\right)\\right] """ def __init__(self,amp=1.,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a Henon-Heiles potential INPUT: amp - amplitude to be applied to the potential (default: 1.) OUTPUT: (none) HISTORY: 2017-10-16 - Written - Bovy (UofT) """ planarPotential.__init__(self,amp=amp,ro=ro,vo=vo) self.hasC= True self.hasC_dxdv= True def _evaluate(self,R,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,phi,t INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: Phi(R,phi,t) HISTORY: 2017-10-16 - Written - Bovy (UofT) """ return 0.5*R*R*(1.+2./3.*R*numpy.sin(3.*phi)) def _Rforce(self,R,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the radial force HISTORY: 2017-10-16 - Written - Bovy (UofT) """ return -R*(1.+R*numpy.sin(3.*phi)) def _phitorque(self,R,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the azimuthal torque HISTORY: 2017-10-16 - Written - Bovy (UofT) """ return -R**3.*numpy.cos(3.*phi) def _R2deriv(self,R,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2017-10-16 - Written - Bovy (UofT) """ return 1.+2.*R*numpy.sin(3.*phi) def _phi2deriv(self,R,phi=0.,t=0.): """ NAME: _phi2deriv PURPOSE: evaluate the second azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the second azimuthal derivative HISTORY: 2017-10-16 - Written - Bovy (UofT) """ return -3.*R**3.*numpy.sin(3.*phi) def _Rphideriv(self,R,phi=0.,t=0.): """ NAME: _Rphideriv PURPOSE: evaluate the mixed radial, azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the mixed radial, azimuthal derivative HISTORY: 2017-10-16 - Written - Bovy (UofT) """ return 3.*R**2.*numpy.cos(3.*phi) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/HomogeneousSpherePotential.py0000644000175100001710000001357614327773303023265 0ustar00runnerdocker############################################################################### # HomogeneousSpherePotential.py: The potential of a homogeneous sphere ############################################################################### import numpy from ..util import conversion from .Potential import Potential class HomogeneousSpherePotential(Potential): """Class that implements the homogeneous sphere potential for :math:`\\rho(r) = \\rho_0 = \\mathrm{constant}` for all :math:`r < R` and zero otherwise. The potential is given by .. math:: \\Phi(r) = \\mathrm{amp}\\times\\left\\{\\begin{array}{lr} (r^2-3R^2), & \\text{for } r < R\\\\ -\\frac{2R^3}{r} & \\text{for } r \\geq R \\end{array}\\right. We have that :math:`\\rho_0 = 3\\,\\mathrm{amp}/[2\\pi G]`. """ def __init__(self,amp=1.,R=1.1,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a homogeneous sphere potential INPUT: amp= amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass density or Gxmass density R= size of the sphere (can be Quantity) normalize= if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2019-12-20 - Written - Bovy (UofT) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='density') R= conversion.parse_length(R,ro=self._ro) self.R= R self._R2= self.R**2. self._R3= self.R**3. if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) self.hasC= True self.hasC_dxdv= True self.hasC_dens= True def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2019-12-20 - Written - Bovy (UofT) """ r2= R**2.+z**2. if r2 < self._R2: return r2-3.*self._R2 else: return -2.*self._R3/numpy.sqrt(r2) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2019-12-20 - Written - Bovy (UofT) """ r2= R**2.+z**2. if r2 < self._R2: return -2.*R else: return -2.*self._R3*R/r2**1.5 def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2019-12-20 - Written - Bovy (UofT) """ r2= R**2.+z**2. if r2 < self._R2: return -2.*z else: return -2.*self._R3*z/r2**1.5 def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _Rderiv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2019-12-20 - Written - Bovy (UofT) """ r2= R**2.+z**2. if r2 < self._R2: return 2. else: return 2.*self._R3/r2**1.5-6.*self._R3*R**2./r2**2.5 def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second vertical derivative HISTORY: 2019-12-20 - Written - Bovy (UofT) """ r2= R**2.+z**2. if r2 < self._R2: return 2. else: return 2.*self._R3/r2**1.5-6.*self._R3*z**2./r2**2.5 def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: d2phi/dR/dz HISTORY: 2019-12-20 - Written - Bovy (UofT) """ r2= R**2.+z**2. if r2 < self._R2: return 0. else: return -6.*self._R3*R*z/r2**2.5 def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2019-12-20 - Written - Bovy (UofT) """ r2= R**2.+z**2. if r2 < self._R2: return 1.5/numpy.pi else: return 0. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/Irrgang13.py0000644000175100001710000000621314327773303017471 0ustar00runnerdocker# Milky-Way mass models from Irrgang et al. (2013) import numpy from ..potential import (MiyamotoNagaiPotential, NFWPotential, PlummerPotential, SCFPotential, scf_compute_coeffs_spherical) from ..util import conversion # Their mass unit mgal_in_msun= 1e5/conversion._G # Model I: updated version of Allen & Santillan # Unit normalizations ro, vo= 8.4, 242. Irrgang13I_bulge= PlummerPotential(\ amp=409.*mgal_in_msun/conversion.mass_in_msol(vo,ro), b=0.23/ro,ro=ro,vo=vo) Irrgang13I_disk= MiyamotoNagaiPotential(\ amp=2856.*mgal_in_msun/conversion.mass_in_msol(vo,ro), a=4.22/ro,b=0.292/ro,ro=ro,vo=vo) # The halo is a little more difficult, because the Irrgang13I halo model is # not in galpy, so we use SCF to represent it (because we're lazy...). The # sharp cut-off in the Irrgang13I halo model makes SCF difficult, so we # replace it with a smooth cut-off; this only affects the very outer halo def Irrgang13I_halo_dens(\ r,amp=1018*mgal_in_msun/conversion.mass_in_msol(vo,ro), ah=2.562/ro,gamma=2.,Lambda=200./ro): r_over_ah_gamma= (r/ah)**(gamma-1.) return amp/4./numpy.pi/ah*r_over_ah_gamma*(r_over_ah_gamma+gamma)/r**2\ /(1.+r_over_ah_gamma)**2.\ *((1.-numpy.tanh((r-Lambda)/(Lambda/20.)))/2.) a_for_scf= 20. # scf_compute_coeffs_spherical currently seems to require a function of 3 parameters... Acos= scf_compute_coeffs_spherical(\ lambda r,z,p: Irrgang13I_halo_dens(r),40,a=a_for_scf)[0] Irrgang13I_halo= SCFPotential(Acos=Acos,a=a_for_scf,ro=ro,vo=vo) # Final model I Irrgang13I= Irrgang13I_bulge+Irrgang13I_disk+Irrgang13I_halo # Model II # Unit normalizations ro, vo= 8.35, 240.4 Irrgang13II_bulge= PlummerPotential(\ amp=175.*mgal_in_msun/conversion.mass_in_msol(vo,ro), b=0.184/ro,ro=ro,vo=vo) Irrgang13II_disk= MiyamotoNagaiPotential(\ amp=2829.*mgal_in_msun/conversion.mass_in_msol(vo,ro), a=4.85/ro,b=0.305/ro,ro=ro,vo=vo) # Again use SCF because the Irrgang13II halo model is not in galpy; because # the halo model is quite different from Hernquist both in the inner and outer # part, need quite a few basis functions... def Irrgang13II_halo_dens(\ r,amp=69725*mgal_in_msun/conversion.mass_in_msol(vo,ro), ah=200./ro): return amp/4./numpy.pi*ah**2./r**2./(r**2.+ah**2.)**1.5 a_for_scf= 0.15 # scf_compute_coeffs_spherical currently seems to require a function of 3 parameters... Acos= scf_compute_coeffs_spherical(\ lambda r,z,p: Irrgang13II_halo_dens(r),75,a=a_for_scf)[0] Irrgang13II_halo= SCFPotential(Acos=Acos,a=a_for_scf,ro=ro,vo=vo) # Final model II Irrgang13II= Irrgang13II_bulge+Irrgang13II_disk+Irrgang13II_halo # Model III # Unit normalizations ro, vo= 8.33, 239.7 Irrgang13III_bulge= PlummerPotential(\ amp=439.*mgal_in_msun/conversion.mass_in_msol(vo,ro), b=0.236/ro,ro=ro,vo=vo) Irrgang13III_disk= MiyamotoNagaiPotential(\ amp=3096.*mgal_in_msun/conversion.mass_in_msol(vo,ro), a=3.262/ro,b=0.289/ro,ro=ro,vo=vo) Irrgang13III_halo= NFWPotential(\ amp=142200.*mgal_in_msun/conversion.mass_in_msol(vo,ro), a=45.02/ro,ro=ro,vo=vo) # Final model III Irrgang13III= Irrgang13III_bulge+Irrgang13III_disk+Irrgang13III_halo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/IsochronePotential.py0000644000175100001710000001466314327773303021555 0ustar00runnerdocker############################################################################### # IsochronePotential.py: The isochrone potential # # - amp # Phi(r)= --------------------- # b + sqrt{b^2+r^2} ############################################################################### import numpy from ..util import conversion from .Potential import Potential class IsochronePotential(Potential): """Class that implements the Isochrone potential .. math:: \\Phi(r) = -\\frac{\\mathrm{amp}}{b+\\sqrt{b^2+r^2}} with :math:`\\mathrm{amp} = GM` the total mass. """ def __init__(self,amp=1.,b=1.,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize an isochrone potential INPUT: amp= amplitude to be applied to the potential, the total mass (default: 1); can be a Quantity with units of mass or Gxmass b= scale radius of the isochrone potential (can be Quantity) normalize= if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2013-09-08 - Written - Bovy (IAS) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') b= conversion.parse_length(b,ro=self._ro) self.b= b self._scale= self.b self.b2= self.b**2. if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) self.hasC= True self.hasC_dxdv= True self.hasC_dens= True def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2013-09-08 - Written - Bovy (IAS) """ r2= R**2.+z**2. rb= numpy.sqrt(r2+self.b2) return -1./(self.b+rb) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2013-09-08 - Written - Bovy (IAS) """ r2= R**2.+z**2. rb= numpy.sqrt(r2+self.b2) dPhidrr= -1./rb/(self.b+rb)**2. return dPhidrr*R def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2013-09-08 - Written - Bovy (IAS) """ r2= R**2.+z**2. rb= numpy.sqrt(r2+self.b2) dPhidrr= -1./rb/(self.b+rb)**2. return dPhidrr*z def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _Rderiv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2013-09-08 - Written - Bovy (IAS) """ r2= R**2.+z**2. rb= numpy.sqrt(r2+self.b2) return -(-self.b**3.-self.b*z**2.+(2.*R**2.-z**2.-self.b**2.)*rb)/\ rb**3./(self.b+rb)**3. def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second vertical derivative HISTORY: 2013-09-08 - Written - Bovy (IAS) """ r2= R**2.+z**2. rb= numpy.sqrt(r2+self.b2) return -(-self.b**3.-self.b*R**2.-(R**2.-2.*z**2.+self.b**2.)*rb)/\ rb**3./(self.b+rb)**3. def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: d2phi/dR/dz HISTORY: 2013-09-08 - Written - Bovy (IAS) """ r2= R**2.+z**2. rb= numpy.sqrt(r2+self.b2) return -R*z*(self.b+3.*rb)/\ rb**3./(self.b+rb)**3. def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2013-09-08 - Written - Bovy (IAS) """ r2= R**2.+z**2. rb= numpy.sqrt(r2+self.b2) return (3.*(self.b+rb)*rb**2.-r2*(self.b+3.*rb))/\ rb**3./(self.b+rb)**3./4./numpy.pi def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the surface density HISTORY: 2018-08-19 - Written - Bovy (UofT) """ r2= R**2.+z**2. rb= numpy.sqrt(r2+self.b2) return self.b*((R*z)/r2-(self.b*R*z*(self.b**2+2.*R**2+z**2)) /((self.b**2+R**2)*r2*rb) +numpy.arctan(z/R)-numpy.arctan(self.b*z/R/rb))/R**3/2./numpy.pi ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/IsothermalDiskPotential.py0000644000175100001710000000327314327773303022541 0ustar00runnerdocker############################################################################### # IsothermalDiskPotential.py: class that implements the one-dimensional # self-gravitating isothermal disk ############################################################################### import numpy from ..util import conversion from .linearPotential import linearPotential class IsothermalDiskPotential(linearPotential): """Class representing the one-dimensional self-gravitating isothermal disk .. math:: \\rho(x) = \\mathrm{amp}\\,\\mathrm{sech}^2\\left(\\frac{x}{2H}\\right) where the scale height :math:`H^2 = \\sigma^2/[8\\pi G \\,\\mathrm{amp}]`. The parameter to setup the disk is the velocity dispersion :math:`\\sigma`. """ def __init__(self,amp=1.,sigma=0.1,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize an IsothermalDiskPotential INPUT: amp - an overall amplitude sigma - velocity dispersion (can be a Quantity) OUTPUT: instance HISTORY: 2018-04-11 - Written - Bovy (UofT) """ linearPotential.__init__(self,amp=amp,ro=ro,vo=vo) sigma= conversion.parse_velocity(sigma,vo=self._vo) self._sigma2= sigma**2. self._H= sigma/numpy.sqrt(8.*numpy.pi*self._amp) self._amp= 1. # Need to manually set to 1, because amp is now contained in the combination of H and sigma^2 self.hasC= True def _evaluate(self,x,t=0.): return 2.*self._sigma2*numpy.log(numpy.cosh(0.5*x/self._H)) def _force(self,x,t=0.): return -self._sigma2*numpy.tanh(0.5*x/self._H)/self._H ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/KGPotential.py0000644000175100001710000000427314327773303020121 0ustar00runnerdockerimport numpy from ..util import conversion from ..util._optional_deps import _APY_LOADED from .linearPotential import linearPotential if _APY_LOADED: from astropy import units class KGPotential(linearPotential): """Class representing the Kuijken & Gilmore (1989) potential .. math:: \\Phi(x) = \\mathrm{amp}\\,\\left(K\\,\\left(\\sqrt{x^2+D^2}-D\\right)+F\\,x^2\\right) """ def __init__(self,K=1.15,F=0.03,D=1.8,amp=1.,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a KGPotential INPUT: K= K parameter (= :math:`2\\pi \\Sigma_{\\mathrm{disk}}`; specify either as surface density or directly as force [i.e., including :math:`2\\pi G`]; can be Quantity) F= F parameter (= :math:`4\\pi\\rho_{\\mathrm{halo}}`; specify either as density or directly as second potential derivative [i.e., including :math:`4\\pi G`]; can be Quantity) D= D parameter (natural units or Quantity length units) amp - an overall amplitude OUTPUT: instance HISTORY: 2010-07-12 - Written - Bovy (NYU) """ linearPotential.__init__(self,amp=amp,ro=ro,vo=vo) D= conversion.parse_length(D,ro=self._ro) K= conversion.parse_force(K,ro=self._ro,vo=self._vo) if _APY_LOADED and isinstance(F,units.Quantity): try: F= F.to(units.Msun/units.pc**3).value\ /conversion.dens_in_msolpc3(self._vo,self._ro)*4.*numpy.pi except units.UnitConversionError: pass if _APY_LOADED and isinstance(F,units.Quantity): try: F= F.to(units.km**2/units.s**2/units.kpc**2).value\ *ro**2/vo**2 except units.UnitConversionError: raise units.UnitConversionError("Units for F not understood; should be density") self._K= K self._F= F self._D= D self._D2= self._D**2. self.hasC= True def _evaluate(self,x,t=0.): return self._K*(numpy.sqrt(x**2.+self._D2)-self._D)+self._F*x**2. def _force(self,x,t=0.): return -x*(self._K/numpy.sqrt(x**2+self._D2)+2.*self._F) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/KingPotential.py0000644000175100001710000000507214327773303020506 0ustar00runnerdocker############################################################################### # KingPotential.py: Potential of a King profile ############################################################################### import numpy from ..util import conversion from .Force import Force from .interpSphericalPotential import interpSphericalPotential class KingPotential(interpSphericalPotential): """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. See also :ref:`King DF `. """ def __init__(self,W0=2.,M=3.,rt=1.5,npt=1001,_sfkdf=None,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 ro=, vo= standard galpy unit scaling parameters OUTPUT: (none; sets up instance) HISTORY: 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 sfkdf= _scalefreekingdf(W0) sfkdf.solve(npt) else: 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. *numpy.interp(r/radius_scale, sfkdf._r, sfkdf._dWdr), rgrid=sfkdf._r*radius_scale, Phi0=-W0*mass_scale/radius_scale, ro=ro,vo=vo) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/KuzminDiskPotential.py0000644000175100001710000001436014327773303021706 0ustar00runnerdocker############################################################################### # KuzminDiskPotential.py: class that implements Kuzmin disk potential # # - amp # Phi(R, z)= --------------------------- # \sqrt{R^2 + (a + |z|)^2} ############################################################################### import numpy from ..util import conversion from .Potential import Potential class KuzminDiskPotential(Potential): """Class that implements the Kuzmin Disk potential .. math:: \\Phi(R,z) = -\\frac{\\mathrm{amp}}{\\sqrt{R^2 + (a + |z|)^2}} with :math:`\\mathrm{amp} = GM` the total mass. """ def __init__(self, amp=1., a=1. ,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a Kuzmin disk Potential INPUT: amp - amplitude to be applied to the potential, the total mass (default: 1); can be a Quantity with units of mass density or Gxmass density a - scale length (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: KuzminDiskPotential object HISTORY: 2016-05-09 - Written - Aladdin """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) self._a = a ## a must be greater or equal to 0. if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): self.normalize(normalize) self.hasC = True self.hasC_dxdv= True return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: potential at (R,z) HISTORY: 2016-05-09 - Written - Aladdin """ return -self._denom(R, z)**-0.5 def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force = -dphi/dR HISTORY: 2016-05-09 - Written - Aladdin """ return -self._denom(R, z)**-1.5 * R def _zforce(self, R, z, phi=0., t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force = -dphi/dz HISTORY: 2016-05-09 - Written - Aladdin """ return -numpy.sign(z) * self._denom(R,z)**-1.5 * (self._a + numpy.fabs(z)) def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2016-05-13 - Written - Aladdin """ return self._denom(R, z)**-1.5 - 3.*R**2 * self._denom(R, z)**-2.5 def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second vertical derivative HISTORY: 2016-05-13 - Written - Aladdin """ a = self._a return self._denom(R, z)**-1.5 - 3. * (a + numpy.fabs(z))**2. * self._denom(R, z)**-2.5 def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2phi/dR/dz HISTORY: 2016-05-13 - Written - Aladdin """ return -3 * numpy.sign(z) * R * (self._a + numpy.fabs(z)) *self._denom(R, z)**-2.5 def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: Sigma (R,z) HISTORY: 2018-08-19 - Written - Bovy (UofT) """ return self._a*(R**2+self._a**2)**-1.5/2./numpy.pi def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R (and z) for this potential; if z=None, integrate to z=inf INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2021-03-04 - Written - Bovy (UofT) """ return 1.-self._a/numpy.sqrt(R**2.+self._a**2.) def _denom(self, R, z): """ NAME: _denom PURPOSE: evaluate R^2 + (a + |z|)^2 which is used in the denominator of most equations INPUT: R - Cylindrical Galactocentric radius z - vertical height OUTPUT: R^2 + (a + |z|)^2 HISTORY: 2016-05-09 - Written - Aladdin """ return (R**2. + (self._a + numpy.fabs(z))**2.) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/KuzminKutuzovStaeckelPotential.py0000644000175100001710000002404214327773303024155 0ustar00runnerdocker############################################################################### # KuzminKutuzovStaeckelPotential.py: # class that implements a simple Staeckel potential # generated by a Kuzmin-Kutuzov potential # - amp # Phi(r)= --------------------------- # \sqrt{lambda} + \sqrt{nu} ############################################################################### import numpy from ..util import conversion # for prolate spherical coordinate transforms from ..util import coords from .Potential import Potential class KuzminKutuzovStaeckelPotential(Potential): """Class that implements the Kuzmin-Kutuzov Staeckel potential .. math:: \\Phi(R,z) = -\\frac{\\mathrm{amp}}{\\sqrt{\\lambda} + \\sqrt{\\nu}} (see, e.g., `Batsleer & Dejonghe 1994 `__) """ def __init__(self,amp=1.,ac=5.,Delta=1.,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a Kuzmin-Kutuzov Staeckel potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass density or Gxmass density ac - axis ratio of the coordinate surfaces; (a/c) = sqrt(-alpha) / sqrt(-gamma) (default: 5.) Delta - focal distance that defines the spheroidal coordinate system (default: 1.); Delta=sqrt(gamma-alpha) (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2015-02-15 - Written - Trick (MPIA) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') Delta= conversion.parse_length(Delta,ro=self._ro) self._ac = ac self._Delta = Delta self._gamma = self._Delta**2 / (1.-self._ac**2) self._alpha = self._gamma - self._Delta**2 if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): self.normalize(normalize) self.hasC = True self.hasC_dxdv = True def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2015-02-15 - Written - Trick (MPIA) """ l,n = coords.Rz_to_lambdanu(R,z,ac=self._ac,Delta=self._Delta) return -1./(numpy.sqrt(l) + numpy.sqrt(n)) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force = -dphi/dR HISTORY: 2015-02-13 - Written - Trick (MPIA) """ l,n = coords.Rz_to_lambdanu (R,z,ac=self._ac,Delta=self._Delta) jac = coords.Rz_to_lambdanu_jac(R,z, Delta=self._Delta) dldR = jac[0,0] dndR = jac[1,0] return - (dldR * self._lderiv(l,n) + \ dndR * self._nderiv(l,n)) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2015-02-13 - Written - Trick (MPIA) """ l,n = coords.Rz_to_lambdanu (R,z,ac=self._ac,Delta=self._Delta) jac = coords.Rz_to_lambdanu_jac(R,z, Delta=self._Delta) dldz = jac[0,1] dndz = jac[1,1] return - (dldz * self._lderiv(l,n) + \ dndz * self._nderiv(l,n)) def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2015-02-13 - Written - Trick (MPIA) """ l,n = coords.Rz_to_lambdanu (R,z,ac=self._ac,Delta=self._Delta) jac = coords.Rz_to_lambdanu_jac (R,z, Delta=self._Delta) hess = coords.Rz_to_lambdanu_hess(R,z, Delta=self._Delta) dldR = jac[0,0] dndR = jac[1,0] d2ldR2 = hess[0,0,0] d2ndR2 = hess[1,0,0] return d2ldR2 * self._lderiv(l,n) + \ d2ndR2 * self._nderiv(l,n) + \ (dldR)**2 * self._l2deriv(l,n) + \ (dndR)**2 * self._n2deriv(l,n) + \ 2.*dldR*dndR * self._lnderiv(l,n) def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second vertical derivative HISTORY: 2015-02-13 - Written - Trick (MPIA) """ l,n = coords.Rz_to_lambdanu (R,z,ac=self._ac,Delta=self._Delta) jac = coords.Rz_to_lambdanu_jac(R,z, Delta=self._Delta) hess = coords.Rz_to_lambdanu_hess(R,z, Delta=self._Delta) dldz = jac[0,1] dndz = jac[1,1] d2ldz2 = hess[0,1,1] d2ndz2 = hess[1,1,1] return d2ldz2 * self._lderiv(l,n) + \ d2ndz2 * self._nderiv(l,n) + \ (dldz)**2 * self._l2deriv(l,n) + \ (dndz)**2 * self._n2deriv(l,n) + \ 2.*dldz*dndz * self._lnderiv(l,n) def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: d2phi/dR/dz HISTORY: 2015-02-13 - Written - Trick (MPIA) """ l,n = coords.Rz_to_lambdanu (R,z,ac=self._ac,Delta=self._Delta) jac = coords.Rz_to_lambdanu_jac(R,z, Delta=self._Delta) hess = coords.Rz_to_lambdanu_hess(R,z, Delta=self._Delta) dldR = jac[0,0] dndR = jac[1,0] dldz = jac[0,1] dndz = jac[1,1] d2ldRdz = hess[0,0,1] d2ndRdz = hess[1,0,1] return d2ldRdz * self._lderiv(l,n) + \ d2ndRdz * self._nderiv(l,n) + \ dldR*dldz * self._l2deriv(l,n) + \ dndR*dndz * self._n2deriv(l,n) + \ (dldR*dndz+dldz*dndR)* self._lnderiv(l,n) def _lderiv(self,l,n): """ NAME: _lderiv PURPOSE: evaluate the derivative w.r.t. lambda for this potential INPUT: l - prolate spheroidal coordinate lambda n - prolate spheroidal coordinate nu OUTPUT: derivative w.r.t. lambda HISTORY: 2015-02-15 - Written - Trick (MPIA) """ return 0.5/numpy.sqrt(l)/(numpy.sqrt(l)+numpy.sqrt(n))**2 def _nderiv(self,l,n): """ NAME: _nderiv PURPOSE: evaluate the derivative w.r.t. nu for this potential INPUT: l - prolate spheroidal coordinate lambda n - prolate spheroidal coordinate nu OUTPUT: derivative w.r.t. nu HISTORY: 2015-02-15 - Written - Trick (MPIA) """ return 0.5/numpy.sqrt(n)/(numpy.sqrt(l)+numpy.sqrt(n))**2 def _l2deriv(self,l,n): """ NAME: _l2deriv PURPOSE: evaluate the second derivative w.r.t. lambda for this potential INPUT: l - prolate spheroidal coordinate lambda n - prolate spheroidal coordinate nu OUTPUT: second derivative w.r.t. lambda HISTORY: 2015-02-15 - Written - Trick (MPIA) """ number = -3.*numpy.sqrt(l) - numpy.sqrt(n) denom = 4. * l**1.5 * (numpy.sqrt(l)+numpy.sqrt(n))**3 return number / denom def _n2deriv(self,l,n): """ NAME: _n2deriv PURPOSE: evaluate the second derivative w.r.t. nu for this potential INPUT: l - prolate spheroidal coordinate lambda n - prolate spheroidal coordinate nu OUTPUT: second derivative w.r.t. nu HISTORY: 2015-02-15 - Written - Trick (MPIA) """ number = -numpy.sqrt(l) - 3.*numpy.sqrt(n) denom = 4. * n**1.5 * (numpy.sqrt(l)+numpy.sqrt(n))**3 return number / denom def _lnderiv(self,l,n): """ NAME: _lnderiv PURPOSE: evaluate the mixed derivative w.r.t. lambda and nu for this potential INPUT: l - prolate spheroidal coordinate lambda n - prolate spheroidal coordinate nu OUTPUT: d2phi/dl/dn HISTORY: 2015-02-13 - Written - Trick (MPIA) """ return -0.5/(numpy.sqrt(l) * numpy.sqrt(n) * (numpy.sqrt(l)+numpy.sqrt(n))**3) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/LogarithmicHaloPotential.py0000644000175100001710000002666414327773303022676 0ustar00runnerdocker############################################################################### # LogarithmicHaloPotential.py: class that implements the logarithmic # potential Phi(r) = vc**2 ln(r) ############################################################################### import warnings import numpy from ..util import conversion, galpyWarning from .Potential import Potential, kms_to_kpcGyrDecorator _CORE=10**-8 class LogarithmicHaloPotential(Potential): """Class that implements the logarithmic potential .. math:: \\Phi(R,z) = \\frac{\\mathrm{amp}}{2}\\,\\ln\\left[R^2+\\left(\\frac{z}{q}\\right)^2+\\mathrm{core}^2\\right] Alternatively, the potential can be made triaxial by adding a parameter :math:`b` .. math:: \\Phi(x,y,z) = \\frac{\\mathrm{amp}}{2}\\,\\ln\\left[x^2+\\left(\\frac{y}{b}\\right)^2+\\left(\\frac{z}{q}\\right)^2+\\mathrm{core}^2\\right] With these definitions, :math:`\\sqrt{\\mathrm{amp}}` is the circular velocity at :math:`r \\gg \\mathrm{core}` at :math:`(y,z) = (0,0)`. """ def __init__(self,amp=1.,core=_CORE,q=1.,b=None,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a logarithmic potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of velocity-squared core - core radius at which the logarithm is cut (can be Quantity) q - potential flattening (z/q)**2. b= (None) if set, shape parameter in y-direction (y --> y/b; see definition) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2010-04-02 - Started - Bovy (NYU) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='velocity2') core= conversion.parse_length(core,ro=self._ro) self.hasC= True self.hasC_dxdv= True self.hasC_dens= True self._core2= core**2. self._q= q self._b= b if not self._b is None: self.isNonAxi= True self._1m1overb2= 1.-1./self._b**2. if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) self._nemo_accname= 'LogPot' return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2010-04-02 - Started - Bovy (NYU) 2010-04-30 - Adapted for R,z - Bovy (NYU) """ if self.isNonAxi: return 1./2.*numpy.log(R**2.*(1.-self._1m1overb2*numpy.sin(phi)**2.) +(z/self._q)**2.+self._core2) else: return 1./2.*numpy.log(R**2.+(z/self._q)**2.+self._core2) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: """ if self.isNonAxi: Rt2= R**2.*(1.-self._1m1overb2*numpy.sin(phi)**2.) return -Rt2/R/(Rt2+(z/self._q)**2.+self._core2) else: return -R/(R**2.+(z/self._q)**2.+self._core2) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: """ if self.isNonAxi: Rt2= R**2.*(1.-self._1m1overb2*numpy.sin(phi)**2.) return -z/self._q**2./(Rt2+(z/self._q)**2.+self._core2) else: return -z/self._q**2./(R**2.+(z/self._q)**2.+self._core2) def _phitorque(self,R,z,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the azimuthal torque HISTORY: """ if self.isNonAxi: Rt2= R**2.*(1.-self._1m1overb2*numpy.sin(phi)**2.) return R**2./(Rt2+(z/self._q)**2.+self._core2)\ *numpy.sin(2.*phi)*self._1m1overb2/2. else: return 0 def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: """ if self.isNonAxi: R2= R**2. Rt2= R2*(1.-self._1m1overb2*numpy.sin(phi)**2.) denom= 1./(Rt2+(z/self._q)**2.+self._core2) denom2= denom**2. return 1./4./numpy.pi\ *(2.*Rt2/R2*(denom-Rt2*denom2)\ +denom/self._q**2.-2.*z**2.*denom2/self._q**4.\ -self._1m1overb2\ *(2.*R2*numpy.sin(2.*phi)**2./4.*self._1m1overb2\ *denom2+denom*numpy.cos(2.*phi))) else: return 1./4./numpy.pi/self._q**2.*((2.*self._q**2.+1.)*self._core2+R**2.\ +(2.-self._q**-2.)*z**2.)/\ (R**2.+(z/self._q)**2.+self._core2)**2. def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2011-10-09 - Written - Bovy (IAS) """ if self.isNonAxi: Rt2= R**2.*(1.-self._1m1overb2*numpy.sin(phi)**2.) denom= 1./(Rt2+(z/self._q)**2.+self._core2) return (denom-2.*Rt2*denom**2.)*Rt2/R**2. else: denom= 1./(R**2.+(z/self._q)**2.+self._core2) return denom-2.*R**2.*denom**2. def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second vertical derivative HISTORY: 2012-07-25 - Written - Bovy (IAS@MPIA) """ if self.isNonAxi: Rt2= R**2.*(1.-self._1m1overb2*numpy.sin(phi)**2.) denom= 1./(Rt2+(z/self._q)**2.+self._core2) return denom/self._q**2.-2.*z**2.*denom**2./self._q**4. else: denom= 1./(R**2.+(z/self._q)**2.+self._core2) return denom/self._q**2.-2.*z**2.*denom**2./self._q**4. def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2Phi/dR/dz HISTORY: 2013-08-28 - Written - Bovy (IAS) """ if self.isNonAxi: Rt2= R**2.*(1.-self._1m1overb2*numpy.sin(phi)**2.) return -2.*Rt2/R*z/self._q**2./(Rt2+(z/self._q)**2.+self._core2)**2. else: return -2.*R*z/self._q**2./(R**2.+(z/self._q)**2.+self._core2)**2. def _phi2deriv(self,R,z,phi=0.,t=0.): """ NAME: _phi2deriv PURPOSE: evaluate the second azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second azimuthal derivative HISTORY: 2017-10-15 - Written - Bovy (UofT) """ if self.isNonAxi: Rt2= R**2.*(1.-self._1m1overb2*numpy.sin(phi)**2.) denom= 1./(Rt2+(z/self._q)**2.+self._core2) return -self._1m1overb2\ *(R**4.*numpy.sin(2.*phi)**2./2.*self._1m1overb2\ *denom**2. +R**2.*denom*numpy.cos(2.*phi)) else: return 0. def _Rphideriv(self,R,z,phi=0.,t=0.): """ NAME: _Rphideriv PURPOSE: evaluate the mixed R,phi derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2Phi/dR/dphi HISTORY: 2017-10-15 - Written - Bovy (UofT) """ if self.isNonAxi: Rt2= R**2.*(1.-self._1m1overb2*numpy.sin(phi)**2.) denom= 1./(Rt2+(z/self._q)**2.+self._core2) return -(denom-Rt2*denom**2.)*R*numpy.sin(2.*phi)*self._1m1overb2 else: return 0. def _phizderiv(self,R,z,phi=0.,t=0.): """ NAME: _phizderiv PURPOSE: evaluate the mixed phi,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2Phi/dz/dphi HISTORY: 2021-04-30 - Written - Bovy (UofT) """ if self.isNonAxi: Rt2= R**2.*(1.-self._1m1overb2*numpy.sin(phi)**2.) denom= 1./(Rt2+(z/self._q)**2.+self._core2) return 2*R**2*z*numpy.sin(phi)*numpy.cos(phi)*self._1m1overb2\ *denom**2/self._q**2 else: return 0. @kms_to_kpcGyrDecorator def _nemo_accpars(self,vo,ro): """ NAME: _nemo_accpars PURPOSE: return the accpars potential parameters for use of this potential with NEMO INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: accpars string HISTORY: 2014-12-18 - Written - Bovy (IAS) """ warnings.warn("NEMO's LogPot does not allow flattening in z (for some reason); therefore, flip y and z in NEMO wrt galpy; also does not allow the triaxial b parameter",galpyWarning) ampl= self._amp*vo**2. return "0,{},{},1.0,{}".format(ampl, self._core2*ro**2.*self._q**(2./3.), #somewhat weird gyrfalcon implementation self._q) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/MN3ExponentialDiskPotential.py0000644000175100001710000002557014327773303023242 0ustar00runnerdocker############################################################################### # MN3ExponentialDiskPotential.py: class that implements the three Miyamoto- # Nagai approximation to a radially # exponential disk potential of Smith et al. # 2015 ############################################################################### import warnings import numpy from ..util import conversion, galpyWarning from .MiyamotoNagaiPotential import MiyamotoNagaiPotential from .Potential import Potential, kms_to_kpcGyrDecorator class MN3ExponentialDiskPotential(Potential): """class that implements the three Miyamoto-Nagai approximation to a radially-exponential disk potential of `Smith et al. 2015 `_ .. math:: \\rho(R,z) = \\mathrm{amp}\\,\\exp\\left(-R/h_R-|z|/h_z\\right) or .. math:: \\rho(R,z) = \\mathrm{amp}\\,\\exp\\left(-R/h_R\\right)\\mathrm{sech}^2\\left(-|z|/h_z\\right) depending on whether sech=True or not. This density is approximated using three Miyamoto-Nagai disks """ def __init__(self,amp=1.,hr=1./3.,hz=1./16., sech=False,posdens=False, normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a 3MN approximation to an exponential disk potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass density or Gxmass density hr - disk scale-length (can be Quantity) hz - scale-height (can be Quantity) sech= (False) if True, hz is the scale height of a sech vertical profile (default is exponential vertical profile) posdens= (False) if True, allow only positive density solutions (Table 2 in Smith et al. rather than Table 1) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: MN3ExponentialDiskPotential object HISTORY: 2015-02-07 - Written - Bovy (IAS) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='density') hr= conversion.parse_length(hr,ro=self._ro) hz= conversion.parse_length(hz,ro=self._ro) self._hr= hr self._hz= hz self._scale= self._hr # Adjust amp for definition self._amp*= 4.*numpy.pi*self._hr**2.*self._hz # First determine b/rd if sech: self._brd= _b_sechhz(self._hz/self._hr) else: self._brd= _b_exphz(self._hz/self._hr) if self._brd < 0.: raise OSError("MN3ExponentialDiskPotential's b/Rd is negative for the given hz") # Check range if (not posdens and self._brd > 3.) \ or (posdens and self._brd > 1.35): warnings.warn("MN3ExponentialDiskPotential's b/Rd = %g is outside of the interpolation range of Smith et al. (2015)" % self._brd, galpyWarning) self._b= self._brd*self._hr # Now setup the various MN disks if posdens: self._mn3= [MiyamotoNagaiPotential(amp=_mass1_tab2(self._brd), a=_a1_tab2(self._brd)*self._hr, b=self._b), MiyamotoNagaiPotential(amp=_mass2_tab2(self._brd), a=_a2_tab2(self._brd)*self._hr, b=self._b), MiyamotoNagaiPotential(amp=_mass3_tab2(self._brd), a=_a3_tab2(self._brd)*self._hr, b=self._b)] else: self._mn3= [MiyamotoNagaiPotential(amp=_mass1_tab1(self._brd), a=_a1_tab1(self._brd)*self._hr, b=self._b), MiyamotoNagaiPotential(amp=_mass2_tab1(self._brd), a=_a2_tab1(self._brd)*self._hr, b=self._b), MiyamotoNagaiPotential(amp=_mass3_tab1(self._brd), a=_a3_tab1(self._brd)*self._hr, b=self._b)] if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): self.normalize(normalize) self.hasC= True self.hasC_dxdv= True self._nemo_accname= 'MiyamotoNagai+MiyamotoNagai+MiyamotoNagai' return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2015-02-07 - Written - Bovy (IAS) """ return self._mn3[0](R,z,phi=phi,t=t)\ +self._mn3[1](R,z,phi=phi,t=t)\ +self._mn3[2](R,z,phi=phi,t=t) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2015-02-07 - Written - Bovy (IAS) """ return self._mn3[0].Rforce(R,z,phi=phi,t=t)\ +self._mn3[1].Rforce(R,z,phi=phi,t=t)\ +self._mn3[2].Rforce(R,z,phi=phi,t=t) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2015-02-07 - Written - Bovy (IAS) """ return self._mn3[0].zforce(R,z,phi=phi,t=t)\ +self._mn3[1].zforce(R,z,phi=phi,t=t)\ +self._mn3[2].zforce(R,z,phi=phi,t=t) def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2015-02-07 - Written - Bovy (IAS) """ return self._mn3[0].dens(R,z,phi=phi,t=t)\ +self._mn3[1].dens(R,z,phi=phi,t=t)\ +self._mn3[2].dens(R,z,phi=phi,t=t) def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2015-02-07 - Written - Bovy (IAS) """ return self._mn3[0].R2deriv(R,z,phi=phi,t=t)\ +self._mn3[1].R2deriv(R,z,phi=phi,t=t)\ +self._mn3[2].R2deriv(R,z,phi=phi,t=t) def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second vertical derivative HISTORY: 2015-02-07 - Written - Bovy (IAS) """ return self._mn3[0].z2deriv(R,z,phi=phi,t=t)\ +self._mn3[1].z2deriv(R,z,phi=phi,t=t)\ +self._mn3[2].z2deriv(R,z,phi=phi,t=t) def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2phi/dR/dz HISTORY: 2015-02-07 - Written - Bovy (IAS) """ return self._mn3[0].Rzderiv(R,z,phi=phi,t=t)\ +self._mn3[1].Rzderiv(R,z,phi=phi,t=t)\ +self._mn3[2].Rzderiv(R,z,phi=phi,t=t) @kms_to_kpcGyrDecorator def _nemo_accpars(self,vo,ro): """ NAME: _nemo_accpars PURPOSE: return the accpars potential parameters for use of this potential with NEMO INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: accpars string HISTORY: 2015-02-09 - Written - Bovy (IAS) """ out= "" # Loop through the self._mn3 MN potentials for ii in range(3): if ii > 0: out+= '#' ampl= self._amp*self._mn3[ii]._amp*vo**2.*ro out+= f"0,{ampl},{self._mn3[ii]._a*ro},{self._mn3[ii]._b*ro}" return out # Equations from Table 1 def _mass1_tab1(brd): return -0.0090*brd**4.+0.0640*brd**3.-0.1653*brd**2.+0.1164*brd+1.9487 def _mass2_tab1(brd): return 0.0173*brd**4.-0.0903*brd**3.+0.0877*brd**2.+0.2029*brd-1.3077 def _mass3_tab1(brd): return -0.0051*brd**4.+0.0287*brd**3.-0.0361*brd**2.-0.0544*brd+0.2242 def _a1_tab1(brd): return -0.0358*brd**4.+0.2610*brd**3.-0.6987*brd**2.-0.1193*brd+2.0074 def _a2_tab1(brd): return -0.0830*brd**4.+0.4992*brd**3.-0.7967*brd**2.-1.2966*brd+4.4441 def _a3_tab1(brd): return -0.0247*brd**4.+0.1718*brd**3.-0.4124*brd**2.-0.5944*brd+0.7333 # Equations from Table 2 def _mass1_tab2(brd): return 0.0036*brd**4.-0.0330*brd**3.+0.1117*brd**2.-0.1335*brd+0.1749 def _mass2_tab2(brd): return -0.0131*brd**4.+0.1090*brd**3.-0.3035*brd**2.+0.2921*brd-5.7976 def _mass3_tab2(brd): return -0.0048*brd**4.+0.0454*brd**3.-0.1425*brd**2.+0.1012*brd+6.7120 def _a1_tab2(brd): return -0.0158*brd**4.+0.0993*brd**3.-0.2070*brd**2.-0.7089*brd+0.6445 def _a2_tab2(brd): return -0.0319*brd**4.+0.1514*brd**3.-0.1279*brd**2.-0.9325*brd+2.6836 def _a3_tab2(brd): return -0.0326*brd**4.+0.1816*brd**3.-0.2943*brd**2.-0.6329*brd+2.3193 # Equations to go from hz to b def _b_exphz(hz): return -0.269*hz**3.+1.080*hz**2.+1.092*hz def _b_sechhz(hz): return -0.033*hz**3.+0.262*hz**2.+0.659*hz ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/McMillan17.py0000644000175100001710000000476514327773303017612 0ustar00runnerdocker# McMillan (2017) potential as first implemented in the galpy framework by # Mackereth & Bovy (2018) import numpy from ..potential import (DiskSCFPotential, NFWPotential, SCFPotential, mwpot_helpers, scf_compute_coeffs_axi) from ..util import conversion # Suppress the numpy floating-point warnings that this code generates... old_error_settings= numpy.seterr(all='ignore') # Unit normalizations ro= 8.21 vo= 233.1 sigo= conversion.surfdens_in_msolpc2(vo=vo,ro=ro) rhoo= conversion.dens_in_msolpc3(vo=vo,ro=ro) #gas disk parameters (fixed in McMillan 2017...) Rd_HI= 7./ro Rm_HI= 4./ro zd_HI= 0.085/ro Sigma0_HI= 53.1/sigo Rd_H2= 1.5/ro Rm_H2= 12./ro zd_H2= 0.045/ro Sigma0_H2= 2180./sigo #parameters of best-fitting model in McMillan (2017) #stellar disks Sigma0_thin= 896./sigo Rd_thin= 2.5/ro zd_thin= 0.3/ro Sigma0_thick= 183./sigo Rd_thick= 3.02/ro zd_thick= 0.9/ro #bulge rho0_bulge= 98.4/rhoo r0_bulge= 0.075/ro rcut= 2.1/ro #DM halo rho0_halo= 0.00854/rhoo rh= 19.6/ro def gas_dens(R,z): return \ mwpot_helpers.expsech2_dens_with_hole(R,z,Rd_HI,Rm_HI,zd_HI,Sigma0_HI)\ +mwpot_helpers.expsech2_dens_with_hole(R,z,Rd_H2,Rm_H2,zd_H2,Sigma0_H2) def stellar_dens(R,z): return mwpot_helpers.expexp_dens(R,z,Rd_thin,zd_thin,Sigma0_thin)\ +mwpot_helpers.expexp_dens(R,z,Rd_thick,zd_thick,Sigma0_thick) def bulge_dens(R,z): return mwpot_helpers.core_pow_dens_with_cut(R,z,1.8,r0_bulge,rcut, rho0_bulge,0.5) #dicts used in DiskSCFPotential sigmadict = [{'type':'exp','h':Rd_HI,'amp':Sigma0_HI, 'Rhole':Rm_HI}, {'type':'exp','h':Rd_H2,'amp':Sigma0_H2, 'Rhole':Rm_H2}, {'type':'exp','h':Rd_thin,'amp':Sigma0_thin}, {'type':'exp','h':Rd_thick,'amp':Sigma0_thick}] hzdict = [{'type':'sech2', 'h':zd_HI}, {'type':'sech2', 'h':zd_H2}, {'type':'exp', 'h':zd_thin}, {'type':'exp', 'h':zd_thick}] #generate separate disk and halo potential - and combined potential McMillan_bulge= SCFPotential(\ Acos=scf_compute_coeffs_axi(bulge_dens,20,10,a=0.1)[0], a=0.1,ro=ro,vo=vo) McMillan_disk= DiskSCFPotential(\ dens=lambda R,z: gas_dens(R,z)+stellar_dens(R,z), Sigma=sigmadict,hz=hzdict,a=2.5,N=30,L=30,ro=ro,vo=vo) McMillan_halo= NFWPotential(amp=rho0_halo*(4*numpy.pi*rh**3), a=rh,ro=ro,vo=vo) # Go back to old floating-point warnings settings numpy.seterr(**old_error_settings) McMillan17= McMillan_disk+McMillan_halo+McMillan_bulge ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/MiyamotoNagaiPotential.py0000644000175100001710000001701114327773303022350 0ustar00runnerdocker############################################################################### # MiyamotoNagaiPotential.py: class that implements the Miyamoto-Nagai # potential # GM # phi(R,z) = - --------------------------------- # \sqrt(R^2+(a+\sqrt(z^2+b^2))^2) ############################################################################### import numpy from ..util import conversion from .Potential import Potential, kms_to_kpcGyrDecorator class MiyamotoNagaiPotential(Potential): """Class that implements the Miyamoto-Nagai potential .. math:: \\Phi(R,z) = -\\frac{\\mathrm{amp}}{\\sqrt{R^2+(a+\\sqrt{z^2+b^2})^2}} with :math:`\\mathrm{amp} = GM` the total mass. """ def __init__(self,amp=1.,a=1.,b=0.1,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a Miyamoto-Nagai potential INPUT: amp - amplitude to be applied to the potential, the total mass (default: 1); can be a Quantity with units of mass or Gxmass a - scale length (can be Quantity) b - scale height (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2010-07-09 - Started - Bovy (NYU) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) b= conversion.parse_length(b,ro=self._ro) self._a= a self._scale= self._a self._b= b self._b2= self._b**2. if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): self.normalize(normalize) self.hasC= True self.hasC_dxdv= True self.hasC_dens= True self._nemo_accname= 'MiyamotoNagai' def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2010-07-09 - Started - Bovy (NYU) """ return -1./numpy.sqrt(R**2.+(self._a+numpy.sqrt(z**2.+self._b2))**2.) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2010-07-09 - Written - Bovy (NYU) """ return -R/(R**2.+(self._a+numpy.sqrt(z**2.+self._b2))**2.)**(3./2.) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2010-07-09 - Written - Bovy (NYU) """ sqrtbz= numpy.sqrt(self._b2+z**2.) asqrtbz= self._a+sqrtbz if isinstance(R,float) and sqrtbz == asqrtbz: return (-z/ (R**2.+(self._a+numpy.sqrt(z**2.+self._b2))**2.)**(3./2.)) else: return (-z*asqrtbz/sqrtbz/ (R**2.+(self._a+numpy.sqrt(z**2.+self._b2))**2.)**(3./2.)) def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2010-08-08 - Written - Bovy (NYU) """ sqrtbz= numpy.sqrt(self._b2+z**2.) asqrtbz= self._a+sqrtbz if isinstance(R,float) and sqrtbz == asqrtbz: return 3./\ (R**2.+sqrtbz**2.)**2.5/4./numpy.pi*self._b2 else: return (self._a*R**2.+(self._a+3.*sqrtbz)*asqrtbz**2.)/\ (R**2.+asqrtbz**2.)**2.5/sqrtbz**3./4./numpy.pi*self._b2 def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2011-10-09 - Written - Bovy (IAS) """ return 1./(R**2.+(self._a+numpy.sqrt(z**2.+self._b2))**2.)**1.5 \ -3.*R**2./(R**2.+(self._a+numpy.sqrt(z**2.+self._b2))**2.)**2.5 def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second vertical derivative HISTORY: 2012-07-25 - Written - Bovy (IAS@MPIA) """ sqrtbz= numpy.sqrt(self._b2+z**2.) asqrtbz= self._a+sqrtbz if isinstance(R,float) and sqrtbz == asqrtbz: return (self._b2+R**2.-2.*z**2.)*(self._b2+R**2.+z**2.)**-2.5 else: return ((self._a**3.*self._b2 + self._a**2.*(3.*self._b2 - 2.* z**2.) *numpy.sqrt(self._b2 + z**2.) + (self._b2 + R**2. - 2.*z**2.)*(self._b2 + z**2.)**1.5 +self._a* (3.*self._b2**2. - 4.*z**4. + self._b2*(R**2. - z**2.)))/ ((self._b2 + z**2.)**1.5* (R**2. + asqrtbz**2.)**2.5)) def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2phi/dR/dz HISTORY: 2013-08-28 - Written - Bovy (IAS) """ sqrtbz= numpy.sqrt(self._b2+z**2.) asqrtbz= self._a+sqrtbz if isinstance(R,float) and sqrtbz == asqrtbz: return -(3.*R*z/(R**2.+asqrtbz**2.)**2.5) else: return -(3.*R*z*asqrtbz /sqrtbz/(R**2.+asqrtbz**2.)**2.5) @kms_to_kpcGyrDecorator def _nemo_accpars(self,vo,ro): """ NAME: _nemo_accpars PURPOSE: return the accpars potential parameters for use of this potential with NEMO INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: accpars string HISTORY: 2014-12-18 - Written - Bovy (IAS) """ ampl= self._amp*vo**2.*ro return f"0,{ampl},{self._a*ro},{self._b*ro}" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/MovingObjectPotential.py0000644000175100001710000001557714327773303022217 0ustar00runnerdocker############################################################################### # MovingObjectPotential.py: class that implements the potential coming from # a moving object ############################################################################### import copy import numpy from .PlummerPotential import PlummerPotential from .Potential import (Potential, _check_c, _isNonAxi, evaluateDensities, evaluatePotentials, evaluateRforces, evaluatezforces, flatten) class MovingObjectPotential(Potential): """ Class that implements the potential coming from a moving object by combining any galpy potential with an integrated galpy orbit. """ def __init__(self,orbit,pot=None,amp=1.0, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a MovingObjectPotential INPUT: orbit - the Orbit of the object (Orbit object) pot - A potential object or list of potential objects representing the potential of the moving object; should be spherical, but this is not checked [default= PlummerPotential(amp=0.06,b=0.01)] amp (=1.) another amplitude to apply to the potential ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2011-04-10 - Started - Bovy (NYU) 2018-10-18 - Re-implemented to represent general object potentials using galpy potential models - James Lane (UofT) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo) # If no potential supplied use a default Plummer sphere if pot is None: pot=PlummerPotential(amp=0.06,b=0.01) self._pot = pot else: pot=flatten(pot) if _isNonAxi(pot): raise NotImplementedError('MovingObjectPotential for non-axisymmetric potentials is not currently supported') self._pot=pot self._orb= copy.deepcopy(orbit) self._orb.turn_physical_off() self.isNonAxi= True self.hasC= _check_c(self._pot) return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z, phi INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z,phi) HISTORY: 2011-04-10 - Started - Bovy (NYU) 2018-10-18 - Updated for general object potential - James Lane (UofT) """ #Cylindrical distance Rdist = _cylR(R,phi,self._orb.R(t),self._orb.phi(t)) orbz = self._orb.z(t) if self._orb.dim() == 3 else 0 #Evaluate potential return evaluatePotentials(self._pot,Rdist,orbz-z,t=t, use_physical=False) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2011-04-10 - Written - Bovy (NYU) 2018-10-18 - Updated for general object potential - James Lane (UofT) """ #Cylindrical distance Rdist = _cylR(R,phi,self._orb.R(t),self._orb.phi(t)) # Difference vector orbz = self._orb.z(t) if self._orb.dim() == 3 else 0 (xd,yd,zd) = _cyldiff(self._orb.R(t), self._orb.phi(t), orbz, R, phi, z) #Evaluate cylindrical radial force RF = evaluateRforces(self._pot,Rdist,zd,t=t,use_physical=False) # Return R force, negative of radial vector to evaluation location. return -RF*(numpy.cos(phi)*xd+numpy.sin(phi)*yd)/Rdist def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2011-04-10 - Written - Bovy (NYU) 2018-10-18 - Updated for general object potential - James Lane (UofT) """ #Cylindrical distance Rdist = _cylR(R,phi,self._orb.R(t),self._orb.phi(t)) # Difference vector orbz = self._orb.z(t) if self._orb.dim() == 3 else 0 (xd,yd,zd) = _cyldiff(self._orb.R(t), self._orb.phi(t), orbz, R, phi, z) #Evaluate and return z force return -evaluatezforces(self._pot,Rdist,zd,t=t,use_physical=False) def _phitorque(self,R,z,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the azimuthal torque HISTORY: 2011-04-10 - Written - Bovy (NYU) 2018-10-18 - Updated for general object potential - James Lane (UofT) """ #Cylindrical distance Rdist = _cylR(R,phi,self._orb.R(t),self._orb.phi(t)) # Difference vector orbz = self._orb.z(t) if self._orb.dim() == 3 else 0 (xd,yd,zd) = _cyldiff(self._orb.R(t), self._orb.phi(t), orbz, R, phi, z) #Evaluate cylindrical radial force. RF = evaluateRforces(self._pot,Rdist,zd,t=t,use_physical=False) # Return phi force, negative of phi vector to evaluate location return -RF*R*(numpy.cos(phi)*yd-numpy.sin(phi)*xd)/Rdist def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2010-08-08 - Written - Bovy (NYU) """ #Cylindrical distance Rdist = _cylR(R,phi,self._orb.R(t),self._orb.phi(t)) # Difference vector orbz = self._orb.z(t) if self._orb.dim() == 3 else 0 (xd,yd,zd) = _cyldiff(self._orb.R(t), self._orb.phi(t), orbz, R, phi, z) # Return the density return evaluateDensities(self._pot,Rdist,zd,t=t,use_physical=False) def _cylR(R1,phi1,R2,phi2): return numpy.sqrt(R1**2.+R2**2.-2.*R1*R2*numpy.cos(phi1-phi2)) # Cosine law def _cyldiff(R1,phi1,z1,R2,phi2,z2): dx = R1*numpy.cos(phi1)-R2*numpy.cos(phi2) dy = R1*numpy.sin(phi1)-R2*numpy.sin(phi2) dz = z1-z2 return (dx,dy,dz) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/NonInertialFrameForce.py0000644000175100001710000003440714327773303022116 0ustar00runnerdocker############################################################################### # NonInertialFrameForce: Class that implements the fictitious forces # present when integrating orbits in a non-intertial # frame ############################################################################### import hashlib import numpy import numpy.linalg from ..util import conversion, coords from .DissipativeForce import DissipativeForce class NonInertialFrameForce(DissipativeForce): """Class that implements the fictitious forces present when integrating orbits in a non-intertial frame. Coordinates in the inertial frame :math:`\\mathbf{x}` and in the non-inertial frame :math:`\\mathbf{r}` are related through rotation and linear motion as .. math:: \\mathbf{x} = \\mathbf{R}\\,\\left(\\mathbf{r} + \\mathbf{x}_0\\right) where :math:`\\mathbf{R}` is a rotation matrix and :math:`\\mathbf{x}_0` is the motion of the origin. The rotation matrix has angular frequencies :math:`\\boldsymbol{\\Omega}` with time derivative :math:`\\dot{\\boldsymbol{\\Omega}}`; :math:`\\boldsymbol{\\Omega}` can be any function of time (note that the sign of :math:`\\boldsymbol{\\Omega}` is such that :math:`\\boldsymbol{\\Omega}` is the frequency of the rotating frame as seen from the inertial frame). The motion of the origin can also be any function of time. This leads to the fictitious force .. math:: \\mathbf{F} = -\\mathbf{a}_0 - \\boldsymbol{\\Omega} \\times ( \\boldsymbol{\\Omega} \\times \\left[\\mathbf{r} + \\mathbf{x}_0\\right]) - \\dot{\\boldsymbol{\\Omega}} \\times \\left[\\mathbf{r}+\\mathbf{x}_0\\right] -2\\boldsymbol{\\Omega}\\times \\left[\\dot{\\mathbf{r}}+\\mathbf{v}_0\\right] where :math:`\\mathbf{a}_0`, :math:`\\mathbf{v}_0`, and :math:`\\mathbf{x}_0` are the acceleration, velocity, and position of the origin of the non-inertial frame, respectively, as a function of time. Note that if the non-inertial frame is not rotating, it is not necessary to specify :math:`\\mathbf{v}_0` and :math:`\\mathbf{x}_0`. In that case, the fictitious force is simply .. math:: \\mathbf{F} = -\\mathbf{a}_0\\quad (\\boldsymbol{\\Omega} = 0) If the non-inertial frame only rotates without any motion of the origin, the fictitious force is the familiar combination of the centrifugal force and the Coriolis force (plus an additional term if :math:`\\dot{\\boldsymbol{\\Omega}}` is not constant) .. math:: \\mathbf{F} = - \\boldsymbol{\\Omega} \\times ( \\boldsymbol{\\Omega} \\times \\mathbf{r}) - \\dot{\\boldsymbol{\\Omega}} \\times \\mathbf{r} -2\\boldsymbol{\\Omega}\\times \\dot{\\mathbf{r}}\\quad (\\mathbf{a}_0=\\mathbf{v}_0=\\mathbf{x}_0=0) The functions of time are passed to the C code for fast orbit integration by attempting to build fast ``numba`` versions of them. Significant speed-ups can therefore be obtained by making sure that the provided functions can be turned into ``nopython=True`` ``numba`` functions (try running ``numba.njit`` on them and then evaluate them to check). """ def __init__(self,amp=1.,Omega=None,Omegadot=None, x0=None,v0=None,a0=None, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a NonInertialFrameForce INPUT: amp= (1.) amplitude to be applied to the potential (default: 1) Omega= (1.) Angular frequency of the rotation of the non-inertial frame as seen from an inertial one; can either be a function of time or a number (when the frequency is assumed to be Omega + Omegadot x t) and in each case can be a list [Omega_x,Omega_y,Omega_z] or a single value Omega_z (when not a function, can be a Quantity; when a function, need to take input time in internal units and output the frequency in internal units; see galpy.util.conversion.time_in_Gyr and galpy.util.conversion.freq_in_XXX conversion functions) Omegadot= (None) Time derivative of the angular frequency of the non-intertial frame's rotation. format should match Omega input ([list of] function[s] when Omega is one, number/list if Omega is a number/list; when a function, need to take input time in internal units and output the frequency derivative in internal units; see galpy.util.conversion.time_in_Gyr and galpy.util.conversion.freq_in_XXX conversion functions) x0= (None) Position vector x_0 (cartesian) of the center of mass of the non-intertial frame (see definition in the class documentation); list of functions [x_0x,x_0y,x_0z]; only necessary when considering both rotation and center-of-mass acceleration of the inertial frame (functions need to take input time in internal units and output the position in internal units; see galpy.util.conversion.time_in_Gyr and divided physical positions by the `ro` parameter in kpc) v0= (None) Velocity vector v_0 (cartesian) of the center of mass of the non-intertial frame (see definition in the class documentation); list of functions [v_0x,v_0y,v_0z]; only necessary when considering both rotation and center-of-mass acceleration of the inertial frame (functions need to take input time in internal units and output the velocity in internal units; see galpy.util.conversion.time_in_Gyr and divided physical positions by the `vo` parameter in km/s) a0= (None) Acceleration vector a_0 (cartesian) of the center of mass of the non-intertial frame (see definition in the class documentation); constant or a list of functions [a_0x,a_0y, a_0z] (functions need to take input time in internal units and output the acceleration in internal units; see galpy.util.conversion.time_in_Gyr and galpy.util.conversion.force_in_XXX conversion functions [force is actually acceleration in galpy]) OUTPUT: (none) HISTORY: 2022-03-02 - Started - Bovy (UofT) 2022-03-26 - Generalized Omega to any function of time - Bovy (UofT) """ DissipativeForce.__init__(self,amp=amp,ro=ro,vo=vo) self._rot_acc= not Omega is None self._omegaz_only= len(numpy.atleast_1d(Omega)) == 1 self._const_freq= Omegadot is None if (self._omegaz_only and callable(Omega)) \ or (not self._omegaz_only and callable(Omega[0])): self._Omega_as_func= True self._Omega= Omega self._Omegadot= Omegadot # Convenient access in Python if not self._omegaz_only: self._Omega_py= lambda t: numpy.array(\ [self._Omega[0](t), self._Omega[1](t), self._Omega[2](t)]) self._Omegadot_py= lambda t: numpy.array(\ [self._Omegadot[0](t), self._Omegadot[1](t), self._Omegadot[2](t)]) else: self._Omega_py= self._Omega self._Omegadot_py= self._Omegadot else: self._Omega_as_func= False self._Omega= conversion.parse_frequency(Omega,ro=self._ro,vo=self._vo) self._Omegadot= conversion.parse_frequency(Omegadot,ro=self._ro,vo=self._vo) self._lin_acc= not (a0 is None) if self._lin_acc: if not callable(a0[0]): self._a0= [lambda t, copy=a0[0]: copy, lambda t, copy=a0[1]: copy, lambda t, copy=a0[2]: copy] else: self._a0= a0 # Convenient access in Python self._a0_py= lambda t: [self._a0[0](t), self._a0[1](t), self._a0[2](t)] if self._lin_acc and self._rot_acc: self._x0= x0 self._v0= v0 # Convenient access in Python self._x0_py= lambda t: numpy.array(\ [self._x0[0](t), self._x0[1](t), self._x0[2](t)]) self._v0_py= lambda t: numpy.array(\ [self._v0[0](t), self._v0[1](t), self._v0[2](t)]) # Useful derived quantities self._Omega2= numpy.linalg.norm(self._Omega)**2. \ if self._rot_acc and not self._Omega_as_func\ else 0. if not self._omegaz_only and not self._Omega_as_func: self._Omega_for_cross= numpy.array([[0.,-self._Omega[2],self._Omega[1]], [self._Omega[2],0.,-self._Omega[0]], [-self._Omega[1],self._Omega[0],0.]]) if not self._const_freq: self._Omegadot_for_cross= \ numpy.array([[0.,-self._Omegadot[2],self._Omegadot[1]], [self._Omegadot[2],0.,-self._Omegadot[0]], [-self._Omegadot[1],self._Omegadot[0],0.]]) self._force_hash= None self.hasC= True return None def _force(self,R,z,phi,t,v): """Internal function that computes the fictitious forces in rectangular coordinates""" new_hash= hashlib.md5(numpy.array([R,phi,z,v[0],v[1],v[2],t])).hexdigest() if new_hash == self._force_hash: return self._cached_force x,y,z= coords.cyl_to_rect(R,phi,z) vx,vy,vz= coords.cyl_to_rect_vec(v[0],v[1],v[2],phi) force= numpy.zeros(3) if self._rot_acc: if self._const_freq: tOmega= self._Omega tOmega2= self._Omega2 elif self._Omega_as_func: tOmega= self._Omega_py(t) tOmega2= numpy.linalg.norm(tOmega)**2. else: tOmega= self._Omega+self._Omegadot*t tOmega2= numpy.linalg.norm(tOmega)**2. if self._omegaz_only: force+= -2.*tOmega*numpy.array([-vy,vx,0.])\ +tOmega2*numpy.array([x,y,0.]) if self._lin_acc: force+= -2.*tOmega*numpy.array([-self._v0[1](t),self._v0[0](t),0.])\ +tOmega2*numpy.array([self._x0[0](t),self._x0[1](t),0.]) if not self._const_freq: if self._Omega_as_func: force-= self._Omegadot_py(t)*numpy.array([-y,x,0.]) if self._lin_acc: force-= self._Omegadot_py(t)\ *numpy.array([-self._x0[1](t),self._x0[0](t),0.]) else: force-= self._Omegadot*numpy.array([-y,x,0.]) if self._lin_acc: force-= self._Omegadot\ *numpy.array([-self._x0[1](t),self._x0[0](t),0.]) else: if self._Omega_as_func: self._Omega_for_cross= \ numpy.array([[0.,-self._Omega[2](t),self._Omega[1](t)], [self._Omega[2](t),0.,-self._Omega[0](t)], [-self._Omega[1](t),self._Omega[0](t),0.]]) if not self._const_freq: self._Omegadot_for_cross= \ numpy.array([[0.,-self._Omegadot[2](t),self._Omegadot[1](t)], [self._Omegadot[2](t),0.,-self._Omegadot[0](t)], [-self._Omegadot[1](t),self._Omegadot[0](t),0.]]) force+= -2.*numpy.dot(self._Omega_for_cross,numpy.array([vx,vy,vz]))\ +tOmega2*numpy.array([x,y,z])\ -tOmega*numpy.dot(tOmega,numpy.array([x,y,z])) if self._lin_acc: force+= -2.*numpy.dot(self._Omega_for_cross,self._v0_py(t))\ +tOmega2*self._x0_py(t)\ -tOmega*numpy.dot(tOmega,self._x0_py(t)) if not self._const_freq: if not self._Omega_as_func: # Already included above when Omega=func force-= 2.*t*numpy.dot(self._Omegadot_for_cross,numpy.array([vx,vy,vz])) force-= numpy.dot(self._Omegadot_for_cross,numpy.array([x,y,z])) if self._lin_acc: if not self._Omega_as_func: force-= 2.*t*numpy.dot(self._Omegadot_for_cross,self._v0_py(t)) force-= numpy.dot(self._Omegadot_for_cross,self._x0_py(t)) if self._lin_acc: force-= self._a0_py(t) self._force_hash= new_hash self._cached_force= force return force def _Rforce(self,R,z,phi=0.,t=0.,v=None): """ NAME: _Rforce PURPOSE: evaluate the radial force for this Force INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time v= current velocity in cylindrical coordinates OUTPUT: the radial force HISTORY: 2022-03-02 - Written - Bovy (UofT) """ force= self._force(R,z,phi,t,v) return numpy.cos(phi)*force[0]+numpy.sin(phi)*force[1] def _phitorque(self,R,z,phi=0.,t=0.,v=None): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this Force INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time v= current velocity in cylindrical coordinates OUTPUT: the azimuthal torque HISTORY: 2022-03-02 - Written - Bovy (UofT) """ force= self._force(R,z,phi,t,v) return R*(-numpy.sin(phi)*force[0]+numpy.cos(phi)*force[1]) def _zforce(self,R,z,phi=0.,t=0.,v=None): """ NAME: _zforce PURPOSE: evaluate the vertical force for this Force INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time v= current velocity in cylindrical coordinates OUTPUT: the vertical force HISTORY: 2022-03-02 - Written - Bovy (UofT) """ return self._force(R,z,phi,t,v)[2] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/NullPotential.py0000644000175100001710000001044414327773303020527 0ustar00runnerdocker############################################################################### # NullPotential.py: class that implements a constant potential ############################################################################### from .Potential import Potential class NullPotential(Potential): """Class that implements a constant potential with, thus, zero forces. Can be used, for example, for integrating orbits in the absence of forces or for adjusting the value of the total gravitational potential, say, at infinity""" normalize= property() # turn off normalize def __init__(self,amp=1.,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a null potential: a constant potential with, thus, zero forces INPUT: amp - constant value of the potential (default: 1); can be a Quantity with units of velocity-squared ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2022-03-18 - Written - Bovy (UofT) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='velocity2') self.hasC= True self.hasC_dxdv= True self.hasC_dens= True return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2022-03-18 - Written - Bovy (UofT) """ return 1. def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2022-03-18 - Written - Bovy (UofT) """ return 0. def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2022-03-18 - Written - Bovy (UofT) """ return 0. def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2022-03-18 - Written - Bovy (UofT) """ return 0. def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2022-03-18 - Written - Bovy (UofT) """ return 0. def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second vertical derivative HISTORY: 2022-03-18 - Written - Bovy (UofT) """ return 0. def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2Phi/dR/dz HISTORY: 2022-03-18 - Written - Bovy (UofT) """ return 0. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/NumericalPotentialDerivativesMixin.py0000644000175100001710000001405314327773303024747 0ustar00runnerdocker############################################################################### # NumericalPotentialDerivativesMixin: helper class to add numerical derivs ############################################################################### class NumericalPotentialDerivativesMixin: """Mixin to add numerical derivatives to a Potential class, use as, e.g., .. highlight:: python .. code-block:: python class PotWithNumericalDerivs(Potential,NumericalPotentialDerivativesMixin): def __init__(self,*args,**kwargs): NumericalPotentialDerivativesMixin.__init__(self,kwargs) # *not* **kwargs! # Remainder of initialization ... def _evaluate(self,R,z,phi=0.,t=0.): # Evaluate the potential # All forces and second derivatives then computed by NumericalPotentialDerivativesMixin to add numerical derivatives to a new potential class ``PotWithNumericalDerivs`` that only implements the potential itself, but not the forces. The class may implement any of the forces or second derivatives, all non-implemented forces/second-derivatives will be computed numerically by adding this Mixin The step used to compute the first (force) and second derivatives can be controlled at object instantiation by the keyword arguments ``dR``, ``dz``, ``dphi`` (for the forces; 1e-8 default) and ``dR2``, ``dz2``, and ``dphi2`` (for the second derivaives; 1e-4 default)""" def __init__(self,kwargs): # no **kwargs to get a reference, not a copy! # For first derivatives self._dR= kwargs.pop('dR',1e-8) self._dphi= kwargs.pop('dphi',1e-8) self._dz= kwargs.pop('dz',1e-8) # For second derivatives self._dR2= kwargs.pop('dR2',1e-4) self._dphi2= kwargs.pop('dphi2',1e-4) self._dz2= kwargs.pop('dz2',1e-4) def _Rforce(self,R,z,phi=0.,t=0.): # Do forward difference because R cannot be negative RplusdR= R+self._dR Rplus2dR= R+2.*self._dR dR= (Rplus2dR-R)/2. return (1.5*self._evaluate(R,z,phi=phi,t=t) -2.*self._evaluate(RplusdR,z,phi=phi,t=t) +0.5*self._evaluate(Rplus2dR,z,phi=phi,t=t))/dR def _zforce(self,R,z,phi=0.,t=0.): # Central difference to get derivative at z=0 right zplusdz= z+self._dz zminusdz= z-self._dz dz= zplusdz-zminusdz return (self._evaluate(R,zminusdz,phi=phi,t=t) -self._evaluate(R,zplusdz,phi=phi,t=t))/dz def _phitorque(self,R,z,phi=0.,t=0.): if not self.isNonAxi: return 0. # Central difference phiplusdphi= phi+self._dphi phiminusdphi= phi-self._dphi dphi= phiplusdphi-phiminusdphi return (self._evaluate(R,z,phi=phiminusdphi,t=t) -self._evaluate(R,z,phi=phiplusdphi,t=t))/dphi def _R2deriv(self,R,z,phi=0.,t=0.): # Do forward difference because R cannot be negative RplusdR= R+self._dR2 Rplus2dR= R+2.*self._dR2 Rplus3dR= R+3.*self._dR2 dR= (Rplus3dR-R)/3. return (2.*self._evaluate(R,z,phi=phi,t=t) -5.*self._evaluate(RplusdR,z,phi=phi,t=t) +4.*self._evaluate(Rplus2dR,z,phi=phi,t=t) -1.*self._evaluate(Rplus3dR,z,phi=phi,t=t))/dR**2. def _z2deriv(self,R,z,phi=0.,t=0.): # Central derivative zplusdz= z+self._dz2 zminusdz= z-self._dz2 dz= (zplusdz-zminusdz)/2. return (self._evaluate(R,zplusdz,phi=phi,t=t) +self._evaluate(R,zminusdz,phi=phi,t=t) -2.*self._evaluate(R,z,phi=phi,t=t))/dz**2. def _phi2deriv(self,R,z,phi=0.,t=0.): if not self.isNonAxi: return 0. # Central derivative phiplusdphi= phi+self._dphi2 phiminusdphi= phi-self._dphi2 dphi= (phiplusdphi-phiminusdphi)/2. return (self._evaluate(R,z,phi=phiplusdphi,t=t) +self._evaluate(R,z,phi=phiminusdphi,t=t) -2.*self._evaluate(R,z,phi=phi,t=t))/dphi**2. def _Rzderiv(self,R,z,phi=0.,t=0.): # Do forward difference in R because R cannot be negative RplusdR= R+self._dR2 Rplus2dR= R+2.*self._dR2 dR= (Rplus2dR-R)/2. zplusdz= z+self._dz2 zminusdz= z-self._dz2 dz= zplusdz-zminusdz return (-1.5*self._evaluate(R,zplusdz,phi=phi,t=t) +2.*self._evaluate(RplusdR,zplusdz,phi=phi,t=t) -0.5*self._evaluate(Rplus2dR,zplusdz,phi=phi,t=t) +1.5*self._evaluate(R,zminusdz,phi=phi,t=t) -2.*self._evaluate(RplusdR,zminusdz,phi=phi,t=t) +0.5*self._evaluate(Rplus2dR,zminusdz,phi=phi,t=t))/dR/dz def _Rphideriv(self,R,z,phi=0.,t=0.): if not self.isNonAxi: return 0. # Do forward difference in R because R cannot be negative RplusdR= R+self._dR2 Rplus2dR= R+2.*self._dR2 dR= (Rplus2dR-R)/2. phiplusdphi= phi+self._dphi2 phiminusdphi= phi-self._dphi2 dphi= phiplusdphi-phiminusdphi return (-1.5*self._evaluate(R,z,phi=phiplusdphi,t=t) +2.*self._evaluate(RplusdR,z,phi=phiplusdphi,t=t) -0.5*self._evaluate(Rplus2dR,z,phi=phiplusdphi,t=t) +1.5*self._evaluate(R,z,phi=phiminusdphi,t=t) -2.*self._evaluate(RplusdR,z,phi=phiminusdphi,t=t) +0.5*self._evaluate(Rplus2dR,z,phi=phiminusdphi,t=t))\ /dR/dphi def _phizderiv(self,R,z,phi=0.,t=0.): if not self.isNonAxi: return 0. # Central derivative phiplusdphi= phi+self._dphi2 phiminusdphi= phi-self._dphi2 dphi= (phiplusdphi-phiminusdphi)/2. zplusdz= z+self._dz2 zminusdz= z-self._dz2 dz= zplusdz-zminusdz return (self._evaluate(R,zplusdz,phi=phiplusdphi,t=t) -self._evaluate(R,zplusdz,phi=phiminusdphi,t=t) -self._evaluate(R,zminusdz,phi=phiplusdphi,t=t) +self._evaluate(R,zminusdz,phi=phiminusdphi,t=t))\ /dz/dphi/2. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/PerfectEllipsoidPotential.py0000644000175100001710000000706614327773303023060 0ustar00runnerdocker############################################################################### # PerfectEllipsoidPotential.py: Potential of the perfect ellipsoid # (de Zeeuw 1985): # # \rho(x,y,z) ~ 1/(1+m^2)^2 # # with m^2 = x^2+y^2/b^2+z^2/c^2 # ############################################################################### import numpy from ..util import conversion from .EllipsoidalPotential import EllipsoidalPotential class PerfectEllipsoidPotential(EllipsoidalPotential): """Potential of the perfect ellipsoid (de Zeeuw 1985): .. math:: \\rho(x,y,z) = \\frac{\\mathrm{amp\\,a}}{\\pi^2\\,bc}\\,\\frac{1}{(m^2+a^2)^2} where :math:`\\mathrm{amp} = GM` is the total mass and :math:`m^2 = x^2+y^2/b^2+z^2/c^2`. """ def __init__(self,amp=1.,a=5.,b=1.,c=1., zvec=None,pa=None,glorder=50, normalize=False,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a perfect ellipsoid potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or G x mass a - scale radius (can be Quantity) b - y-to-x axis ratio of the density c - z-to-x axis ratio of the density zvec= (None) If set, a unit vector that corresponds to the z axis pa= (None) If set, the position angle of the x axis (rad or Quantity) glorder= (50) if set, compute the relevant force and potential integrals with Gaussian quadrature of this order ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2018-08-06 - Started - Bovy (UofT) """ EllipsoidalPotential.__init__(self,amp=amp,b=b,c=c, zvec=zvec,pa=pa,glorder=glorder, ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) self.a= a self.a2= self.a**2 self._scale= self.a # Adjust amp self._amp*= self.a/(numpy.pi**2*self._b*self._c) if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) self.hasC= not self._glorder is None self.hasC_dxdv= False self.hasC_dens= self.hasC # works if mdens is defined, necessary for hasC return None def _psi(self,m): """\\psi(m) = -\\int_m^\\infty d m^2 \rho(m^2)""" return -1./(self.a2+m**2) def _mdens(self,m): """Density as a function of m""" return (self.a2+m**2)**-2 def _mdens_deriv(self,m): """Derivative of the density as a function of m""" return -4.*m*(self.a2+m**2)**-3 def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R (and z) for this potential; if z=None, integrate to ellipsoidal boundary INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2021-03-08 - Written - Bovy (UofT) """ if not z is None: raise AttributeError # Hack to fall back to general return 2.*numpy.pi*self._b*self._c/self.a\ *(numpy.arctan(R/self.a)-R*self.a/(1.+R**2.)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/PlummerPotential.py0000644000175100001710000002136214327773303021237 0ustar00runnerdocker############################################################################### # PlummerPotential.py: class that implements the Plummer potential # GM # phi(R,z) = - --------------------------------- # \sqrt(R^2+z^2+b^2) ############################################################################### import numpy from ..util import conversion from .Potential import Potential, kms_to_kpcGyrDecorator class PlummerPotential(Potential): """Class that implements the Plummer potential .. math:: \\Phi(R,z) = -\\frac{\\mathrm{amp}}{\\sqrt{R^2+z^2+b^2}} with :math:`\\mathrm{amp} = GM` the total mass. """ def __init__(self,amp=1.,b=0.8,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a Plummer potential INPUT: amp - amplitude to be applied to the potential, the total mass (default: 1); can be a Quantity with units of mass or Gxmass b - scale parameter (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2015-06-15 - Written - Bovy (IAS) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') self._b= conversion.parse_length(b,ro=self._ro) self._scale= self._b self._b2= self._b**2. if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): self.normalize(normalize) self.hasC= True self.hasC_dxdv= True self.hasC_dens= True self._nemo_accname= 'Plummer' def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2015-06-15 - Started - Bovy (IAS) """ return -1./numpy.sqrt(R**2.+z**2.+self._b2) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2015-06-15 - Written - Bovy (IAS) """ dPhidrr= -(R**2.+z**2.+self._b2)**-1.5 return dPhidrr*R def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2015-06-15 - Written - Bovy (IAS) """ dPhidrr= -(R**2.+z**2.+self._b2)**-1.5 return dPhidrr*z def _rforce_jax(self,r): """ NAME: _rforce_jax PURPOSE: evaluate the spherical radial force for this potential using JAX INPUT: r - Galactocentric spherical radius OUTPUT: the radial force HISTORY: 2021-12-14 - Written - Lane (UofT) """ # No need for actual JAX! return -self._amp*r*(r**2.+self._b2)**-1.5 def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2015-06-15 - Written - Bovy (IAS) """ return 3./4./numpy.pi*self._b2*(R**2.+z**2.+self._b2)**-2.5 def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2018-08-19 - Written - Bovy (UofT) """ Rb= R**2.+self._b2 return self._b2*z*(3.*Rb+2.*z**2.)/Rb**2.*(Rb+z**2.)**-1.5/2./numpy.pi def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2015-06-15 - Written - Bovy (IAS) """ return (self._b2-2.*R**2.+z**2.)*(R**2.+z**2.+self._b2)**-2.5 def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second vertical derivative HISTORY: 2015-06-15 - Written - Bovy (IAS) """ return (self._b2+R**2.-2.*z**2.)*(R**2.+z**2.+self._b2)**-2.5 def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2phi/dR/dz HISTORY: 2015-06-15 - Written - Bovy (IAS) """ return -3.*R*z*(R**2.+z**2.+self._b2)**-2.5 def _ddensdr(self,r,t=0.): """ NAME: _ddensdr PURPOSE: evaluate the radial density derivative for this potential INPUT: r - spherical radius t= time OUTPUT: the density derivative HISTORY: 2021-12-15 - Written - Lane (UofT) """ return self._amp*(-15.)/4./numpy.pi*self._b2*r*(r**2+self._b2)**-3.5 def _d2densdr2(self,r,t=0.): """ NAME: _d2densdr2 PURPOSE: evaluate the second radial density derivative for this potential INPUT: r - spherical radius t= time OUTPUT: the 2nd density derivative HISTORY: 2021-12-15 - Written - Lane (UofT) """ return self._amp*(-15.)/4./numpy.pi*self._b2*((r**2.+self._b2)**-3.5\ -7.*r**2.*(r**2+self._b2)**-4.5) def _ddenstwobetadr(self,r,beta=0): """ NAME: _ddenstwobetadr PURPOSE: evaluate the radial density derivative x r^(2beta) for this potential INPUT: r - spherical radius beta= (0) OUTPUT: d (rho x r^{2beta} ) / d r HISTORY: 2021-03-15 - Written - Lane (UofT) """ return self._amp*3./4./numpy.pi*self._b2*r**(2.*beta-1.)\ *(2.*beta*(r**2.+self._b2)**-2.5-5.*r**2.*(r**2.+self._b2)**-3.5) def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2020-10-02 - Written - Bovy (UofT) """ if z is not None: raise AttributeError # use general implementation r2= R**2. return (1.+self._b2/r2)**-1.5 # written so it works for r=numpy.inf @kms_to_kpcGyrDecorator def _nemo_accpars(self,vo,ro): """ NAME: _nemo_accpars PURPOSE: return the accpars potential parameters for use of this potential with NEMO INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: accpars string HISTORY: 2014-12-18 - Written - Bovy (IAS) """ ampl= self._amp*vo**2.*ro return f"0,{ampl},{self._b*ro}" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/Potential.py0000644000175100001710000033325214327773303017701 0ustar00runnerdocker############################################################################### # Potential.py: top-level class for a full potential # # Evaluate by calling the instance: Pot(R,z,phi) # # API for Potentials: # function _evaluate(self,R,z,phi) returns Phi(R,z,phi) # for orbit integration you need # function _Rforce(self,R,z,phi) return -d Phi d R # function _zforce(self,R,z,phi) return - d Phi d Z # density # function _dens(self,R,z,phi) return BOVY?? # for epicycle frequency # function _R2deriv(self,R,z,phi) return d2 Phi dR2 ############################################################################### import os import os.path import pickle import warnings from functools import wraps import numpy from scipy import integrate, optimize from ..util import conversion, coords, galpyWarning, plot from ..util._optional_deps import _APY_LOADED from ..util.conversion import (freq_in_Gyr, get_physical, physical_conversion, potential_physical_input, velocity_in_kpcGyr) from .DissipativeForce import DissipativeForce, _isDissipative from .Force import Force from .plotEscapecurve import _INF, plotEscapecurve from .plotRotcurve import plotRotcurve, vcirc if _APY_LOADED: from astropy import units def check_potential_inputs_not_arrays(func): """ NAME: check_potential_inputs_not_arrays PURPOSE: Decorator to check inputs and throw TypeError if any of the inputs are arrays for Potentials that do not support array evaluation HISTORY: 2017-summer - Written for SpiralArmsPotential - Jack Hong (UBC) 2019-05-23 - Moved to Potential for more general use - Bovy (UofT) """ @wraps(func) def func_wrapper(self,R,z,phi,t): if (hasattr(R,'shape') and R.shape != () and len(R) > 1) \ or (hasattr(z,'shape') and z.shape != () and len(z) > 1) \ or (hasattr(phi,'shape') and phi.shape != () and len(phi) > 1) \ or (hasattr(t,'shape') and t.shape != () and len(t) > 1): raise TypeError(f'Methods in {self.__class__.__name__} do not accept array inputs. Please input scalars') return func(self,R,z,phi,t) return func_wrapper class Potential(Force): """Top-level class for a potential""" def __init__(self,amp=1.,ro=None,vo=None,amp_units=None): """ NAME: __init__ PURPOSE: INPUT: amp - amplitude to be applied when evaluating the potential and its forces amp_units - ('mass', 'velocity2', 'density') type of units that amp should have if it has units OUTPUT: HISTORY: """ Force.__init__(self,amp=amp,ro=ro,vo=vo,amp_units=amp_units) self.dim= 3 self.isRZ= True self.isNonAxi= False self.hasC= False self.hasC_dxdv= False self.hasC_dens= False return None @potential_physical_input @physical_conversion('energy',pop=True) def __call__(self,R,z,phi=0.,t=0.,dR=0,dphi=0): """ NAME: __call__ PURPOSE: evaluate the potential at (R,z,phi,t) INPUT: R - Cylindrical Galactocentric radius (can be Quantity) z - vertical height (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) OUTPUT: Phi(R,z,t) HISTORY: 2010-04-16 - Written - Bovy (NYU) """ return self._call_nodecorator(R,z,phi=phi,t=t,dR=dR,dphi=dphi) def _call_nodecorator(self,R,z,phi=0.,t=0.,dR=0.,dphi=0): if dR == 0 and dphi == 0: try: rawOut= self._evaluate(R,z,phi=phi,t=t) except AttributeError: #pragma: no cover raise PotentialError("'_evaluate' function not implemented for this potential") if rawOut is None: return rawOut else: return self._amp*rawOut elif dR == 1 and dphi == 0: return -self.Rforce(R,z,phi=phi,t=t,use_physical=False) elif dR == 0 and dphi == 1: return -self.phitorque(R,z,phi=phi,t=t,use_physical=False) elif dR == 2 and dphi == 0: return self.R2deriv(R,z,phi=phi,t=t,use_physical=False) elif dR == 0 and dphi == 2: return self.phi2deriv(R,z,phi=phi,t=t,use_physical=False) elif dR == 1 and dphi == 1: return self.Rphideriv(R,z,phi=phi,t=t,use_physical=False) elif dR != 0 or dphi != 0: raise NotImplementedError('Higher-order derivatives not implemented for this potential') @potential_physical_input @physical_conversion('force',pop=True) def Rforce(self,R,z,phi=0.,t=0.): """ NAME: Rforce PURPOSE: evaluate cylindrical radial force F_R (R,z) INPUT: R - Cylindrical Galactocentric radius (can be Quantity) z - vertical height (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) OUTPUT: F_R (R,z,phi,t) HISTORY: 2010-04-16 - Written - Bovy (NYU) """ return self._Rforce_nodecorator(R,z,phi=phi,t=t) def _Rforce_nodecorator(self,R,z,phi=0.,t=0.): # Separate, so it can be used during orbit integration try: return self._amp*self._Rforce(R,z,phi=phi,t=t) except AttributeError: #pragma: no cover raise PotentialError("'_Rforce' function not implemented for this potential") @potential_physical_input @physical_conversion('force',pop=True) def zforce(self,R,z,phi=0.,t=0.): """ NAME: zforce PURPOSE: evaluate the vertical force F_z (R,z,t) INPUT: R - Cylindrical Galactocentric radius (can be Quantity) z - vertical height (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) OUTPUT: F_z (R,z,phi,t) HISTORY: 2010-04-16 - Written - Bovy (NYU) """ return self._zforce_nodecorator(R,z,phi=phi,t=t) def _zforce_nodecorator(self,R,z,phi=0.,t=0.): # Separate, so it can be used during orbit integration try: return self._amp*self._zforce(R,z,phi=phi,t=t) except AttributeError: #pragma: no cover raise PotentialError("'_zforce' function not implemented for this potential") @potential_physical_input @physical_conversion('forcederivative',pop=True) def r2deriv(self,R,z,phi=0.,t=0.): """ NAME: r2deriv PURPOSE: evaluate the second spherical radial derivative INPUT: R - Cylindrical Galactocentric radius (can be Quantity) z - vertical height (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) OUTPUT: d2phi/dr2 HISTORY: 2018-03-21 - Written - Webb (UofT) """ r= numpy.sqrt(R**2.+z**2.) return (self.R2deriv(R,z,phi=phi,t=t,use_physical=False)*R/r\ +self.Rzderiv(R,z,phi=phi,t=t,use_physical=False)*z/r)*R/r\ +(self.Rzderiv(R,z,phi=phi,t=t,use_physical=False)*R/r\ +self.z2deriv(R,z,phi=phi,t=t,use_physical=False)*z/r)*z/r @potential_physical_input @physical_conversion('density',pop=True) def dens(self,R,z,phi=0.,t=0.,forcepoisson=False): """ NAME: dens PURPOSE: evaluate the density rho(R,z,t) INPUT: R - Cylindrical Galactocentric radius (can be Quantity) z - vertical height (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) KEYWORDS: forcepoisson= if True, calculate the density through the Poisson equation, even if an explicit expression for the density exists OUTPUT: rho (R,z,phi,t) HISTORY: 2010-08-08 - Written - Bovy (NYU) """ try: if forcepoisson: raise AttributeError #Hack! return self._amp*self._dens(R,z,phi=phi,t=t) except AttributeError: #Use the Poisson equation to get the density return (-self.Rforce(R,z,phi=phi,t=t,use_physical=False)/R +self.R2deriv(R,z,phi=phi,t=t,use_physical=False) +self.phi2deriv(R,z,phi=phi,t=t,use_physical=False)/R**2. +self.z2deriv(R,z,phi=phi,t=t,use_physical=False))/4./numpy.pi @potential_physical_input @physical_conversion('surfacedensity',pop=True) def surfdens(self,R,z,phi=0.,t=0.,forcepoisson=False): """ NAME: surfdens PURPOSE: evaluate the surface density :math:`\\Sigma(R,z,\\phi,t) = \\int_{-z}^{+z} dz' \\rho(R,z',\\phi,t)` INPUT: R - Cylindrical Galactocentric radius (can be Quantity) z - vertical height (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) KEYWORDS: forcepoisson= if True, calculate the surface density through the Poisson equation, even if an explicit expression for the surface density exists OUTPUT: Sigma (R,z,phi,t) HISTORY: 2018-08-19 - Written - Bovy (UofT) 2021-04-19 - Adjusted for non-z-symmetric densities - Bovy (UofT) """ try: if forcepoisson: raise AttributeError #Hack! return self._amp*self._surfdens(R,z,phi=phi,t=t) except AttributeError: #Use the Poisson equation to get the surface density return (-self.zforce(R,numpy.fabs(z),phi=phi,t=t,use_physical=False) +self.zforce(R,-numpy.fabs(z),phi=phi,t=t,use_physical=False) +integrate.quad(\ lambda x: -self.Rforce(R,x,phi=phi,t=t,use_physical=False)/R +self.R2deriv(R,x,phi=phi,t=t,use_physical=False) +self.phi2deriv(R,x,phi=phi,t=t,use_physical=False)/R**2., -numpy.fabs(z),numpy.fabs(z))[0])/4./numpy.pi def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the surface density HISTORY: 2018-08-19 - Written - Bovy (UofT) 2021-04-19 - Adjusted for non-z-symmetric densities - Bovy (UofT) """ return integrate.quad(lambda x: self._dens(R,x,phi=phi,t=t), -numpy.fabs(z),numpy.fabs(z))[0] @potential_physical_input @physical_conversion('mass',pop=True) def mass(self,R,z=None,t=0.,forceint=False): """ NAME: mass PURPOSE: evaluate the mass enclosed INPUT: R - Cylindrical Galactocentric radius (can be Quantity) z= (None) vertical height up to which to integrate (can be Quantity) t - time (optional; can be Quantity) forceint= if True, calculate the mass through integration of the density, even if an explicit expression for the mass exists OUTPUT: Mass enclosed within the spherical shell with radius R if z is None else mass in the slab z return -R*integrate.quad(lambda x: self.Rforce(R,x,t=t, use_physical=False), -z,z)[0]/2.\ -integrate.quad(lambda x: x*self.zforce(x,z,t=t, use_physical=False), 0.,R)[0] @physical_conversion('position',pop=True) def rhalf(self,t=0.,INF=numpy.inf): """ NAME: rhalf PURPOSE: calculate the half-mass radius, the radius of the spherical shell that contains half the total mass INPUT: t= (0.) time (optional; can be Quantity) INF= (numpy.inf) radius at which the total mass is calculated (internal units, just set this to something very large) OUTPUT: half-mass radius HISTORY: 2021-03-18 - Written - Bovy (UofT) """ return rhalf(self,t=t,INF=INF,use_physical=False) @potential_physical_input @physical_conversion('time',pop=True) def tdyn(self,R,t=0.): """ NAME: tdyn PURPOSE: calculate the dynamical time from tdyn^2 = 3pi/[G] INPUT: R - Galactocentric radius (can be Quantity) t= (0.) time (optional; can be Quantity) OUTPUT: Dynamical time HISTORY: 2021-03-18 - Written - Bovy (UofT) """ return 2.*numpy.pi*R*numpy.sqrt(R/self.mass(R,use_physical=False)) @physical_conversion('mass',pop=False) def mvir(self,H=70.,Om=0.3,t=0.,overdens=200.,wrtcrit=False, forceint=False,ro=None,vo=None, use_physical=False): # use_physical necessary bc of pop=False, does nothing inside """ NAME: mvir PURPOSE: calculate the virial mass INPUT: H= (default: 70) Hubble constant in km/s/Mpc Om= (default: 0.3) Omega matter overdens= (200) overdensity which defines the virial radius wrtcrit= (False) if True, the overdensity is wrt the critical density rather than the mean matter density ro= distance scale in kpc or as Quantity (default: object-wide, which if not set is 8 kpc)) vo= velocity scale in km/s or as Quantity (default: object-wide, which if not set is 220 km/s)) KEYWORDS: forceint= if True, calculate the mass through integration of the density, even if an explicit expression for the mass exists OUTPUT: M( 0.: if lower: rtry/= 2. else: rtry*= 2. return rtry @physical_conversion('position',pop=True) def rE(Pot,E,t=0.): """ NAME: rE PURPOSE: calculate the radius of a circular orbit with energy E INPUT: Pot - Potential instance or list thereof E - Energy (can be Quantity) t - time (optional; can be Quantity) OUTPUT: radius HISTORY: 2022-04-06 - Written - Bovy (UofT) NOTE: An efficient way to call this function on many objects is provided as the Orbit method rE """ Pot= flatten(Pot) E= conversion.parse_energy(E,**conversion.get_physical(Pot)) #Find interval rstart= _rEFindStart(1.,E,Pot,t=t) try: return optimize.brentq(_rEfunc,10.**-5.,rstart, args=(E,Pot,t), maxiter=200,disp=False) except ValueError: #Probably E small and starting rE to great rlower= _rEFindStart(10.**-5.,E,Pot,t=t,lower=True) return optimize.brentq(_rEfunc,rlower,rstart, args=(E,Pot,t)) def _rEfunc(rE,E,pot,t=0.): """Function that gives vc^2/2+Pot(rc)-E""" thisvcirc= vcirc(pot,rE,t=t,use_physical=False) return thisvcirc**2./2.+_evaluatePotentials(pot,rE,0.,t=t)-E def _rEFindStart(rE,E,pot,t=0.,lower=False): """find a starting interval for rE""" rtry= 2.*rE while (2.*lower-1.)*_rEfunc(rtry,E,pot,t=t) > 0.: if lower: rtry/= 2. else: rtry*= 2. return rtry @physical_conversion('action',pop=True) def LcE(Pot,E,t=0.): """ NAME: LcE PURPOSE: calculate the angular momentum of a circular orbit with energy E INPUT: Pot - Potential instance or list thereof E - Energy (can be Quantity) t - time (optional; can be Quantity) OUTPUT: Lc(E) HISTORY: 2022-04-06 - Written - Bovy (UofT) """ thisrE= rE(Pot,E,t=t,use_physical=False) return thisrE*vcirc(Pot,thisrE,use_physical=False) @physical_conversion('position',pop=True) def lindbladR(Pot,OmegaP,m=2,t=0.,**kwargs): """ NAME: lindbladR PURPOSE: calculate the radius of a Lindblad resonance INPUT: Pot - Potential instance or list of such instances OmegaP - pattern speed (can be Quantity) m= order of the resonance (as in m(O-Op)=kappa (negative m for outer) use m='corotation' for corotation +scipy.optimize.brentq xtol,rtol,maxiter kwargs t - time (optional; can be Quantity) OUTPUT: radius of Linblad resonance, None if there is no resonance HISTORY: 2011-10-09 - Written - Bovy (IAS) """ Pot= flatten(Pot) OmegaP= conversion.parse_frequency(OmegaP,**conversion.get_physical(Pot)) if isinstance(m,str): if 'corot' in m.lower(): corotation= True else: raise OSError("'m' input not recognized, should be an integer or 'corotation'") else: corotation= False if corotation: try: out= optimize.brentq(_corotationR_eq,0.0000001,1000., args=(Pot,OmegaP,t),**kwargs) except ValueError: try: # Sometimes 0.0000001 is numerically too small to start... out= optimize.brentq(_corotationR_eq,0.01,1000., args=(Pot,OmegaP,t),**kwargs) except ValueError: return None except RuntimeError: #pragma: no cover raise return out else: try: out= optimize.brentq(_lindbladR_eq,0.0000001,1000., args=(Pot,OmegaP,m,t),**kwargs) except ValueError: return None except RuntimeError: #pragma: no cover raise return out def _corotationR_eq(R,Pot,OmegaP,t=0.): return omegac(Pot,R,t=t,use_physical=False)-OmegaP def _lindbladR_eq(R,Pot,OmegaP,m,t=0.): return m*(omegac(Pot,R,t=t,use_physical=False)-OmegaP)\ -epifreq(Pot,R,t=t,use_physical=False) @potential_physical_input @physical_conversion('frequency',pop=True) def omegac(Pot,R,t=0.): """ NAME: omegac PURPOSE: calculate the circular angular speed velocity at R in potential Pot INPUT: Pot - Potential instance or list of such instances R - Galactocentric radius (can be Quantity) t - time (optional; can be Quantity) OUTPUT: circular angular speed HISTORY: 2011-10-09 - Written - Bovy (IAS) """ from ..potential import evaluateplanarRforces try: return numpy.sqrt(-evaluateplanarRforces(Pot,R,t=t,use_physical=False)/R) except PotentialError: from ..potential import RZToplanarPotential Pot= RZToplanarPotential(Pot) return numpy.sqrt(-evaluateplanarRforces(Pot,R,t=t,use_physical=False)/R) def nemo_accname(Pot): """ NAME: nemo_accname PURPOSE: return the accname potential name for use of this potential or list of potentials with NEMO INPUT: Pot - Potential instance or list of such instances OUTPUT: Acceleration name in the correct format to give to accname= HISTORY: 2014-12-18 - Written - Bovy (IAS) """ Pot= flatten(Pot) if isinstance(Pot,list): out= '' for ii,pot in enumerate(Pot): if ii > 0: out+= '+' out+= pot.nemo_accname() return out elif isinstance(Pot,Potential): return Pot.nemo_accname() else: #pragma: no cover raise PotentialError("Input to 'nemo_accname' is neither a Potential-instance or a list of such instances") def nemo_accpars(Pot,vo,ro): """ NAME: nemo_accpars PURPOSE: return the accpars potential parameters for use of this potential or list of potentials with NEMO INPUT: Pot - Potential instance or list of such instances vo - velocity unit in km/s ro - length unit in kpc OUTPUT: accpars string in the correct format to give to accpars HISTORY: 2014-12-18 - Written - Bovy (IAS) """ Pot= flatten(Pot) if isinstance(Pot,list): out= '' for ii,pot in enumerate(Pot): if ii > 0: out+= '#' out+= pot.nemo_accpars(vo,ro) return out elif isinstance(Pot,Potential): return Pot.nemo_accpars(vo,ro) else: #pragma: no cover raise PotentialError("Input to 'nemo_accpars' is neither a Potential-instance or a list of such instances") def to_amuse(Pot,t=0.,tgalpy=0.,reverse=False,ro=None,vo=None): # pragma: no cover """ NAME: to_amuse PURPOSE: Return an AMUSE representation of a galpy Potential or list of Potentials INPUT: Pot - Potential instance or list of such instances t= (0.) Initial time in AMUSE (can be in internal galpy units or AMUSE units) tgalpy= (0.) Initial time in galpy (can be in internal galpy units or AMUSE units); because AMUSE initial times have to be positive, this is useful to set if the initial time in galpy is negative reverse= (False) set whether the galpy potential evolves forwards or backwards in time (default: False); because AMUSE can only integrate forward in time, this is useful to integrate backward in time in AMUSE ro= (default taken from Pot) length unit in kpc vo= (default taken from Pot) velocity unit in km/s OUTPUT: AMUSE representation of Pot HISTORY: 2019-08-04 - Written - Bovy (UofT) 2019-08-12 - Implemented actual function - Webb (UofT) """ try: from . import amuse except ImportError: raise ImportError("To obtain an AMUSE representation of a galpy potential, you need to have AMUSE installed") Pot= flatten(Pot) if ro is None or vo is None: physical_dict= get_physical(Pot) if ro is None: ro= physical_dict.get('ro') if vo is None: vo= physical_dict.get('vo') return amuse.galpy_profile(Pot,t=t,tgalpy=tgalpy,ro=ro,vo=vo) def turn_physical_off(Pot): """ NAME: turn_physical_off PURPOSE: turn off automatic returning of outputs in physical units INPUT: (none) OUTPUT: (none) HISTORY: 2016-01-30 - Written - Bovy (UofT) """ if isinstance(Pot,list): for pot in Pot: turn_physical_off(pot) else: Pot.turn_physical_off() return None def turn_physical_on(Pot,ro=None,vo=None): """ NAME: turn_physical_on PURPOSE: turn on automatic returning of outputs in physical units INPUT: ro= reference distance (kpc; can be Quantity) vo= reference velocity (km/s; can be Quantity) OUTPUT: (none) HISTORY: 2016-01-30 - Written - Bovy (UofT) """ if isinstance(Pot,list): for pot in Pot: turn_physical_on(pot,ro=ro,vo=vo) else: Pot.turn_physical_on(ro=ro,vo=vo) return None def _flatten_list(L): for item in L: try: yield from _flatten_list(item) except TypeError: yield item def flatten(Pot): """ NAME: flatten PURPOSE: flatten a possibly nested list of Potential instances into a flat list INPUT: Pot - list (possibly nested) of Potential instances OUTPUT: Flattened list of Potential instances HISTORY: 2018-03-14 - Written - Bovy (UofT) """ if isinstance(Pot, Potential): return Pot elif isinstance(Pot, list): return list(_flatten_list(Pot)) else: return Pot def _check_c(Pot,dxdv=False,dens=False): """ NAME: _check_c PURPOSE: check whether a potential or list thereof has a C implementation INPUT: Pot - Potential instance or list of such instances dxdv= (False) check whether the potential has dxdv implementation dens= (False) check whether the potential has its density implemented in C OUTPUT: True if a C implementation exists, False otherwise HISTORY: 2014-02-17 - Written - Bovy (IAS) 2017-07-01 - Generalized to dxdv, added general support for WrapperPotentials, and added support for planarPotentials """ Pot= flatten(Pot) from ..potential import linearPotential, planarPotential if dxdv: hasC_attr= 'hasC_dxdv' elif dens: hasC_attr= 'hasC_dens' else: hasC_attr= 'hasC' from .WrapperPotential import parentWrapperPotential if isinstance(Pot,list): return numpy.all(numpy.array([_check_c(p,dxdv=dxdv,dens=dens) for p in Pot], dtype='bool')) elif isinstance(Pot,parentWrapperPotential): return bool(Pot.__dict__[hasC_attr]*_check_c(Pot._pot)) elif isinstance(Pot,Force) or isinstance(Pot,planarPotential) \ or isinstance(Pot,linearPotential): return Pot.__dict__[hasC_attr] def _dim(Pot): """ NAME: _dim PURPOSE: Determine the dimensionality of this potential INPUT: Pot - Potential instance or list of such instances OUTPUT: Minimum of the dimensionality of all potentials if list; otherwise Pot.dim HISTORY: 2016-04-19 - Written - Bovy (UofT) """ from ..potential import linearPotential, planarPotential if isinstance(Pot,list): return numpy.amin(numpy.array([_dim(p) for p in Pot],dtype='int')) elif isinstance(Pot,(Potential,planarPotential,linearPotential, DissipativeForce)): return Pot.dim def _isNonAxi(Pot): """ NAME: _isNonAxi PURPOSE: Determine whether this potential is non-axisymmetric INPUT: Pot - Potential instance or list of such instances OUTPUT: True or False depending on whether the potential is non-axisymmetric (note that some potentials might return True, even though for some parameter values they are axisymmetric) HISTORY: 2016-06-16 - Written - Bovy (UofT) """ isList= isinstance(Pot,list) if isList: isAxis= [not _isNonAxi(p) for p in Pot] nonAxi= not numpy.prod(numpy.array(isAxis)) else: nonAxi= Pot.isNonAxi return nonAxi def kms_to_kpcGyrDecorator(func): """Decorator to convert velocities from km/s to kpc/Gyr""" @wraps(func) def kms_to_kpcGyr_wrapper(*args,**kwargs): return func(args[0],velocity_in_kpcGyr(args[1],1.),args[2],**kwargs) return kms_to_kpcGyr_wrapper @potential_physical_input @physical_conversion('position',pop=True) def rtide(Pot,R,z,phi=0.,t=0.,M=None): """ NAME: rtide PURPOSE: Calculate the tidal radius for object of mass M assuming a circular orbit as .. math:: r_t^3 = \\frac{GM_s}{\\Omega^2-\\mathrm{d}^2\\Phi/\\mathrm{d}r^2} where :math:`M_s` is the cluster mass, :math:`\\Omega` is the circular frequency, and :math:`\\Phi` is the gravitational potential. For non-spherical potentials, we evaluate :math:`\\Omega^2 = (1/r)(\\mathrm{d}\\Phi/\\mathrm{d}r)` and evaluate the derivatives at the given position of the cluster. INPUT: Pot - Potential instance or list of such instances R - Galactocentric radius (can be Quantity) z - height (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) M - (default = None) Mass of object (can be Quantity) OUTPUT: Tidal Radius HISTORY: 2018-03-21 - Written - Webb (UofT) """ Pot= flatten(Pot) if M is None: #Make sure an object mass is given raise PotentialError("Mass parameter M= needs to be set to compute tidal radius") r= numpy.sqrt(R**2.+z**2.) omegac2=-evaluaterforces(Pot,R,z,phi=phi,t=t,use_physical=False)/r d2phidr2= evaluater2derivs(Pot,R,z,phi=phi,t=t,use_physical=False) return (M/(omegac2-d2phidr2))**(1./3.) @potential_physical_input @physical_conversion('forcederivative',pop=True) def ttensor(Pot,R,z,phi=0.,t=0.,eigenval=False): """ NAME: ttensor PURPOSE: Calculate the tidal tensor Tij=-d(Psi)(dxidxj) INPUT: Pot - Potential instance or list of such instances R - Galactocentric radius (can be Quantity) z - height (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) eigenval - return eigenvalues if true (optional; boolean) OUTPUT: Tidal Tensor HISTORY: 2018-03-21 - Written - Webb (UofT) """ Pot= flatten(Pot) if _isNonAxi(Pot): raise PotentialError("Tidal tensor calculation is currently only implemented for axisymmetric potentials") #Evaluate forces, angles and derivatives Rderiv= -evaluateRforces(Pot,R,z,phi=phi,t=t,use_physical=False) phideriv= -evaluatephitorques(Pot,R,z,phi=phi,t=t,use_physical=False) R2deriv= evaluateR2derivs(Pot,R,z,phi=phi,t=t,use_physical=False) z2deriv= evaluatez2derivs(Pot,R,z,phi=phi,t=t,use_physical=False) phi2deriv= evaluatephi2derivs(Pot,R,z,phi=phi,t=t,use_physical=False) Rzderiv= evaluateRzderivs(Pot,R,z,phi=phi,t=t,use_physical=False) Rphideriv= evaluateRphiderivs(Pot,R,z,phi=phi,t=t,use_physical=False) #Temporarily set zphideriv to zero until zphideriv is added to Class zphideriv=0.0 cosphi=numpy.cos(phi) sinphi=numpy.sin(phi) cos2phi=cosphi**2.0 sin2phi=sinphi**2.0 R2=R**2.0 R3=R**3.0 # Tidal tensor txx= R2deriv*cos2phi-Rphideriv*2.*cosphi*sinphi/R+Rderiv*sin2phi/R\ +phi2deriv*sin2phi/R2+phideriv*2.*cosphi*sinphi/R2 tyx= R2deriv*sinphi*cosphi+Rphideriv*(cos2phi-sin2phi)/R\ -Rderiv*sinphi*cosphi/R-phi2deriv*sinphi*cosphi/R2+phideriv*(sin2phi-cos2phi)/R2 tzx= Rzderiv*cosphi-zphideriv*sinphi/R tyy= R2deriv*sin2phi+Rphideriv*2.*cosphi*sinphi/R+Rderiv*cos2phi/R\ +phi2deriv*cos2phi/R2-phideriv*2.*sinphi*cosphi/R2 txy=tyx tzy= Rzderiv*sinphi+zphideriv*cosphi/R txz= tzx tyz= tzy tzz=z2deriv tij= -numpy.array([[txx,txy,txz],[tyx,tyy,tyz],[tzx,tzy,tzz]]) if eigenval: return numpy.linalg.eigvals(tij) else: return tij @physical_conversion('position',pop=True) def zvc(Pot,R,E,Lz,phi=0.,t=0.): """ NAME: zvc PURPOSE: Calculate the zero-velocity curve: z such that Phi(R,z) + Lz/[2R^2] = E (assumes that F_z(R,z) = negative at positive z such that there is a single solution) INPUT: Pot - Potential instance or list of such instances R - Galactocentric radius (can be Quantity) E - Energy (can be Quantity) Lz - Angular momentum (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) OUTPUT: z such that Phi(R,z) + Lz/[2R^2] = E HISTORY: 2020-08-20 - Written - Bovy (UofT) """ Pot= flatten(Pot) R= conversion.parse_length(R,**get_physical(Pot)) E= conversion.parse_energy(E,**get_physical(Pot)) Lz= conversion.parse_angmom(Lz,**get_physical(Pot)) Lz2over2R2= Lz**2./2./R**2. # Check z=0 and whether a solution exists if numpy.fabs(_evaluatePotentials(Pot,R,0.,phi=phi,t=t)+Lz2over2R2-E) < 1e-8: return 0. elif _evaluatePotentials(Pot,R,0.,phi=phi,t=t)+Lz2over2R2 > E: return numpy.nan # s.t. this does not get plotted # Find starting value zstart= 1. zmax= 1000. while E-_evaluatePotentials(Pot,R,zstart,phi=phi,t=t)-Lz2over2R2 > 0. \ and zstart < zmax: zstart*= 2. try: out= optimize.brentq(\ lambda z: _evaluatePotentials(Pot,R,z,phi=phi,t=t)+Lz2over2R2-E, 0.,zstart) except ValueError: raise ValueError('No solution for the zero-velocity curve found for this combination of parameters') return out @physical_conversion('position',pop=True) def zvc_range(Pot,E,Lz,phi=0.,t=0.): """ NAME: zvc_range PURPOSE: Calculate the minimum and maximum radius for which the zero-velocity curve exists for this energy and angular momentum (R such that Phi(R,0) + Lz/[2R^2] = E) INPUT: Pot - Potential instance or list of such instances E - Energy (can be Quantity) Lz - Angular momentum (can be Quantity) phi - azimuth (optional; can be Quantity) t - time (optional; can be Quantity) OUTPUT: Solutions R such that Phi(R,0) + Lz/[2R^2] = E HISTORY: 2020-08-20 - Written - Bovy (UofT) """ Pot= flatten(Pot) E= conversion.parse_energy(E,**get_physical(Pot)) Lz= conversion.parse_angmom(Lz,**get_physical(Pot)) Lz2over2= Lz**2./2. # Check whether a solution exists RLz= rl(Pot,Lz,t=t,use_physical=False) Rstart= RLz if _evaluatePotentials(Pot,Rstart,0.,phi=phi,t=t)+Lz2over2/Rstart**2. > E: return numpy.array([numpy.nan,numpy.nan]) # Find starting value for Rmin Rstartmin= 1e-8 while _evaluatePotentials(Pot,Rstart,0,phi=phi,t=t)\ +Lz2over2/Rstart**2. < E and Rstart > Rstartmin: Rstart/= 2. Rmin= optimize.brentq(\ lambda R: _evaluatePotentials(Pot,R,0,phi=phi,t=t) +Lz2over2/R**2.-E,Rstart,RLz) # Find starting value for Rmax Rstart= RLz Rstartmax= 1000. while _evaluatePotentials(Pot,Rstart,0,phi=phi,t=t)\ +Lz2over2/Rstart**2. < E and Rstart < Rstartmax: Rstart*= 2. Rmax= optimize.brentq(\ lambda R: _evaluatePotentials(Pot,R,0,phi=phi,t=t) +Lz2over2/R**2.-E,RLz,Rstart) return numpy.array([Rmin,Rmax]) @physical_conversion('position',pop=True) def rhalf(Pot,t=0.,INF=numpy.inf): """ NAME: rhalf PURPOSE: calculate the half-mass radius, the radius of the spherical shell that contains half the total mass INPUT: Pot - Potential instance or list thereof t= (0.) time (optional; can be Quantity) INF= (numpy.inf) radius at which the total mass is calculated (internal units, just set this to something very large) OUTPUT: half-mass radius HISTORY: 2021-03-18 - Written - Bovy (UofT) """ Pot= flatten(Pot) tot_mass= mass(Pot,INF,t=t) #Find interval rhi= _rhalfFindStart(1.,Pot,tot_mass,t=t) rlo= _rhalfFindStart(1.,Pot,tot_mass,t=t,lower=True) return optimize.brentq(_rhalffunc,rlo,rhi, args=(Pot,tot_mass,t), maxiter=200,disp=False) def _rhalffunc(rh,pot,tot_mass,t=0.): return mass(pot,rh,t=t)/tot_mass-0.5 def _rhalfFindStart(rh,pot,tot_mass,t=0.,lower=False): """find a starting interval for rhalf""" rtry= 2.*rh while (2.*lower-1.)*_rhalffunc(rtry,pot,tot_mass,t=t) > 0.: if lower: rtry/= 2. else: rtry*= 2. return rtry @potential_physical_input @physical_conversion('time',pop=True) def tdyn(Pot,R,t=0.): """ NAME: tdyn PURPOSE: calculate the dynamical time from tdyn^2 = 3pi/[G] INPUT: Pot - Potential instance or list thereof R - Galactocentric radius (can be Quantity) t= (0.) time (optional; can be Quantity) OUTPUT: Dynamical time HISTORY: 2021-03-18 - Written - Bovy (UofT) """ return 2.*numpy.pi*R*numpy.sqrt(R/mass(Pot,R,use_physical=False)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/PowerSphericalPotential.py0000644000175100001710000002336014327773303022545 0ustar00runnerdocker############################################################################### # PowerSphericalPotential.py: General class for potentials derived from # densities with two power-laws # # amp # rho(r)= --------- # r^\alpha ############################################################################### import numpy from scipy import special from ..util import conversion from .Potential import Potential class PowerSphericalPotential(Potential): """Class that implements spherical potentials that are derived from power-law density models .. math:: \\rho(r) = \\frac{\\mathrm{amp}}{r_1^3}\\,\\left(\\frac{r_1}{r}\\right)^{\\alpha} """ def __init__(self,amp=1.,alpha=1.,normalize=False,r1=1., ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a power-law-density potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass alpha - power-law exponent r1= (1.) reference radius for amplitude (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2010-07-10 - Written - Bovy (NYU) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') r1= conversion.parse_length(r1,ro=self._ro) self.alpha= alpha # Back to old definition if self.alpha != 3.: self._amp*= r1**(self.alpha-3.)*4.*numpy.pi/(3.-self.alpha) if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): self.normalize(normalize) self.hasC= True self.hasC_dxdv= True self.hasC_dens= True def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2010-07-10 - Started - Bovy (NYU) """ r2= R**2.+z**2. if self.alpha == 2.: return numpy.log(r2)/2. elif isinstance(r2,(float,int)) and r2 == 0 and self.alpha > 2: return -numpy.inf else: out= -r2**(1.-self.alpha/2.)/(self.alpha-2.) if isinstance(r2,numpy.ndarray) and self.alpha > 2: out[r2 == 0]= -numpy.inf return out def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2010-07-10 - Written - Bovy (NYU) """ return -R/(R**2.+z**2.)**(self.alpha/2.) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2010-07-10 - Written - Bovy (NYU) """ return -z/(R**2.+z**2.)**(self.alpha/2.) def _rforce_jax(self,r): """ NAME: _rforce_jax PURPOSE: evaluate the spherical radial force for this potential using JAX INPUT: r - Galactocentric spherical radius OUTPUT: the radial force HISTORY: 2021-02-14 - Written - Bovy (UofT) """ # No need for actual JAX! return -self._amp/r**(self.alpha-1.) def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _Rderiv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2011-10-09 - Written - Bovy (NYU) """ return 1./(R**2.+z**2.)**(self.alpha/2.)\ -self.alpha*R**2./(R**2.+z**2.)**(self.alpha/2.+1.) def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second vertical derivative HISTORY: 2012-07-26 - Written - Bovy (IAS@MPIA) """ return self._R2deriv(z,R) #Spherical potential def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2phi/dR/dz HISTORY: 2013-08-28 - Written - Bovy (IAs) """ return -self.alpha*R*z*(R**2.+z**2.)**(-1.-self.alpha/2.) def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2013-01-09 - Written - Bovy (IAS) """ r= numpy.sqrt(R**2.+z**2.) return (3.-self.alpha)/4./numpy.pi/r**self.alpha def _ddensdr(self,r,t=0.): """ NAME: _ddensdr PURPOSE: evaluate the radial density derivative for this potential INPUT: r - spherical radius t= time OUTPUT: the density derivative HISTORY: 2021-02-25 - Written - Bovy (UofT) """ return -self._amp\ *self.alpha*(3.-self.alpha)/4./numpy.pi/r**(self.alpha+1.) def _d2densdr2(self,r,t=0.): """ NAME: _d2densdr2 PURPOSE: evaluate the second radial density derivative for this potential INPUT: r - spherical radius t= time OUTPUT: the 2nd density derivative HISTORY: 2021-02-25 - Written - Bovy (UofT) """ return self._amp*(self.alpha+1.)*self.alpha\ *(3.-self.alpha)/4./numpy.pi/r**(self.alpha+2.) def _ddenstwobetadr(self,r,beta=0): """ NAME: _ddenstwobetadr PURPOSE: evaluate the radial density derivative x r^(2beta) for this potential INPUT: r - spherical radius beta= (0) OUTPUT: d (rho x r^{2beta} ) / d r HISTORY: 2021-02-14 - Written - Bovy (UofT) """ return -self._amp*(self.alpha-2.*beta)\ *(3.-self.alpha)/4./numpy.pi/r**(self.alpha+1.-2.*beta) def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the surface density HISTORY: 2018-08-19 - Written - Bovy (UofT) """ return (3.-self.alpha)/2./numpy.pi*z*R**-self.alpha\ *special.hyp2f1(0.5,self.alpha/2.,1.5,-(z/R)**2) class KeplerPotential(PowerSphericalPotential): """Class that implements the Kepler (point mass) potential .. math:: \\Phi(r) = -\\frac{\\mathrm{amp}}{r} with :math:`\\mathrm{amp} = GM` the mass. """ def __init__(self,amp=1.,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a Kepler, point-mass potential INPUT: amp - amplitude to be applied to the potential, the mass of the point mass (default: 1); can be a Quantity with units of mass density or Gxmass density alpha - inner power normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2010-07-10 - Written - Bovy (NYU) """ PowerSphericalPotential.__init__(self,amp=amp,normalize=normalize, alpha=3.,ro=ro,vo=vo) def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2014-07-02 - Written - Bovy (IAS) """ return 1. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/PowerSphericalPotentialwCutoff.py0000644000175100001710000002440014327773303024077 0ustar00runnerdocker############################################################################### # PowerSphericalPotentialwCutoff.py: spherical power-law potential w/ cutoff # # amp # rho(r)= --------- e^{-(r/rc)^2} # r^\alpha ############################################################################### import numpy from scipy import special from ..util import conversion from ..util._optional_deps import _JAX_LOADED from .Potential import Potential, kms_to_kpcGyrDecorator if _JAX_LOADED: import jax.numpy as jnp import jax.scipy.special as jspecial class PowerSphericalPotentialwCutoff(Potential): """Class that implements spherical potentials that are derived from power-law density models .. math:: \\rho(r) = \\mathrm{amp}\\,\\left(\\frac{r_1}{r}\\right)^\\alpha\\,\\exp\\left(-(r/rc)^2\\right) """ def __init__(self,amp=1.,alpha=1.,rc=1.,normalize=False,r1=1., ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a power-law-density potential INPUT: amp= amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass density or Gxmass density alpha= inner power rc= cut-off radius (can be Quantity) r1= (1.) reference radius for amplitude (can be Quantity) normalize= if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2013-06-28 - Written - Bovy (IAS) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='density') r1= conversion.parse_length(r1,ro=self._ro) rc= conversion.parse_length(rc,ro=self._ro) self.alpha= alpha # Back to old definition self._amp*= r1**self.alpha self.rc= rc self._scale= self.rc if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) self.hasC= True self.hasC_dxdv= True self.hasC_dens= True self._nemo_accname= 'PowSphwCut' def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2013-06-28 - Started - Bovy (IAS) """ r= numpy.sqrt(R**2.+z**2.) out= 2.*numpy.pi*self.rc**(3.-self.alpha)*(1/self.rc*special.gamma(1.-self.alpha/2.)*special.gammainc(1.-self.alpha/2.,(r/self.rc)**2.)-special.gamma(1.5-self.alpha/2.)*special.gammainc(1.5-self.alpha/2.,(r/self.rc)**2.)/r) if isinstance(r,(float,int)): if r == 0: return 0. else: return out else: out[r==0]= 0. return out def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2013-06-26 - Written - Bovy (IAS) """ r= numpy.sqrt(R*R+z*z) return -self._mass(r)*R/r**3. def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2013-06-26 - Written - Bovy (IAS) """ r= numpy.sqrt(R*R+z*z) return -self._mass(r)*z/r**3. def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _Rderiv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2013-06-28 - Written - Bovy (IAS) """ r= numpy.sqrt(R*R+z*z) return 4.*numpy.pi*r**(-2.-self.alpha)*numpy.exp(-(r/self.rc)**2.)*R**2.\ +self._mass(r)/r**5.*(z**2.-2.*R**2.) def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second vertical derivative HISTORY: 2013-06-28 - Written - Bovy (IAS) """ r= numpy.sqrt(R*R+z*z) return 4.*numpy.pi*r**(-2.-self.alpha)*numpy.exp(-(r/self.rc)**2.)*z**2.\ +self._mass(r)/r**5.*(R**2.-2.*z**2.) def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: d2phi/dR/dz HISTORY: 2013-08-28 - Written - Bovy (IAS) """ r= numpy.sqrt(R*R+z*z) return R*z*(4.*numpy.pi*r**(-2.-self.alpha)*numpy.exp(-(r/self.rc)**2.) -3.*self._mass(r)/r**5.) def _rforce_jax(self,r): """ NAME: _rforce_jax PURPOSE: evaluate the spherical radial force for this potential using JAX; use incomplete gamma implementation rather than hypergeometric, because JAX doesn't have the hypergeometric functions currently INPUT: r - Galactocentric spherical radius OUTPUT: the radial force HISTORY: 2022-05-10 - Written - Bovy (UofT) """ if not _JAX_LOADED: # pragma: no cover raise ImportError("Making use of the _rforce_jax function requires the google/jax library") return -self._amp*2.*numpy.pi*self.rc**(3.-self.alpha)\ *jspecial.gammainc(1.5-0.5*self.alpha,(r/self.rc)**2.)\ *numpy.exp(jspecial.gammaln(1.5-0.5*self.alpha))/r**2 def _ddensdr(self,r,t=0.): """ NAME: _ddensdr PURPOSE: evaluate the radial density derivative for this potential INPUT: r - spherical radius t= time OUTPUT: the density derivative HISTORY: 2021-12-15 - Written - Lane (UofT) """ return -self._amp*r**(-1.-self.alpha)*numpy.exp(-(r/self.rc)**2.)\ *(2.*r**2./self.rc**2.+self.alpha) def _d2densdr2(self,r,t=0.): """ NAME: _d2densdr2 PURPOSE: evaluate the second radial density derivative for this potential INPUT: r - spherical radius t= time OUTPUT: the 2nd density derivative HISTORY: 2021-12-15 - Written - Lane (UofT) """ return self._amp*r**(-2.-self.alpha)*numpy.exp(-(r/self.rc)**2)\ *(self.alpha**2.+self.alpha+4*self.alpha*r**2./self.rc**2.\ -2.*r**2./self.rc**2.+4.*r**4./self.rc**4.) def _ddenstwobetadr(self,r,beta=0): """ NAME: _ddenstwobetadr PURPOSE: evaluate the radial density derivative x r^(2beta) for this potential INPUT: r - spherical radius beta= (0) OUTPUT: d (rho x r^{2beta} ) / d r HISTORY: 2021-03-15 - Written - Lane (UofT) """ if not _JAX_LOADED: # pragma: no cover raise ImportError("Making use of _rforce_jax function requires the google/jax library") return -self._amp*jnp.exp(-(r/self.rc)**2.)/r**(self.alpha-2.*beta)\ *((self.alpha-2.*beta)/r+2.*r/self.rc**2.) def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2013-06-28 - Written - Bovy (IAS) """ r= numpy.sqrt(R**2.+z**2.) return 1./r**self.alpha*numpy.exp(-(r/self.rc)**2.) def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2013-XX-XX - Written - Bovy (IAS) 2021-04-07 - Switched to hypergeometric function equivalent to incomplete gamma function for better behavior at alpha < -3 - Bovy (UofT) """ if z is not None: raise AttributeError # use general implementation return 2.*numpy.pi*R**(3.-self.alpha)/(1.5-self.alpha/2.)\ *special.hyp1f1(1.5-self.alpha/2.,2.5-self.alpha/2., -(R/self.rc)**2.) @kms_to_kpcGyrDecorator def _nemo_accpars(self,vo,ro): """ NAME: _nemo_accpars PURPOSE: return the accpars potential parameters for use of this potential with NEMO INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: accpars string HISTORY: 2014-12-18 - Written - Bovy (IAS) """ ampl= self._amp*vo**2.*ro**(self.alpha-2.) return f"0,{ampl},{self.alpha},{self.rc*ro}" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/PowerTriaxialPotential.py0000644000175100001710000000625214327773303022411 0ustar00runnerdocker############################################################################### # PowerTriaxialPotential: Potential of a triaxial power-law # # amp # rho(x,y,z)= --------- # m^\alpha # # with m^2 = x^2+y^2/b^2+z^2/c^2 # ############################################################################### import numpy from ..util import conversion from .EllipsoidalPotential import EllipsoidalPotential class PowerTriaxialPotential(EllipsoidalPotential): """Class that implements triaxial potentials that are derived from power-law density models (including an elliptical power law) .. math:: \\rho(r) = \\frac{\\mathrm{amp}}{r_1^3}\\,\\left(\\frac{r_1}{m}\\right)^{\\alpha} where :math:`m^2 = x^2+y^2/b^2+z^2/c^2`. """ def __init__(self,amp=1.,alpha=1.,r1=1.,b=1.,c=1., zvec=None,pa=None,glorder=50, normalize=False,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a triaxial power-law potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass alpha - power-law exponent r1= (1.) reference radius for amplitude (can be Quantity) b - y-to-x axis ratio of the density c - z-to-x axis ratio of the density zvec= (None) If set, a unit vector that corresponds to the z axis pa= (None) If set, the position angle of the x axis (rad or Quantity) glorder= (50) if set, compute the relevant force and potential integrals with Gaussian quadrature of this order ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2021-05-07 - Started - Bovy (UofT) """ EllipsoidalPotential.__init__(self,amp=amp,b=b,c=c, zvec=zvec,pa=pa,glorder=glorder, ro=ro,vo=vo,amp_units='mass') r1= conversion.parse_length(r1,ro=self._ro) self.alpha= alpha # Back to old definition if self.alpha != 3.: self._amp*= r1**(self.alpha-3.)*4.*numpy.pi/(3.-self.alpha) # Multiply in constants self._amp*= (3.-self.alpha)/4./numpy.pi if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) self.hasC= not self._glorder is None self.hasC_dxdv= False self.hasC_dens= self.hasC # works if mdens is defined, necessary for hasC return None def _psi(self,m): """\\psi(m) = -\\int_m^\\infty d m^2 \rho(m^2)""" return 2./(2.-self.alpha)*m**(2.-self.alpha) def _mdens(self,m): """Density as a function of m""" return m**-self.alpha def _mdens_deriv(self,m): """Derivative of the density as a function of m""" return -self.alpha*m**-(1.+self.alpha) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/PseudoIsothermalPotential.py0000644000175100001710000001377314327773303023114 0ustar00runnerdocker############################################################################### # PseudoIsothermalPotential.py: class that implements the pseudo-isothermal # halo potential ############################################################################### import numpy from ..util import conversion from .Potential import Potential class PseudoIsothermalPotential(Potential): """Class that implements the pseudo-isothermal potential .. math:: \\rho(r) = \\frac{\\mathrm{amp}}{4\\,\\pi\\, a^3}\\,\\frac{1}{1+(r/a)^2} """ def __init__(self,amp=1.,a=1.,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a pseudo-isothermal potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a - core radius (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2015-12-04 - Started - Bovy (UofT) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) self.hasC= True self.hasC_dxdv= True self.hasC_dens= True self._a= a self._a2= a**2. self._a3= a**3. if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2015-12-04 - Started - Bovy (UofT) """ r2= R**2.+z**2. r= numpy.sqrt(r2) out= (0.5*numpy.log(1+r2/self._a2) +self._a/r*numpy.arctan(r/self._a))/self._a if isinstance(r,(float,int)): if r == 0: return 1./self._a else: return out else: out[r==0]= 1./self._a return out def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2015-12-04 - Started - Bovy (UofT) """ r2= R**2.+z**2. r= numpy.sqrt(r2) return -(1./r-self._a/r2*numpy.arctan(r/self._a))/self._a*R/r def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2015-12-04 - Started - Bovy (UofT) """ r2= R**2.+z**2. r= numpy.sqrt(r2) return -(1./r-self._a/r2*numpy.arctan(r/self._a))/self._a*z/r def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2015-12-04 - Started - Bovy (UofT) """ return 1./(1.+(R**2.+z**2.)/self._a2)/4./numpy.pi/self._a3 def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2011-10-09 - Written - Bovy (IAS) """ r2= R**2.+z**2. r= numpy.sqrt(r2) return (1./r2*(1.-R**2./r2*(3.*self._a2+2.*r2)/(self._a2+r2))\ +self._a/r2/r*(3.*R**2./r2-1.)*numpy.arctan(r/self._a))\ /self._a def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second vertical derivative HISTORY: 2012-07-25 - Written - Bovy (IAS@MPIA) """ r2= R**2.+z**2. r= numpy.sqrt(r2) return (1./r2*(1.-z**2./r2*(3.*self._a2+2.*r2)/(self._a2+r2))\ +self._a/r2/r*(3.*z**2./r2-1.)*numpy.arctan(r/self._a))\ /self._a def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2Phi/dR/dz HISTORY: 2013-08-28 - Written - Bovy (IAS) """ r2= R**2.+z**2. r= numpy.sqrt(r2) return (3.*self._a/r2/r2*numpy.arctan(r/self._a)\ -1./r2/r*((3.*self._a2+2.*r2)/(r2+self._a2)))*R*z/r\ /self._a ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/RazorThinExponentialDiskPotential.py0000644000175100001710000002143614327773303024562 0ustar00runnerdocker############################################################################### # RazorThinExponentialDiskPotential.py: class that implements the razor thin # exponential disk potential # # rho(R,z) = rho_0 e^-R/h_R delta(z) ############################################################################### import numpy from scipy import special from ..util import conversion from .Potential import Potential class RazorThinExponentialDiskPotential(Potential): """Class that implements the razor-thin exponential disk potential .. math:: \\rho(R,z) = \\mathrm{amp}\\,\\exp\\left(-R/h_R\\right)\\,\\delta(z) """ def __init__(self,amp=1.,hr=1./3.,normalize=False, ro=None,vo=None, new=True,glorder=100): """ NAME: __init__ PURPOSE: initialize a razor-thin-exponential disk potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of surface-mass or Gxsurface-mass hr - disk scale-length (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: RazorThinExponentialDiskPotential object HISTORY: 2012-12-27 - Written - Bovy (IAS) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='surfacedensity') hr= conversion.parse_length(hr,ro=self._ro) self._new= new self._glorder= glorder self._hr= hr self._scale= self._hr self._alpha= 1./self._hr self._glx, self._glw= numpy.polynomial.legendre.leggauss(self._glorder) if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: potential at (R,z) HISTORY: 2012-12-26 - Written - Bovy (IAS) """ if self._new: if numpy.fabs(z) < 10.**-6.: y= 0.5*self._alpha*R return -numpy.pi*R*(special.i0(y)*special.k1(y)-special.i1(y)*special.k0(y)) kalphamax= 10. ks= kalphamax*0.5*(self._glx+1.) weights= kalphamax*self._glw sqrtp= numpy.sqrt(z**2.+(ks+R)**2.) sqrtm= numpy.sqrt(z**2.+(ks-R)**2.) evalInt= numpy.arcsin(2.*ks/(sqrtp+sqrtm))*ks*special.k0(self._alpha*ks) return -2.*self._alpha*numpy.sum(weights*evalInt) raise NotImplementedError("Not new=True not implemented for RazorThinExponentialDiskPotential") def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: Rforce PURPOSE: evaluate radial force K_R (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: K_R (R,z) HISTORY: 2012-12-27 - Written - Bovy (IAS) """ if self._new: #if R > 6.: return self._kp(R,z) if numpy.fabs(z) < 10.**-6.: y= 0.5*self._alpha*R return -2.*numpy.pi*y*(special.i0(y)*special.k0(y)-special.i1(y)*special.k1(y)) kalphamax1= R ks1= kalphamax1*0.5*(self._glx+1.) weights1= kalphamax1*self._glw sqrtp= numpy.sqrt(z**2.+(ks1+R)**2.) sqrtm= numpy.sqrt(z**2.+(ks1-R)**2.) evalInt1= ks1**2.*special.k0(ks1*self._alpha)*((ks1+R)/sqrtp-(ks1-R)/sqrtm)/numpy.sqrt(R**2.+z**2.-ks1**2.+sqrtp*sqrtm)/(sqrtp+sqrtm) if R < 10.: kalphamax2= 10. ks2= (kalphamax2-kalphamax1)*0.5*(self._glx+1.)+kalphamax1 weights2= (kalphamax2-kalphamax1)*self._glw sqrtp= numpy.sqrt(z**2.+(ks2+R)**2.) sqrtm= numpy.sqrt(z**2.+(ks2-R)**2.) evalInt2= ks2**2.*special.k0(ks2*self._alpha)*((ks2+R)/sqrtp-(ks2-R)/sqrtm)/numpy.sqrt(R**2.+z**2.-ks2**2.+sqrtp*sqrtm)/(sqrtp+sqrtm) return -2.*numpy.sqrt(2.)*self._alpha*numpy.sum(weights1*evalInt1 +weights2*evalInt2) else: return -2.*numpy.sqrt(2.)*self._alpha*numpy.sum(weights1*evalInt1) raise NotImplementedError("Not new=True not implemented for RazorThinExponentialDiskPotential") def _zforce(self,R,z,phi=0.,t=0.): """ NAME: zforce PURPOSE: evaluate vertical force K_z (R,z) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: K_z (R,z) HISTORY: 2012-12-27 - Written - Bovy (IAS) """ if self._new: #if R > 6.: return self._kp(R,z) if numpy.fabs(z) < 10.**-6.: return 0. kalphamax1= R ks1= kalphamax1*0.5*(self._glx+1.) weights1= kalphamax1*self._glw sqrtp= numpy.sqrt(z**2.+(ks1+R)**2.) sqrtm= numpy.sqrt(z**2.+(ks1-R)**2.) evalInt1= ks1**2.*special.k0(ks1*self._alpha)*(1./sqrtp+1./sqrtm)/numpy.sqrt(R**2.+z**2.-ks1**2.+sqrtp*sqrtm)/(sqrtp+sqrtm) if R < 10.: kalphamax2= 10. ks2= (kalphamax2-kalphamax1)*0.5*(self._glx+1.)+kalphamax1 weights2= (kalphamax2-kalphamax1)*self._glw sqrtp= numpy.sqrt(z**2.+(ks2+R)**2.) sqrtm= numpy.sqrt(z**2.+(ks2-R)**2.) evalInt2= ks2**2.*special.k0(ks2*self._alpha)*(1./sqrtp+1./sqrtm)/numpy.sqrt(R**2.+z**2.-ks2**2.+sqrtp*sqrtm)/(sqrtp+sqrtm) return -z*2.*numpy.sqrt(2.)*self._alpha*numpy.sum(weights1*evalInt1 +weights2*evalInt2) else: return -z*2.*numpy.sqrt(2.)*self._alpha*numpy.sum(weights1*evalInt1) raise NotImplementedError("Not new=True not implemented for RazorThinExponentialDiskPotential") def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: R2deriv PURPOSE: evaluate R2 derivative INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: -d K_R (R,z) d R HISTORY: 2012-12-27 - Written - Bovy (IAS) """ if self._new: if numpy.fabs(z) < 10.**-6.: y= 0.5*self._alpha*R return numpy.pi*self._alpha*(special.i0(y)*special.k0(y)-special.i1(y)*special.k1(y)) \ +numpy.pi/4.*self._alpha**2.*R*(special.i1(y)*(3.*special.k0(y)+special.kn(2,y))-special.k1(y)*(3.*special.i0(y)+special.iv(2,y))) raise AttributeError("'R2deriv' for RazorThinExponentialDisk not implemented for z =/= 0") def _z2deriv(self,R,z,phi=0.,t=0.): #pragma: no cover """ NAME: z2deriv PURPOSE: evaluate z2 derivative INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: -d K_z (R,z) d z HISTORY: 2012-12-27 - Written - Bovy (IAS) """ return numpy.infty def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: Sigma (R,z) HISTORY: 2018-08-19 - Written - Bovy (UofT) """ return numpy.exp(-self._alpha*R) def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R (and z) for this potential; if z=None, integrate to z=inf INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2021-03-04 - Written - Bovy (UofT) """ return 2.*numpy.pi*(1.-numpy.exp(-self._alpha*R)*(1.+self._alpha*R))\ /self._alpha**2. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/RingPotential.py0000644000175100001710000001443114327773303020514 0ustar00runnerdocker############################################################################### # RingPotential.py: The gravitational potential of a thin, circular ring ############################################################################### import numpy from scipy import special from ..util import conversion from .Potential import Potential class RingPotential(Potential): """Class that implements the potential of an infinitesimally-thin, circular ring .. math:: \\rho(R,z) = \\frac{\\mathrm{amp}}{2\\pi\\,R_0}\\,\\delta(R-R_0)\\,\\delta(z) with :math:`\\mathrm{amp} = GM` the mass of the ring. """ def __init__(self,amp=1.,a=0.75,normalize=False,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a circular ring potential INPUT: amp - mass of the ring (default: 1); can be a Quantity with units of mass or Gxmass a= (0.75) radius of the ring (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1.; note that because the force is always positive at r < a, this does not work if a > 1 ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2018-08-04 - Written - Bovy (UofT) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) self.a= a self.a2= self.a**2 self._amp/= 2.*numpy.pi*self.a if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): if self.a > 1.: raise ValueError('RingPotential with normalize= for a > 1 is not supported (because the force is always positive at r=1)') self.normalize(normalize) self.hasC= False self.hasC_dxdv= False def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2018-08-04 - Written - Bovy (UofT) """ # Stable as r -> infty m= 4.*self.a/((numpy.sqrt(R)+self.a/numpy.sqrt(R))**2+z**2/R) return -4.*self.a/numpy.sqrt((R+self.a)**2+z**2)*special.ellipk(m) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2018-08-04 - Written - Bovy (UofT) """ m= 4.*R*self.a/((R+self.a)**2+z**2) return -2.*self.a/R/numpy.sqrt((R+self.a)**2+z**2)\ *(m*(R**2-self.a2-z**2)/4./(1.-m)/self.a/R*special.ellipe(m) +special.ellipk(m)) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2018-08-04 - Written - Bovy (UofT) """ m= 4.*R*self.a/((R+self.a)**2+z**2) return -4.*z*self.a/(1.-m)*((R+self.a)**2+z**2)**-1.5*special.ellipe(m) def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _Rderiv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2018-08-04 - Written - Bovy (UofT) """ Raz2= (R+self.a)**2+z**2 Raz= numpy.sqrt(Raz2) m= 4.*R*self.a/Raz2 R2ma2mz2o4aR1m= (R**2-self.a2-z**2)/4./self.a/R/(1.-m) return (2*R**2+self.a2+3*R*self.a+z**2)/R/Raz2*self._Rforce(R,z)\ +2.*self.a/R/Raz*(m*(R**2+self.a2+z**2)/4./(1.-m)/self.a/R**2\ *special.ellipe(m)\ +(R2ma2mz2o4aR1m/(1.-m)*special.ellipe(m) +0.5*R2ma2mz2o4aR1m*(special.ellipe(m)-special.ellipk(m)) +0.5*(special.ellipe(m)/(1.-m)-special.ellipk(m))/m)\ *4*self.a*(self.a2+z**2-R**2)/Raz2**2) def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second vertical derivative HISTORY: 2018-08-04 - Written - Bovy (UofT) """ Raz2= (R+self.a)**2+z**2 m= 4.*R*self.a/Raz2 # Explicitly swapped in zforce here, so the z/z can be cancelled # and z=0 is handled properly return -4.*(3.*z**2/Raz2-1. +4.*((1.+m)/(1.-m)-special.ellipk(m)/special.ellipe(m))\ *self.a*R*z**2/Raz2**2/m)\ *self.a/(1.-m)*((R+self.a)**2+z**2)**-1.5*special.ellipe(m) def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2phi/dR/dz HISTORY: 2018-08-04 - Written - Bovy (UofT) """ Raz2= (R+self.a)**2+z**2 m= 4.*R*self.a/Raz2 return (3.*(R+self.a)/Raz2 -2.*((1.+m)/(1.-m)-special.ellipk(m)/special.ellipe(m))\ *self.a*(self.a2+z**2-R**2)/Raz2**2/m)*self._zforce(R,z) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/RotateAndTiltWrapperPotential.py0000644000175100001710000003665514327773303023710 0ustar00runnerdocker############################################################################### # RotateAndTiltWrapperPotential.py: Wrapper to rotate and tilt the z-axis # of a potential ############################################################################### import numpy from ..util import _rotate_to_arbitrary_vector, conversion, coords from .Potential import (_evaluatephitorques, _evaluatePotentials, _evaluateRforces, _evaluatezforces, check_potential_inputs_not_arrays, evaluateDensities, evaluatephi2derivs, evaluatephizderivs, evaluateR2derivs, evaluateRphiderivs, evaluateRzderivs, evaluatez2derivs) from .WrapperPotential import WrapperPotential # Only implement 3D wrapper class RotateAndTiltWrapperPotential(WrapperPotential): """Potential wrapper that allows a potential to be rotated in 3D according to three orientation angles. These angles can either be specified using: * A rotation around the original z-axis (`galaxy_pa`) and the new direction of the z-axis (`zvec`) or * A rotation around the original z-axis (`galaxy_pa`), the `inclination`, and a rotation around the new z axis (`sky_pa`). The second option allows one to specify the inclination and sky position angle (measured from North) in the usual manner in extragalactic observations. A final `offset` option allows one to apply a static offset in Cartesian coordinate space to be applied to the potential following the rotation and tilt.""" def __init__(self,amp=1.,inclination=None,galaxy_pa=None,sky_pa=None, zvec=None,offset=None,pot=None, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a RotateAndTiltWrapper Potential INPUT: amp= (1.) overall amplitude to apply to the potential pot= Potential instance or list thereof for the potential to rotate and tilt Orientation angles as galaxy_pa= rotation angle of the original potential around the original z axis (can be a Quantity) and either 1) zvec= 3D vector specifying the direction of the rotated z axis 2) inclination= usual inclination angle (with the line-of-sight being the z axis) sky_pa= rotation angle around the inclined z axis (usual sky position angle measured from North) offset= optional static offset in Cartesian coordinates (can be a Quantity) OUTPUT: (none) HISTORY: 2021-03-29 - Started - Mackereth (UofT) 2021-04-18 - Added inclination, sky_pa, galaxy_pa setup - Bovy (UofT) 2022-03-14 - added offset kwarg - Mackereth (UofT) """ WrapperPotential.__init__(self,amp=amp,pot=pot,ro=ro,vo=vo, _init=True) inclination= conversion.parse_angle(inclination) sky_pa= conversion.parse_angle(sky_pa) galaxy_pa= conversion.parse_angle(galaxy_pa) zvec, galaxy_pa= self._parse_inclination(inclination,sky_pa, zvec,galaxy_pa) self._offset= conversion.parse_length(offset,ro=self._ro) self._setup_zvec_pa(zvec,galaxy_pa) self._norot = False if (self._rot == numpy.eye(3)).all(): self._norot = True self.hasC= True self.hasC_dxdv= True self.isNonAxi= True def _parse_inclination(self,inclination,sky_pa,zvec,galaxy_pa): if inclination is None: return (zvec,galaxy_pa) if sky_pa is None: sky_pa= 0. zvec_rot= numpy.dot(\ numpy.array([[numpy.sin(sky_pa),numpy.cos(sky_pa),0.], [-numpy.cos(sky_pa),numpy.sin(sky_pa),0.], [0.,0.,1]]), numpy.array([[1.,0.,0.], [0.,-numpy.cos(inclination),-numpy.sin(inclination)], [0.,-numpy.sin(inclination),numpy.cos(inclination)]])) zvec= numpy.dot(zvec_rot,numpy.array([0.,0.,1.])) int_rot= _rotate_to_arbitrary_vector(numpy.array([[0.,0.,1.]]), zvec,inv=False)[0] pa= numpy.dot(int_rot,numpy.dot(zvec_rot,[1.,0.,0.])) return (zvec,galaxy_pa+numpy.arctan2(pa[1],pa[0])) def _setup_zvec_pa(self,zvec,pa): if not pa is None: pa_rot= numpy.array([[numpy.cos(pa),numpy.sin(pa),0.], [-numpy.sin(pa),numpy.cos(pa),0.], [0.,0.,1.]]) else: pa_rot = numpy.eye(3) if not zvec is None: if not isinstance(zvec,numpy.ndarray): zvec= numpy.array(zvec) zvec/= numpy.sqrt(numpy.sum(zvec**2.)) zvec_rot= _rotate_to_arbitrary_vector(\ numpy.array([[0.,0.,1.]]),zvec,inv=True)[0] else: zvec_rot = numpy.eye(3) self._rot = numpy.dot(pa_rot,zvec_rot) self._inv_rot = numpy.linalg.inv(self._rot) return None def __getattr__(self,attribute): return super()\ .__getattr__(attribute) @check_potential_inputs_not_arrays def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2021-04-18 - Written - Bovy (UofT) """ x,y,z= coords.cyl_to_rect(R,phi,z) if numpy.isinf(R): y= 0. if self._norot: xyzp = numpy.array([x,y,z]) else: xyzp = numpy.dot(self._rot,numpy.array([x,y,z])) if self._offset: xyzp+= self._offset Rp,phip,zp = coords.rect_to_cyl(xyzp[0],xyzp[1],xyzp[2]) return _evaluatePotentials(self._pot,Rp,zp,phi=phip,t=t) @check_potential_inputs_not_arrays def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2021-04-18 - Written - Bovy (UofT) """ Fxyz= self._force_xyz(R,z,phi=phi,t=t) return numpy.cos(phi)*Fxyz[0]+numpy.sin(phi)*Fxyz[1] @check_potential_inputs_not_arrays def _phitorque(self,R,z,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque (torque) for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the azimuthal torque (torque) HISTORY: 2021-04-18 - Written - Bovy (UofT) """ Fxyz= self._force_xyz(R,z,phi=phi,t=t) return R*(-numpy.sin(phi)*Fxyz[0] + numpy.cos(phi)*Fxyz[1]) @check_potential_inputs_not_arrays def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2021-04-18 - Written - Bovy (UofT) """ return self._force_xyz(R,z,phi=phi,t=t)[2] def _force_xyz(self,R,z,phi=0.,t=0.): """Get the rectangular forces in the transformed frame""" x,y,z= coords.cyl_to_rect(R,phi,z) if self._norot: xyzp = numpy.array([x,y,z]) else: xyzp = numpy.dot(self._rot,numpy.array([x,y,z])) if self._offset: xyzp+= self._offset Rp,phip,zp =coords.rect_to_cyl(xyzp[0],xyzp[1],xyzp[2]) Rforcep= _evaluateRforces(self._pot,Rp,zp,phi=phip,t=t) phitorquep= _evaluatephitorques(self._pot,Rp,zp,phi=phip,t=t) zforcep= _evaluatezforces(self._pot,Rp,zp,phi=phip,t=t) xforcep= numpy.cos(phip)*Rforcep-numpy.sin(phip)*phitorquep/Rp yforcep= numpy.sin(phip)*Rforcep+numpy.cos(phip)*phitorquep/Rp return numpy.dot(self._inv_rot, numpy.array([xforcep,yforcep,zforcep])) @check_potential_inputs_not_arrays def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2021-04-19 - Written - Bovy (UofT) """ phi2= self._2ndderiv_xyz(R,z,phi=phi,t=t) return numpy.cos(phi)**2.*phi2[0,0]+numpy.sin(phi)**2.*phi2[1,1]\ +2.*numpy.cos(phi)*numpy.sin(phi)*phi2[0,1] @check_potential_inputs_not_arrays def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed radial, vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed radial, vertical derivative HISTORY: 2021-04-19 - Written - Bovy (UofT) """ phi2= self._2ndderiv_xyz(R,z,phi=phi,t=t) return numpy.cos(phi)*phi2[0,2]+numpy.sin(phi)*phi2[1,2] @check_potential_inputs_not_arrays def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second vertical derivative HISTORY: 2021-04-19 - Written - Bovy (UofT) """ return self._2ndderiv_xyz(R,z,phi=phi,t=t)[2,2] @check_potential_inputs_not_arrays def _phi2deriv(self,R,z,phi=0.,t=0.): """ NAME: _phi2deriv PURPOSE: evaluate the second azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second azimuthal derivative HISTORY: 2021-04-19 - Written - Bovy (UofT) """ Fxyz= self._force_xyz(R,z,phi=phi,t=t) phi2= self._2ndderiv_xyz(R,z,phi=phi,t=t) return R**2.*(numpy.sin(phi)**2.*phi2[0,0] +numpy.cos(phi)**2.*phi2[1,1]\ -2.*numpy.cos(phi)*numpy.sin(phi)*phi2[0,1])\ +R*(numpy.cos(phi)*Fxyz[0]+numpy.sin(phi)*Fxyz[1]) @check_potential_inputs_not_arrays def _Rphideriv(self,R,z,phi=0.,t=0.): """ NAME: _Rphideriv PURPOSE: evaluate the mixed radial, azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed radial, azimuthal derivative HISTORY: 2021-04-19 - Written - Bovy (UofT) """ Fxyz= self._force_xyz(R,z,phi=phi,t=t) phi2= self._2ndderiv_xyz(R,z,phi=phi,t=t) return R*numpy.cos(phi)*numpy.sin(phi)*\ (phi2[1,1]-phi2[0,0])+R*numpy.cos(2.*phi)*phi2[0,1]\ +numpy.sin(phi)*Fxyz[0]-numpy.cos(phi)*Fxyz[1] @check_potential_inputs_not_arrays def _phizderiv(self,R,z,phi=0.,t=0.): """ NAME: _phizderiv PURPOSE: evaluate the mixed azimuthal, vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed azimuthal, vertical derivative HISTORY: 2021-04-30 - Written - Bovy (UofT) """ phi2= self._2ndderiv_xyz(R,z,phi=phi,t=t) return R*(numpy.cos(phi)*phi2[1,2]-numpy.sin(phi)*phi2[0,2]) def _2ndderiv_xyz(self,R,z,phi=0.,t=0.): """Get the rectangular forces in the transformed frame""" x,y,z= coords.cyl_to_rect(R,phi,z) if self._norot: xyzp = numpy.array([x,y,z]) else: xyzp = numpy.dot(self._rot,numpy.array([x,y,z])) if self._offset: xyzp+= self._offset Rp,phip,zp =coords.rect_to_cyl(xyzp[0],xyzp[1],xyzp[2]) Rforcep= _evaluateRforces(self._pot,Rp,zp,phi=phip,t=t) phitorquep= _evaluatephitorques(self._pot,Rp,zp,phi=phip,t=t) R2derivp= evaluateR2derivs(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) phi2derivp= evaluatephi2derivs(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) z2derivp= evaluatez2derivs(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) Rzderivp= evaluateRzderivs(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) Rphiderivp= evaluateRphiderivs(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) phizderivp= evaluatephizderivs(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) cp, sp= numpy.cos(phip), numpy.sin(phip) cp2, sp2, cpsp= cp**2., sp**2., cp*sp Rp2= Rp*Rp x2derivp= R2derivp*cp2-2.*Rphiderivp*cpsp/Rp+phi2derivp*sp2/Rp2\ -Rforcep*sp2/Rp-2.*phitorquep*cpsp/Rp2 y2derivp= R2derivp*sp2+2.*Rphiderivp*cpsp/Rp+phi2derivp*cp2/Rp2\ -Rforcep*cp2/Rp+2.*phitorquep*cpsp/Rp2 xyderivp= R2derivp*cpsp+Rphiderivp*(cp2-sp2)/Rp-phi2derivp*cpsp/Rp2\ +Rforcep*cpsp/Rp+phitorquep*(cp2-sp2)/Rp2 xzderivp= Rzderivp*cp-phizderivp*sp/Rp yzderivp= Rzderivp*sp+phizderivp*cp/Rp return numpy.dot(self._inv_rot,numpy.dot(\ numpy.array([[x2derivp,xyderivp,xzderivp], [xyderivp,y2derivp,yzderivp], [xzderivp,yzderivp,z2derivp]]), self._inv_rot.T)) @check_potential_inputs_not_arrays def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2021-04-18 - Written - Bovy (UofT) """ x,y,z= coords.cyl_to_rect(R,phi,z) if numpy.isinf(R): y= 0. if self._norot: xyzp = numpy.array([x,y,z]) else: xyzp = numpy.dot(self._rot,numpy.array([x,y,z])) if self._offset: xyzp+= self._offset Rp,phip,zp = coords.rect_to_cyl(xyzp[0],xyzp[1],xyzp[2]) return evaluateDensities(self._pot,Rp,zp,phi=phip,t=t, use_physical=False) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/SCFPotential.py0000644000175100001710000011656414327773303020242 0ustar00runnerdockerimport hashlib import numpy from numpy.polynomial.legendre import leggauss from scipy import integrate from scipy.special import gamma, gammaln, lpmn from ..util import conversion, coords from ..util._optional_deps import _APY_LOADED from .Potential import Potential if _APY_LOADED: from astropy import units from .NumericalPotentialDerivativesMixin import \ NumericalPotentialDerivativesMixin class SCFPotential(Potential,NumericalPotentialDerivativesMixin): """Class that implements the `Hernquist & Ostriker (1992) `_ Self-Consistent-Field-type potential. Note that we divide the amplitude by 2 such that :math:`Acos = \\delta_{0n}\\delta_{0l}\\delta_{0m}` and :math:`Asin = 0` corresponds to :ref:`Galpy's Hernquist Potential `. .. math:: \\rho(r, \\theta, \\phi) = \\frac{amp}{2}\\sum_{n=0}^{\\infty} \\sum_{l=0}^{\\infty} \\sum_{m=0}^l N_{lm} P_{lm}(\\cos(\\theta)) \\tilde{\\rho}_{nl}(r) \\left(A_{cos, nlm} \\cos(m\\phi) + A_{sin, nlm} \\sin(m\\phi)\\right) where .. math:: \\tilde{\\rho}_{nl}(r) = \\frac{K_{nl}}{\\sqrt{\\pi}} \\frac{(a r)^l}{(r/a) (a + r)^{2l + 3}} C_{n}^{2l + 3/2}(\\xi) .. math:: \\Phi(r, \\theta, \\phi) = \\sum_{n=0}^{\\infty} \\sum_{l=0}^{\\infty} \\sum_{m=0}^l N_{lm} P_{lm}(\\cos(\\theta)) \\tilde{\\Phi}_{nl}(r) \\left(A_{cos, nlm} \\cos(m\\phi) + A_{sin, nlm} \\sin(m\\phi)\\right) where .. math:: \\tilde{\\Phi}_{nl}(r) = -\\sqrt{4 \\pi}K_{nl} \\frac{(ar)^l}{(a + r)^{2l + 1}} C_{n}^{2l + 3/2}(\\xi) where .. math:: \\xi = \\frac{r - a}{r + a} \\qquad N_{lm} = \\sqrt{\\frac{2l + 1}{4\\pi} \\frac{(l - m)!}{(l + m)!}}(2 - \\delta_{m0}) \\qquad K_{nl} = \\frac{1}{2} n (n + 4l + 3) + (l + 1)(2l + 1) and :math:`P_{lm}` is the Associated Legendre Polynomials whereas :math:`C_n^{\\alpha}` is the Gegenbauer polynomial. """ def __init__(self, amp=1., Acos=numpy.array([[[1]]]),Asin=None, a = 1., normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a SCF Potential from a set of expansion coefficients (use SCFPotential.from_density to directly initialize from a density) INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass Acos - The real part of the expansion coefficient (NxLxL matrix, or optionally NxLx1 if Asin=None) Asin - The imaginary part of the expansion coefficient (NxLxL matrix or None) a - scale length (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: SCFPotential object HISTORY: 2016-05-13 - Written - Aladdin Seaifan (UofT) """ NumericalPotentialDerivativesMixin.__init__(self,{}) # just use default dR etc. Potential.__init__(self,amp=amp/2.,ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) ##Errors shape = Acos.shape errorMessage = None if len(shape) != 3: errorMessage="Acos must be a 3 dimensional numpy array" elif Asin is not None and shape[1] != shape[2]: errorMessage="The second and third dimension of the expansion coefficients must have the same length" elif Asin is None and not (shape[2] == 1 or shape[1] == shape[2]): errorMessage="The third dimension must have length=1 or equal to the length of the second dimension" elif Asin is None and shape[1] > 1 and numpy.any(Acos[:,:,1:] !=0): errorMessage="Acos has non-zero elements at indices m>0, which implies a non-axi symmetric potential.\n" +\ "Asin=None which implies an axi symmetric potential.\n" + \ "Contradiction." elif Asin is not None and Asin.shape != shape: errorMessage = "The shape of Asin does not match the shape of Acos." if errorMessage is not None: raise RuntimeError(errorMessage) ##Warnings warningMessage=None if numpy.any(numpy.triu(Acos,1) != 0) or (Asin is not None and numpy.any(numpy.triu(Asin,1) != 0)): warningMessage="Found non-zero values at expansion coefficients where m > l\n" + \ "The Mth and Lth dimension is expected to make a lower triangular matrix.\n" + \ "All values found above the diagonal will be ignored." if warningMessage is not None: raise RuntimeWarning(warningMessage) ##Is non axi? self.isNonAxi= True if Asin is None or shape[1] == 1 or (numpy.all(Acos[:,:,1:] == 0) and numpy.all(Asin[:,:,:]==0)): self.isNonAxi = False self._a = a NN = self._Nroot(Acos.shape[1], Acos.shape[2]) self._Acos= Acos*NN[numpy.newaxis,:,:] if Asin is not None: self._Asin = Asin*NN[numpy.newaxis,:,:] else: self._Asin = numpy.zeros_like(Acos) self._force_hash= None self.hasC= True self.hasC_dxdv=True self.hasC_dens=True if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): self.normalize(normalize) return None @classmethod def from_density(cls,dens,N,L=None,a=1.,symmetry=None, radial_order=None,costheta_order=None,phi_order=None, ro=None,vo=None): """ NAME: from_density PURPOSE: initialize an SCF Potential from from a given density INPUT: dens - density function that takes parameters R, z and phi; z and phi are optional for spherical profiles, phi is optional for axisymmetric profiles. The density function must take input positions in internal units (R/ro, z/ro), but can return densities in physical units. You can use the member dens of Potential instances or the density from evaluateDensities N - Number of radial basis functions L - Number of costheta basis functions; for non-axisymmetric profiles also sets the number of azimuthal (phi) basis functions to M = 2L+1) a - expansion scale length (can be Quantity) symmetry= (None) symmetry of the profile to assume: 'spherical', 'axisymmetry', or None (for the general, non-axisymmetric case) radial_order - Number of sample points for the radial integral. If None, radial_order=max(20, N + 3/2L + 1) costheta_order - Number of sample points of the costheta integral. If None, If costheta_order=max(20, L + 1) phi_order - Number of sample points of the phi integral. If None, If costheta_order=max(20, L + 1) ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: SCFPotential object HISTORY: 2022-06-20 - Written - Jo Bovy (UofT) """ # Dummy object for ro/vo handling, to ensure consistency dumm= cls(ro=ro,vo=vo) internal_ro= dumm._ro internal_vo= dumm._vo a= conversion.parse_length(a,ro=internal_ro) if not symmetry is None and symmetry.startswith('spher'): Acos, Asin= scf_compute_coeffs_spherical(dens,N,a=a, radial_order=radial_order) elif not symmetry is None and symmetry.startswith('axi'): Acos, Asin= scf_compute_coeffs_axi(dens,N,L,a=a, radial_order=radial_order, costheta_order=costheta_order) else: Acos, Asin= scf_compute_coeffs(dens,N,L,a=a, radial_order=radial_order, costheta_order=costheta_order, phi_order=phi_order) # Turn on physical outputs if input density was physical if _APY_LOADED: # First need to determine number of parameters, like in # scf_compute_coeffs_spherical/axi numOfParam = 0 try: dens(0) numOfParam=1 except: try: dens(0,0) numOfParam=2 except: numOfParam=3 param= [1]*numOfParam try: dens(*param).to(units.kg/units.m**3) except (AttributeError,units.UnitConversionError): # We'll just assume that unit conversion means density # is scalar Quantity pass else: ro= internal_ro vo= internal_vo return cls(Acos=Acos,Asin=Asin,a=a,ro=ro,vo=vo) def _Nroot(self, L, M=None): """ NAME: _Nroot PURPOSE: Evaluate the square root of equation (3.15) with the (2 - del_m,0) term outside the square root INPUT: L - evaluate Nroot for 0 <= l <= L M - evaluate Nroot for 0 <= m <= M OUTPUT: The square root of equation (3.15) with the (2 - del_m,0) outside HISTORY: 2016-05-16 - Written - Aladdin Seaifan (UofT) """ if M is None: M =L NN = numpy.zeros((L,M),float) l = numpy.arange(0,L)[:,numpy.newaxis] m = numpy.arange(0,M)[numpy.newaxis, :] nLn = gammaln(l-m+1) - gammaln(l+m+1) NN[:,:] = ((2*l+1.)/(4.*numpy.pi) * numpy.e**nLn)**.5 * 2 NN[:,0] /= 2. NN = numpy.tril(NN) return NN def _calculateXi(self, r): """ NAME: _calculateXi PURPOSE: Calculate xi given r INPUT: r - Evaluate at radius r OUTPUT: xi HISTORY: 2016-05-18 - Written - Aladdin Seaifan (UofT) """ a = self._a if r == 0: return -1 else: return (1.-a/r)/(1.+a/r) def _rhoTilde(self, r, N,L): """ NAME: _rhoTilde PURPOSE: Evaluate rho_tilde as defined in equation 3.9 and 2.24 for 0 <= n < N and 0 <= l < L INPUT: r - Evaluate at radius r N - size of the N dimension L - size of the L dimension OUTPUT: rho tilde HISTORY: 2016-05-17 - Written - Aladdin Seaifan (UofT) """ xi = self._calculateXi(r) CC = _C(xi,N,L) a = self._a rho = numpy.zeros((N,L), float) n = numpy.arange(0,N, dtype=float)[:, numpy.newaxis] l = numpy.arange(0, L, dtype=float)[numpy.newaxis,:] K = 0.5 * n * (n + 4*l + 3) + (l + 1.)*(2*l + 1) rho[:,:] = K * ((a*r)**l) / ((r/a)*(a + r)**(2*l + 3.)) * CC[:,:]* (numpy.pi)**-0.5 return rho def _phiTilde(self, r, N,L): """ NAME: _phiTilde PURPOSE: Evaluate phi_tilde as defined in equation 3.10 and 2.25 for 0 <= n < N and 0 <= l < L INPUT: r - Evaluate at radius r N - size of the N dimension L - size of the L dimension OUTPUT: phi tilde HISTORY: 2016-05-17 - Written - Aladdin Seaifan (UofT) """ xi = self._calculateXi(r) CC = _C(xi,N,L) a = self._a phi = numpy.zeros((N,L), float) n = numpy.arange(0,N)[:, numpy.newaxis] l = numpy.arange(0, L)[numpy.newaxis,:] if r == 0: phi[:,:]= -1./a* CC[:,:]*(4*numpy.pi)**0.5 else: phi[:,:] = - a**l*r**(-l-1.)/ ((1.+a/r)**(2*l + 1.)) * CC[:,:]* (4*numpy.pi)**0.5 return phi def _compute(self, funcTilde, R, z, phi): """ NAME: _compute PURPOSE: evaluate the NxLxM density or potential INPUT: funcTidle - must be _rhoTilde or _phiTilde R - Cylindrical Galactocentric radius z - vertical height phi - azimuth OUTPUT: An NxLxM density or potential at (R,z, phi) HISTORY: 2016-05-18 - Written - Aladdin Seaifan (UofT) """ Acos, Asin = self._Acos, self._Asin N, L, M = Acos.shape r, theta, phi = coords.cyl_to_spher(R,z,phi) PP = lpmn(M-1,L-1,numpy.cos(theta))[0].T ##Get the Legendre polynomials func_tilde = funcTilde(r, N, L) ## Tilde of the function of interest func = numpy.zeros((N,L,M), float) ## The function of interest (density or potential) m = numpy.arange(0, M)[numpy.newaxis, numpy.newaxis, :] mcos = numpy.cos(m*phi) msin = numpy.sin(m*phi) func = func_tilde[:,:,None]*(Acos[:,:,:]*mcos + Asin[:,:,:]*msin)*PP[None,:,:] return func def _computeArray(self, funcTilde, R, z, phi): """ NAME: _computeArray PURPOSE: evaluate the density or potential for a given array of coordinates INPUT: funcTidle - must be _rhoTilde or _phiTilde R - Cylindrical Galactocentric radius z - vertical height phi - azimuth OUTPUT: density or potential evaluated at (R,z, phi) HISTORY: 2016-06-02 - Written - Aladdin Seaifan (UofT) """ R = numpy.array(R,dtype=float); z = numpy.array(z,dtype=float); phi = numpy.array(phi,dtype=float); shape = (R*z*phi).shape if shape == (): return numpy.sum(self._compute(funcTilde, R,z,phi)) R = R*numpy.ones(shape); z = z*numpy.ones(shape); phi = phi*numpy.ones(shape); func = numpy.zeros(shape, float) li = _cartesian(shape) for i in range(li.shape[0]): j= tuple(numpy.split(li[i], li.shape[1])) func[j] = numpy.sum(self._compute(funcTilde, R[j][0],z[j][0],phi[j][0])) return func def _dens(self, R, z, phi=0., t=0.): """ NAME: _dens PURPOSE: evaluate the density at (R,z, phi) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: density at (R,z, phi) HISTORY: 2016-05-17 - Written - Aladdin Seaifan (UofT) """ if not self.isNonAxi and phi is None: phi= 0. return self._computeArray(self._rhoTilde, R,z,phi) def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R (and z) for this potential; if z=None, integrate spherical INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2021-03-09 - Written - Bovy (UofT) 2021-03-18 - Switched to using Gauss' theorem - Bovy (UofT) """ if not z is None: raise AttributeError # Hack to fall back to general # when integrating over spherical volume, all non-zero l,m vanish N= len(self._Acos) return R**2.*numpy.sum(self._Acos[:,0,0]*self._dphiTilde(R,N,1)[:,0]) def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at (R,z, phi) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: potential at (R,z, phi) HISTORY: 2016-05-17 - Written - Aladdin Seaifan (UofT) """ if not self.isNonAxi and phi is None: phi= 0. return self._computeArray(self._phiTilde, R,z,phi) def _dphiTilde(self, r, N, L): """ NAME: _dphiTilde PURPOSE: Evaluate the derivative of phiTilde with respect to r INPUT: r - spherical radius N - size of the N dimension L - size of the L dimension OUTPUT: the derivative of phiTilde with respect to r HISTORY: 2016-06-06 - Written - Aladdin Seaifan (UofT) """ a = self._a l = numpy.arange(0, L, dtype=float)[numpy.newaxis, :] n = numpy.arange(0, N, dtype=float)[:, numpy.newaxis] xi = self._calculateXi(r) dC = _dC(xi,N,L) return -(4*numpy.pi)**.5 * (numpy.power(a*r, l)*(l*(a + r)*numpy.power(r,-1) -(2*l + 1))/((a + r)**(2*l + 2))*_C(xi,N,L) + a**-1*(1 - xi)**2 * (a*r)**l / (a + r)**(2*l + 1) *dC/2.) def _computeforce(self,R,z,phi=0,t=0): """ NAME: _computeforce PURPOSE: Evaluate the first derivative of Phi with respect to R, z and phi INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: dPhi/dr, dPhi/dtheta, dPhi/dphi HISTORY: 2016-06-07 - Written - Aladdin Seaifan (UofT) """ Acos, Asin = self._Acos, self._Asin N, L, M = Acos.shape r, theta, phi = coords.cyl_to_spher(R,z,phi) new_hash= hashlib.md5(numpy.array([R, z,phi])).hexdigest() if new_hash == self._force_hash: dPhi_dr = self._cached_dPhi_dr dPhi_dtheta = self._cached_dPhi_dtheta dPhi_dphi = self._cached_dPhi_dphi else: PP, dPP = lpmn(M-1,L-1,numpy.cos(theta)) ##Get the Legendre polynomials PP = PP.T[None,:,:] dPP = dPP.T[None,:,:] phi_tilde = self._phiTilde(r, N, L)[:,:,numpy.newaxis] dphi_tilde = self._dphiTilde(r,N,L)[:,:,numpy.newaxis] m = numpy.arange(0, M)[numpy.newaxis, numpy.newaxis, :] mcos = numpy.cos(m*phi) msin = numpy.sin(m*phi) dPhi_dr = -numpy.sum((Acos*mcos + Asin*msin)*PP*dphi_tilde) dPhi_dtheta = -numpy.sum((Acos*mcos + Asin*msin)*phi_tilde*dPP*(-numpy.sin(theta))) dPhi_dphi =-numpy.sum(m*(Asin*mcos - Acos*msin)*phi_tilde*PP) self._force_hash = new_hash self._cached_dPhi_dr = dPhi_dr self._cached_dPhi_dtheta = dPhi_dtheta self._cached_dPhi_dphi = dPhi_dphi return dPhi_dr,dPhi_dtheta,dPhi_dphi def _computeforceArray(self,dr_dx, dtheta_dx, dphi_dx, R, z, phi): """ NAME: _computeforceArray PURPOSE: evaluate the forces in the x direction for a given array of coordinates INPUT: dr_dx - the derivative of r with respect to the chosen variable x dtheta_dx - the derivative of theta with respect to the chosen variable x dphi_dx - the derivative of phi with respect to the chosen variable x R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: The forces in the x direction HISTORY: 2016-06-02 - Written - Aladdin Seaifan (UofT) """ R = numpy.array(R,dtype=float); z = numpy.array(z,dtype=float); phi = numpy.array(phi,dtype=float); shape = (R*z*phi).shape if shape == (): dPhi_dr,dPhi_dtheta,dPhi_dphi = \ self._computeforce(R,z,phi) return dr_dx*dPhi_dr + dtheta_dx*dPhi_dtheta +dPhi_dphi*dphi_dx R = R*numpy.ones(shape); z = z* numpy.ones(shape); phi = phi* numpy.ones(shape); force = numpy.zeros(shape, float) dr_dx = dr_dx*numpy.ones(shape); dtheta_dx = dtheta_dx*numpy.ones(shape);dphi_dx = dphi_dx*numpy.ones(shape); li = _cartesian(shape) for i in range(li.shape[0]): j = tuple(numpy.split(li[i], li.shape[1])) dPhi_dr,dPhi_dtheta,dPhi_dphi = \ self._computeforce(R[j][0],z[j][0],phi[j][0]) force[j] = dr_dx[j][0]*dPhi_dr + dtheta_dx[j][0]*dPhi_dtheta +dPhi_dphi*dphi_dx[j][0] return force def _Rforce(self, R, z, phi=0, t=0): """ NAME: _Rforce PURPOSE: evaluate the radial force at (R,z, phi) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: radial force at (R,z, phi) HISTORY: 2016-06-06 - Written - Aladdin Seaifan (UofT) """ if not self.isNonAxi and phi is None: phi= 0. r, theta, phi = coords.cyl_to_spher(R,z,phi) #x = R dr_dR = numpy.divide(R,r); dtheta_dR = numpy.divide(z,r**2); dphi_dR = 0 return self._computeforceArray(dr_dR, dtheta_dR, dphi_dR, R,z,phi) def _zforce(self, R, z, phi=0., t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force at (R,z, phi) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: vertical force at (R,z, phi) HISTORY: 2016-06-06 - Written - Aladdin Seaifan (UofT) """ if not self.isNonAxi and phi is None: phi= 0. r, theta, phi = coords.cyl_to_spher(R,z,phi) #x = z dr_dz = numpy.divide(z,r); dtheta_dz = numpy.divide(-R,r**2); dphi_dz = 0 return self._computeforceArray(dr_dz, dtheta_dz, dphi_dz, R,z,phi) def _phitorque(self, R,z,phi=0,t=0): """ NAME: _phitorque PURPOSE: evaluate the azimuth force at (R,z, phi) INPUT: R - Cylindrical Galactocentric radius z - vertical height phi - azimuth t - time OUTPUT: azimuth force at (R,z, phi) HISTORY: 2016-06-06 - Written - Aladdin Seaifan (UofT) """ if not self.isNonAxi and phi is None: phi= 0. r, theta, phi = coords.cyl_to_spher(R,z,phi) #x = phi dr_dphi = 0; dtheta_dphi = 0; dphi_dphi = 1 return self._computeforceArray(dr_dphi, dtheta_dphi, dphi_dphi, R,z,phi) def OmegaP(self): return 0 def _xiToR(xi, a =1): return a*numpy.divide((1. + xi),(1. - xi)) def _RToxi(r, a=1): out= numpy.divide((r/a-1.),(r/a+1.),where=True^numpy.isinf(r)) if numpy.any(numpy.isinf(r)): if hasattr(r,'__len__'): out[numpy.isinf(r)]= 1. else: return 1. return out def _C(xi,N,L,alpha=lambda x: 2*x + 3./2,singleL=False): """ NAME: _C PURPOSE: Evaluate C_n,l (the Gegenbauer polynomial) for 0 <= l < L and 0<= n < N INPUT: xi - radial transformed variable N - Size of the N dimension L - Size of the L dimension alpha = A lambda function of l. Default alpha = 2l + 3/2 singleL= (False), if True only compute the L-th polynomial OUTPUT: An LxN Gegenbauer Polynomial HISTORY: 2016-05-16 - Written - Aladdin Seaifan (UofT) 2021-02-22 - Upgraded to array xi - Bovy (UofT) 2021-02-22 - Added singleL for use in compute...nbody - Bovy (UofT) """ floatIn= False if isinstance(xi,(float,int)): floatIn= True xi= numpy.array([xi]) if singleL: Ls= [L] else: Ls= range(L) CC= numpy.zeros((N,len(Ls),len(xi))) for l,ll in enumerate(Ls): for n in range(N): a= alpha(ll) if n==0: CC[n,l]= 1. continue elif n==1: CC[n,l]= 2.*a*xi if n + 1 != N: CC[n+1,l]= (2*(n + a)*xi*CC[n,l]-(n + 2*a - 1)*CC[n-1,l])\ /(n+1.) if floatIn: return CC[:,:,0] else: return CC def _dC(xi, N, L): l = numpy.arange(0,L)[numpy.newaxis, :] CC = _C(xi,N + 1,L, alpha = lambda x: 2*x + 5./2) CC = numpy.roll(CC, 1, axis=0)[:-1,:] CC[0, :] = 0 CC *= 2*(2*l + 3./2) return CC def scf_compute_coeffs_spherical_nbody(pos,N,mass=1.,a=1.): """ NAME: scf_compute_coeffs_spherical_nbody PURPOSE: Numerically compute the expansion coefficients for a spherical expansion for a given $N$-body set of points INPUT: pos - positions of particles in rectangular coordinates with shape [3,n] N - size of the Nth dimension of the expansion coefficients mass= (1.) mass of particles (scalar or array with size n) a= (1.) parameter used to scale the radius OUTPUT: (Acos,Asin) - Expansion coefficients for density dens that can be given to SCFPotential.__init__ HISTORY: 2020-11-18 - Written - Morgan Bennett (UofT) 2021-02-22 - Sped-up - Bovy (UofT) """ Acos = numpy.zeros((N,1,1), float) Asin = None r= numpy.sqrt(pos[0]**2+pos[1]**2+pos[2]**2) RhoSum= numpy.einsum('j,ij',mass/(1.+r/a),_C(_RToxi(r,a=a),N,1)[:,0]) n = numpy.arange(0,N) K = 4*(n + 3./2)/((n + 2)*(n + 1)*(1 + n*(n + 3.)/2.)) Acos[n,0,0] = 2*K*RhoSum return Acos, Asin def _scf_compute_determine_dens_kwargs(dens,param): try: param[0]= 1. dens(*param,use_physical=False) except: dens_kw= {} else: dens_kw= {'use_physical': False} return dens_kw def scf_compute_coeffs_spherical(dens, N, a=1., radial_order=None): """ NAME: scf_compute_coeffs_spherical PURPOSE: Numerically compute the expansion coefficients for a given spherical density INPUT: dens - A density function that takes a parameter R N - size of expansion coefficients a= (1.) parameter used to scale the radius radial_order - Number of sample points of the radial integral. If None, radial_order=max(20, N + 1) OUTPUT: (Acos,Asin) - Expansion coefficients for density dens that can be given to SCFPotential.__init__ HISTORY: 2016-05-18 - Written - Aladdin Seaifan (UofT) """ numOfParam = 0 try: dens(0) numOfParam=1 except: try: dens(0,0) numOfParam=2 except: numOfParam=3 param = [0]*numOfParam dens_kw= _scf_compute_determine_dens_kwargs(dens,param) def integrand(xi): r = _xiToR(xi, a) R = r param[0] = R return a**3. * dens(*param,**dens_kw)*(1 + xi)**2. * (1 - xi)**-3. \ * _C(xi, N, 1)[:,0] Acos = numpy.zeros((N,1,1), float) Asin = None Ksample = [max(N + 1, 20)] if radial_order != None: Ksample[0] = radial_order integrated = _gaussianQuadrature(integrand, [[-1., 1.]], Ksample=Ksample) n = numpy.arange(0,N) K = 16*numpy.pi*(n + 3./2)/((n + 2)*(n + 1)*(1 + n*(n + 3.)/2.)) Acos[n,0,0] = 2*K*integrated return Acos, Asin def scf_compute_coeffs_axi_nbody(pos,N,L,mass=1.,a=1.): """ NAME: scf_compute_coeffs_axi_nbody PURPOSE: Numerically compute the expansion coefficients for a given $N$-body set of points assuming that the density is axisymmetric INPUT: pos - positions of particles in rectangular coordinates with shape [3,n] N - size of the Nth dimension of the expansion coefficients L - size of the Lth dimension of the expansion coefficients mass= (1.) mass of particles (scalar or array with size n) a= (1.) parameter used to scale the radius OUTPUT: (Acos,Asin) - Expansion coefficients for density dens that can be given to SCFPotential.__init__ HISTORY: 2021-02-22 - Written based on general code - Bovy (UofT) """ r= numpy.sqrt(pos[0]**2+pos[1]**2+pos[2]**2) costheta = pos[2]/r mass= numpy.atleast_1d(mass) Acos, Asin= numpy.zeros([N,L,1]), None Pll= numpy.ones(len(r)) # Set up Assoc. Legendre recursion # (n,l) dependent constant n= numpy.arange(0,N)[:,numpy.newaxis] l= numpy.arange(0,L)[numpy.newaxis,:] Knl= 0.5*n*(n+4.*l+3.)+(l+1)*(2.*l+1.) Inl= -Knl*2.*numpy.pi/2.**(8.*l+6.)*gamma(n+4.*l+3.)\ /gamma(n+1)/(n+2.*l+1.5)/gamma(2.*l+1.5)**2/numpy.sqrt(2.*l+1) # Set up Assoc. Legendre recursion Plm= Pll Plmm1= 0. for ll in range(L): # Compute Gegenbauer polys for this l Cn= _C(_RToxi(r,a=a),N,ll,singleL=True) phinlm= -(r/a)**ll/(1.+r/a)**(2.*ll+1)*Cn[:,0]*Plm # Acos Sum= numpy.sum(mass[numpy.newaxis,:]*phinlm,axis=-1) Acos[:,ll,0]= Sum/Inl[:,ll] # Recurse Assoc. Legendre if ll < L: tmp= Plm Plm= ((2*ll+1.)*costheta*Plm-ll*Plmm1)/(ll+1) Plmm1= tmp return Acos,Asin def scf_compute_coeffs_axi(dens, N, L, a=1.,radial_order=None, costheta_order=None): """ NAME: scf_compute_coeffs_axi PURPOSE: Numerically compute the expansion coefficients for a given axi-symmetric density INPUT: dens - A density function that takes a parameter R and z N - size of the Nth dimension of the expansion coefficients L - size of the Lth dimension of the expansion coefficients a - parameter used to shift the basis functions radial_order - Number of sample points of the radial integral. If None, radial_order=max(20, N + 3/2L + 1) costheta_order - Number of sample points of the costheta integral. If None, If costheta_order=max(20, L + 1) OUTPUT: (Acos,Asin) - Expansion coefficients for density dens that can be given to SCFPotential.__init__ HISTORY: 2016-05-20 - Written - Aladdin Seaifan (UofT) """ numOfParam = 0 try: dens(0,0) numOfParam=2 except: numOfParam=3 param = [0]*numOfParam dens_kw= _scf_compute_determine_dens_kwargs(dens,param) def integrand(xi, costheta): l = numpy.arange(0, L)[numpy.newaxis, :] r = _xiToR(xi,a) R = r*numpy.sqrt(1 - costheta**2.) z = r*costheta Legandre = lpmn(0,L-1,costheta)[0].T[numpy.newaxis,:,0] dV = (1. + xi)**2. * numpy.power(1. - xi, -4.) phi_nl = a**3*(1. + xi)**l * (1. - xi)**(l + 1.)*_C(xi, N, L)[:,:] * Legandre param[0] = R param[1] = z return phi_nl*dV * dens(*param,**dens_kw) Acos = numpy.zeros((N,L,1), float) Asin = None ##This should save us some computation time since we're only taking the double integral once, rather then L times Ksample = [max(N + 3*L//2 + 1, 20) , max(L + 1,20) ] if radial_order != None: Ksample[0] = radial_order if costheta_order != None: Ksample[1] = costheta_order integrated = _gaussianQuadrature(integrand, [[-1, 1], [-1, 1]], Ksample = Ksample)*(2*numpy.pi) n = numpy.arange(0,N)[:,numpy.newaxis] l = numpy.arange(0,L)[numpy.newaxis,:] K = .5*n*(n + 4*l + 3) + (l + 1)*(2*l + 1) #I = -K*(4*numpy.pi)/(2.**(8*l + 6)) * gamma(n + 4*l + 3)/(gamma(n + 1)*(n + 2*l + 3./2)*gamma(2*l + 3./2)**2) ##Taking the ln of I will allow bigger size coefficients lnI = -(8*l + 6)*numpy.log(2) + gammaln(n + 4*l + 3) - gammaln(n + 1) - numpy.log(n + 2*l + 3./2) - 2*gammaln(2*l + 3./2) I = -K*(4*numpy.pi) * numpy.e**(lnI) constants = -2.**(-2*l)*(2*l + 1.)**.5 Acos[:,:,0] = 2*I**-1 * integrated*constants return Acos, Asin def scf_compute_coeffs_nbody(pos,N,L,mass=1.,a=1.): """ NAME: scf_compute_coeffs_nbody PURPOSE: Numerically compute the expansion coefficients for a given $N$-body set of points INPUT: pos - positions of particles in rectangular coordinates with shape [3,n] N - size of the Nth dimension of the expansion coefficients L - size of the Lth and Mth dimension of the expansion coefficients mass= (1.) mass of particles (scalar or array with size n) a= (1.) parameter used to scale the radius OUTPUT: (Acos,Asin) - Expansion coefficients for density dens that can be given to SCFPotential.__init__ HISTORY: 2020-11-18 - Written - Morgan Bennett (UofT) """ r= numpy.sqrt(pos[0]**2+pos[1]**2+pos[2]**2) phi= numpy.arctan2(pos[1],pos[0]) costheta= pos[2]/r sintheta= numpy.sqrt(1.-costheta**2.) mass= numpy.atleast_1d(mass) Acos, Asin= numpy.zeros([N,L,L]), numpy.zeros([N,L,L]) Pll= numpy.ones(len(r)) # Set up Assoc. Legendre recursion # (n,l) dependent constant n= numpy.arange(0,N)[:,numpy.newaxis] l= numpy.arange(0,L)[numpy.newaxis,:] Knl= 0.5*n*(n+4.*l+3.)+(l+1)*(2.*l+1.) Inl= -Knl*2.*numpy.pi/2.**(8.*l+6.)*gamma(n+4.*l+3.)\ /gamma(n+1)/(n+2.*l+1.5)/gamma(2.*l+1.5)**2 for mm in range(L): # Loop over m cosmphi= numpy.cos(phi*mm) sinmphi= numpy.sin(phi*mm) # Set up Assoc. Legendre recursion Plm= Pll Plmm1= 0. for ll in range(mm,L): # Compute Gegenbauer polys for this l Cn= _C(_RToxi(r,a=a),N,ll,singleL=True) phinlm= -(r/a)**ll/(1.+r/a)**(2.*ll+1)*Cn[:,0]*Plm # Acos Sum= numpy.sqrt((2.*ll+1)*gamma(ll-mm+1)/gamma(ll+mm+1))\ *numpy.sum((mass*cosmphi)[numpy.newaxis,:]*phinlm,axis=-1) Acos[:,ll,mm]= Sum/Inl[:,ll] # Asin Sum= numpy.sqrt((2.*ll+1)*gamma(ll-mm+1)/gamma(ll+mm+1))\ *numpy.sum((mass*sinmphi)[numpy.newaxis,:]*phinlm,axis=-1) Asin[:,ll,mm]= Sum/Inl[:,ll] # Recurse Assoc. Legendre if ll < L: tmp= Plm Plm= ((2*ll+1.)*costheta*Plm-(ll+mm)*Plmm1)/(ll-mm+1) Plmm1= tmp # Recurse Assoc. Legendre Pll*= -(2*mm+1.)*sintheta return Acos,Asin def scf_compute_coeffs(dens,N,L,a=1., radial_order=None,costheta_order=None,phi_order=None): """ NAME: scf_compute_coeffs PURPOSE: Numerically compute the expansion coefficients for a given triaxial density INPUT: dens - A density function that takes a parameter R, z and phi N - size of the Nth dimension of the expansion coefficients L - size of the Lth and Mth dimension of the expansion coefficients a - parameter used to shift the basis functions radial_order - Number of sample points of the radial integral. If None, radial_order=max(20, N + 3/2L + 1) costheta_order - Number of sample points of the costheta integral. If None, If costheta_order=max(20, L + 1) phi_order - Number of sample points of the phi integral. If None, If costheta_order=max(20, L + 1) OUTPUT: (Acos,Asin) - Expansion coefficients for density dens that can be given to SCFPotential.__init__ HISTORY: 2016-05-27 - Written - Aladdin Seaifan (UofT) """ dens_kw= _scf_compute_determine_dens_kwargs(dens,[0.1,0.1,0.1]) def integrand(xi, costheta, phi): l = numpy.arange(0, L)[numpy.newaxis, :, numpy.newaxis] m = numpy.arange(0, L)[numpy.newaxis,numpy.newaxis,:] r = _xiToR(xi, a) R = r*numpy.sqrt(1 - costheta**2.) z = r*costheta Legandre = lpmn(L - 1,L-1,costheta)[0].T[numpy.newaxis,:,:] dV = (1. + xi)**2. * numpy.power(1. - xi, -4.) phi_nl = - a**3*(1. + xi)**l * (1. - xi)**(l + 1.)*_C(xi, N, L)[:,:,numpy.newaxis] * Legandre return dens(R,z, phi,**dens_kw) * phi_nl[numpy.newaxis, :,:,:]*numpy.array([numpy.cos(m*phi), numpy.sin(m*phi)])*dV Acos = numpy.zeros((N,L,L), float) Asin = numpy.zeros((N,L,L), float) Ksample = [max(N + 3*L//2 + 1,20), max(L + 1,20 ), max(L + 1,20)] if radial_order != None: Ksample[0] = radial_order if costheta_order != None: Ksample[1] = costheta_order if phi_order != None: Ksample[2] = phi_order integrated = _gaussianQuadrature(integrand, [[-1., 1.], [-1., 1.], [0, 2*numpy.pi]], Ksample = Ksample) n = numpy.arange(0,N)[:,numpy.newaxis, numpy.newaxis] l = numpy.arange(0,L)[numpy.newaxis,:, numpy.newaxis] m = numpy.arange(0,L)[numpy.newaxis,numpy.newaxis,:] K = .5*n*(n + 4*l + 3) + (l + 1)*(2*l + 1) Nln = .5*gammaln(l - m + 1) - .5*gammaln(l + m + 1) - (2*l)*numpy.log(2) NN = numpy.e**(Nln) NN[numpy.where(NN == numpy.inf)] = 0 ## To account for the fact that m can't be bigger than l constants = NN*(2*l + 1.)**.5 lnI = -(8*l + 6)*numpy.log(2) + gammaln(n + 4*l + 3) - gammaln(n + 1) - numpy.log(n + 2*l + 3./2) - 2*gammaln(2*l + 3./2) I = -K*(4*numpy.pi) * numpy.e**(lnI) Acos[:,:,:],Asin[:,:,:] = 2*(I**-1.)[numpy.newaxis,:,:,:] * integrated * constants[numpy.newaxis,:,:,:] return Acos, Asin def _cartesian(arraySizes, out=None): """ NAME: cartesian PURPOSE: Generate a cartesian product of input arrays. INPUT: arraySizes - list of size of arrays out - Array to place the cartesian product in. OUTPUT: 2-D array of shape (product(arraySizes), len(arraySizes)) containing cartesian products formed of input arrays. HISTORY: 2016-06-02 - Obtained from http://stackoverflow.com/questions/1208118/using-numpy-to-build-an-array-of-all-combinations-of-two-arrays """ arrays = [] for i in range(len(arraySizes)): arrays.append(numpy.arange(0, arraySizes[i])) arrays = [numpy.asarray(x) for x in arrays] dtype = arrays[0].dtype n = numpy.prod([x.size for x in arrays]) if out is None: out = numpy.zeros([n, len(arrays)], dtype=dtype) m = n // arrays[0].size out[:,0] = numpy.repeat(arrays[0], m) if arrays[1:]: _cartesian(arraySizes[1:], out=out[0:m,1:]) for j in range(1, arrays[0].size): out[j*m:(j+1)*m,1:] = out[0:m,1:] return out def _gaussianQuadrature(integrand, bounds, Ksample=[20], roundoff=0): """ NAME: _gaussianQuadrature PURPOSE: Numerically take n integrals over a function that returns a float or an array INPUT: integrand - The function you're integrating over. bounds - The bounds of the integral in the form of [[a_0, b_0], [a_1, b_1], ... , [a_n, b_n]] where a_i is the lower bound and b_i is the upper bound Ksample - Number of sample points in the form of [K_0, K_1, ..., K_n] where K_i is the sample point of the ith integral. roundoff - if the integral is less than this value, round it to 0. OUTPUT: The integral of the function integrand HISTORY: 2016-05-24 - Written - Aladdin Seaifan (UofT) """ ##Maps the sample point and weights xp = numpy.zeros((len(bounds), numpy.max(Ksample)), float) wp = numpy.zeros((len(bounds), numpy.max(Ksample)), float) for i in range(len(bounds)): x,w = leggauss(Ksample[i]) ##Calculates the sample points and weights a,b = bounds[i] xp[i, :Ksample[i]] = .5*(b-a)*x + .5*(b+a) wp[i, :Ksample[i]] = .5*(b - a)*w ##Determines the shape of the integrand s = 0. shape=None s_temp = integrand(*numpy.zeros(len(bounds))) if type(s_temp).__name__ == numpy.ndarray.__name__ : shape = s_temp.shape s = numpy.zeros(shape, float) #gets all combinations of indices from each integrand li = _cartesian(Ksample) ##Performs the actual integration for i in range(li.shape[0]): index = (numpy.arange(len(bounds)),li[i]) s+= numpy.prod(wp[index])*integrand(*xp[index]) ##Rounds values that are less than roundoff to zero if shape!= None: s[numpy.where(numpy.fabs(s) < roundoff)] = 0 else: s *= numpy.fabs(s) >roundoff return s ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/SnapshotRZPotential.py0000644000175100001710000006032714327773303021675 0ustar00runnerdockerimport copy import hashlib from os import system import numpy from scipy import interpolate from ..util._optional_deps import _PYNBODY_LOADED from .interpRZPotential import (calc_2dsplinecoeffs_c, interpRZPotential, scalarVectorDecorator, zsymDecorator) from .Potential import Potential if _PYNBODY_LOADED: import pynbody from pynbody import gravity from pynbody.units import NoUnit class SnapshotRZPotential(Potential): """Class that implements an axisymmetrized version of the potential of an N-body snapshot (requires `pynbody `__) `_evaluate`, `_Rforce`, and `_zforce` calculate a hash for the array of points that is passed in by the user. The hash and corresponding potential/force arrays are stored -- if a subsequent request matches a previously computed hash, the previous results are returned and not recalculated. """ def __init__(self, s, num_threads=None,nazimuths=4, ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a SnapshotRZ potential object INPUT: s - a simulation snapshot loaded with pynbody num_threads= (4) number of threads to use for calculation nazimuths= (4) number of azimuths to average over ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: instance HISTORY: 2013 - Written - Rok Roskar (ETH) 2014-11-24 - Edited for merging into main galpy - Bovy (IAS) """ if not _PYNBODY_LOADED: #pragma: no cover raise ImportError("The SnapShotRZPotential class is designed to work with pynbody snapshots, which cannot be loaded (probably because it is not installed) -- obtain from pynbody.github.io") Potential.__init__(self,amp=1.0,ro=ro,vo=vo) self._s = s self._point_hash = {} if num_threads is None: self._num_threads= pynbody.config['number_of_threads'] else: self._num_threads = num_threads # Set up azimuthal averaging self._naz= nazimuths self._cosaz= numpy.cos(numpy.arange(self._naz,dtype='float')\ /self._naz*2.*numpy.pi) self._sinaz= numpy.sin(numpy.arange(self._naz,dtype='float')\ /self._naz*2.*numpy.pi) self._zones= numpy.ones(self._naz) self._zzeros= numpy.zeros(self._naz) return None @scalarVectorDecorator def _evaluate(self, R,z,phi=None,t=None,dR=None,dphi=None) : pot, acc = self._setup_potential(R,z) return pot @scalarVectorDecorator def _Rforce(self, R,z,phi=None,t=None,dR=None,dphi=None) : pot, acc = self._setup_potential(R,z) return acc[:,0] @scalarVectorDecorator def _zforce(self, R,z,phi=None,t=None,dR=None,dphi=None) : pot, acc = self._setup_potential(R,z) return acc[:,1] def _setup_potential(self, R, z, use_pkdgrav = False) : # compute the hash for the requested grid new_hash = hashlib.md5(numpy.array([R,z])).hexdigest() # if we computed for these points before, return; otherwise compute if new_hash in self._point_hash : pot, rz_acc = self._point_hash[new_hash] # if use_pkdgrav : else : # set up the four points per R,z pair to mimic axisymmetry points = numpy.zeros((len(R),self._naz,3)) for i in range(len(R)) : points[i] = numpy.array([R[i]*self._cosaz,R[i]*self._sinaz, z[i]*self._zones]).T points_new = points.reshape(points.size//3,3) pot, acc = gravity.calc.direct(self._s,points_new,num_threads=self._num_threads) pot = pot.reshape(len(R),self._naz) acc = acc.reshape(len(R),self._naz,3) # need to average the potentials pot = pot.mean(axis=1) # get the radial accelerations rz_acc = numpy.zeros((len(R),2)) rvecs= numpy.array([self._cosaz,self._sinaz,self._zzeros]).T for i in range(len(R)) : for j,rvec in enumerate(rvecs) : rz_acc[i,0] += acc[i,j].dot(rvec) rz_acc[i,1] += acc[i,j,2] rz_acc /= self._naz # store the computed values for reuse self._point_hash[new_hash] = [pot,rz_acc] return pot, rz_acc class InterpSnapshotRZPotential(interpRZPotential) : """ Interpolated axisymmetrized potential extracted from a simulation output (see ``interpRZPotential`` and ``SnapshotRZPotential``) """ def __init__(self, s, ro=None,vo=None, rgrid=(numpy.log(0.01),numpy.log(20.),101), zgrid=(0.,1.,101), interpepifreq = False, interpverticalfreq = False, interpPot = True, enable_c = True, logR = True, zsym = True, numcores=None,nazimuths=4,use_pkdgrav = False) : """ NAME: __init__ PURPOSE: Initialize an InterpSnapshotRZPotential instance INPUT: s - a simulation snapshot loaded with pynbody rgrid - R grid to be given to linspace as in rs= linspace(*rgrid) zgrid - z grid to be given to linspace as in zs= linspace(*zgrid) logR - if True, rgrid is in the log of R so logrs= linspace(*rgrid) interpPot, interpepifreq, interpverticalfreq= if True, interpolate these functions (interpPot=True also interpolates the R and zforce) enable_c= enable use of C for interpolations zsym= if True (default), the potential is assumed to be symmetric around z=0 (so you can use, e.g., zgrid=(0.,1.,101)). numcores= if set to an integer, use this many cores nazimuths= (4) number of azimuths to average over use_pkdgrav= (False) use PKDGRAV to calculate the snapshot's potential and forces (CURRENTLY NOT IMPLEMENTED) ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: instance HISTORY: 2013 - Written - Rok Roskar (ETH) 2014-11-24 - Edited for merging into main galpy - Bovy (IAS) """ if not _PYNBODY_LOADED: #pragma: no cover raise ImportError("The InterpSnapRZShotPotential class is designed to work with pynbody snapshots, which cannot be loaded (probably because it is not installed) -- obtain from pynbody.github.io") # initialize using the base class Potential.__init__(self,amp=1.0,ro=ro,vo=vo) # other properties if numcores is None: self._numcores= pynbody.config['number_of_threads'] else: self._numcores = numcores self._s = s # Set up azimuthal averaging self._naz= nazimuths self._cosaz= numpy.cos(numpy.arange(self._naz,dtype='float')\ /self._naz*2.*numpy.pi) self._sinaz= numpy.sin(numpy.arange(self._naz,dtype='float')\ /self._naz*2.*numpy.pi) self._zones= numpy.ones(self._naz) self._zzeros= numpy.zeros(self._naz) # the interpRZPotential class sets these flags self._enable_c = enable_c self.hasC = True # set up the flags for interpolated quantities # since the potential and force are always calculated together, # set the force interpolations to true if potential is true and # vice versa self._interpPot = interpPot self._interpRforce = self._interpPot self._interpzforce = self._interpPot self._interpvcirc = self._interpPot # these require additional calculations so set them separately self._interpepifreq = interpepifreq self._interpverticalfreq = interpverticalfreq # make the potential accessible at points beyond the grid self._origPot = SnapshotRZPotential(s,self._numcores) # setup the grid self._zsym = zsym self._logR = logR self._rgrid = numpy.linspace(*rgrid) if logR : self._rgrid = numpy.exp(self._rgrid) self._logrgrid = numpy.log(self._rgrid) rs = self._logrgrid else : rs = self._rgrid self._zgrid = numpy.linspace(*zgrid) # calculate the grids self._setup_potential(self._rgrid,self._zgrid,use_pkdgrav=use_pkdgrav) if enable_c and interpPot: self._potGrid_splinecoeffs = calc_2dsplinecoeffs_c(self._potGrid) self._rforceGrid_splinecoeffs = calc_2dsplinecoeffs_c(self._rforceGrid) self._zforceGrid_splinecoeffs = calc_2dsplinecoeffs_c(self._zforceGrid) else : self._potInterp= interpolate.RectBivariateSpline(rs, self._zgrid, self._potGrid, kx=3,ky=3,s=0.) self._rforceInterp= interpolate.RectBivariateSpline(rs, self._zgrid, self._rforceGrid, kx=3,ky=3,s=0.) self._zforceInterp= interpolate.RectBivariateSpline(rs, self._zgrid, self._zforceGrid, kx=3,ky=3,s=0.) if interpepifreq: self._R2interp = interpolate.RectBivariateSpline(rs, self._zgrid, self._R2derivGrid, kx=3,ky=3,s=0.) if interpverticalfreq: self._z2interp = interpolate.RectBivariateSpline(rs, self._zgrid, self._z2derivGrid, kx=3,ky=3,s=0.) if interpepifreq and interpverticalfreq: self._Rzinterp = interpolate.RectBivariateSpline(rs, self._zgrid, self._RzderivGrid, kx=3,ky=3,s=0.) # setup the derived quantities if interpPot: self._vcircGrid = numpy.sqrt(self._rgrid*(-self._rforceGrid[:,0])) self._vcircInterp = interpolate.InterpolatedUnivariateSpline(rs, self._vcircGrid, k=3) if interpepifreq: self._epifreqGrid = numpy.sqrt(self._R2derivGrid[:,0] - 3./self._rgrid*self._rforceGrid[:,0]) goodindx= True^numpy.isnan(self._epifreqGrid) self._epifreqInterp=\ interpolate.InterpolatedUnivariateSpline(rs[goodindx], self._epifreqGrid[goodindx], k=3) self._epigoodindx= goodindx if interpverticalfreq: self._verticalfreqGrid = numpy.sqrt(numpy.abs(self._z2derivGrid[:,0])) goodindx= True^numpy.isnan(self._verticalfreqGrid) self._verticalfreqInterp=\ interpolate.InterpolatedUnivariateSpline(rs[goodindx], self._verticalfreqGrid[goodindx],k=3) self._verticalgoodindx= goodindx def _setup_potential(self, R, z, use_pkdgrav = False, dr = 0.0001) : """ Calculates the potential and force grids for the snapshot for use with other galpy functions. **Input**: *R*: R grid coordinates (numpy array) *z*: z grid coordinates (numpy array) **Optional Keywords**: *use_pkdgrav*: (False) whether to use pkdgrav for the gravity calculation *dr*: (0.01) offset to use for the gradient calculation - the points are positioned at +/- dr from the central point """ # set up the four points per R,z pair to mimic axisymmetry points = numpy.zeros((len(R),len(z),self._naz,3)) for i in range(len(R)) : for j in range(len(z)) : points[i,j] = numpy.array([R[i]*self._cosaz,R[i]*self._sinaz, z[j]*self._zones]).T points_new = points.reshape(points.size//3,3) self.points = points_new # set up the points to calculate the second derivatives zgrad_points = numpy.zeros((len(points_new)*2,3)) rgrad_points = numpy.zeros((len(points_new)*2,3)) for i,p in enumerate(points_new) : zgrad_points[i*2] = p zgrad_points[i*2][2] -= dr zgrad_points[i*2+1] = p zgrad_points[i*2+1][2] += dr rgrad_points[i*2] = p rgrad_points[i*2][:2] -= p[:2]/numpy.sqrt(numpy.dot(p[:2],p[:2]))*dr rgrad_points[i*2+1] = p rgrad_points[i*2+1][:2] += p[:2]/numpy.sqrt(numpy.dot(p[:2],p[:2]))*dr if use_pkdgrav : #pragma: no cover raise RuntimeError("using pkdgrav not currently implemented") sn = pynbody.snapshot._new(len(self._s.d)+len(self._s.g)+len(self._s.s)+len(points_new)) print("setting up %d grid points"%(len(points_new))) #sn['pos'][0:len(self.s)] = self.s['pos'] #sn['mass'][0:len(self.s)] = self.s['mass'] #sn['phi'] = 0.0 #sn['eps'] = 1e3 #sn['eps'][0:len(self.s)] = self.s['eps'] #sn['vel'][0:len(self.s)] = self.s['vel'] #sn['mass'][len(self.s):] = 1e-10 sn['pos'][len(self._s):] = points_new sn['mass'][len(self._s):] = 0.0 sn.write(fmt=pynbody.tipsy.TipsySnap, filename='potgridsnap') command = '~/bin/pkdgrav2_pthread -sz %d -n 0 +std -o potgridsnap -I potgridsnap +potout +overwrite %s'%(self._numcores, self._s._paramfile['filename']) print(command) system(command) sn = pynbody.load('potgridsnap') acc = sn['accg'][len(self._s):].reshape(len(R)*len(z),self._naz,3) pot = sn['pot'][len(self._s):].reshape(len(R)*len(z),self._naz) else : if self._interpPot: pot, acc = gravity.calc.direct(self._s,points_new,num_threads=self._numcores) pot = pot.reshape(len(R)*len(z),self._naz) acc = acc.reshape(len(R)*len(z),self._naz,3) # need to average the potentials pot = pot.mean(axis=1) # get the radial accelerations rz_acc = numpy.zeros((len(R)*len(z),2)) rvecs= numpy.array([self._cosaz,self._sinaz,self._zzeros]).T # reshape the acc to make sure we have a leading index even # if we are only evaluating a single point, i.e. we have # shape = (1,4,3) not (4,3) acc = acc.reshape((len(rz_acc),self._naz,3)) for i in range(len(R)*len(z)) : for j,rvec in enumerate(rvecs) : rz_acc[i,0] += acc[i,j].dot(rvec) rz_acc[i,1] += acc[i,j,2] rz_acc /= self._naz self._potGrid = pot.reshape((len(R),len(z))) self._rforceGrid = rz_acc[:,0].reshape((len(R),len(z))) self._zforceGrid = rz_acc[:,1].reshape((len(R),len(z))) # compute the force gradients # first get the accelerations if self._interpverticalfreq : zgrad_pot, zgrad_acc = gravity.calc.direct(self._s,zgrad_points,num_threads=self._numcores) # each point from the points used above for pot and acc is straddled by # two points to get the gradient across it. Compute the gradient by # using a finite difference zgrad = numpy.zeros(len(points_new)) # do a loop through the pairs of points -- reshape the array # so that each item is the pair of acceleration vectors # then calculate the gradient from the two points for i,zacc in enumerate(zgrad_acc.reshape((len(zgrad_acc)//2,2,3))) : zgrad[i] = ((zacc[1]-zacc[0])/(dr*2.0))[2] # reshape the arrays self._z2derivGrid = -zgrad.reshape((len(zgrad)//self._naz,self._naz)).mean(axis=1).reshape((len(R),len(z))) # do the same for the radial component if self._interpepifreq: rgrad_pot, rgrad_acc = gravity.calc.direct(self._s,rgrad_points,num_threads=self._numcores) rgrad = numpy.zeros(len(points_new)) for i,racc in enumerate(rgrad_acc.reshape((len(rgrad_acc)//2,2,3))) : point = points_new[i] point[2] = 0.0 rvec = point/numpy.sqrt(numpy.dot(point,point)) rgrad_vec = (numpy.dot(racc[1],rvec)- numpy.dot(racc[0],rvec)) / (dr*2.0) rgrad[i] = rgrad_vec self._R2derivGrid = -rgrad.reshape((len(rgrad)//self._naz,self._naz)).mean(axis=1).reshape((len(R),len(z))) # do the same for the mixed radial-vertical component if self._interpepifreq and self._interpverticalfreq: # re-use this Rzgrad = numpy.zeros(len(points_new)) for i,racc in enumerate(rgrad_acc.reshape((len(rgrad_acc)//2,2,3))) : Rzgrad[i] = ((racc[1]-racc[0])/(dr*2.0))[2] # reshape the arrays self._RzderivGrid = -Rzgrad.reshape((len(Rzgrad)//self._naz,self._naz)).mean(axis=1).reshape((len(R),len(z))) @scalarVectorDecorator @zsymDecorator(False) def _R2deriv(self,R,Z,phi=0.,t=0.): return self._R2interp(R,Z) @scalarVectorDecorator @zsymDecorator(False) def _z2deriv(self,R,Z,phi=None,t=None): return self._z2interp(R,Z) @scalarVectorDecorator @zsymDecorator(True) def _Rzderiv(self,R,Z,phi=None,t=None): return self._Rzinterp(R,Z) def normalize(self, R0=8.0) : """ Normalize all positions by R0 and velocities by Vc(R0). If :class:`~scipy.interpolate.RectBivariateSpline` or :class:`~scipy.interpolate.InterpolatedUnivariateSpline` are used, redefine them for use with the rescaled coordinates. To undo the normalization, call :func:`~galpy.potential.SnapshotPotential.InterpSnapshotPotential.denormalize`. """ Vc0 = self.vcirc(R0) Phi0 = numpy.abs(self.Rforce(R0,0.0)) self._normR0 = R0 self._normVc0 = Vc0 self._normPhi0 = Phi0 # rescale the simulation if not isinstance(self._s['pos'].units,NoUnit): self._posunit = self._s['pos'].units self._s['pos'].convert_units('%s kpc'%R0) else: self._posunit = None if not isinstance(self._s['vel'].units,NoUnit): self._velunit = self._s['vel'].units self._s['vel'].convert_units('%s km s**-1'%Vc0) else: self._velunit = None # rescale the grid self._rgrid /= R0 if self._logR: self._logrgrid -= numpy.log(R0) rs = self._logrgrid else : rs = self._rgrid self._zgrid /= R0 # rescale the potential self._amp /= Phi0 self._savedsplines = {} # rescale anything using splines if not self._enable_c and self._interpPot : for spline,name in zip([self._potInterp, self._rforceInterp, self._zforceInterp], ["pot", "rforce", "zforce"]): self._savedsplines[name] = spline self._potInterp= interpolate.RectBivariateSpline(rs, self._zgrid, self._potGrid/R0, kx=3,ky=3,s=0.) self._rforceInterp= interpolate.RectBivariateSpline(rs, self._zgrid, self._rforceGrid, kx=3,ky=3,s=0.) self._zforceInterp= interpolate.RectBivariateSpline(rs, self._zgrid, self._zforceGrid, kx=3,ky=3,s=0.) elif self._enable_c and self._interpPot: self._potGrid_splinecoeffs= calc_2dsplinecoeffs_c(self._potGrid/R0) if self._interpPot : self._savedsplines['vcirc'] = self._vcircInterp self._vcircInterp = interpolate.InterpolatedUnivariateSpline(rs, self._vcircGrid/Vc0, k=3) if self._interpepifreq: self._savedsplines['R2deriv'] = self._R2interp self._savedsplines['epifreq'] = self._epifreqInterp self._R2interp = interpolate.RectBivariateSpline(rs, self._zgrid, self._R2derivGrid, kx=3,ky=3,s=0.) self._epifreqInterp = interpolate.InterpolatedUnivariateSpline(rs[self._epigoodindx], self._epifreqGrid[self._epigoodindx]/numpy.sqrt(Phi0/R0), k=3) if self._interpverticalfreq: self._savedsplines['z2deriv'] = self._z2interp self._savedsplines['verticalfreq'] = self._verticalfreqInterp self._z2interp = interpolate.RectBivariateSpline(rs, self._zgrid, self._z2derivGrid, kx=3,ky=3,s=0.) self._verticalfreqInterp = interpolate.InterpolatedUnivariateSpline(rs[self._verticalgoodindx], self._verticalfreqGrid[self._verticalgoodindx]/numpy.sqrt(Phi0/R0), k=3) def denormalize(self) : """ Undo the normalization. """ R0 = self._normR0 Vc0 = self._normVc0 Phi0 = self._normPhi0 # rescale the simulation if not self._posunit is None: self._s['pos'].convert_units(self._posunit) if not self._velunit is None: self._s['vel'].convert_units(self._velunit) # rescale the grid self._rgrid *= R0 if self._logR: self._logrgrid += numpy.log(R0) rs = self._logrgrid else : rs = self._rgrid self._zgrid *= R0 # rescale the potential self._amp *= Phi0 # restore the splines if not self._enable_c and self._interpPot : self._potInterp= self._savedsplines['pot'] self._rforceInterp= self._savedsplines['rforce'] self._zforceInterp= self._savedsplines['zforce'] elif self._enable_c and self._interpPot: self._potGrid_splinecoeffs= calc_2dsplinecoeffs_c(self._potGrid) if self._interpPot : self._vcircInterp = self._savedsplines['vcirc'] if self._interpepifreq : self._R2interp = self._savedsplines['R2deriv'] self._epifreqInterp = self._savedsplines['epifreq'] if self._interpverticalfreq: self._z2interp = self._savedsplines['z2deriv'] self._verticalfreqInterp = self._savedsplines['verticalfreq'] # Pickling functions def __getstate__(self): print("Here") pdict= copy.copy(self.__dict__) # Deconstruct _s pdict['_pos']= self._s['pos'] pdict['_mass']= self._s['mass'] pdict['_eps']= self._s['eps'] # rm _s and _origPot, del pdict['_s'] del pdict['_origPot'] return pdict def __setstate__(self,pdict): # Set up snapshot again for origPot pdict['_s']= pynbody.new(star=len(pdict['_mass'])) pdict['_s']['pos']= pdict['_pos'] pdict['_s']['mass']= pdict['_mass'] pdict['_s']['eps']= pdict['_eps'] # Transfer __dict__ del pdict['_pos'] del pdict['_mass'] del pdict['_eps'] self.__dict__= pdict # Now setup origPotnagain self._origPot = SnapshotRZPotential(self._s,self._numcores) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/SoftenedNeedleBarPotential.py0000644000175100001710000001643714327773303023136 0ustar00runnerdocker############################################################################### # SoftenedNeedleBarPotential.py: class that implements the softened needle # bar potential from Long & Murali (1992) ############################################################################### import hashlib import numpy from ..util import conversion, coords from .Potential import Potential class SoftenedNeedleBarPotential(Potential): """Class that implements the softened needle bar potential from `Long & Murali (1992) `__ .. math:: \\Phi(x,y,z) = \\frac{\\mathrm{amp}}{2a}\\,\\ln\\left(\\frac{x-a+T_-}{x+a+T_+}\\right) where .. math:: T_{\\pm} = \\sqrt{(a\\pm x)^2 + y^2+(b+\\sqrt{z^2+c^2})^2} For a prolate bar, set :math:`b` to zero. """ def __init__(self,amp=1.,a=4.,b=0.,c=1.,normalize=False, pa=0.4,omegab=1.8,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a softened-needle bar potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass a= (4.) Bar half-length (can be Quantity) b= (1.) Triaxial softening length (can be Quantity) c= (1.) Prolate softening length (can be Quantity) pa= (0.4) The position angle of the x axis (rad or Quantity) omegab= (1.8) Pattern speed (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2016-11-02 - Started - Bovy (UofT) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) b= conversion.parse_length(b,ro=self._ro) c= conversion.parse_length(c,ro=self._ro) pa= conversion.parse_angle(pa) omegab= conversion.parse_frequency(omegab,ro=self._ro,vo=self._vo) self._a= a self._b= b self._c2= c**2. self._pa= pa self._omegab= omegab self._force_hash= None self.hasC= True self.hasC_dxdv= False if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) self.isNonAxi= True return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2016-11-02 - Started - Bovy (UofT) """ x,y,z= self._compute_xyz(R,phi,z,t) Tp, Tm= self._compute_TpTm(x,y,z) return numpy.log((x-self._a+Tm)/(x+self._a+Tp))/2./self._a def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2016-11-02 - Written - Bovy (UofT) """ self._compute_xyzforces(R,z,phi,t) return numpy.cos(phi)*self._cached_Fx+numpy.sin(phi)*self._cached_Fy def _phitorque(self,R,z,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the azimuthal torque HISTORY: 2016-11-02 - Written - Bovy (UofT) """ self._compute_xyzforces(R,z,phi,t) return R*(-numpy.sin(phi)*self._cached_Fx\ +numpy.cos(phi)*self._cached_Fy) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2016-11-02 - Written - Bovy (UofT) """ self._compute_xyzforces(R,z,phi,t) return self._cached_Fz def OmegaP(self): """ NAME: OmegaP PURPOSE: return the pattern speed INPUT: (none) OUTPUT: pattern speed HISTORY: 2016-11-02 - Written - Bovy (UofT) """ return self._omegab def _compute_xyz(self,R,phi,z,t): return coords.cyl_to_rect(R,phi-self._pa-self._omegab*t,z) def _compute_TpTm(self,x,y,z): secondpart= y**2.+(self._b+numpy.sqrt(self._c2+z**2.))**2. return (numpy.sqrt((self._a+x)**2.+secondpart), numpy.sqrt((self._a-x)**2.+secondpart)) def _compute_xyzforces(self,R,z,phi,t): # Compute all rectangular forces new_hash= hashlib.md5(numpy.array([R,phi,z,t])).hexdigest() if new_hash != self._force_hash: x,y,z= self._compute_xyz(R,phi,z,t) Tp, Tm= self._compute_TpTm(x,y,z) Fx= self._xforce_xyz(x,y,z,Tp,Tm) Fy= self._yforce_xyz(x,y,z,Tp,Tm) Fz= self._zforce_xyz(x,y,z,Tp,Tm) self._force_hash= new_hash tp= self._pa+self._omegab*t cp, sp= numpy.cos(tp), numpy.sin(tp) self._cached_Fx= cp*Fx-sp*Fy self._cached_Fy= sp*Fx+cp*Fy self._cached_Fz= Fz def _xforce_xyz(self,x,y,z,Tp,Tm): return -2.*x/Tp/Tm/(Tp+Tm) def _yforce_xyz(self,x,y,z,Tp,Tm): return -y/2./Tp/Tm*(Tp+Tm-4.*x**2./(Tp+Tm))\ /(y**2.+(self._b+numpy.sqrt(z**2.+self._c2))**2.) def _zforce_xyz(self,x,y,z,Tp,Tm): zc= numpy.sqrt(z**2.+self._c2) return self._yforce_xyz(x,y,z,Tp,Tm)*z/y*(self._b+zc)/zc def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2016-11-04 - Written - Bovy (UofT/CCA) """ x,y,z= self._compute_xyz(R,phi,z,t) zc= numpy.sqrt(z**2.+self._c2) bzc2= (self._b+zc)**2. bigA= self._b*y**2.+(self._b+3.*zc)*bzc2 bigC= y**2.+bzc2 return self._c2/24./numpy.pi/self._a/bigC**2./zc**3.\ *((x+self._a)*(3.*bigA*bigC+(2.*bigA+self._b*bigC)*(x+self._a)**2.)\ /(bigC+(x+self._a)**2.)**1.5\ -(x-self._a)*(3.*bigA*bigC+(2.*bigA+self._b*bigC)*(x-self._a)**2.)\ /(bigC+(x-self._a)**2.)**1.5) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/SolidBodyRotationWrapperPotential.py0000644000175100001710000000433214327773303024565 0ustar00runnerdocker############################################################################### # SolidBodyRotationWrapperPotential.py: Wrapper to make a potential rotate # with a fixed pattern speed, around # the z axis ############################################################################### from ..util import conversion from .WrapperPotential import parentWrapperPotential class SolidBodyRotationWrapperPotential(parentWrapperPotential): """Potential wrapper class that implements solid-body rotation around the z-axis. Can be used to make a bar or other perturbation rotate. The potential is rotated by replacing .. math:: \\phi \\rightarrow \\phi + \\Omega \\times t + \\mathrm{pa} with :math:`\\Omega` the fixed pattern speed and :math:`\\mathrm{pa}` the position angle at :math:`t=0`. """ def __init__(self,amp=1.,pot=None,omega=1.,pa=0.,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a SolidBodyRotationWrapper Potential INPUT: amp - amplitude to be applied to the potential (default: 1.) pot - Potential instance or list thereof; this potential is made to rotate around the z axis by the wrapper omega= (1.) the pattern speed (can be a Quantity) pa= (0.) the position angle (can be a Quantity) OUTPUT: (none) HISTORY: 2017-08-22 - Started - Bovy (UofT) """ omega= conversion.parse_frequency(omega,ro=self._ro,vo=self._vo) pa= conversion.parse_angle(pa) self._omega= omega self._pa= pa self.hasC= True self.hasC_dxdv= True def OmegaP(self): """ NAME: OmegaP PURPOSE: return the pattern speed INPUT: (none) OUTPUT: pattern speed HISTORY: 2016-11-02 - Written - Bovy (UofT) """ return self._omega def _wrap(self,attribute,*args,**kwargs): kwargs['phi']= \ kwargs.get('phi',0.)-self._omega*kwargs.get('t',0.)-self._pa return self._wrap_pot_func(attribute)(self._pot,*args,**kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/SphericalPotential.py0000644000175100001710000001377414327773303021540 0ustar00runnerdocker############################################################################### # SphericalPotential.py: base class for potentials corresponding to # spherical density profiles ############################################################################### import numpy from scipy import integrate from .Potential import Potential class SphericalPotential(Potential): """Base class for spherical potentials. Implement a specific spherical density distribution with this form by inheriting from this class and defining functions: * ``_revaluate(self,r,t=0.)``: the potential as a function of ``r`` and time; * ``_rforce(self,r,t=0.)``: the radial force as a function of ``r`` and time; * ``_r2deriv(self,r,t=0.)``: the second radial derivative of the potential as a function of ``r`` and time; * ``_rdens(self,r,t=0.)``: the density as a function of ``r`` and time (if *not* implemented, calculated using the Poisson equation). """ def __init__(self,amp=1.,ro=None,vo=None,amp_units=None): """ NAME: __init__ PURPOSE: initialize a spherical potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units that depend on the specific spherical potential amp_units - ('mass', 'velocity2', 'density') type of units that amp should have if it has units (passed to Potential.__init__) ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2020-03-30 - Written - Bovy (UofT) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units=amp_units) return None def _rdens(self,r,t=0.): """Implement using the Poisson equation in case this isn't implemented""" return (self._r2deriv(r,t=t)-2.*self._rforce(r,t=t)/r)/4./numpy.pi def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2020-03-30 - Written - Bovy (UofT) """ r= numpy.sqrt(R**2.+z**2.) return self._revaluate(r,t=t) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2020-03-30 - Written - Bovy (UofT) """ r= numpy.sqrt(R**2.+z**2.) return self._rforce(r,t=t)*R/r def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2020-03-30 - Written - Bovy (UofT) """ r= numpy.sqrt(R**2.+z**2.) return self._rforce(r,t=t)*z/r def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2020-03-30 - Written - Bovy (UofT) """ r= numpy.sqrt(R**2.+z**2.) return self._r2deriv(r,t=t)*R**2./r**2.-self._rforce(r,t=t)*z**2./r**3. def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2020-03-30 - Written - Bovy (UofT) """ r= numpy.sqrt(R**2.+z**2.) return self._r2deriv(r,t=t)*z**2./r**2.-self._rforce(r,t=t)*R**2./r**3. def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed radial, vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the mixed radial, vertical derivative HISTORY: 2020-03-30 - Written - Bovy (UofT) """ r= numpy.sqrt(R**2.+z**2.) return self._r2deriv(r,t=t)*R*z/r**2.+self._rforce(r,t=t)*R*z/r**3. def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2020-03-30 - Written - Bovy (UofT) """ r= numpy.sqrt(R**2.+z**2.) return self._rdens(r,t=t) def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2021-03-15 - Written - Bovy (UofT) """ if z is not None: raise AttributeError # use general implementation R= numpy.float64(R) # Avoid indexing issues return -R**2.*self._rforce(R,t=t) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/SphericalShellPotential.py0000644000175100001710000000654714327773303022530 0ustar00runnerdocker############################################################################### # SphericalShellPotential.py: The gravitational potential of a thin, # spherical shell ############################################################################### import numpy from ..util import conversion from .SphericalPotential import SphericalPotential class SphericalShellPotential(SphericalPotential): """Class that implements the potential of an infinitesimally-thin, spherical shell .. math:: \\rho(r) = \\frac{\\mathrm{amp}}{4\\pi\\,a^2}\\,\\delta(r-a) with :math:`\\mathrm{amp} = GM` the mass of the shell. """ def __init__(self,amp=1.,a=0.75,normalize=False,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a spherical shell potential INPUT: amp - mass of the shell (default: 1); can be a Quantity with units of mass or Gxmass a= (0.75) radius of the shell (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1.; note that because the force is always zero at r < a, this does not work if a > 1 ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2018-08-04 - Written - Bovy (UofT) 2020-03-30 - Re-implemented using SphericalPotential - Bovy (UofT) """ SphericalPotential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) self.a= a self.a2= a**2 if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): if self.a > 1.: raise ValueError('SphericalShellPotential with normalize= for a > 1 is not supported (because the force is always 0 at r=1)') self.normalize(normalize) self.hasC= False self.hasC_dxdv= False def _revaluate(self,r,t=0.): """The potential as a function of r""" if r <= self.a: return -1./self.a else: return -1./r def _rforce(self,r,t=0.): """The force as a function of r""" if r <= self.a: return 0. else: return -1/r**2. def _r2deriv(self,r,t=0.): """The second radial derivative as a function of r""" if r <= self.a: return 0. else: return -2./r**3. def _rdens(self,r,t=0.): """The density as a function of r""" if r != self.a: return 0. else: # pragma: no cover return numpy.infty def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2018-08-04 - Written - Bovy (UofT) """ if R > self.a: return 0. h= numpy.sqrt(self.a2-R**2) if z < h: return 0. else: return 1./(2.*numpy.pi*self.a*h) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/SpiralArmsPotential.py0000644000175100001710000007112214327773303021672 0ustar00runnerdocker############################################################################### # SpiralArmsPotential.py: class that implements the spiral arms potential # from Cox and Gomez (2002) # # https://arxiv.org/abs/astro-ph/0207635 # # Phi(r, phi, z) = -4*pi*G*H*rho0*exp(-(r-r0)/Rs)*sum(Cn/(Kn*Dn)*cos(n*gamma)*sech(Kn*z/Bn)^Bn) ############################################################################### import numpy from ..util import conversion from .Potential import Potential class SpiralArmsPotential(Potential): """Class that implements the spiral arms potential from (`Cox and Gomez 2002 `__). Should be used to modulate an existing potential (density is positive in the arms, negative outside; note that because of this, a contour plot of this potential will appear to have twice as many arms, where half are the underdense regions). .. math:: \\Phi(R, \\phi, z) = -4 \\pi GH \\,\\rho_0 exp \\left( -\\frac{R-r_{ref}}{R_s} \\right) \\sum{\\frac{C_n}{K_n D_n} \\,\\cos(n \\gamma) \\,\\mathrm{sech}^{B_n} \\left( \\frac{K_n z}{B_n} \\right)} where .. math:: K_n &= \\frac{n N}{R \\sin(\\alpha)} \\\\ B_n &= K_n H (1 + 0.4 K_n H) \\\\ D_n &= \\frac{1 + K_n H + 0.3 (K_n H)^2}{1 + 0.3 K_n H} \\\\ and .. math:: \\gamma = N \\left[\\phi - \\phi_{ref} - \\frac{\\ln(R/r_{ref})}{\\tan(\\alpha)} \\right] The default of :math:`C_n=[1]` gives a sinusoidal profile for the potential. An alternative from `Cox and Gomez (2002) `__ creates a density that behaves approximately as a cosine squared in the arms but is separated by a flat interarm region by setting .. math:: C_n = \\left[\\frac{8}{3 \\pi}\\,,\\frac{1}{2} \\,, \\frac{8}{15 \\pi}\\right] """ normalize= property() # turn off normalize def __init__(self, amp=1, ro=None, vo=None, amp_units='density', N=2, alpha=0.2, r_ref=1, phi_ref=0, Rs=0.3, H=0.125, omega=0, Cs=[1]): """ NAME: __init__ PURPOSE: initialize a spiral arms potential INPUT: :amp: amplitude to be applied to the potential (default: 1); can be a Quantity with units of density. (:math:`amp = 4 \\pi G \\rho_0`) :ro: distance scales for translation into internal units (default from configuration file) :vo: velocity scales for translation into internal units (default from configuration file) :N: number of spiral arms :alpha: pitch angle of the logarithmic spiral arms in radians (can be Quantity) :r_ref: fiducial radius where :math:`\\rho = \\rho_0` (:math:`r_0` in the paper by Cox and Gomez) (can be Quantity) :phi_ref: reference angle (:math:`\\phi_p(r_0)` in the paper by Cox and Gomez) (can be Quantity) :Rs: radial scale length of the drop-off in density amplitude of the arms (can be Quantity) :H: scale height of the stellar arm perturbation (can be Quantity) :Cs: list of constants multiplying the :math:`\\cos(n \\gamma)` terms :omega: rotational pattern speed of the spiral arms (can be Quantity) OUTPUT: (none) HISTORY: Started - 2017-05-12 Jack Hong (UBC) Completed - 2017-07-04 Jack Hong (UBC) """ Potential.__init__(self, amp=amp, ro=ro, vo=vo, amp_units=amp_units) alpha= conversion.parse_angle(alpha) r_ref= conversion.parse_length(r_ref,ro=self._ro) phi_ref= conversion.parse_angle(phi_ref) Rs= conversion.parse_length(Rs,ro=self._ro) H= conversion.parse_length(H,ro=self._ro) omega= conversion.parse_frequency(omega,ro=self._ro,vo=self._vo) self._N = -N # trick to flip to left handed coordinate system; flips sign for phi and phi_ref, but also alpha. self._alpha = -alpha # we don't want sign for alpha to change, so flip alpha. (see eqn. 3 in the paper) self._sin_alpha = numpy.sin(-alpha) self._tan_alpha = numpy.tan(-alpha) self._r_ref = r_ref self._phi_ref = phi_ref self._Rs = Rs self._H = H self._Cs = self._Cs0 = numpy.array(Cs) self._ns = self._ns0 = numpy.arange(1, len(Cs) + 1) self._omega = omega self._rho0 = 1 / (4 * numpy.pi) self._HNn = self._HNn0 = self._H * self._N * self._ns0 self.isNonAxi = True # Potential is not axisymmetric self.hasC = True # Potential has C implementation to speed up orbit integrations self.hasC_dxdv = True # Potential has C implementation of second derivatives def _evaluate(self, R, z, phi=0, t=0): """ NAME: _evaluate PURPOSE: Evaluate the potential at the given coordinates. (without the amp factor; handled by super class) INPUT: :param R: galactocentric cylindrical radius :param z: vertical height :param phi: azimuth :param t: time OUTPUT: :return: Phi(R, z, phi, t) HISTORY: 2017-05-12 Jack Hong (UBC) """ if isinstance(R,numpy.ndarray) or isinstance(z,numpy.ndarray): nR= len(R) if isinstance(R,numpy.ndarray) else len(z) self._Cs=numpy.transpose(numpy.array([self._Cs0,]*nR)) self._ns=numpy.transpose(numpy.array([self._ns0,]*nR)) self._HNn=numpy.transpose(numpy.array([self._HNn0,]*nR)) else: self._Cs=self._Cs0 self._ns=self._ns0 self._HNn=self._HNn0 Ks = self._K(R) Bs = self._B(R) Ds = self._D(R) return -self._H * numpy.exp(-(R-self._r_ref) / self._Rs) \ * numpy.sum(self._Cs / Ks / Ds * numpy.cos(self._ns * self._gamma(R, phi - self._omega * t)) / numpy.cosh(Ks * z / Bs) ** Bs,axis=0) def _Rforce(self, R, z, phi=0, t=0): """ NAME: _Rforce PURPOSE: Evaluate the radial force for this potential at the given coordinates. (-dPhi/dR) INPUT: :param R: galactocentric cylindrical radius :param z: vertical height :param phi: azimuth :param t: time OUTPUT: :return: the radial force HISTORY: 2017-05-12 Jack Hong (UBC) """ if isinstance(R,numpy.ndarray) or isinstance(z,numpy.ndarray): nR= len(R) if isinstance(R,numpy.ndarray) else len(z) self._Cs=numpy.transpose(numpy.array([self._Cs0,]*nR)) self._ns=numpy.transpose(numpy.array([self._ns0,]*nR)) self._HNn=numpy.transpose(numpy.array([self._HNn0,]*nR)) else: self._Cs=self._Cs0 self._ns=self._ns0 self._HNn=self._HNn0 He = self._H * numpy.exp(-(R-self._r_ref)/self._Rs) Ks = self._K(R) Bs = self._B(R) Ds = self._D(R) dKs_dR = self._dK_dR(R) dBs_dR = self._dB_dR(R) dDs_dR = self._dD_dR(R) g = self._gamma(R, phi - self._omega * t) dg_dR = self._dgamma_dR(R) cos_ng = numpy.cos(self._ns * g) sin_ng = numpy.sin(self._ns * g) zKB = z * Ks / Bs sechzKB = 1 / numpy.cosh(zKB) return -He * numpy.sum(self._Cs * sechzKB**Bs / Ds * ((self._ns * dg_dR / Ks * sin_ng + cos_ng * (z * numpy.tanh(zKB) * (dKs_dR/Ks - dBs_dR/Bs) - dBs_dR / Ks * numpy.log(sechzKB) + dKs_dR / Ks**2 + dDs_dR / Ds / Ks)) + cos_ng / Ks / self._Rs),axis=0) def _zforce(self, R, z, phi=0, t=0): """ NAME: _zforce PURPOSE: Evaluate the vertical force for this potential at the given coordinates. (-dPhi/dz) INPUT: :param R: galactocentric cylindrical radius :param z: vertical height :param phi: azimuth :param t: time OUTPUT: :return: the vertical force HISTORY: 2017-05-25 Jack Hong (UBC) """ if isinstance(R,numpy.ndarray) or isinstance(z,numpy.ndarray): nR= len(R) if isinstance(R,numpy.ndarray) else len(z) self._Cs=numpy.transpose(numpy.array([self._Cs0,]*nR)) self._ns=numpy.transpose(numpy.array([self._ns0,]*nR)) self._HNn=numpy.transpose(numpy.array([self._HNn0,]*nR)) else: self._Cs=self._Cs0 self._ns=self._ns0 self._HNn=self._HNn0 Ks = self._K(R) Bs = self._B(R) Ds = self._D(R) zK_B = z * Ks / Bs return -self._H * numpy.exp(-(R-self._r_ref) / self._Rs) \ * numpy.sum(self._Cs / Ds * numpy.cos(self._ns * self._gamma(R, phi - self._omega * t)) * numpy.tanh(zK_B) / numpy.cosh(zK_B)**Bs,axis=0) def _phitorque(self, R, z, phi=0, t=0): """ NAME: _phitorque PURPOSE: Evaluate the azimuthal torque in cylindrical coordinates. (-dPhi/dphi) INPUT: :param R: galactocentric cylindrical radius :param z: vertical height :param phi: azimuth :param t: time OUTPUT: :return: the azimuthal torque HISTORY: 2017-05-25 Jack Hong (UBC) """ if isinstance(R,numpy.ndarray) or isinstance(z,numpy.ndarray): nR= len(R) if isinstance(R,numpy.ndarray) else len(z) self._Cs=numpy.transpose(numpy.array([self._Cs0,]*nR)) self._ns=numpy.transpose(numpy.array([self._ns0,]*nR)) self._HNn=numpy.transpose(numpy.array([self._HNn0,]*nR)) else: self._Cs=self._Cs0 self._ns=self._ns0 self._HNn=self._HNn0 g = self._gamma(R, phi - self._omega * t) Ks = self._K(R) Bs = self._B(R) Ds = self._D(R) return -self._H * numpy.exp(-(R-self._r_ref) / self._Rs) \ * numpy.sum(self._N * self._ns * self._Cs / Ds / Ks / numpy.cosh(z * Ks / Bs)**Bs * numpy.sin(self._ns * g),axis=0) def _R2deriv(self, R, z, phi=0, t=0): """ NAME: _R2deriv PURPOSE: Evaluate the second (cylindrical) radial derivative of the potential. (d^2 potential / d R^2) INPUT: :param R: galactocentric cylindrical radius :param z: vertical height :param phi: azimuth :param t: time OUTPUT: :return: the second radial derivative HISTORY: 2017-05-31 Jack Hong (UBC) """ if isinstance(R,numpy.ndarray) or isinstance(z,numpy.ndarray): nR= len(R) if isinstance(R,numpy.ndarray) else len(z) self._Cs=numpy.transpose(numpy.array([self._Cs0,]*nR)) self._ns=numpy.transpose(numpy.array([self._ns0,]*nR)) self._HNn=numpy.transpose(numpy.array([self._HNn0,]*nR)) else: self._Cs=self._Cs0 self._ns=self._ns0 self._HNn=self._HNn0 Rs = self._Rs He = self._H * numpy.exp(-(R-self._r_ref)/self._Rs) Ks = self._K(R) Bs = self._B(R) Ds = self._D(R) dKs_dR = self._dK_dR(R) dBs_dR = self._dB_dR(R) dDs_dR = self._dD_dR(R) R_sina = R * self._sin_alpha HNn_R_sina = self._HNn / R_sina HNn_R_sina_2 = HNn_R_sina**2 x = R * (0.3 * HNn_R_sina + 1) * self._sin_alpha d2Ks_dR2 = 2 * self._N * self._ns / R**3 / self._sin_alpha d2Bs_dR2 = HNn_R_sina / R**2 * (2.4 * HNn_R_sina + 2) d2Ds_dR2 = self._sin_alpha / R / x * (self._HNn* (0.18 * self._HNn * (HNn_R_sina + 0.3 * HNn_R_sina_2 + 1) / x**2 + 2 / R_sina - 0.6 * HNn_R_sina * (1 + 0.6 * HNn_R_sina) / x - 0.6 * (HNn_R_sina + 0.3 * HNn_R_sina_2 + 1) / x + 1.8 * self._HNn / R_sina**2)) g = self._gamma(R, phi - self._omega * t) dg_dR = self._dgamma_dR(R) d2g_dR2 = self._N / R**2 / self._tan_alpha sin_ng = numpy.sin(self._ns * g) cos_ng = numpy.cos(self._ns * g) zKB = z * Ks / Bs sechzKB = 1 / numpy.cosh(zKB) sechzKB_Bs = sechzKB**Bs log_sechzKB = numpy.log(sechzKB) tanhzKB = numpy.tanh(zKB) ztanhzKB = z * tanhzKB return -He / Rs * (numpy.sum(self._Cs * sechzKB_Bs / Ds * ((self._ns * dg_dR / Ks * sin_ng + cos_ng * (ztanhzKB * (dKs_dR/Ks - dBs_dR/Bs) - dBs_dR / Ks * log_sechzKB + dKs_dR / Ks**2 + dDs_dR / Ds / Ks)) - (Rs * (1 / Ks * ((ztanhzKB * (dBs_dR / Bs * Ks - dKs_dR) + log_sechzKB * dBs_dR) - dDs_dR / Ds) * (self._ns * dg_dR * sin_ng + cos_ng * (ztanhzKB * Ks * (dKs_dR/Ks - dBs_dR/Bs) - dBs_dR * log_sechzKB + dKs_dR / Ks + dDs_dR / Ds)) + (self._ns * (sin_ng * (d2g_dR2 / Ks - dg_dR / Ks**2 * dKs_dR) + dg_dR**2 / Ks * cos_ng * self._ns) + z * (-sin_ng * self._ns * dg_dR * tanhzKB * (dKs_dR/Ks - dBs_dR/Bs) + cos_ng * (z * (dKs_dR/Bs - dBs_dR/Bs**2 * Ks) * (1-tanhzKB**2) * (dKs_dR/Ks - dBs_dR/Bs) + tanhzKB * (d2Ks_dR2/Ks-(dKs_dR/Ks)**2 - d2Bs_dR2/Bs + (dBs_dR/Bs)**2))) + (cos_ng * (dBs_dR/Ks * ztanhzKB * (dKs_dR/Bs - dBs_dR/Bs**2*Ks) -(d2Bs_dR2/Ks-dBs_dR*dKs_dR/Ks**2) * log_sechzKB) + dBs_dR/Ks * log_sechzKB * sin_ng * self._ns * dg_dR) + ((cos_ng * (d2Ks_dR2 / Ks**2 - 2 * dKs_dR**2 / Ks**3) - dKs_dR / Ks**2 * sin_ng * self._ns * dg_dR) + (cos_ng * (d2Ds_dR2 / Ds / Ks - (dDs_dR/Ds)**2 / Ks - dDs_dR / Ds / Ks**2 * dKs_dR) - sin_ng * self._ns * dg_dR * dDs_dR / Ds / Ks)))) - 1 / Ks * (cos_ng / Rs + (cos_ng * ((dDs_dR * Ks + Ds * dKs_dR) / (Ds * Ks) - (ztanhzKB * (dBs_dR / Bs * Ks - dKs_dR) + log_sechzKB * dBs_dR)) + sin_ng * self._ns * dg_dR)))),axis=0)) def _z2deriv(self, R, z, phi=0, t=0): """ NAME: _z2deriv PURPOSE: Evaluate the second (cylindrical) vertical derivative of the potential. (d^2 potential / d z^2) INPUT: :param R: galactocentric cylindrical radius :param z: vertical height :param phi: azimuth :param t: time OUTPUT: :return: the second vertical derivative HISTORY: 2017-05-26 Jack Hong (UBC) """ if isinstance(R,numpy.ndarray) or isinstance(z,numpy.ndarray): nR= len(R) if isinstance(R,numpy.ndarray) else len(z) self._Cs=numpy.transpose(numpy.array([self._Cs0,]*nR)) self._ns=numpy.transpose(numpy.array([self._ns0,]*nR)) self._HNn=numpy.transpose(numpy.array([self._HNn0,]*nR)) else: self._Cs=self._Cs0 self._ns=self._ns0 self._HNn=self._HNn0 g = self._gamma(R, phi - self._omega * t) Ks = self._K(R) Bs = self._B(R) Ds = self._D(R) zKB = z * Ks / Bs tanh2_zKB = numpy.tanh(zKB)**2 return -self._H * numpy.exp(-(R-self._r_ref)/self._Rs) \ * numpy.sum(self._Cs * Ks / Ds * ((tanh2_zKB - 1) / Bs + tanh2_zKB) * numpy.cos(self._ns * g) / numpy.cosh(zKB)**Bs,axis=0) def _phi2deriv(self, R, z, phi=0, t=0): """ NAME: _phi2deriv PURPOSE: Evaluate the second azimuthal derivative of the potential in cylindrical coordinates. (d^2 potential / d phi^2) INPUT: :param R: galactocentric cylindrical radius :param z: vertical height :param phi: azimuth :param t: time OUTPUT: :return: d^2 potential / d phi^2 HISTORY: 2017-05-29 Jack Hong (UBC) """ if isinstance(R,numpy.ndarray) or isinstance(z,numpy.ndarray): nR= len(R) if isinstance(R,numpy.ndarray) else len(z) self._Cs=numpy.transpose(numpy.array([self._Cs0,]*nR)) self._ns=numpy.transpose(numpy.array([self._ns0,]*nR)) self._HNn=numpy.transpose(numpy.array([self._HNn0,]*nR)) else: self._Cs=self._Cs0 self._ns=self._ns0 self._HNn=self._HNn0 g = self._gamma(R, phi - self._omega * t) Ks = self._K(R) Bs = self._B(R) Ds = self._D(R) return self._H * numpy.exp(-(R-self._r_ref) / self._Rs) \ * numpy.sum(self._Cs * self._N**2. * self._ns**2. / Ds / Ks / numpy.cosh(z*Ks/Bs)**Bs * numpy.cos(self._ns*g),axis=0) def _Rzderiv(self, R, z, phi=0., t=0.): """ NAME: _Rzderiv PURPOSE: Evaluate the mixed (cylindrical) radial and vertical derivative of the potential (d^2 potential / dR dz). INPUT: :param R: galactocentric cylindrical radius :param z: vertical height :param phi: azimuth :param t: time OUTPUT: :return: d^2 potential / dR dz HISTORY: 2017-05-12 Jack Hong (UBC) """ if isinstance(R,numpy.ndarray) or isinstance(z,numpy.ndarray): nR= len(R) if isinstance(R,numpy.ndarray) else len(z) self._Cs=numpy.transpose(numpy.array([self._Cs0,]*nR)) self._ns=numpy.transpose(numpy.array([self._ns0,]*nR)) self._HNn=numpy.transpose(numpy.array([self._HNn0,]*nR)) else: self._Cs=self._Cs0 self._ns=self._ns0 self._HNn=self._HNn0 Rs = self._Rs He = self._H * numpy.exp(-(R-self._r_ref)/self._Rs) Ks = self._K(R) Bs = self._B(R) Ds = self._D(R) dKs_dR = self._dK_dR(R) dBs_dR = self._dB_dR(R) dDs_dR = self._dD_dR(R) g = self._gamma(R, phi - self._omega * t) dg_dR = self._dgamma_dR(R) cos_ng = numpy.cos(self._ns * g) sin_ng = numpy.sin(self._ns * g) zKB = z * Ks / Bs sechzKB = 1 / numpy.cosh(zKB) sechzKB_Bs = sechzKB**Bs log_sechzKB = numpy.log(sechzKB) tanhzKB = numpy.tanh(zKB) return - He * numpy.sum(sechzKB_Bs * self._Cs / Ds * (Ks * tanhzKB * (self._ns * dg_dR / Ks * sin_ng + cos_ng * (z * tanhzKB * (dKs_dR/Ks - dBs_dR/Bs) - dBs_dR / Ks * log_sechzKB + dKs_dR / Ks**2 + dDs_dR / Ds / Ks)) - cos_ng * ((zKB * (dKs_dR/Ks - dBs_dR/Bs) * (1 - tanhzKB**2) + tanhzKB * (dKs_dR/Ks - dBs_dR/Bs) + dBs_dR / Bs * tanhzKB) - tanhzKB / Rs)),axis=0) def _Rphideriv(self, R, z, phi=0,t=0): """ NAME: _Rphideriv PURPOSE: Return the mixed radial and azimuthal derivative of the potential in cylindrical coordinates (d^2 potential / dR dphi) INPUT: :param R: galactocentric cylindrical radius :param z: vertical height :param phi: azimuth :param t: time OUTPUT: :return: the mixed radial and azimuthal derivative HISTORY: 2017-06-09 Jack Hong (UBC) """ if isinstance(R,numpy.ndarray) or isinstance(z,numpy.ndarray): nR= len(R) if isinstance(R,numpy.ndarray) else len(z) self._Cs=numpy.transpose(numpy.array([self._Cs0,]*nR)) self._ns=numpy.transpose(numpy.array([self._ns0,]*nR)) self._HNn=numpy.transpose(numpy.array([self._HNn0,]*nR)) else: self._Cs=self._Cs0 self._ns=self._ns0 self._HNn=self._HNn0 He = self._H * numpy.exp(-(R - self._r_ref) / self._Rs) Ks = self._K(R) Bs = self._B(R) Ds = self._D(R) dKs_dR = self._dK_dR(R) dBs_dR = self._dB_dR(R) dDs_dR = self._dD_dR(R) g = self._gamma(R, phi - self._omega * t) dg_dR = self._dgamma_dR(R) cos_ng = numpy.cos(self._ns * g) sin_ng = numpy.sin(self._ns * g) zKB = z * Ks / Bs sechzKB = 1 / numpy.cosh(zKB) sechzKB_Bs = sechzKB ** Bs return - He * numpy.sum(self._Cs * sechzKB_Bs / Ds * self._ns * self._N * (- self._ns * dg_dR / Ks * cos_ng + sin_ng * (z * numpy.tanh(zKB) * (dKs_dR / Ks - dBs_dR / Bs) + 1/Ks * (-dBs_dR * numpy.log(sechzKB) + dKs_dR / Ks + dDs_dR / Ds + 1 / self._Rs))),axis=0) def _phizderiv(self, R, z, phi=0, t=0): """ NAME: _phizderiv PURPOSE: Evaluate the mixed azimuthal, vertical derivative for this potential at the given coordinates. (-dPhi/dz) INPUT: :param R: galactocentric cylindrical radius :param z: vertical height :param phi: azimuth :param t: time OUTPUT: :return: mixed azimuthal, vertical derivative HISTORY: 2021-04-30 - Jo Bovy (UofT) """ if isinstance(R,numpy.ndarray) or isinstance(z,numpy.ndarray): nR= len(R) if isinstance(R,numpy.ndarray) else len(z) self._Cs=numpy.transpose(numpy.array([self._Cs0,]*nR)) self._ns=numpy.transpose(numpy.array([self._ns0,]*nR)) self._HNn=numpy.transpose(numpy.array([self._HNn0,]*nR)) else: self._Cs=self._Cs0 self._ns=self._ns0 self._HNn=self._HNn0 Ks = self._K(R) Bs = self._B(R) Ds = self._D(R) zK_B = z * Ks / Bs return -self._H * numpy.exp(-(R-self._r_ref) / self._Rs) \ * numpy.sum(self._Cs / Ds * self._ns * self._N * numpy.sin(self._ns * self._gamma(R, phi - self._omega * t)) * numpy.tanh(zK_B) / numpy.cosh(zK_B)**Bs,axis=0) def _dens(self, R, z, phi=0, t=0): """ NAME: _dens PURPOSE: Evaluate the density. If not given, the density is computed using the Poisson equation from the first and second derivatives of the potential (if all are implemented). INPUT: :param R: galactocentric cylindrical radius :param z: vertical height :param phi: azimuth :param t: time OUTPUT: :return: the density HISTORY: 2017-05-12 Jack Hong (UBC) """ if isinstance(R,numpy.ndarray) or isinstance(z,numpy.ndarray): nR= len(R) if isinstance(R,numpy.ndarray) else len(z) self._Cs=numpy.transpose(numpy.array([self._Cs0,]*nR)) self._ns=numpy.transpose(numpy.array([self._ns0,]*nR)) self._HNn=numpy.transpose(numpy.array([self._HNn0,]*nR)) else: self._Cs=self._Cs0 self._ns=self._ns0 self._HNn=self._HNn0 g = self._gamma(R, phi - self._omega * t) Ks = self._K(R) Bs = self._B(R) Ds = self._D(R) ng = self._ns * g zKB = z * Ks / Bs sech_zKB = 1 / numpy.cosh(zKB) tanh_zKB = numpy.tanh(zKB) log_sech_zKB = numpy.log(sech_zKB) # numpy of E as defined in the appendix of the paper. E = 1 + Ks * self._H / Ds * (1 - 0.3 / (1 + 0.3 * Ks * self._H) ** 2) - R / self._Rs \ - (Ks * self._H) * (1 + 0.8 * Ks * self._H) * log_sech_zKB \ - 0.4 * (Ks * self._H) ** 2 * zKB * tanh_zKB # numpy array of rE' as define in the appendix of the paper. rE = -Ks * self._H / Ds * (1 - 0.3 * (1 - 0.3 * Ks * self._H) / (1 + 0.3 * Ks * self._H) ** 3) \ + (Ks * self._H / Ds * (1 - 0.3 / (1 + 0.3 * Ks * self._H) ** 2)) - R / self._Rs \ + Ks * self._H * (1 + 1.6 * Ks * self._H) * log_sech_zKB \ - (0.4 * (Ks * self._H) ** 2 * zKB * sech_zKB) ** 2 / Bs \ + 1.2 * (Ks * self._H) ** 2 * zKB * tanh_zKB return numpy.sum(self._Cs * self._rho0 * (self._H / (Ds * R)) * numpy.exp(-(R - self._r_ref) / self._Rs) * sech_zKB**Bs * (numpy.cos(ng) * (Ks * R * (Bs + 1) / Bs * sech_zKB**2 - 1 / Ks / R * (E**2 + rE)) - 2 * numpy.sin(ng)* E * numpy.cos(self._alpha)),axis=0) def OmegaP(self): """ NAME: OmegaP PURPOSE: Return the pattern speed. (used to compute the Jacobi integral for orbits). INPUT: :param self OUTPUT: :return: the pattern speed HISTORY: 2017-06-09 Jack Hong (UBC) """ return self._omega def _gamma(self, R, phi): """Return gamma. (eqn 3 in the paper)""" return self._N * (phi - self._phi_ref - numpy.log(R / self._r_ref) / self._tan_alpha) def _dgamma_dR(self, R): """Return the first derivative of gamma wrt R.""" return -self._N / R / self._tan_alpha def _K(self, R): """Return numpy array from K1 up to and including Kn. (eqn. 5)""" return self._ns * self._N / R / self._sin_alpha def _dK_dR(self, R): """Return numpy array of dK/dR from K1 up to and including Kn.""" return -self._ns * self._N / R**2 / self._sin_alpha def _B(self, R): """Return numpy array from B1 up to and including Bn. (eqn. 6)""" HNn_R = self._HNn / R return HNn_R / self._sin_alpha * (0.4 * HNn_R / self._sin_alpha + 1) def _dB_dR(self, R): """Return numpy array of dB/dR from B1 up to and including Bn.""" return -self._HNn / R**3 / self._sin_alpha**2 * (0.8 * self._HNn + R * self._sin_alpha) def _D(self, R): """Return numpy array from D1 up to and including Dn. (eqn. 7)""" return (0.3 * self._HNn**2 / self._sin_alpha / R + self._HNn + R * self._sin_alpha) / (0.3 * self._HNn + R * self._sin_alpha) def _dD_dR(self, R): """Return numpy array of dD/dR from D1 up to and including Dn.""" HNn_R_sina = self._HNn / R / self._sin_alpha return HNn_R_sina * (0.3 * (HNn_R_sina + 0.3 * HNn_R_sina**2. + 1) / R / (0.3 * HNn_R_sina + 1)**2 - (1/R * (1 + 0.6 * HNn_R_sina) / (0.3 * HNn_R_sina + 1))) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/SteadyLogSpiralPotential.py0000644000175100001710000001617214327773303022667 0ustar00runnerdocker############################################################################### # SteadyLogSpiralPotential: a steady-state spiral potential ############################################################################### import numpy from ..util import conversion from .planarPotential import planarPotential _degtorad= numpy.pi/180. class SteadyLogSpiralPotential(planarPotential): """Class that implements a steady-state spiral potential .. math:: \\Phi(R,\\phi) = \\frac{\\mathrm{amp}\\times A}{\\alpha}\\,\\cos\\left(\\alpha\\,\\ln R - m\\,(\\phi-\\Omega_s\\,t-\\gamma)\\right) Can be grown in a similar way as the DehnenBarPotential, but using :math:`T_s = 2\\pi/\\Omega_s` to normalize :math:`t_{\\mathrm{form}}` and :math:`T_{\\mathrm{steady}}`. """ def __init__(self,amp=1.,omegas=0.65,A=-0.035, alpha=-7.,m=2,gamma=numpy.pi/4.,p=None, tform=None,tsteady=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a logarithmic spiral potential INPUT: amp - amplitude to be applied to the potential (default: 1., A below) gamma - angle between sun-GC line and the line connecting the peak of the spiral pattern at the Solar radius (in rad; default=45 degree; or can be Quantity) A - amplitude (alpha*potential-amplitude; default=0.035; can be Quantity omegas= - pattern speed (default=0.65; can be Quantity) m= number of arms Either provide: a) alpha= b) p= pitch angle (rad; can be Quantity) tform - start of spiral growth / spiral period (default: -Infinity) tsteady - time from tform at which the spiral is fully grown / spiral period (default: 2 periods) OUTPUT: (none) HISTORY: 2011-03-27 - Started - Bovy (NYU) """ planarPotential.__init__(self,amp=amp,ro=ro,vo=vo) gamma= conversion.parse_angle(gamma) p= conversion.parse_angle(p) A= conversion.parse_energy(A,vo=self._vo) omegas= conversion.parse_frequency(omegas,ro=self._ro,vo=self._vo) self._omegas= omegas self._A= A self._m= m self._gamma= gamma if not p is None: self._alpha= self._m/numpy.tan(p) else: self._alpha= alpha self._ts= 2.*numpy.pi/self._omegas if not tform is None: self._tform= tform*self._ts else: self._tform= None if not tsteady is None: self._tsteady= self._tform+tsteady*self._ts else: if self._tform is None: self._tsteady= None else: self._tsteady= self._tform+2.*self._ts self.hasC= True def _evaluate(self,R,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,phi,t INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: Phi(R,phi,t) HISTORY: 2011-03-27 - Started - Bovy (NYU) """ if not self._tform is None: if t < self._tform: smooth= 0. elif t < self._tsteady: deltat= t-self._tform xi= 2.*deltat/(self._tsteady-self._tform)-1. smooth= (3./16.*xi**5.-5./8*xi**3.+15./16.*xi+.5) else: #spiral is fully on smooth= 1. else: smooth= 1. return smooth*self._A/self._alpha*numpy.cos(self._alpha*numpy.log(R) -self._m*(phi-self._omegas*t -self._gamma)) def _Rforce(self,R,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the radial force HISTORY: 2010-11-24 - Written - Bovy (NYU) """ if not self._tform is None: if t < self._tform: smooth= 0. elif t < self._tsteady: deltat= t-self._tform xi= 2.*deltat/(self._tsteady-self._tform)-1. smooth= (3./16.*xi**5.-5./8*xi**3.+15./16.*xi+.5) else: #spiral is fully on smooth= 1. else: smooth= 1. return smooth*self._A/R*numpy.sin(self._alpha*numpy.log(R) -self._m*(phi-self._omegas*t -self._gamma)) def _phitorque(self,R,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the azimuthal torque HISTORY: 2010-11-24 - Written - Bovy (NYU) """ if not self._tform is None: if t < self._tform: smooth= 0. elif t < self._tsteady: deltat= t-self._tform xi= 2.*deltat/(self._tsteady-self._tform)-1. smooth= (3./16.*xi**5.-5./8*xi**3.+15./16.*xi+.5) else: #spiral is fully on smooth= 1. else: smooth= 1. return -smooth*self._A/self._alpha*self._m*numpy.sin(self._alpha*numpy.log(R) -self._m*(phi -self._omegas*t -self._gamma)) def wavenumber(self,R): """ NAME: wavenumber PURPOSE: return the wavenumber at radius R (d f(R)/ d R in Phi_a(R) = F(R) e^[i f(R)]; see Binney & Tremaine 2008) INPUT: R - Cylindrical radius OUTPUT: wavenumber at R HISTORY: 2014-08-23 - Written - Bovy (IAS) """ return self._alpha/R def OmegaP(self): """ NAME: OmegaP PURPOSE: return the pattern speed INPUT: (none) OUTPUT: pattern speed HISTORY: 2011-10-10 - Written - Bovy (IAS) """ return self._omegas def m(self): """ NAME: m PURPOSE: return the number of arms INPUT: (none) OUTPUT: number of arms HISTORY: 2014-08-23 - Written - Bovy (IAS) """ return self._m def tform(self): #pragma: no cover """ NAME: tform PURPOSE: return formation time of the bar INPUT: (none) OUTPUT: tform in normalized units HISTORY: 2011-03-08 - Written - Bovy (NYU) """ return self._tform ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/TimeDependentAmplitudeWrapperPotential.py0000644000175100001710000000551214327773303025550 0ustar00runnerdocker############################################################################### # TimeDependentAmplitudeWrapperPotential.py: Wrapper to change the amplitude # of any potential with an # arbitrary function of time ############################################################################### from inspect import signature from numbers import Number from numpy import empty from .WrapperPotential import parentWrapperPotential class TimeDependentAmplitudeWrapperPotential(parentWrapperPotential): """Potential wrapper class that allows the amplitude of any potential to be any function of time. That is, the amplitude of a potential gets modulated to .. math:: \\mathrm{amp} \\rightarrow \\mathrm{amp} \\times A(t) where :math:`A(t)` is an arbitrary function of time. Note that `amp` itself can already be a function of time. """ def __init__(self,amp=1.,A=None,pot=None,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a TimeDependentAmplitudeWrapperPotential INPUT: amp - amplitude to be applied to the potential (default: 1.) A - function of time giving the time-dependence of the amplitude; should be able to be called with a single time and return a numbers.Number (that is, a number); input time is in internal units (see galpy.util.conversion.time_in_Gyr to convert) and output is a dimensionless amplitude modulation pot - Potential instance or list thereof; the amplitude of this will modified by this wrapper OUTPUT: (none) HISTORY: 2022-03-29 - Started - Bovy (UofT) """ if not callable(A): raise TypeError("A= input to TimeDependentAmplitudeWrapperPotential should be a function") # Check whether there is at least a single parameter and not more than a single # argument, such that the function can be called as A(t) Aparams= signature(A).parameters nparams= 0 for param in Aparams.keys(): if Aparams[param].default == Aparams[param].empty \ or nparams == 0: nparams+= 1 if nparams != 1: raise TypeError("A= input to TimeDependentAmplitudeWrapperPotential should be a function that can be called with a single parameter") # Finally, check that A only returns a single value if not isinstance(A(0.),Number): raise TypeError("A= function needs to return a number (specifically, a numbers.Number)") self._A= A self.hasC= True self.hasC_dxdv= True def _wrap(self,attribute,*args,**kwargs): return self._A(kwargs.get('t',0.))\ *self._wrap_pot_func(attribute)(self._pot,*args,**kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/TransientLogSpiralPotential.py0000644000175100001710000001114014327773303023373 0ustar00runnerdocker############################################################################### # TransientLogSpiralPotential: a transient spiral potential ############################################################################### import numpy from ..util import conversion from .planarPotential import planarPotential _degtorad= numpy.pi/180. class TransientLogSpiralPotential(planarPotential): """Class that implements a steady-state spiral potential .. math:: \\Phi(R,\\phi) = \\frac{\\mathrm{amp}(t)}{\\alpha}\\,\\cos\\left(\\alpha\\,\\ln R - m\\,(\\phi-\\Omega_s\\,t-\\gamma)\\right) where .. math:: \\mathrm{amp}(t) = \\mathrm{amp}\\,\\times A\\,\\exp\\left(-\\frac{[t-t_0]^2}{2\\,\\sigma^2}\\right) """ def __init__(self,amp=1.,omegas=0.65,A=-0.035, alpha=-7.,m=2,gamma=numpy.pi/4.,p=None, sigma=1.,to=0.,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a transient logarithmic spiral potential localized around to INPUT: amp - amplitude to be applied to the potential (default: 1., A below) gamma - angle between sun-GC line and the line connecting the peak of the spiral pattern at the Solar radius (in rad; default=45 degree; can be Quantity) A - amplitude (alpha*potential-amplitude; default=0.035; can be Quantity) omegas= - pattern speed (default=0.65; can be Quantity) m= number of arms to= time at which the spiral peaks (can be Quantity) sigma= "spiral duration" (sigma in Gaussian amplitude; can be Quantity) Either provide: a) alpha= b) p= pitch angle (rad; can be Quantity) OUTPUT: (none) HISTORY: 2011-03-27 - Started - Bovy (NYU) """ planarPotential.__init__(self,amp=amp,ro=ro,vo=vo) gamma= conversion.parse_angle(gamma) p= conversion.parse_angle(p) A= conversion.parse_energy(A,vo=self._vo) omegas= conversion.parse_frequency(omegas,ro=self._ro,vo=self._vo) to= conversion.parse_time(to,ro=self._ro,vo=self._vo) sigma= conversion.parse_time(sigma,ro=self._ro,vo=self._vo) self._omegas= omegas self._A= A self._m= m self._gamma= gamma self._to= to self._sigma2= sigma**2. if not p is None: self._alpha= self._m/numpy.tan(p) else: self._alpha= alpha self.hasC= True def _evaluate(self,R,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,phi,t INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: Phi(R,phi,t) HISTORY: 2011-03-27 - Started - Bovy (NYU) """ return self._A*numpy.exp(-(t-self._to)**2./2./self._sigma2)\ /self._alpha*numpy.cos(self._alpha*numpy.log(R) -self._m*(phi-self._omegas*t-self._gamma)) def _Rforce(self,R,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the radial force HISTORY: 2010-11-24 - Written - Bovy (NYU) """ return self._A*numpy.exp(-(t-self._to)**2./2./self._sigma2)\ /R*numpy.sin(self._alpha*numpy.log(R) -self._m*(phi-self._omegas*t-self._gamma)) def _phitorque(self,R,phi=0.,t=0.): """ NAME: _phitorque PURPOSE: evaluate the azimuthal torque for this potential INPUT: R - Galactocentric cylindrical radius phi - azimuth t - time OUTPUT: the azimuthal torque HISTORY: 2010-11-24 - Written - Bovy (NYU) """ return -self._A*numpy.exp(-(t-self._to)**2./2./self._sigma2)\ /self._alpha*self._m*numpy.sin(self._alpha*numpy.log(R) -self._m*(phi-self._omegas*t -self._gamma)) def OmegaP(self): """ NAME: OmegaP PURPOSE: return the pattern speed INPUT: (none) OUTPUT: pattern speed HISTORY: 2011-10-10 - Written - Bovy (IAS) """ return self._omegas ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/TriaxialGaussianPotential.py0000644000175100001710000000767614327773303023102 0ustar00runnerdocker############################################################################### # TriaxialGaussianPotential.py: Potential of a triaxial Gaussian stratified # on similar ellipsoids # # \rho(x,y,z) ~ exp(-m^2/[2\sigma^2]) # # with m^2 = x^2+y^2/b^2+z^2/c^2 # ############################################################################### import numpy from scipy import special from ..util import conversion from .EllipsoidalPotential import EllipsoidalPotential class TriaxialGaussianPotential(EllipsoidalPotential): """Potential of a triaxial Gaussian (`Emsellem et al. 1994 `__): .. math:: \\rho(x,y,z) = \\frac{\\mathrm{amp}}{(2\\pi\\,\\sigma)^{3/2}\\,b\\,c}\\,e^{-\\frac{m^2}{2\\sigma^2}} where :math:`\\mathrm{amp} = GM` is the total mass and :math:`m^2 = x^2+y^2/b^2+z^2/c^2`. """ def __init__(self,amp=1.,sigma=5.,b=1.,c=1., zvec=None,pa=None,glorder=50, normalize=False,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a Gaussian potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass sigma - Gaussian dispersion scale (can be Quantity) b - y-to-x axis ratio of the density c - z-to-x axis ratio of the density zvec= (None) If set, a unit vector that corresponds to the z axis pa= (None) If set, the position angle of the x axis (rad or Quantity) glorder= (50) if set, compute the relevant force and potential integrals with Gaussian quadrature of this order ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2020-08-18 - Started - Bovy (UofT) """ EllipsoidalPotential.__init__(self,amp=amp,b=b,c=c, zvec=zvec,pa=pa,glorder=glorder, ro=ro,vo=vo,amp_units='mass') sigma= conversion.parse_length(sigma,ro=self._ro) self._sigma= sigma self._twosigma2= 2.*self._sigma**2 self._scale= self._sigma # Adjust amp self._amp/= (2.*numpy.pi)**1.5*self._sigma**3.*self._b*self._c if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) self.hasC= not self._glorder is None self.hasC_dxdv= False self.hasC_dens= self.hasC # works if mdens is defined, necessary for hasC return None def _psi(self,m): """\\psi(m) = -\\int_m^\\infty d m^2 \rho(m^2)""" return -self._twosigma2*numpy.exp(-m**2./self._twosigma2) def _mdens(self,m): """Density as a function of m""" return numpy.exp(-m**2/self._twosigma2) def _mdens_deriv(self,m): """Derivative of the density as a function of m""" return -2.*m*numpy.exp(-m**2/self._twosigma2)/self._twosigma2 def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R (and z) for this potential; if z=None, integrate to ellipsoidal boundary INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2021-03-09 - Written - Bovy (UofT) """ if not z is None: raise AttributeError # Hack to fall back to general return numpy.pi*self._b*self._c*self._twosigma2*self._sigma\ *(numpy.sqrt(2.*numpy.pi)*special.erf(R/self._sigma/numpy.sqrt(2.)) -2.*R/self._sigma*numpy.exp(-R**2./self._twosigma2)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/TwoPowerSphericalPotential.py0000644000175100001710000014306314327773303023242 0ustar00runnerdocker############################################################################### # TwoPowerSphericalPotential.py: General class for potentials derived from # densities with two power-laws # # amp # rho(r)= ------------------------------------ # (r/a)^\alpha (1+r/a)^(\beta-\alpha) ############################################################################### import numpy from scipy import optimize, special from ..util import conversion from ..util._optional_deps import _APY_LOADED, _JAX_LOADED from .Potential import Potential, kms_to_kpcGyrDecorator if _APY_LOADED: from astropy import units if _JAX_LOADED: import jax.numpy as jnp class TwoPowerSphericalPotential(Potential): """Class that implements spherical potentials that are derived from two-power density models .. math:: \\rho(r) = \\frac{\\mathrm{amp}}{4\\,\\pi\\,a^3}\\,\\frac{1}{(r/a)^\\alpha\\,(1+r/a)^{\\beta-\\alpha}} """ def __init__(self,amp=1.,a=5.,alpha=1.5,beta=3.5,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a two-power-density potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a - scale radius (can be Quantity) alpha - inner power beta - outer power normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2010-07-09 - Started - Bovy (NYU) """ # Instantiate Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') # _specialSelf for special cases (Dehnen class, Dehnen core, Hernquist, Jaffe, NFW) self._specialSelf= None if ((self.__class__ == TwoPowerSphericalPotential) & (alpha == round(alpha)) & (beta == round(beta))): if int(alpha) == 0 and int(beta) == 4: self._specialSelf=\ DehnenCoreSphericalPotential(amp=1.,a=a, normalize=False) elif int(alpha) == 1 and int(beta) == 4: self._specialSelf=\ HernquistPotential(amp=1.,a=a,normalize=False) elif int(alpha) == 2 and int(beta) == 4: self._specialSelf= JaffePotential(amp=1.,a=a,normalize=False) elif int(alpha) == 1 and int(beta) == 3: self._specialSelf= NFWPotential(amp=1.,a=a,normalize=False) # correcting quantities a= conversion.parse_length(a,ro=self._ro) # setting properties self.a= a self._scale= self.a self.alpha= alpha self.beta= beta if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2010-07-09 - Started - Bovy (NYU) """ if self._specialSelf is not None: return self._specialSelf._evaluate(R,z,phi=phi,t=t) elif self.beta == 3.: r= numpy.sqrt(R**2.+z**2.) return (1./self.a)\ *(r-self.a*(r/self.a)**(3.-self.alpha)/(3.-self.alpha)\ *special.hyp2f1(3.-self.alpha, 2.-self.alpha, 4.-self.alpha, -r/self.a))/(self.alpha-2.)/r else: r= numpy.sqrt(R**2.+z**2.) return special.gamma(self.beta-3.)\ *((r/self.a)**(3.-self.beta)/special.gamma(self.beta-1.)\ *special.hyp2f1(self.beta-3., self.beta-self.alpha, self.beta-1., -self.a/r) -special.gamma(3.-self.alpha)/special.gamma(self.beta-self.alpha))/r def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2010-07-09 - Written - Bovy (NYU) """ if self._specialSelf is not None: return self._specialSelf._Rforce(R,z,phi=phi,t=t) else: r= numpy.sqrt(R**2.+z**2.) return -R/r**self.alpha*self.a**(self.alpha-3.)/(3.-self.alpha)\ *special.hyp2f1(3.-self.alpha, self.beta-self.alpha, 4.-self.alpha, -r/self.a) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2010-07-09 - Written - Bovy (NYU) """ if self._specialSelf is not None: return self._specialSelf._zforce(R,z,phi=phi,t=t) else: r= numpy.sqrt(R**2.+z**2.) return -z/r**self.alpha*self.a**(self.alpha-3.)/(3.-self.alpha)\ *special.hyp2f1(3.-self.alpha, self.beta-self.alpha, 4.-self.alpha, -r/self.a) def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2010-08-08 - Written - Bovy (NYU) """ r= numpy.sqrt(R**2.+z**2.) return (self.a/r)**self.alpha/(1.+r/self.a)**(self.beta-self.alpha)/4./numpy.pi/self.a**3. def _ddensdr(self,r,t=0.): """ NAME: _ddensdr PURPOSE: s evaluate the radial density derivative for this potential INPUT: r - spherical radius t= time OUTPUT: the density derivative HISTORY: 2021-02-05 - Written - Bovy (UofT) """ return -self._amp*(self.a/r)**(self.alpha-1.)\ *(1.+r/self.a)**(self.alpha-self.beta-1.)\ *(self.a*self.alpha+r*self.beta)/r**2/4./numpy.pi/self.a**3. def _d2densdr2(self,r,t=0.): """ NAME: _d2densdr2 PURPOSE: evaluate the second radial density derivative for this potential INPUT: r - spherical radius t= time OUTPUT: the 2nd density derivative HISTORY: 2021-02-05 - Written - Bovy (UofT) """ return self._amp*(self.a/r)**(self.alpha-2.)\ *(1.+r/self.a)**(self.alpha-self.beta-2.)\ *(self.alpha*(self.alpha+1.)*self.a**2+ 2.*self.alpha*self.a*(self.beta+1.)*r +self.beta*(self.beta+1.)*r**2)/r**4/4./numpy.pi/self.a**3. def _ddenstwobetadr(self,r,beta=0): """ NAME: _ddenstwobetadr PURPOSE: evaluate the radial density derivative x r^(2beta) for this potential INPUT: r - spherical radius beta= (0) OUTPUT: d (rho x r^{2beta} ) / d r HISTORY: 2021-02-14 - Written - Bovy (UofT) """ return self._amp/4./numpy.pi/self.a**3.\ *r**(2.*beta-2.)*(self.a/r)**(self.alpha-1.)\ *(1.+r/self.a)**(self.alpha-self.beta-1.)\ *(self.a*(2.*beta-self.alpha)+r*(2.*beta-self.beta)) def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second cylindrically radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second cylindrically radial derivative HISTORY: 2020-11-23 - Written - Beane (CfA) """ r = numpy.sqrt(R**2.+z**2.) A = self.a**(self.alpha-3.)/(3.-self.alpha) hyper = special.hyp2f1(3.-self.alpha, self.beta-self.alpha, 4.-self.alpha, -r/self.a) hyper_deriv = (3.-self.alpha) * (self.beta - self.alpha) / (4.-self.alpha) \ * special.hyp2f1(4.-self.alpha, 1.+self.beta-self.alpha, 5.-self.alpha, -r/self.a) term1 = A * r**(-self.alpha) * hyper term2 = -self.alpha * A * R**2. * r**(-self.alpha-2.) * hyper term3 = -A * R**2 * r**(-self.alpha-1.) / self.a * hyper_deriv return term1 + term2 + term3 def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the mixed radial/vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the mixed radial/vertical derivative HISTORY: 2020-11-28 - Written - Beane (CfA) """ r = numpy.sqrt(R**2.+z**2.) A = self.a**(self.alpha-3.)/(3.-self.alpha) hyper = special.hyp2f1(3.-self.alpha, self.beta-self.alpha, 4.-self.alpha, -r/self.a) hyper_deriv = (3.-self.alpha) * (self.beta - self.alpha) / (4.-self.alpha) \ * special.hyp2f1(4.-self.alpha, 1.+self.beta-self.alpha, 5.-self.alpha, -r/self.a) term1 = -self.alpha * A * R * r**(-self.alpha-2.) * z * hyper term2 = -A * R * r**(-self.alpha-1.) * z / self.a * hyper_deriv return term1 + term2 def _z2deriv(self,R,z,phi=0.,t=0.): """ NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second vertical derivative HISTORY: 2012-07-26 - Written - Bovy (IAS@MPIA) """ return self._R2deriv(numpy.fabs(z),R) #Spherical potential def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2014-04-01 - Written - Erkal (IoA) """ if z is not None: raise AttributeError # use general implementation return (R/self.a)**(3.-self.alpha)/(3.-self.alpha)\ *special.hyp2f1(3.-self.alpha,-self.alpha+self.beta, 4.-self.alpha,-R/self.a) class DehnenSphericalPotential(TwoPowerSphericalPotential): """Class that implements the Dehnen Spherical Potential from `Dehnen (1993) `_ .. math:: \\rho(r) = \\frac{\\mathrm{amp}(3-\\alpha)}{4\\,\\pi\\,a^3}\\,\\frac{1}{(r/a)^{\\alpha}\\,(1+r/a)^{4-\\alpha}} """ def __init__(self,amp=1.,a=1.,alpha=1.5,normalize=False,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a Dehnen Spherical Potential; note that the amplitude definition used here does NOT match that of Dehnen (1993) INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a - scale radius (can be Quantity) alpha - inner power, restricted to [0, 3) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2019-10-07 - Started - Starkman (UofT) """ if (alpha < 0.) or (alpha >= 3.): raise OSError('DehnenSphericalPotential requires 0 <= alpha < 3') # instantiate TwoPowerSphericalPotential.__init__( self,amp=amp,a=a,alpha=alpha,beta=4, normalize=normalize,ro=ro,vo=vo) # make special-self and protect subclasses self._specialSelf= None if ((self.__class__ == DehnenSphericalPotential) & (alpha == round(alpha))): if round(alpha) == 0: self._specialSelf=\ DehnenCoreSphericalPotential(amp=1.,a=a, normalize=False) elif round(alpha) == 1: self._specialSelf=\ HernquistPotential(amp=1.,a=a,normalize=False) elif round(alpha) == 2: self._specialSelf= JaffePotential(amp=1.,a=a,normalize=False) # set properties self.hasC= True self.hasC_dxdv= True self.hasC_dens= True return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2019-11-20 - Written - Starkman (UofT) """ if self._specialSelf is not None: return self._specialSelf._evaluate(R,z,phi=phi,t=t) else: # valid for alpha != 2, 3 r= numpy.sqrt(R**2.+z**2.) return -(1.-1./(1.+self.a/r)**(2.-self.alpha))/\ (self.a * (2.-self.alpha) * (3.-self.alpha)) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2019-11-20 - Written - Starkman (UofT) """ if self._specialSelf is not None: return self._specialSelf._Rforce(R,z,phi=phi,t=t) else: r= numpy.sqrt(R**2.+z**2.) return -R/r**self.alpha*(self.a+r)**(self.alpha-3.)/(3.-self.alpha) def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second radial derivative HISTORY: 2019-10-11 - Written - Starkman (UofT) """ if self._specialSelf is not None: return self._specialSelf._R2deriv(R, z, phi=phi, t=t) a, alpha = self.a, self.alpha r = numpy.sqrt(R**2. + z**2.) # formula not valid for alpha=2,3, (integers?) return (numpy.power(r, -2.-alpha)*numpy.power(r+a, alpha-4.)* (-a*r**2. + (2.*R**2.-z**2.)*r + a*alpha*R**2.)/ (alpha - 3.)) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2019-11-21 - Written - Starkman (UofT) """ if self._specialSelf is not None: return self._specialSelf._zforce(R,z,phi=phi,t=t) else: r= numpy.sqrt(R**2.+z**2.) return -z/r**self.alpha*(self.a+r)**(self.alpha-3.)/(3.-self.alpha) def _z2deriv(self,R,z,phi=0.,t=0.): r""" NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second vertical derivative HISTORY: 2019-10-20 - Written - Starkman (UofT) """ return self._R2deriv(z,R,phi=phi,t=t) def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: d2phi/dR/dz HISTORY: 2019-10-11 - Written - Starkman (UofT) """ if self._specialSelf is not None: return self._specialSelf._Rzderiv(R, z, phi=phi, t=t) a, alpha= self.a, self.alpha r= numpy.sqrt(R**2.+z**2.) return ((R*z*numpy.power(r,-2.-alpha)*numpy.power(a+r,alpha-4.) *(3*r+a*alpha))/(alpha-3)) def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2019-11-20 - Written - Starkman (UofT) """ r= numpy.sqrt(R**2.+z**2.) return (self.a/r)**self.alpha/(1.+r/self.a)**(4.-self.alpha)/4./numpy.pi/self.a**3. def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2019-11-20 - Written - Starkman (UofT) """ if z is not None: raise AttributeError # use general implementation return 1./(1.+self.a/R)**(3.-self.alpha)/(3.-self.alpha) # written so it works for r=numpy.inf class DehnenCoreSphericalPotential(DehnenSphericalPotential): """Class that implements the Dehnen Spherical Potential from `Dehnen (1993) `_ with alpha=0 (corresponding to an inner core) .. math:: \\rho(r) = \\frac{\\mathrm{amp}}{12\\,\\pi\\,a^3}\\,\\frac{1}{(1+r/a)^{4}} """ def __init__(self,amp=1.,a=1.,normalize=False,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a cored Dehnen Spherical Potential; note that the amplitude definition used here does NOT match that of Dehnen (1993) INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a - scale radius (can be Quantity) alpha - inner power, restricted to [0, 3) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2019-10-07 - Started - Starkman (UofT) """ DehnenSphericalPotential.__init__( self,amp=amp,a=a,alpha=0, normalize=normalize,ro=ro,vo=vo) # set properties explicitly self.hasC= True self.hasC_dxdv= True self.hasC_dens= True return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2019-11-20 - Written - Starkman (UofT) """ r= numpy.sqrt(R**2.+z**2.) return -(1.-1./(1.+self.a/r)**2.)/(6.*self.a) def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2019-11-20 - Written - Starkman (UofT) """ return -R/numpy.power(numpy.sqrt(R**2.+z**2.)+self.a,3.)/3. def _rforce_jax(self,r): """ NAME: _rforce_jax PURPOSE: evaluate the spherical radial force for this potential using JAX INPUT: r - Galactocentric spherical radius OUTPUT: the radial force HISTORY: 2021-02-25 - Written - Bovy (UofT) """ # No need for actual JAX! return -self._amp*r/(r+self.a)**3./3. def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second radial derivative HISTORY: 2019-10-11 - Written - Starkman (UofT) """ r = numpy.sqrt(R**2.+z**2.) return -(((2.*R**2.-z**2.)-self.a*r)/(3.*r*numpy.power(r+self.a,4.))) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2019-11-21 - Written - Starkman (UofT) """ r= numpy.sqrt(R**2.+z**2.) return -z/numpy.power(self.a+r,3.)/3. def _z2deriv(self,R,z,phi=0.,t=0.): r""" NAME: _z2deriv PURPOSE: evaluate the second vertical derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second vertical derivative HISTORY: 2019-10-20 - Written - Starkman (UofT) """ return self._R2deriv(z,R,phi=phi,t=t) def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: d2phi/dR/dz HISTORY: 2019-10-11 - Written - Starkman (UofT) """ a= self.a r= numpy.sqrt(R**2.+z**2.) return -(R * z/r/numpy.power(a+r,4.)) def _dens(self,R,z,phi=0.,t=0.): """ NAME: _dens PURPOSE: evaluate the density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the density HISTORY: 2019-11-20 - Written - Starkman (UofT) """ r= numpy.sqrt(R**2.+z**2.) return 1./(1.+r/self.a)**4./4./numpy.pi/self.a**3. def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2019-11-20 - Written - Starkman (UofT) """ if z is not None: raise AttributeError # use general implementation return 1./(1.+self.a/R)**3./3. # written so it works for r=numpy.inf class HernquistPotential(DehnenSphericalPotential): """Class that implements the Hernquist potential .. math:: \\rho(r) = \\frac{\\mathrm{amp}}{4\\,\\pi\\,a^3}\\,\\frac{1}{(r/a)\\,(1+r/a)^{3}} """ def __init__(self,amp=1.,a=1.,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a Hernquist potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass (note that amp is 2 x [total mass] for the chosen definition of the Hernquist potential) a - scale radius (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2010-07-09 - Written - Bovy (NYU) """ DehnenSphericalPotential.__init__( self,amp=amp,a=a,alpha=1, normalize=normalize,ro=ro,vo=vo) self._nemo_accname= 'Dehnen' # set properties explicitly self.hasC= True self.hasC_dxdv= True self.hasC_dens= True return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2010-07-09 - Started - Bovy (NYU) """ return -1./(1.+numpy.sqrt(R**2.+z**2.)/self.a)/2./self.a def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the radial force HISTORY: 2010-07-09 - Written - Bovy (NYU) """ sqrtRz= numpy.sqrt(R**2.+z**2.) return -R/self.a/sqrtRz/(1.+sqrtRz/self.a)**2./2./self.a def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the vertical force HISTORY: 2010-07-09 - Written - Bovy (NYU) """ sqrtRz= numpy.sqrt(R**2.+z**2.) return -z/self.a/sqrtRz/(1.+sqrtRz/self.a)**2./2./self.a def _rforce_jax(self,r): """ NAME: _rforce_jax PURPOSE: evaluate the spherical radial force for this potential using JAX INPUT: r - Galactocentric spherical radius OUTPUT: the radial force HISTORY: 2021-02-14 - Written - Bovy (UofT) """ # No need for actual JAX! return -self._amp/2./(r+self.a)**2. def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: the second radial derivative HISTORY: 2011-10-09 - Written - Bovy (IAS) """ sqrtRz= numpy.sqrt(R**2.+z**2.) return (self.a*z**2.+(z**2.-2.*R**2.)*sqrtRz)/sqrtRz**3.\ /(self.a+sqrtRz)**3./2. def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t- time OUTPUT: d2phi/dR/dz HISTORY: 2013-08-28 - Written - Bovy (IAS) """ sqrtRz= numpy.sqrt(R**2.+z**2.) return -R*z*(self.a+3.*sqrtRz)*(sqrtRz*(self.a+sqrtRz))**-3./2. def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the surface density HISTORY: 2018-08-19 - Written - Bovy (UofT) """ r= numpy.sqrt(R**2.+z**2.) Rma= numpy.sqrt(R**2.-self.a**2.+0j) if Rma == 0.: return (-12.*self.a**3-5.*self.a*z**2 +numpy.sqrt(1.+z**2/self.a**2)\ *(12.*self.a**3-self.a*z**2+2/self.a*z**4))\ /30./numpy.pi*z**-5. else: return self.a*((2.*self.a**2.+R**2.)*Rma**-5\ *(numpy.arctan(z/Rma)-numpy.arctan(self.a*z/r/Rma)) +z*(5.*self.a**3.*r-4.*self.a**4 +self.a**2*(2.*r**2.+R**2) -self.a*r*(5.*R**2.+3.*z**2.)+R**2.*r**2.) /(self.a**2.-R**2.)**2. /(r**2-self.a**2.)**2.).real/4./numpy.pi def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: calculate the mass out to a given radius INPUT: R - radius at which to return the enclosed mass z - (don't specify this) vertical height OUTPUT: mass in natural units HISTORY: 2014-01-29 - Written - Bovy (IAS) """ if z is not None: raise AttributeError # use general implementation return 1./(1.+self.a/R)**2./2. # written so it works for r=numpy.inf @kms_to_kpcGyrDecorator def _nemo_accpars(self,vo,ro): """ NAME: _nemo_accpars PURPOSE: return the accpars potential parameters for use of this potential with NEMO INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: accpars string HISTORY: 2018-09-14 - Written - Bovy (UofT) """ GM= self._amp*vo**2.*ro/2. return f"0,1,{GM},{self.a*ro},0" class JaffePotential(DehnenSphericalPotential): """Class that implements the Jaffe potential .. math:: \\rho(r) = \\frac{\\mathrm{amp}}{4\\,\\pi\\,a^3}\\,\\frac{1}{(r/a)^2\\,(1+r/a)^{2}} """ def __init__(self,amp=1.,a=1.,normalize=False, ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a Jaffe potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a - scale radius (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2010-07-09 - Written - Bovy (NYU) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) self.a= a self._scale= self.a self.alpha= 2 self.beta= 4 if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) self.hasC= True self.hasC_dxdv= True self.hasC_dens= True return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2010-07-09 - Started - Bovy (NYU) """ return -numpy.log(1.+self.a/numpy.sqrt(R**2.+z**2.))/self.a def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2010-07-09 - Written - Bovy (NYU) """ sqrtRz= numpy.sqrt(R**2.+z**2.) return -R/sqrtRz**3./(1.+self.a/sqrtRz) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2010-07-09 - Written - Bovy (NYU) """ sqrtRz= numpy.sqrt(R**2.+z**2.) return -z/sqrtRz**3./(1.+self.a/sqrtRz) def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2011-10-09 - Written - Bovy (IAS) """ sqrtRz= numpy.sqrt(R**2.+z**2.) return (self.a*(z**2.-R**2.)+(z**2.-2.*R**2.)*sqrtRz)\ /sqrtRz**4./(self.a+sqrtRz)**2. def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2phi/dR/dz HISTORY: 2013-08-28 - Written - Bovy (IAS) """ sqrtRz= numpy.sqrt(R**2.+z**2.) return -R*z*(2.*self.a+3.*sqrtRz)*sqrtRz**-4.\ *(self.a+sqrtRz)**-2. def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the surface density HISTORY: 2018-08-19 - Written - Bovy (UofT) """ r= numpy.sqrt(R**2.+z**2.) Rma= numpy.sqrt(R**2.-self.a**2.+0j) if Rma == 0.: return (3.*z**2.-2.*self.a**2. +2.*numpy.sqrt(1.+(z/self.a)**2.)\ *(self.a**2.-2.*z**2.) +3.*z**3./self.a*numpy.arctan(z/self.a))\ /self.a/z**3./6./numpy.pi else: return ((2.*self.a**2.-R**2.)*Rma**-3\ *(numpy.arctan(z/Rma)-numpy.arctan(self.a*z/r/Rma)) +numpy.arctan(z/R)/R -self.a*z/(R**2-self.a**2)/(r+self.a)).real\ /self.a/2./numpy.pi def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: calculate the mass out to a given radius INPUT: R - radius at which to return the enclosed mass z - (don't specify this) vertical height OUTPUT: mass in natural units HISTORY: 2014-01-29 - Written - Bovy (IAS) """ if z is not None: raise AttributeError # use general implementation return 1./(1.+self.a/R) # written so it works for r=numpy.inf class NFWPotential(TwoPowerSphericalPotential): """Class that implements the NFW potential .. math:: \\rho(r) = \\frac{\\mathrm{amp}}{4\\,\\pi\\,a^3}\\,\\frac{1}{(r/a)\\,(1+r/a)^{2}} """ def __init__(self,amp=1.,a=1.,normalize=False, rmax=None,vmax=None, conc=None,mvir=None, vo=None,ro=None, H=70.,Om=0.3,overdens=200.,wrtcrit=False): """ NAME: __init__ PURPOSE: Initialize a NFW potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a - scale radius (can be Quantity) normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. Alternatively, NFW potentials can be initialized in the following two manners: a) rmax= radius where the rotation curve peaks (can be a Quantity, otherwise assumed to be in internal units) vmax= maximum circular velocity (can be a Quantity, otherwise assumed to be in internal units) b) conc= concentration mvir= virial mass in 10^12 Msolar in which case you also need to supply the following keywords H= (default: 70) Hubble constant in km/s/Mpc Om= (default: 0.3) Omega matter overdens= (200) overdensity which defines the virial radius wrtcrit= (False) if True, the overdensity is wrt the critical density rather than the mean matter density ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2010-07-09 - Written - Bovy (NYU) 2014-04-03 - Initialization w/ concentration and mass - Bovy (IAS) 2020-04-29 - Initialization w/ rmax and vmax - Bovy (UofT) """ Potential.__init__(self,amp=amp,ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) if conc is None and rmax is None: self.a= a self.alpha= 1 self.beta= 3 if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): self.normalize(normalize) elif not rmax is None: if _APY_LOADED and isinstance(rmax,units.Quantity): rmax= conversion.parse_length(rmax,ro=self._ro) self._roSet= True if _APY_LOADED and isinstance(vmax,units.Quantity): vmax= conversion.parse_velocity(vmax,vo=self._vo) self._voSet= True self.a= rmax/2.1625815870646098349 self._amp= vmax**2.*self.a/0.21621659550187311005 else: if wrtcrit: od= overdens/conversion.dens_in_criticaldens(self._vo, self._ro,H=H) else: od= overdens/conversion.dens_in_meanmatterdens(self._vo, self._ro, H=H,Om=Om) mvirNatural= mvir*100./conversion.mass_in_1010msol(self._vo, self._ro) rvir= (3.*mvirNatural/od/4./numpy.pi)**(1./3.) self.a= rvir/conc self._amp= mvirNatural/(numpy.log(1.+conc)-conc/(1.+conc)) # Turn on physical output, because mass is given in 1e12 Msun (see #465) self._roSet= True self._voSet= True self._scale= self.a self.hasC= True self.hasC_dxdv= True self.hasC_dens= True self._nemo_accname= 'NFW' return None def _evaluate(self,R,z,phi=0.,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential at R,z INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: Phi(R,z) HISTORY: 2010-07-09 - Started - Bovy (NYU) """ r= numpy.sqrt(R**2.+z**2.) if isinstance(r,(float,int)) and r == 0: return -1./self.a elif isinstance(r,(float,int)): return -special.xlogy(1./r,1.+r/self.a) # stable as r -> infty else: out= -special.xlogy(1./r,1.+r/self.a) # stable as r -> infty out[r == 0]= -1./self.a return out def _Rforce(self,R,z,phi=0.,t=0.): """ NAME: _Rforce PURPOSE: evaluate the radial force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial force HISTORY: 2010-07-09 - Written - Bovy (NYU) """ Rz= R**2.+z**2. sqrtRz= numpy.sqrt(Rz) return R*(1./Rz/(self.a+sqrtRz)-numpy.log(1.+sqrtRz/self.a)/sqrtRz/Rz) def _zforce(self,R,z,phi=0.,t=0.): """ NAME: _zforce PURPOSE: evaluate the vertical force for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the vertical force HISTORY: 2010-07-09 - Written - Bovy (NYU) """ Rz= R**2.+z**2. sqrtRz= numpy.sqrt(Rz) return z*(1./Rz/(self.a+sqrtRz)-numpy.log(1.+sqrtRz/self.a)/sqrtRz/Rz) def _rforce_jax(self,r): """ NAME: _rforce_jax PURPOSE: evaluate the spherical radial force for this potential using JAX INPUT: r - Galactocentric spherical radius OUTPUT: the radial force HISTORY: 2021-02-14 - Written - Bovy (UofT) """ if not _JAX_LOADED: # pragma: no cover raise ImportError("Making use of _rforce_jax function requires the google/jax library") return self._amp*(1./r/(self.a+r)-jnp.log(1.+r/self.a)/r**2.) def _R2deriv(self,R,z,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second radial derivative HISTORY: 2011-10-09 - Written - Bovy (IAS) """ Rz= R**2.+z**2. sqrtRz= numpy.sqrt(Rz) return (3.*R**4.+2.*R**2.*(z**2.+self.a*sqrtRz)\ -z**2.*(z**2.+self.a*sqrtRz)\ -(2.*R**2.-z**2.)*(self.a**2.+R**2.+z**2.+2.*self.a*sqrtRz)\ *numpy.log(1.+sqrtRz/self.a))\ /Rz**2.5/(self.a+sqrtRz)**2. def _Rzderiv(self,R,z,phi=0.,t=0.): """ NAME: _Rzderiv PURPOSE: evaluate the mixed R,z derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: d2phi/dR/dz HISTORY: 2013-08-28 - Written - Bovy (IAS) """ Rz= R**2.+z**2. sqrtRz= numpy.sqrt(Rz) return -R*z*(-4.*Rz-3.*self.a*sqrtRz+3.*(self.a**2.+Rz+2.*self.a*sqrtRz)*numpy.log(1.+sqrtRz/self.a))*Rz**-2.5*(self.a+sqrtRz)**-2. def _surfdens(self,R,z,phi=0.,t=0.): """ NAME: _surfdens PURPOSE: evaluate the surface density for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the surface density HISTORY: 2018-08-19 - Written - Bovy (UofT) """ r= numpy.sqrt(R**2.+z**2.) Rma= numpy.sqrt(R**2.-self.a**2.+0j) if Rma == 0.: za2= (z/self.a)**2 return self.a*(2.+numpy.sqrt(za2+1.)*(za2-2.))/6./numpy.pi/z**3 else: return (self.a*Rma**-3\ *(numpy.arctan(self.a*z/r/Rma)-numpy.arctan(z/Rma)) +z/(r+self.a)/(R**2.-self.a**2.)).real/2./numpy.pi def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: calculate the mass out to a given radius INPUT: R - radius at which to return the enclosed mass z - (don't specify this) vertical height OUTPUT: mass in natural units HISTORY: 2014-01-29 - Written - Bovy (IAS) """ if z is not None: raise AttributeError # use general implementation return numpy.log(1+R/self.a)-R/self.a/(1.+R/self.a) @conversion.physical_conversion('position',pop=False) def rvir(self,H=70.,Om=0.3,t=0.,overdens=200.,wrtcrit=False,ro=None,vo=None, use_physical=False): # use_physical necessary bc of pop=False, does nothing inside """ NAME: rvir PURPOSE: calculate the virial radius for this density distribution INPUT: H= (default: 70) Hubble constant in km/s/Mpc Om= (default: 0.3) Omega matter overdens= (200) overdensity which defines the virial radius wrtcrit= (False) if True, the overdensity is wrt the critical density rather than the mean matter density ro= distance scale in kpc or as Quantity (default: object-wide, which if not set is 8 kpc)) vo= velocity scale in km/s or as Quantity (default: object-wide, which if not set is 220 km/s)) OUTPUT: virial radius HISTORY: 2014-01-29 - Written - Bovy (IAS) """ if ro is None: ro= self._ro if vo is None: vo= self._vo if wrtcrit: od= overdens/conversion.dens_in_criticaldens(vo,ro,H=H) else: od= overdens/conversion.dens_in_meanmatterdens(vo,ro, H=H,Om=Om) dc= 12.*self.dens(self.a,0.,t=t,use_physical=False)/od x= optimize.brentq(lambda y: (numpy.log(1.+y)-y/(1.+y))/y**3.-1./dc, 0.01,100.) return x*self.a @conversion.physical_conversion('position',pop=True) def rmax(self): """ NAME: rmax PURPOSE: calculate the radius at which the rotation curve peaks INPUT: (none) OUTPUT: Radius at which the rotation curve peaks HISTORY: 2020-02-05 - Written - Bovy (UofT) """ # Magical number, solve(derivative (ln(1+x)-x/(1+x))/x wrt x=0,x) return 2.1625815870646098349*self.a @conversion.physical_conversion('velocity',pop=True) def vmax(self): """ NAME: vmax PURPOSE: calculate the maximum rotation curve velocity INPUT: (none) OUTPUT: Peak velocity in the rotation curve HISTORY: 2020-02-05 - Written - Bovy (UofT) """ # 0.21621659550187311005 = (numpy.log(1.+rmax/a)-rmax/(a+rmax))*a/rmax return numpy.sqrt(0.21621659550187311005*self._amp/self.a) @kms_to_kpcGyrDecorator def _nemo_accpars(self,vo,ro): """ NAME: _nemo_accpars PURPOSE: return the accpars potential parameters for use of this potential with NEMO INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: accpars string HISTORY: 2014-12-18 - Written - Bovy (IAS) """ ampl= self._amp*vo**2.*ro vmax= numpy.sqrt(ampl/self.a/ro*0.2162165954) #Take that factor directly from gyrfalcon return f"0,{self.a*ro},{vmax}" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/TwoPowerTriaxialPotential.py0000644000175100001710000004333714327773303023110 0ustar00runnerdocker############################################################################### # TwoPowerTriaxialPotential.py: General class for triaxial potentials # derived from densities with two power-laws # # amp/[4pia^3] # rho(r)= ------------------------------------ # (m/a)^\alpha (1+m/a)^(\beta-\alpha) # # with # # m^2 = x^2 + y^2/b^2 + z^2/c^2 ############################################################################### import numpy from scipy import special from ..util import conversion from .EllipsoidalPotential import EllipsoidalPotential class TwoPowerTriaxialPotential(EllipsoidalPotential): """Class that implements triaxial potentials that are derived from two-power density models .. math:: \\rho(x,y,z) = \\frac{\\mathrm{amp}}{4\\,\\pi\\,a^3}\\,\\frac{1}{(m/a)^\\alpha\\,(1+m/a)^{\\beta-\\alpha}} with .. math:: m^2 = x'^2 + \\frac{y'^2}{b^2}+\\frac{z'^2}{c^2} and :math:`(x',y',z')` is a rotated frame wrt :math:`(x,y,z)` specified by parameters ``zvec`` and ``pa`` which specify (a) ``zvec``: the location of the :math:`z'` axis in the :math:`(x,y,z)` frame and (b) ``pa``: the position angle of the :math:`x'` axis wrt the :math:`\\tilde{x}` axis, that is, the :math:`x` axis after rotating to ``zvec``. Note that this general class of potentials does *not* automatically revert to the special TriaxialNFWPotential, TriaxialHernquistPotential, or TriaxialJaffePotential when using their (alpha,beta) values (like TwoPowerSphericalPotential). """ def __init__(self,amp=1.,a=5.,alpha=1.5,beta=3.5,b=1.,c=1., zvec=None,pa=None,glorder=50, normalize=False,ro=None,vo=None): """ NAME: __init__ PURPOSE: initialize a triaxial two-power-density potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a - scale radius (can be Quantity) alpha - inner power (0 <= alpha < 3) beta - outer power ( beta > 2) b - y-to-x axis ratio of the density c - z-to-x axis ratio of the density zvec= (None) If set, a unit vector that corresponds to the z axis pa= (None) If set, the position angle of the x axis (rad or Quantity) glorder= (50) if set, compute the relevant force and potential integrals with Gaussian quadrature of this order normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2016-05-30 - Started - Bovy (UofT) 2018-08-07 - Re-written using the general EllipsoidalPotential class - Bovy (UofT) """ EllipsoidalPotential.__init__(self,amp=amp,b=b,c=c, zvec=zvec,pa=pa,glorder=glorder, ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) self.a= a self._scale= self.a if beta <= 2. or alpha >= 3.: raise OSError('TwoPowerTriaxialPotential requires 0 <= alpha < 3 and beta > 2') self.alpha= alpha self.beta= beta self.betaminusalpha= self.beta-self.alpha self.twominusalpha= 2.-self.alpha self.threeminusalpha= 3.-self.alpha if self.twominusalpha != 0.: self.psi_inf= special.gamma(self.beta-2.)\ *special.gamma(3.-self.alpha)\ /special.gamma(self.betaminusalpha) # Adjust amp self._amp/= (4.*numpy.pi*self.a**3) if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) return None def _psi(self,m): """\\psi(m) = -\\int_m^\\infty d m^2 \rho(m^2)""" if self.twominusalpha == 0.: return -2.*self.a**2*(self.a/m)**self.betaminusalpha\ /self.betaminusalpha\ *special.hyp2f1(self.betaminusalpha, self.betaminusalpha, self.betaminusalpha+1, -self.a/m) else: return -2.*self.a**2\ *(self.psi_inf-(m/self.a)**self.twominusalpha\ /self.twominusalpha\ *special.hyp2f1(self.twominusalpha, self.betaminusalpha, self.threeminusalpha, -m/self.a)) def _mdens(self,m): """Density as a function of m""" return (self.a/m)**self.alpha/(1.+m/self.a)**(self.betaminusalpha) def _mdens_deriv(self,m): """Derivative of the density as a function of m""" return -self._mdens(m)*(self.a*self.alpha+self.beta*m)/m/(self.a+m) def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R (and z) for this potential; if z=None, integrate to ellipsoidal boundary INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2021-03-09 - Written - Bovy (UofT) """ if not z is None: raise AttributeError # Hack to fall back to general return 4.*numpy.pi*self.a**self.alpha\ *R**(3.-self.alpha)/(3.-self.alpha)*self._b*self._c\ *special.hyp2f1(3.-self.alpha,self.betaminusalpha, 4.-self.alpha,-R/self.a) class TriaxialHernquistPotential(EllipsoidalPotential): """Class that implements the triaxial Hernquist potential .. math:: \\rho(x,y,z) = \\frac{\\mathrm{amp}}{4\\,\\pi\\,a^3}\\,\\frac{1}{(m/a)\\,(1+m/a)^{3}} with .. math:: m^2 = x'^2 + \\frac{y'^2}{b^2}+\\frac{z'^2}{c^2} and :math:`(x',y',z')` is a rotated frame wrt :math:`(x,y,z)` specified by parameters ``zvec`` and ``pa`` which specify (a) ``zvec``: the location of the :math:`z'` axis in the :math:`(x,y,z)` frame and (b) ``pa``: the position angle of the :math:`x'` axis wrt the :math:`\\tilde{x}` axis, that is, the :math:`x` axis after rotating to ``zvec``. """ def __init__(self,amp=1.,a=2.,normalize=False,b=1.,c=1.,zvec=None,pa=None, glorder=50,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a triaxial Hernquist potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a - scale radius (can be Quantity) b - y-to-x axis ratio of the density c - z-to-x axis ratio of the density zvec= (None) If set, a unit vector that corresponds to the z axis pa= (None) If set, the position angle of the x axis glorder= (50) if set, compute the relevant force and potential integrals with Gaussian quadrature of this order normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2010-07-09 - Written - Bovy (UofT) 2018-08-07 - Re-written using the general EllipsoidalPotential class - Bovy (UofT) """ EllipsoidalPotential.__init__(self,amp=amp,b=b,c=c, zvec=zvec,pa=pa,glorder=glorder, ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) self.a= a self._scale= self.a # Adjust amp self.a4= self.a**4 self._amp/= (4.*numpy.pi*self.a**3) if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): self.normalize(normalize) self.hasC= not self._glorder is None self.hasC_dxdv= False self.hasC_dens= self.hasC # works if mdens is defined, necessary for hasC return None def _psi(self,m): """\\psi(m) = -\\int_m^\\infty d m^2 \rho(m^2)""" return -self.a4/(m+self.a)**2. def _mdens(self,m): """Density as a function of m""" return self.a4/m/(m+self.a)**3 def _mdens_deriv(self,m): """Derivative of the density as a function of m""" return -self.a4*(self.a+4.*m)/m**2/(self.a+m)**4 def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R (and z) for this potential; if z=None, integrate to ellipsoidal boundary INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2021-03-16 - Written - Bovy (UofT) """ if not z is None: raise AttributeError # Hack to fall back to general return 4.*numpy.pi*self.a4/self.a/(1.+self.a/R)**2./2.*self._b*self._c class TriaxialJaffePotential(EllipsoidalPotential): """Class that implements the Jaffe potential .. math:: \\rho(x,y,z) = \\frac{\\mathrm{amp}}{4\\,\\pi\\,a^3}\\,\\frac{1}{(m/a)^2\\,(1+m/a)^{2}} with .. math:: m^2 = x'^2 + \\frac{y'^2}{b^2}+\\frac{z'^2}{c^2} and :math:`(x',y',z')` is a rotated frame wrt :math:`(x,y,z)` specified by parameters ``zvec`` and ``pa`` which specify (a) ``zvec``: the location of the :math:`z'` axis in the :math:`(x,y,z)` frame and (b) ``pa``: the position angle of the :math:`x'` axis wrt the :math:`\\tilde{x}` axis, that is, the :math:`x` axis after rotating to ``zvec``. """ def __init__(self,amp=1.,a=2.,b=1.,c=1.,zvec=None,pa=None,normalize=False, glorder=50,ro=None,vo=None): """ NAME: __init__ PURPOSE: Initialize a Jaffe potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a - scale radius (can be Quantity) b - y-to-x axis ratio of the density c - z-to-x axis ratio of the density zvec= (None) If set, a unit vector that corresponds to the z axis pa= (None) If set, the position angle of the x axis glorder= (50) if set, compute the relevant force and potential integrals with Gaussian quadrature of this order normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2010-07-09 - Written - Bovy (UofT) 2018-08-07 - Re-written using the general EllipsoidalPotential class - Bovy (UofT) """ EllipsoidalPotential.__init__(self,amp=amp,b=b,c=c, zvec=zvec,pa=pa,glorder=glorder, ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) self.a= a self._scale= self.a # Adjust amp self.a2= self.a**2 self._amp/= (4.*numpy.pi*self.a2*self.a) if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): #pragma: no cover self.normalize(normalize) self.hasC= not self._glorder is None self.hasC_dxdv= False self.hasC_dens= self.hasC # works if mdens is defined, necessary for hasC return None def _psi(self,m): """\\psi(m) = -\\int_m^\\infty d m^2 \rho(m^2)""" return 2.*self.a2*(1./(1.+m/self.a)+numpy.log(1./(1.+self.a/m))) def _mdens(self,m): """Density as a function of m""" return self.a2/m**2/(1.+m/self.a)**2 def _mdens_deriv(self,m): """Derivative of the density as a function of m""" return -2.*self.a2**2*(self.a+2.*m)/m**3/(self.a+m)**3 def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R (and z) for this potential; if z=None, integrate to ellipsoidal boundary INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2021-03-16 - Written - Bovy (UofT) """ if not z is None: raise AttributeError # Hack to fall back to general return 4.*numpy.pi*self.a*self.a2/(1.+self.a/R)*self._b*self._c class TriaxialNFWPotential(EllipsoidalPotential): """Class that implements the triaxial NFW potential .. math:: \\rho(x,y,z) = \\frac{\\mathrm{amp}}{4\\,\\pi\\,a^3}\\,\\frac{1}{(m/a)\\,(1+m/a)^{2}} with .. math:: m^2 = x'^2 + \\frac{y'^2}{b^2}+\\frac{z'^2}{c^2} and :math:`(x',y',z')` is a rotated frame wrt :math:`(x,y,z)` specified by parameters ``zvec`` and ``pa`` which specify (a) ``zvec``: the location of the :math:`z'` axis in the :math:`(x,y,z)` frame and (b) ``pa``: the position angle of the :math:`x'` axis wrt the :math:`\\tilde{x}` axis, that is, the :math:`x` axis after rotating to ``zvec``. """ def __init__(self,amp=1.,a=2.,b=1.,c=1.,zvec=None,pa=None, normalize=False, conc=None,mvir=None, glorder=50,vo=None,ro=None, H=70.,Om=0.3,overdens=200.,wrtcrit=False): """ NAME: __init__ PURPOSE: Initialize a triaxial NFW potential INPUT: amp - amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a - scale radius (can be Quantity) b - y-to-x axis ratio of the density c - z-to-x axis ratio of the density zvec= (None) If set, a unit vector that corresponds to the z axis pa= (None) If set, the position angle of the x axis glorder= (50) if set, compute the relevant force and potential integrals with Gaussian quadrature of this order normalize - if True, normalize such that vc(1.,0.)=1., or, if given as a number, such that the force is this fraction of the force necessary to make vc(1.,0.)=1. Alternatively, NFW potentials can be initialized using conc= concentration mvir= virial mass in 10^12 Msolar in which case you also need to supply the following keywords H= (default: 70) Hubble constant in km/s/Mpc Om= (default: 0.3) Omega matter overdens= (200) overdensity which defines the virial radius wrtcrit= (False) if True, the overdensity is wrt the critical density rather than the mean matter density ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: (none) HISTORY: 2016-05-30 - Written - Bovy (UofT) 2018-08-06 - Re-written using the general EllipsoidalPotential class - Bovy (UofT) """ EllipsoidalPotential.__init__(self,amp=amp,b=b,c=c, zvec=zvec,pa=pa,glorder=glorder, ro=ro,vo=vo,amp_units='mass') a= conversion.parse_length(a,ro=self._ro) if conc is None: self.a= a else: from ..potential import NFWPotential dumb= NFWPotential(mvir=mvir,conc=conc,ro=self._ro,vo=self._vo, H=H,Om=Om,wrtcrit=wrtcrit,overdens=overdens) self.a= dumb.a self._amp= dumb._amp self._scale= self.a self.hasC= not self._glorder is None self.hasC_dxdv= False self.hasC_dens= self.hasC # works if mdens is defined, necessary for hasC # Adjust amp self.a3= self.a**3 self._amp/= (4.*numpy.pi*self.a3) if normalize or \ (isinstance(normalize,(int,float)) \ and not isinstance(normalize,bool)): self.normalize(normalize) return None def _psi(self,m): """\\psi(m) = -\\int_m^\\infty d m^2 \rho(m^2)""" return -2.*self.a3/(self.a+m) def _mdens(self,m): """Density as a function of m""" return self.a/m/(1.+m/self.a)**2 def _mdens_deriv(self,m): """Derivative of the density as a function of m""" return -self.a3*(self.a+3.*m)/m**2/(self.a+m)**3 def _mass(self,R,z=None,t=0.): """ NAME: _mass PURPOSE: evaluate the mass within R (and z) for this potential; if z=None, integrate to ellipsoidal boundary INPUT: R - Galactocentric cylindrical radius z - vertical height t - time OUTPUT: the mass enclosed HISTORY: 2021-03-16 - Written - Bovy (UofT) """ if not z is None: raise AttributeError # Hack to fall back to general return 4.*numpy.pi*self.a3*self._b*self._c\ *(numpy.log(1+R/self.a)-R/self.a/(1.+R/self.a)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/WrapperPotential.py0000644000175100001710000002224214327773303021234 0ustar00runnerdocker############################################################################### # WrapperPotential.py: Super-class for wrapper potentials ############################################################################### from ..util.conversion import get_physical, physical_compatible from .planarPotential import (_evaluateplanarphitorques, _evaluateplanarPotentials, _evaluateplanarRforces, evaluateplanarR2derivs, planarPotential) from .Potential import (Potential, _dim, _evaluatephitorques, _evaluatePotentials, _evaluateRforces, _evaluatezforces, _isNonAxi, evaluateDensities, evaluateR2derivs, evaluateRzderivs, evaluatez2derivs) def _new_obj(cls, kwargs, args): """Maps kwargs to cls.__new__""" return cls.__new__(cls, *args, **kwargs) class parentWrapperPotential: """'Dummy' class only used to delegate wrappers to either 2D planarWrapperPotential or 3D WrapperPotential based on pot's dimensionality, using a little python object creation magic...""" def __new__(cls,*args,**kwargs): if kwargs.pop('_init',False): # When we get here recursively, just create new object return object.__new__(cls) # Decide whether superclass is Wrapper or planarWrapper based on dim pot= kwargs.get('pot',None) if _dim(pot) == 2: parentWrapperPotential= planarWrapperPotential elif _dim(pot) == 3: parentWrapperPotential= WrapperPotential else: raise ValueError("WrapperPotentials are only supported in 3D and 2D") # Create object from custom class that derives from correct wrapper, # make sure to turn off normalization for all wrappers kwargs['_init']= True # to break recursion above # __reduce__ method to allow pickling reduce= lambda self: (_new_obj, (cls, kwargs, args), self.__dict__) out= type.__new__(type,'_%s' % cls.__name__, (parentWrapperPotential,cls), {'normalize':property(), '__reduce__':reduce})(*args,**kwargs) kwargs.pop('_init',False) # This runs init for the subclass (the specific wrapper) cls.__init__(out,*args,**kwargs) return out class WrapperPotential(Potential): def __init__(self,amp=1.,pot=None,ro=None,vo=None,_init=None,**kwargs): """ NAME: __init__ PURPOSE: initialize a WrapperPotential, a super-class for wrapper potentials INPUT: amp - amplitude to be applied to the potential (default: 1.) pot - Potential instance or list thereof OUTPUT: (none) HISTORY: 2017-06-26 - Started - Bovy (UofT) """ if not _init: return None # Don't run __init__ at the end of setup Potential.__init__(self,amp=amp,ro=ro,vo=vo) self._pot= pot self.isNonAxi= _isNonAxi(self._pot) # Check whether units are consistent between the wrapper and the # wrapped potential assert physical_compatible(self,self._pot), \ """Physical unit conversion parameters (ro,vo) are not """\ """compatible between this wrapper and the wrapped potential""" # Transfer unit system if set for wrapped potential, but not here phys_wrapped= get_physical(self._pot,include_set=True) if not self._roSet and phys_wrapped['roSet']: self.turn_physical_on(ro=phys_wrapped['ro'],vo=False) if not self._voSet and phys_wrapped['voSet']: self.turn_physical_on(vo=phys_wrapped['vo'],ro=False) def __repr__(self): wrapped_repr= repr(self._pot) return Potential.__repr__(self) + ', wrapper of' \ + ''.join([f'\n\t{s}' for s in wrapped_repr.split('\n')]) def __getattr__(self,attribute): if attribute == '_evaluate' \ or attribute == '_Rforce' or attribute == '_zforce' \ or attribute == '_phitorque' \ or attribute == '_R2deriv' or attribute == '_z2deriv' \ or attribute == '_Rzderiv' or attribute == '_phi2deriv' \ or attribute == '_Rphideriv' or attribute == '_dens': return lambda R,Z,phi=0.,t=0.: \ self._wrap(attribute,R,Z,phi=phi,t=t) else: return super().__getattr__(attribute) def _wrap_pot_func(self,attribute): if attribute == '_evaluate': return lambda p,R,Z,phi=0.,t=0.: \ _evaluatePotentials(p,R,Z,phi=phi,t=t) elif attribute == '_dens': return lambda p,R,Z,phi=0.,t=0.: \ evaluateDensities(p,R,Z,phi=phi,t=t,use_physical=False) elif attribute == '_Rforce': return lambda p,R,Z,phi=0.,t=0.: \ _evaluateRforces(p,R,Z,phi=phi,t=t) elif attribute == '_zforce': return lambda p,R,Z,phi=0.,t=0.: \ _evaluatezforces(p,R,Z,phi=phi,t=t) elif attribute == '_phitorque': return lambda p,R,Z,phi=0.,t=0.: \ _evaluatephitorques(p,R,Z,phi=phi,t=t) elif attribute == '_R2deriv': return lambda p,R,Z,phi=0.,t=0.: \ evaluateR2derivs(p,R,Z,phi=phi,t=t,use_physical=False) elif attribute == '_z2deriv': return lambda p,R,Z,phi=0.,t=0.: \ evaluatez2derivs(p,R,Z,phi=phi,t=t,use_physical=False) elif attribute == '_Rzderiv': return lambda p,R,Z,phi=0.,t=0.: \ evaluateRzderivs(p,R,Z,phi=phi,t=t,use_physical=False) elif attribute == '_phi2deriv': return lambda p,R,Z,phi=0.,t=0.: \ _evaluatePotentials(p,R,Z,phi=phi,t=t,dphi=2) elif attribute == '_Rphideriv': return lambda p,R,Z,phi=0.,t=0.: \ _evaluatePotentials(p,R,Z,phi=phi,t=t,dR=1,dphi=1) else: #pragma: no cover raise AttributeError("Attribute %s not found in for this WrapperPotential" % attribute) class planarWrapperPotential(planarPotential): def __init__(self,amp=1.,pot=None,ro=None,vo=None,_init=None,**kwargs): """ NAME: __init__ PURPOSE: initialize a WrapperPotential, a super-class for wrapper potentials INPUT: amp - amplitude to be applied to the potential (default: 1.) pot - Potential instance or list thereof; the amplitude of this will be grown by this wrapper OUTPUT: (none) HISTORY: 2017-06-26 - Started - Bovy (UofT) """ if not _init: return None # Don't run __init__ at the end of setup planarPotential.__init__(self,amp=amp,ro=ro,vo=vo) self._pot= pot self.isNonAxi= _isNonAxi(self._pot) # Check whether units are consistent between the wrapper and the # wrapped potential assert physical_compatible(self,self._pot), \ """Physical unit conversion parameters (ro,vo) are not """\ """compatible between this wrapper and the wrapped potential""" # Transfer unit system if set for wrapped potential, but not here phys_wrapped= get_physical(self._pot,include_set=True) if not self._roSet and phys_wrapped['roSet']: self.turn_physical_on(ro=phys_wrapped['ro'],vo=False) if not self._voSet and phys_wrapped['voSet']: self.turn_physical_on(vo=phys_wrapped['vo'],ro=False) def __repr__(self): wrapped_repr= repr(self._pot) return Potential.__repr__(self) + ', wrapper of' \ + ''.join([f'\n\t{s}' for s in wrapped_repr.split('\n')]) def __getattr__(self,attribute): if attribute == '_evaluate' \ or attribute == '_Rforce' \ or attribute == '_phitorque' \ or attribute == '_R2deriv' \ or attribute == '_phi2deriv' \ or attribute == '_Rphideriv': return lambda R,phi=0.,t=0.: \ self._wrap(attribute,R,phi=phi,t=t) else: return super().__getattr__(attribute) def _wrap_pot_func(self,attribute): if attribute == '_evaluate': return lambda p,R,phi=0.,t=0.: \ _evaluateplanarPotentials(p,R,phi=phi,t=t) elif attribute == '_Rforce': return lambda p,R,phi=0.,t=0.: \ _evaluateplanarRforces(p,R,phi=phi,t=t) elif attribute == '_phitorque': return lambda p,R,phi=0.,t=0.: \ _evaluateplanarphitorques(p,R,phi=phi,t=t) elif attribute == '_R2deriv': return lambda p,R,phi=0.,t=0.: \ evaluateplanarR2derivs(p,R,phi=phi,t=t,use_physical=False) elif attribute == '_phi2deriv': return lambda p,R,phi=0.,t=0.: \ _evaluateplanarPotentials(p,R,phi=phi,t=t,dphi=2) elif attribute == '_Rphideriv': return lambda p,R,phi=0.,t=0.: \ _evaluateplanarPotentials(p,R,phi=phi,t=t,dR=1,dphi=1) else: #pragma: no cover raise AttributeError("Attribute %s not found in for this WrapperPotential" % attribute) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/__init__.py0000644000175100001710000002401114327773303017467 0ustar00runnerdockerfrom . import (AdiabaticContractionWrapperPotential, AnyAxisymmetricRazorThinDiskPotential, AnySphericalPotential, BurkertPotential, ChandrasekharDynamicalFrictionForce, CorotatingRotationWrapperPotential, CosmphiDiskPotential, DehnenBarPotential, DehnenSmoothWrapperPotential, DiskSCFPotential, DoubleExponentialDiskPotential, EllipticalDiskPotential, FerrersPotential, FlattenedPowerPotential, Force, GaussianAmplitudeWrapperPotential, HenonHeilesPotential, HomogeneousSpherePotential, IsochronePotential, IsothermalDiskPotential, KGPotential, KingPotential, KuzminDiskPotential, KuzminKutuzovStaeckelPotential, LogarithmicHaloPotential, MiyamotoNagaiPotential, MN3ExponentialDiskPotential, MovingObjectPotential, NonInertialFrameForce, NullPotential, NumericalPotentialDerivativesMixin, PerfectEllipsoidPotential, PlummerPotential, Potential, PowerSphericalPotential, PowerSphericalPotentialwCutoff, PowerTriaxialPotential, PseudoIsothermalPotential, RazorThinExponentialDiskPotential, RingPotential, RotateAndTiltWrapperPotential, SCFPotential, SnapshotRZPotential, SoftenedNeedleBarPotential, SolidBodyRotationWrapperPotential, SphericalShellPotential, SpiralArmsPotential, SteadyLogSpiralPotential, TimeDependentAmplitudeWrapperPotential, TransientLogSpiralPotential, TriaxialGaussianPotential, TwoPowerSphericalPotential, TwoPowerTriaxialPotential, interpRZPotential, interpSphericalPotential, linearPotential, planarPotential, plotEscapecurve, plotRotcurve, verticalPotential) # # Functions # evaluatePotentials= Potential.evaluatePotentials evaluateDensities= Potential.evaluateDensities evaluateSurfaceDensities= Potential.evaluateSurfaceDensities mass= Potential.mass evaluateRforces= Potential.evaluateRforces evaluatephitorques= Potential.evaluatephitorques evaluatephiforces= Potential.evaluatephiforces evaluatezforces= Potential.evaluatezforces evaluaterforces= Potential.evaluaterforces evaluateR2derivs= Potential.evaluateR2derivs evaluatez2derivs= Potential.evaluatez2derivs evaluateRzderivs= Potential.evaluateRzderivs evaluatephi2derivs= Potential.evaluatephi2derivs evaluateRphiderivs= Potential.evaluateRphiderivs evaluatephizderivs= Potential.evaluatephizderivs evaluater2derivs= Potential.evaluater2derivs RZToplanarPotential= planarPotential.RZToplanarPotential toPlanarPotential= planarPotential.toPlanarPotential RZToverticalPotential= verticalPotential.RZToverticalPotential toVerticalPotential= verticalPotential.toVerticalPotential plotPotentials= Potential.plotPotentials plotDensities= Potential.plotDensities plotSurfaceDensities= Potential.plotSurfaceDensities plotplanarPotentials= planarPotential.plotplanarPotentials plotlinearPotentials= linearPotential.plotlinearPotentials calcRotcurve= plotRotcurve.calcRotcurve vcirc= plotRotcurve.vcirc dvcircdR= plotRotcurve.dvcircdR epifreq= Potential.epifreq verticalfreq= Potential.verticalfreq flattening= Potential.flattening rl= Potential.rl omegac= Potential.omegac vterm= Potential.vterm lindbladR= Potential.lindbladR plotRotcurve= plotRotcurve.plotRotcurve calcEscapecurve= plotEscapecurve.calcEscapecurve vesc= plotEscapecurve.vesc plotEscapecurve= plotEscapecurve.plotEscapecurve evaluateplanarPotentials= planarPotential.evaluateplanarPotentials evaluateplanarRforces= planarPotential.evaluateplanarRforces evaluateplanarR2derivs= planarPotential.evaluateplanarR2derivs evaluateplanarphitorques= planarPotential.evaluateplanarphitorques evaluateplanarphiforces= planarPotential.evaluateplanarphiforces evaluatelinearPotentials= linearPotential.evaluatelinearPotentials evaluatelinearForces= linearPotential.evaluatelinearForces PotentialError= Potential.PotentialError LinShuReductionFactor= planarPotential.LinShuReductionFactor nemo_accname= Potential.nemo_accname nemo_accpars= Potential.nemo_accpars turn_physical_off= Potential.turn_physical_off turn_physical_on= Potential.turn_physical_on _dim= Potential._dim _isNonAxi= Potential._isNonAxi scf_compute_coeffs_spherical_nbody = SCFPotential.scf_compute_coeffs_spherical_nbody scf_compute_coeffs_axi_nbody = SCFPotential.scf_compute_coeffs_axi_nbody scf_compute_coeffs_nbody = SCFPotential.scf_compute_coeffs_nbody scf_compute_coeffs_spherical = SCFPotential.scf_compute_coeffs_spherical scf_compute_coeffs_axi = SCFPotential.scf_compute_coeffs_axi scf_compute_coeffs = SCFPotential.scf_compute_coeffs rtide= Potential.rtide ttensor= Potential.ttensor flatten= Potential.flatten to_amuse= Potential.to_amuse zvc= Potential.zvc zvc_range= Potential.zvc_range rhalf= Potential.rhalf tdyn= Potential.tdyn rE= Potential.rE LcE= Potential.LcE # # Classes # Force= Force.Force Potential= Potential.Potential planarAxiPotential= planarPotential.planarAxiPotential planarPotential= planarPotential.planarPotential linearPotential= linearPotential.linearPotential MiyamotoNagaiPotential= MiyamotoNagaiPotential.MiyamotoNagaiPotential IsochronePotential= IsochronePotential.IsochronePotential DoubleExponentialDiskPotential= DoubleExponentialDiskPotential.DoubleExponentialDiskPotential LogarithmicHaloPotential= LogarithmicHaloPotential.LogarithmicHaloPotential KeplerPotential= PowerSphericalPotential.KeplerPotential PowerSphericalPotential= PowerSphericalPotential.PowerSphericalPotential PowerSphericalPotentialwCutoff= PowerSphericalPotentialwCutoff.PowerSphericalPotentialwCutoff DehnenSphericalPotential= TwoPowerSphericalPotential.DehnenSphericalPotential DehnenCoreSphericalPotential= TwoPowerSphericalPotential.DehnenCoreSphericalPotential NFWPotential= TwoPowerSphericalPotential.NFWPotential JaffePotential= TwoPowerSphericalPotential.JaffePotential HernquistPotential= TwoPowerSphericalPotential.HernquistPotential TwoPowerSphericalPotential= TwoPowerSphericalPotential.TwoPowerSphericalPotential KGPotential= KGPotential.KGPotential interpRZPotential= interpRZPotential.interpRZPotential DehnenBarPotential= DehnenBarPotential.DehnenBarPotential SteadyLogSpiralPotential= SteadyLogSpiralPotential.SteadyLogSpiralPotential TransientLogSpiralPotential= TransientLogSpiralPotential.TransientLogSpiralPotential MovingObjectPotential= MovingObjectPotential.MovingObjectPotential EllipticalDiskPotential= EllipticalDiskPotential.EllipticalDiskPotential LopsidedDiskPotential= CosmphiDiskPotential.LopsidedDiskPotential CosmphiDiskPotential= CosmphiDiskPotential.CosmphiDiskPotential RazorThinExponentialDiskPotential= RazorThinExponentialDiskPotential.RazorThinExponentialDiskPotential FlattenedPowerPotential= FlattenedPowerPotential.FlattenedPowerPotential InterpSnapshotRZPotential = SnapshotRZPotential.InterpSnapshotRZPotential SnapshotRZPotential = SnapshotRZPotential.SnapshotRZPotential BurkertPotential= BurkertPotential.BurkertPotential MN3ExponentialDiskPotential= MN3ExponentialDiskPotential.MN3ExponentialDiskPotential KuzminKutuzovStaeckelPotential = KuzminKutuzovStaeckelPotential.KuzminKutuzovStaeckelPotential PlummerPotential = PlummerPotential.PlummerPotential PseudoIsothermalPotential = PseudoIsothermalPotential.PseudoIsothermalPotential KuzminDiskPotential = KuzminDiskPotential.KuzminDiskPotential TriaxialHernquistPotential= TwoPowerTriaxialPotential.TriaxialHernquistPotential TriaxialNFWPotential= TwoPowerTriaxialPotential.TriaxialNFWPotential TriaxialJaffePotential= TwoPowerTriaxialPotential.TriaxialJaffePotential TwoPowerTriaxialPotential= TwoPowerTriaxialPotential.TwoPowerTriaxialPotential FerrersPotential= FerrersPotential.FerrersPotential SCFPotential = SCFPotential.SCFPotential SoftenedNeedleBarPotential= SoftenedNeedleBarPotential.SoftenedNeedleBarPotential DiskSCFPotential = DiskSCFPotential.DiskSCFPotential SpiralArmsPotential = SpiralArmsPotential.SpiralArmsPotential HenonHeilesPotential= HenonHeilesPotential.HenonHeilesPotential ChandrasekharDynamicalFrictionForce= ChandrasekharDynamicalFrictionForce.ChandrasekharDynamicalFrictionForce SphericalShellPotential= SphericalShellPotential.SphericalShellPotential RingPotential= RingPotential.RingPotential PerfectEllipsoidPotential= PerfectEllipsoidPotential.PerfectEllipsoidPotential IsothermalDiskPotential= IsothermalDiskPotential.IsothermalDiskPotential NumericalPotentialDerivativesMixin= NumericalPotentialDerivativesMixin.NumericalPotentialDerivativesMixin HomogeneousSpherePotential= HomogeneousSpherePotential.HomogeneousSpherePotential interpSphericalPotential= interpSphericalPotential.interpSphericalPotential TriaxialGaussianPotential= TriaxialGaussianPotential.TriaxialGaussianPotential KingPotential= KingPotential.KingPotential AnyAxisymmetricRazorThinDiskPotential= AnyAxisymmetricRazorThinDiskPotential.AnyAxisymmetricRazorThinDiskPotential AnySphericalPotential= AnySphericalPotential.AnySphericalPotential #Wrappers DehnenSmoothWrapperPotential= DehnenSmoothWrapperPotential.DehnenSmoothWrapperPotential SolidBodyRotationWrapperPotential= SolidBodyRotationWrapperPotential.SolidBodyRotationWrapperPotential CorotatingRotationWrapperPotential= CorotatingRotationWrapperPotential.CorotatingRotationWrapperPotential GaussianAmplitudeWrapperPotential= GaussianAmplitudeWrapperPotential.GaussianAmplitudeWrapperPotential RotateAndTiltWrapperPotential = RotateAndTiltWrapperPotential.RotateAndTiltWrapperPotential AdiabaticContractionWrapperPotential= AdiabaticContractionWrapperPotential.AdiabaticContractionWrapperPotential PowerTriaxialPotential= PowerTriaxialPotential.PowerTriaxialPotential NonInertialFrameForce= NonInertialFrameForce.NonInertialFrameForce NullPotential= NullPotential.NullPotential TimeDependentAmplitudeWrapperPotential= TimeDependentAmplitudeWrapperPotential.TimeDependentAmplitudeWrapperPotential # MW potential models, now in galpy.potential.mwpotentials, but keep these two # for tests, backwards compatibility, and convenience from . import mwpotentials MWPotential= mwpotentials._MWPotential MWPotential2014= mwpotentials.MWPotential2014 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/amuse.py0000644000175100001710000002004714327773303017047 0ustar00runnerdocker# galpy.potential.amuse: AMUSE representation of galpy potentials import numpy from amuse.support.literature import LiteratureReferencesMixIn from amuse.units import units from amuse.units.quantities import ScalarQuantity from .. import potential from ..util import conversion class galpy_profile(LiteratureReferencesMixIn): """ User-defined potential from galpy .. [#] Bovy, J, 2015, galpy: A Python Library for Galactic Dynamics, Astrophys. J. Supp. 216, 29 [2015ApJS..216...29B] """ def __init__(self,pot,t = 0.,tgalpy = 0.,ro=8,vo=220.,reverse=False): """ NAME: __init__ PURPOSE: initialize a galpy potential for use with AMUSE INPUT: pot - galpy potential object or list of such objects t - start time for AMUSE simulation (can be an AMUSE Quantity) tgalpy - start time for galpy potential, can be less than zero (can be Quantity) ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) reverse - set whether galpy potential evolves forwards or backwards in time (default: False) OUTPUT: (none) HISTORY: 2019-08-12 - Written - Webb (UofT) """ LiteratureReferencesMixIn.__init__(self) self.pot= pot self.ro= ro self.vo= vo self.reverse= reverse #Initialize model time if isinstance(t,ScalarQuantity): self.model_time= t else: self.model_time= \ t*conversion.time_in_Gyr(ro=self.ro,vo=self.vo) \ | units.Gyr #Initialize galpy time if isinstance(tgalpy,ScalarQuantity): self.tgalpy= tgalpy.value_in(units.Gyr)\ /conversion.time_in_Gyr(ro=self.ro,vo=self.vo) else: self.tgalpy= tgalpy def evolve_model(self,time): """ NAME: evolve_model PURPOSE: evolve time parameters to t_end INPUT: time - time to evolve potential to OUTPUT: None HISTORY: 2019-08-12 - Written - Webb (UofT) """ dt= time-self.model_time self.model_time= time if self.reverse: self.tgalpy-= dt.value_in(units.Gyr)\ /conversion.time_in_Gyr(ro=self.ro,vo=self.vo) else: self.tgalpy+= dt.value_in(units.Gyr)\ /conversion.time_in_Gyr(ro=self.ro,vo=self.vo) def get_potential_at_point(self,eps,x,y,z): """ NAME: get_potential_at_point PURPOSE: Get potential at a given location in the potential INPUT: eps - softening length (necessary for AMUSE, but not used by galpy potential) x,y,z - position in the potential OUTPUT: Phi(x,y,z) HISTORY: 2019-08-12 - Written - Webb (UofT) 2019-11-06 - added physical compatibility - Starkman (UofT) """ R= numpy.sqrt(x.value_in(units.kpc)**2.+y.value_in(units.kpc)**2.) zed= z.value_in(units.kpc) phi= numpy.arctan2(y.value_in(units.kpc),x.value_in(units.kpc)) res= potential.evaluatePotentials(self.pot,R/self.ro,zed/self.ro, phi=phi,t=self.tgalpy, ro=self.ro,vo=self.vo, use_physical=False) return res * self.vo**2 | units.kms**2 def get_gravity_at_point(self,eps,x,y,z): """ NAME: get_gravity_at_point PURPOSE: Get acceleration due to potential at a given location in the potential INPUT: eps - softening length (necessary for AMUSE, but not used by galpy potential) x,y,z - position in the potential OUTPUT: ax,ay,az HISTORY: 2019-08-12 - Written - Webb (UofT) 2019-11-06 - added physical compatibility - Starkman (UofT) """ R= numpy.sqrt(x.value_in(units.kpc)**2.+y.value_in(units.kpc)**2.) zed= z.value_in(units.kpc) phi= numpy.arctan2(y.value_in(units.kpc),x.value_in(units.kpc)) # Cylindrical force Rforce= potential.evaluateRforces(self.pot,R/self.ro,zed/self.ro, phi=phi,t=self.tgalpy, use_physical=False) phitorque= potential.evaluatephitorques(self.pot,R/self.ro,zed/self.ro, phi=phi,t=self.tgalpy, use_physical=False)\ /(R/self.ro) zforce=potential.evaluatezforces(self.pot,R/self.ro,zed/self.ro, phi=phi,t=self.tgalpy, use_physical=False) # Convert cylindrical force --> rectangular cp, sp= numpy.cos(phi), numpy.sin(phi) ax= (Rforce*cp - phitorque*sp)\ *conversion.force_in_kmsMyr(ro=self.ro,vo=self.vo) \ | units.kms / units.Myr ay= (Rforce*sp + phitorque*cp)\ *conversion.force_in_kmsMyr(ro=self.ro,vo=self.vo) \ | units.kms / units.Myr az= zforce\ *conversion.force_in_kmsMyr(ro=self.ro,vo=self.vo) \ | units.kms / units.Myr return ax,ay,az def mass_density(self,x,y,z): """ NAME: mass_density PURPOSE: Get mass density at a given location in the potential INPUT: eps - softening length (necessary for AMUSE, but not used by galpy potential) x,y,z - position in the potential OUTPUT: the density HISTORY: 2019-08-12 - Written - Webb (UofT) 2019-11-06 - added physical compatibility - Starkman (UofT) """ R= numpy.sqrt(x.value_in(units.kpc)**2.+y.value_in(units.kpc)**2.) zed= z.value_in(units.kpc) phi= numpy.arctan2(y.value_in(units.kpc),x.value_in(units.kpc)) res= (potential.evaluateDensities(self.pot,R/self.ro,zed/self.ro, phi=phi,t=self.tgalpy, ro=self.ro,vo=self.vo, use_physical=False) * conversion.dens_in_msolpc3(self.vo,self.ro)) return res | units.MSun / units.parsec**3 def circular_velocity(self,r): """ NAME: circular_velocity PURPOSE: Get circular velocity at a given radius in the potential INPUT: r - radius in the potential OUTPUT: the circular velocity HISTORY: 2019-08-12 - Written - Webb (UofT) 2019-11-06 - added physical compatibility - Starkman (UofT) """ res= potential.vcirc(self.pot,r.value_in(units.kpc)/self.ro,phi=0, t=self.tgalpy,ro=self.ro,vo=self.vo, use_physical=False) return res * self.vo | units.kms def enclosed_mass(self,r): """ NAME: enclosed_mass PURPOSE: Get mass enclosed within a given radius in the potential INPUT: r - radius in the potential OUTPUT: the mass enclosed HISTORY: 2019-08-12 - Written - Webb (UofT) 2019-11-06 - added physical compatibility - Starkman (UofT) """ vc= potential.vcirc(self.pot,r.value_in(units.kpc)/self.ro,phi=0, t=self.tgalpy,ro=self.ro,vo=self.vo, use_physical=False) * self.vo return (vc**2.)*r.value_in(units.parsec)/conversion._G \ | units.MSun def stop(self): """ NAME: stop PURPOSE: Stop the potential model (necessary function for AMUSE) INPUT: None OUTPUT: None HISTORY: 2019-08-12 - Written - Webb (UofT) """ pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/interpRZPotential.py0000644000175100001710000007434614327773303021405 0ustar00runnerdockerimport copy import ctypes import ctypes.util from functools import wraps import numpy from numpy.ctypeslib import ndpointer from scipy import interpolate from ..util import _load_extension_libs, multi from ..util.conversion import physical_conversion from .Potential import Potential _DEBUG= False _lib, ext_loaded= _load_extension_libs.load_libgalpy() def scalarVectorDecorator(func): """Decorator to return scalar outputs as a set""" @wraps(func) def scalar_wrapper(*args,**kwargs): if numpy.array(args[1]).shape == () \ and numpy.array(args[2]).shape == (): #only if both R and z are scalars scalarOut= True args= (args[0],numpy.array([args[1]]),numpy.array([args[2]])) elif numpy.array(args[1]).shape == () \ and not numpy.array(args[2]).shape == (): #R scalar, z vector scalarOut= False args= (args[0],args[1]*numpy.ones_like(args[2]),args[2]) elif not numpy.array(args[1]).shape == () \ and numpy.array(args[2]).shape == (): #R vector, z scalar scalarOut= False args= (args[0],args[1],args[2]*numpy.ones_like(args[1])) else: scalarOut= False result= func(*args,**kwargs) if scalarOut: return result[0] else: return result return scalar_wrapper def zsymDecorator(odd): """Decorator to deal with zsym=True input; set odd=True if the function is an odd function of z (like zforce)""" def wrapper(func): @wraps(func) def zsym_wrapper(*args,**kwargs): if args[0]._zsym: out= func(args[0],args[1],numpy.fabs(args[2]),**kwargs) else: out= func(*args,**kwargs) if odd and args[0]._zsym: return sign(args[2])*out else: return out return zsym_wrapper return wrapper def scalarDecorator(func): """Decorator to return scalar output for 1D functions (vcirc,etc.)""" @wraps(func) def scalar_wrapper(*args,**kwargs): if numpy.array(args[1]).shape == (): scalarOut= True args= (args[0],numpy.array([args[1]])) else: scalarOut= False result= func(*args,**kwargs) if scalarOut: return result[0] else: return result return scalar_wrapper class interpRZPotential(Potential): """Class that interpolates a given potential on a grid for fast orbit integration""" def __init__(self, RZPot=None,rgrid=(numpy.log(0.01),numpy.log(20.),101), zgrid=(0.,1.,101),logR=True, interpPot=False,interpRforce=False,interpzforce=False, interpDens=False, interpvcirc=False, interpdvcircdr=False, interpepifreq=False,interpverticalfreq=False, ro=None,vo=None, use_c=False,enable_c=False,zsym=True, numcores=None): """ NAME: __init__ PURPOSE: Initialize an interpRZPotential instance INPUT: RZPot - RZPotential to be interpolated rgrid - R grid to be given to linspace as in rs= linspace(*rgrid) zgrid - z grid to be given to linspace as in zs= linspace(*zgrid) logR - if True, rgrid is in the log of R so logrs= linspace(*rgrid) interpPot, interpRforce, interpzforce, interpDens,interpvcirc, interpepifreq, interpverticalfreq, interpdvcircdr= if True, interpolate these functions use_c= use C to speed up the calculation of the grid enable_c= enable use of C for interpolations zsym= if True (default), the potential is assumed to be symmetric around z=0 (so you can use, e.g., zgrid=(0.,1.,101)). numcores= if set to an integer, use this many cores (only used for vcirc, dvcircdR, epifreq, and verticalfreq; NOT NECESSARILY FASTER, TIME TO MAKE SURE) ro=, vo= distance and velocity scales for translation into internal units (default from configuration file) OUTPUT: instance HISTORY: 2010-07-21 - Written - Bovy (NYU) 2013-01-24 - Started with new implementation - Bovy (IAS) """ if isinstance(RZPot,interpRZPotential): from ..potential import PotentialError raise PotentialError('Cannot setup interpRZPotential with another interpRZPotential') # Propagate ro and vo roSet= True voSet= True if ro is None: if isinstance(RZPot,list): ro= RZPot[0]._ro roSet= RZPot[0]._roSet else: ro= RZPot._ro roSet= RZPot._roSet if vo is None: if isinstance(RZPot,list): vo= RZPot[0]._vo voSet= RZPot[0]._voSet else: vo= RZPot._vo voSet= RZPot._voSet Potential.__init__(self,amp=1.,ro=ro,vo=vo) # Turn off physical if it hadn't been on if not roSet: self._roSet= False if not voSet: self._voSet= False self._origPot= RZPot self._rgrid= numpy.linspace(*rgrid) self._logR= logR if self._logR: self._rgrid= numpy.exp(self._rgrid) self._logrgrid= numpy.log(self._rgrid) self._zgrid= numpy.linspace(*zgrid) self._interpPot= interpPot self._interpRforce= interpRforce self._interpzforce= interpzforce self._interpDens= interpDens self._interpvcirc= interpvcirc self._interpdvcircdr= interpdvcircdr self._interpepifreq= interpepifreq self._interpverticalfreq= interpverticalfreq self._enable_c= enable_c*ext_loaded self.hasC= self._enable_c self._zsym= zsym if interpPot: if use_c*ext_loaded: self._potGrid, err= calc_potential_c(self._origPot,self._rgrid,self._zgrid) else: from ..potential import evaluatePotentials potGrid= numpy.zeros((len(self._rgrid),len(self._zgrid))) for ii in range(len(self._rgrid)): for jj in range(len(self._zgrid)): potGrid[ii,jj]= evaluatePotentials(self._origPot,self._rgrid[ii],self._zgrid[jj]) self._potGrid= potGrid if self._logR: self._potInterp= interpolate.RectBivariateSpline(self._logrgrid, self._zgrid, self._potGrid, kx=3,ky=3,s=0.) else: self._potInterp= interpolate.RectBivariateSpline(self._rgrid, self._zgrid, self._potGrid, kx=3,ky=3,s=0.) if enable_c*ext_loaded: self._potGrid_splinecoeffs= calc_2dsplinecoeffs_c(self._potGrid) if interpRforce: if use_c*ext_loaded: self._rforceGrid, err= calc_potential_c(self._origPot,self._rgrid,self._zgrid,rforce=True) else: from ..potential import evaluateRforces rforceGrid= numpy.zeros((len(self._rgrid),len(self._zgrid))) for ii in range(len(self._rgrid)): for jj in range(len(self._zgrid)): rforceGrid[ii,jj]= evaluateRforces(self._origPot,self._rgrid[ii],self._zgrid[jj]) self._rforceGrid= rforceGrid if self._logR: self._rforceInterp= interpolate.RectBivariateSpline(self._logrgrid, self._zgrid, self._rforceGrid, kx=3,ky=3,s=0.) else: self._rforceInterp= interpolate.RectBivariateSpline(self._rgrid, self._zgrid, self._rforceGrid, kx=3,ky=3,s=0.) if enable_c*ext_loaded: self._rforceGrid_splinecoeffs= calc_2dsplinecoeffs_c(self._rforceGrid) if interpzforce: if use_c*ext_loaded: self._zforceGrid, err= calc_potential_c(self._origPot,self._rgrid,self._zgrid,zforce=True) else: from ..potential import evaluatezforces zforceGrid= numpy.zeros((len(self._rgrid),len(self._zgrid))) for ii in range(len(self._rgrid)): for jj in range(len(self._zgrid)): zforceGrid[ii,jj]= evaluatezforces(self._origPot,self._rgrid[ii],self._zgrid[jj]) self._zforceGrid= zforceGrid if self._logR: self._zforceInterp= interpolate.RectBivariateSpline(self._logrgrid, self._zgrid, self._zforceGrid, kx=3,ky=3,s=0.) else: self._zforceInterp= interpolate.RectBivariateSpline(self._rgrid, self._zgrid, self._zforceGrid, kx=3,ky=3,s=0.) if enable_c*ext_loaded: self._zforceGrid_splinecoeffs= calc_2dsplinecoeffs_c(self._zforceGrid) if interpDens: from ..potential import evaluateDensities densGrid= numpy.zeros((len(self._rgrid),len(self._zgrid))) for ii in range(len(self._rgrid)): for jj in range(len(self._zgrid)): densGrid[ii,jj]= evaluateDensities(self._origPot,self._rgrid[ii],self._zgrid[jj]) self._densGrid= densGrid if self._logR: self._densInterp= interpolate.RectBivariateSpline(self._logrgrid, self._zgrid, numpy.log(self._densGrid+10.**-10.), kx=3,ky=3,s=0.) else: self._densInterp= interpolate.RectBivariateSpline(self._rgrid, self._zgrid, numpy.log(self._densGrid+10.**-10.), kx=3,ky=3,s=0.) if interpvcirc: from ..potential import vcirc if not numcores is None: self._vcircGrid= multi.parallel_map((lambda x: vcirc(self._origPot,self._rgrid[x])), list(range(len(self._rgrid))),numcores=numcores) else: self._vcircGrid= numpy.array([vcirc(self._origPot,r) for r in self._rgrid]) if self._logR: self._vcircInterp= interpolate.InterpolatedUnivariateSpline(self._logrgrid,self._vcircGrid,k=3) else: self._vcircInterp= interpolate.InterpolatedUnivariateSpline(self._rgrid,self._vcircGrid,k=3) if interpdvcircdr: from ..potential import dvcircdR if not numcores is None: self._dvcircdrGrid= multi.parallel_map((lambda x: dvcircdR(self._origPot,self._rgrid[x])), list(range(len(self._rgrid))),numcores=numcores) else: self._dvcircdrGrid= numpy.array([dvcircdR(self._origPot,r) for r in self._rgrid]) if self._logR: self._dvcircdrInterp= interpolate.InterpolatedUnivariateSpline(self._logrgrid,self._dvcircdrGrid,k=3) else: self._dvcircdrInterp= interpolate.InterpolatedUnivariateSpline(self._rgrid,self._dvcircdrGrid,k=3) if interpepifreq: from ..potential import epifreq if not numcores is None: self._epifreqGrid= numpy.array(multi.parallel_map((lambda x: epifreq(self._origPot,self._rgrid[x])), list(range(len(self._rgrid))),numcores=numcores)) else: self._epifreqGrid= numpy.array([epifreq(self._origPot,r) for r in self._rgrid]) indx= True^numpy.isnan(self._epifreqGrid) if numpy.sum(indx) < 4: if self._logR: self._epifreqInterp= interpolate.InterpolatedUnivariateSpline(self._logrgrid[indx],self._epifreqGrid[indx],k=1) else: self._epifreqInterp= interpolate.InterpolatedUnivariateSpline(self._rgrid[indx],self._epifreqGrid[indx],k=1) else: if self._logR: self._epifreqInterp= interpolate.InterpolatedUnivariateSpline(self._logrgrid[indx],self._epifreqGrid[indx],k=3) else: self._epifreqInterp= interpolate.InterpolatedUnivariateSpline(self._rgrid[indx],self._epifreqGrid[indx],k=3) if interpverticalfreq: from ..potential import verticalfreq if not numcores is None: self._verticalfreqGrid= multi.parallel_map((lambda x: verticalfreq(self._origPot,self._rgrid[x])), list(range(len(self._rgrid))),numcores=numcores) else: self._verticalfreqGrid= numpy.array([verticalfreq(self._origPot,r) for r in self._rgrid]) if self._logR: self._verticalfreqInterp= interpolate.InterpolatedUnivariateSpline(self._logrgrid,self._verticalfreqGrid,k=3) else: self._verticalfreqInterp= interpolate.InterpolatedUnivariateSpline(self._rgrid,self._verticalfreqGrid,k=3) return None @scalarVectorDecorator @zsymDecorator(False) def _evaluate(self,R,z,phi=0.,t=0.): from ..potential import evaluatePotentials if self._interpPot: out= numpy.empty(R.shape) indx= (R >= self._rgrid[0])*(R <= self._rgrid[-1])\ *(z <= self._zgrid[-1])*(z >= self._zgrid[0]) if numpy.sum(indx) > 0: if self._enable_c: out[indx]= eval_potential_c(self,R[indx],z[indx])[0]/self._amp else: if self._logR: out[indx]= self._potInterp.ev(numpy.log(R[indx]),z[indx]) else: out[indx]= self._potInterp.ev(R[indx],z[indx]) if numpy.sum(True^indx) > 0: out[True^indx]= evaluatePotentials(self._origPot, R[True^indx], z[True^indx]) return out else: return evaluatePotentials(self._origPot,R,z) @scalarVectorDecorator @zsymDecorator(False) def _Rforce(self,R,z,phi=0.,t=0.): from ..potential import evaluateRforces if self._interpRforce: out= numpy.empty(R.shape) indx= (R >= self._rgrid[0])*(R <= self._rgrid[-1])\ *(z <= self._zgrid[-1])*(z >= self._zgrid[0]) if numpy.sum(indx) > 0: if self._enable_c: out[indx]= eval_force_c(self,R[indx],z[indx])[0]/self._amp else: if self._logR: out[indx]= self._rforceInterp.ev(numpy.log(R[indx]),z[indx]) else: out[indx]= self._rforceInterp.ev(R[indx],z[indx]) if numpy.sum(True^indx) > 0: out[True^indx]= evaluateRforces(self._origPot, R[True^indx], z[True^indx]) return out else: return evaluateRforces(self._origPot,R,z) @scalarVectorDecorator @zsymDecorator(True) def _zforce(self,R,z,phi=0.,t=0.): from ..potential import evaluatezforces if self._interpzforce: out= numpy.empty(R.shape) indx= (R >= self._rgrid[0])*(R <= self._rgrid[-1])\ *(z <= self._zgrid[-1])*(z >= self._zgrid[0]) if numpy.sum(indx) > 0: if self._enable_c: out[indx]= eval_force_c(self,R[indx],z[indx], zforce=True)[0]/self._amp else: if self._logR: out[indx]= self._zforceInterp.ev(numpy.log(R[indx]), z[indx]) else: out[indx]= self._zforceInterp.ev(R[indx],z[indx]) if numpy.sum(True^indx) > 0: out[True^indx]= evaluatezforces(self._origPot, R[True^indx], z[True^indx]) return out else: return evaluatezforces(self._origPot,R,z) def _Rzderiv(self,R,z,phi=0.,t=0.): from ..potential import evaluateRzderivs return evaluateRzderivs(self._origPot,R,z) @scalarVectorDecorator @zsymDecorator(False) def _dens(self,R,z,phi=0.,t=0.): from ..potential import evaluateDensities if self._interpDens: out= numpy.empty(R.shape) indx= (R >= self._rgrid[0])*(R <= self._rgrid[-1])\ *(z <= self._zgrid[-1])*(z >= self._zgrid[0]) if numpy.sum(indx) > 0: if self._logR: out[indx]= numpy.exp(self._densInterp.ev(numpy.log(R[indx]),z[indx]))-10.**-10. else: out[indx]= numpy.exp(self._densInterp.ev(R[indx],z[indx]))-10.**-10. if numpy.sum(True^indx) > 0: out[True^indx]= evaluateDensities(self._origPot, R[True^indx], z[True^indx]) return out else: return evaluateDensities(self._origPot,R,z) @physical_conversion('velocity',pop=True) @scalarDecorator def vcirc(self,R): from ..potential import vcirc if self._interpvcirc: indx= (R >= self._rgrid[0])*(R <= self._rgrid[-1]) out= numpy.empty(R.shape) if numpy.sum(indx) > 0: if self._logR: out[indx]= self._vcircInterp(numpy.log(R[indx])) else: out[indx]= self._vcircInterp(R[indx]) if numpy.sum(True^indx) > 0: out[True^indx]= vcirc(self._origPot,R[True^indx]) return out else: return vcirc(self._origPot,R) @physical_conversion('frequency',pop=True) @scalarDecorator def dvcircdR(self,R): from ..potential import dvcircdR if self._interpdvcircdr: indx= (R >= self._rgrid[0])*(R <= self._rgrid[-1]) out= numpy.empty(R.shape) if numpy.sum(indx) > 0: if self._logR: out[indx]= self._dvcircdrInterp(numpy.log(R[indx])) else: out[indx]= self._dvcircdrInterp(R[indx]) if numpy.sum(True^indx) > 0: out[True^indx]= dvcircdR(self._origPot,R[True^indx]) return out else: return dvcircdR(self._origPot,R) @physical_conversion('frequency',pop=True) @scalarDecorator def epifreq(self,R): from ..potential import epifreq if self._interpepifreq: indx= (R >= self._rgrid[0])*(R <= self._rgrid[-1]) out= numpy.empty(R.shape) if numpy.sum(indx) > 0: if self._logR: out[indx]= self._epifreqInterp(numpy.log(R[indx])) else: out[indx]= self._epifreqInterp(R[indx]) if numpy.sum(True^indx) > 0: out[True^indx]= epifreq(self._origPot,R[True^indx]) return out else: return epifreq(self._origPot,R) @physical_conversion('frequency',pop=True) @scalarDecorator def verticalfreq(self,R): from ..potential import verticalfreq if self._interpverticalfreq: indx= (R >= self._rgrid[0])*(R <= self._rgrid[-1]) out= numpy.empty(R.shape) if numpy.sum(indx) > 0: if self._logR: out[indx]= self._verticalfreqInterp(numpy.log(R[indx])) else: out[indx]= self._verticalfreqInterp(R[indx]) if numpy.sum(True^indx) > 0: out[True^indx]= verticalfreq(self._origPot,R[True^indx]) return out else: return verticalfreq(self._origPot,R) def calc_potential_c(pot,R,z,rforce=False,zforce=False): """ NAME: calc_potential_c PURPOSE: Use C to calculate the potential on a grid INPUT: pot - Potential or list of such instances R - grid in R z - grid in z rforce=, zforce= if either of these is True, calculate the radial or vertical force instead OUTPUT: potential on the grid (2D array) HISTORY: 2013-01-24 - Written - Bovy (IAS) 2013-01-29 - Added forces - Bovy (IAS) """ from ..orbit.integrateFullOrbit import \ _parse_pot # here bc otherwise there is an infinite loop from ..orbit.integratePlanarOrbit import _prep_tfuncs #Parse the potential npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Set up result arrays out= numpy.empty((len(R),len(z))) err= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') if rforce: interppotential_calc_potentialFunc= _lib.calc_rforce elif zforce: interppotential_calc_potentialFunc= _lib.calc_zforce else: interppotential_calc_potentialFunc= _lib.calc_potential interppotential_calc_potentialFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements, first store old order f_cont= [R.flags['F_CONTIGUOUS'], z.flags['F_CONTIGUOUS']] R= numpy.require(R,dtype=numpy.float64,requirements=['C','W']) z= numpy.require(z,dtype=numpy.float64,requirements=['C','W']) out= numpy.require(out,dtype=numpy.float64,requirements=['C','W']) #Run the C code interppotential_calc_potentialFunc(len(R), R, len(z), z, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, out, ctypes.byref(err)) #Reset input arrays if f_cont[0]: R= numpy.asfortranarray(R) if f_cont[1]: z= numpy.asfortranarray(z) return (out,err.value) def calc_2dsplinecoeffs_c(array2d): """ NAME: calc_2dsplinecoeffs_c PURPOSE: Use C to calculate spline coefficients for a 2D array INPUT: array2d OUTPUT: new array with spline coeffs HISTORY: 2013-01-24 - Written - Bovy (IAS) """ #Set up result arrays out= copy.copy(array2d) out= numpy.require(out,dtype=numpy.float64,requirements=['C','W']) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') interppotential_calc_2dsplinecoeffs= _lib.samples_to_coefficients interppotential_calc_2dsplinecoeffs.argtypes= [ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ctypes.c_int] #Run the C code interppotential_calc_2dsplinecoeffs(out,out.shape[1],out.shape[0]) return out def eval_potential_c(pot,R,z): """ NAME: eval_potential_c PURPOSE: Use C to evaluate the interpolated potential INPUT: pot - Potential or list of such instances R - array z - array OUTPUT: potential evaluated R and z HISTORY: 2013-01-24 - Written - Bovy (IAS) """ from ..orbit.integrateFullOrbit import \ _parse_pot # here bc otherwise there is an infinite loop from ..orbit.integratePlanarOrbit import _prep_tfuncs #Parse the potential npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot,potforactions=True) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Set up result arrays out= numpy.empty(len(R)) err= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') interppotential_calc_potentialFunc= _lib.eval_potential interppotential_calc_potentialFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements, first store old order f_cont= [R.flags['F_CONTIGUOUS'], z.flags['F_CONTIGUOUS']] R= numpy.require(R,dtype=numpy.float64,requirements=['C','W']) z= numpy.require(z,dtype=numpy.float64,requirements=['C','W']) out= numpy.require(out,dtype=numpy.float64,requirements=['C','W']) #Run the C code interppotential_calc_potentialFunc(len(R), R, z, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, out, ctypes.byref(err)) #Reset input arrays if f_cont[0]: R= numpy.asfortranarray(R) if f_cont[1]: z= numpy.asfortranarray(z) return (out,err.value) def eval_force_c(pot,R,z,zforce=False): """ NAME: eval_force_c PURPOSE: Use C to evaluate the interpolated potential's forces INPUT: pot - Potential or list of such instances R - array z - array zforce= if True, return the vertical force, otherwise return the radial force OUTPUT: force evaluated R and z HISTORY: 2013-01-29 - Written - Bovy (IAS) """ from ..orbit.integrateFullOrbit import \ _parse_pot # here bc otherwise there is an infinite loop from ..orbit.integratePlanarOrbit import _prep_tfuncs #Parse the potential npot, pot_type, pot_args, pot_tfuncs= _parse_pot(pot) pot_tfuncs= _prep_tfuncs(pot_tfuncs) #Set up result arrays out= numpy.empty(len(R)) err= ctypes.c_int(0) #Set up the C code ndarrayFlags= ('C_CONTIGUOUS','WRITEABLE') if zforce: interppotential_calc_forceFunc= _lib.eval_zforce else: interppotential_calc_forceFunc= _lib.eval_rforce interppotential_calc_forceFunc.argtypes= [ctypes.c_int, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.int32,flags=ndarrayFlags), ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.c_void_p, ndpointer(dtype=numpy.float64,flags=ndarrayFlags), ctypes.POINTER(ctypes.c_int)] #Array requirements, first store old order f_cont= [R.flags['F_CONTIGUOUS'], z.flags['F_CONTIGUOUS']] R= numpy.require(R,dtype=numpy.float64,requirements=['C','W']) z= numpy.require(z,dtype=numpy.float64,requirements=['C','W']) out= numpy.require(out,dtype=numpy.float64,requirements=['C','W']) #Run the C code interppotential_calc_forceFunc(len(R), R, z, ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, out, ctypes.byref(err)) #Reset input arrays if f_cont[0]: R= numpy.asfortranarray(R) if f_cont[1]: z= numpy.asfortranarray(z) return (out,err.value) def sign(x): out= numpy.ones_like(x) out[(x < 0.)]= -1. return out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/interpSphericalPotential.py0000644000175100001710000001103714327773303022750 0ustar00runnerdocker###################3###################3###################3################## # interpSphericalPotential.py: build spherical potential through interpolation ###################3###################3###################3################## import numpy from scipy import interpolate from ..util.conversion import get_physical, physical_compatible from .Potential import _evaluatePotentials, _evaluateRforces from .SphericalPotential import SphericalPotential class interpSphericalPotential(SphericalPotential): """__init__(self,rforce=None,rgrid=numpy.geomspace(0.01,20,101),Phi0=None,ro=None,vo=None) Class that interpolates a spherical potential on a grid""" def __init__(self,rforce=None,rgrid=numpy.geomspace(0.01,20,101),Phi0=None, ro=None,vo=None): """__init__(self,rforce=None,rgrid=numpy.geomspace(0.01,20,101),Phi0=None,ro=None,vo=None) NAME: __init__ PURPOSE: initialize an interpolated, spherical potential INPUT: rforce= (None) Either a) function that gives the radial force (in internal units) as a function of r (in internal units) or b) a galpy Potential instance or list thereof rgrid= (numpy.geomspace(0.01,20,101)) radial grid in internal units on which to evaluate the potential for interpolation (note that beyond rgrid[-1], the potential is extrapolated as -GM(= self._rmax]= -self._total_mass/r[r >= self._rmax]+self._Phimax out[r < self._rmax]= -self._pot_spline(r[r < self._rmax])+self._Phi0 return out def _rforce(self,r,t=0.): out= numpy.empty_like(r) out[r >= self._rmax]= -self._total_mass/r[r >= self._rmax]**2. out[r < self._rmax]= self._force_spline(r[r < self._rmax]) return out def _r2deriv(self,r,t=0.): out= numpy.empty_like(r) out[r >= self._rmax]= -2.*self._total_mass/r[r >= self._rmax]**3. out[r < self._rmax]= -self._r2deriv_spline(r[r < self._rmax]) return out def _rdens(self,r,t=0.): out= numpy.empty_like(r) out[r >= self._rmax]= 0. # Fall back onto Poisson eqn., implemented in SphericalPotential out[r < self._rmax]= SphericalPotential._rdens(self,r[r < self._rmax]) return out ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667233486.7134671 galpy-1.8.1/galpy/potential/interppotential_c_ext/0000755000175100001710000000000014327773317021770 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/interppotential_c_ext/interppotential_calc_potential.c0000644000175100001710000001334514327773303030417 0ustar00runnerdocker/* C code for calculating a potential and its forces on a grid */ #ifdef _WIN32 #include #endif #include #include #include #include #ifdef _OPENMP #include #endif #define CHUNKSIZE 1 //Potentials #include #include #include #include #include //Macros to export functions in DLL on different OS #if defined(_WIN32) #define EXPORT __declspec(dllexport) #elif defined(__GNUC__) #define EXPORT __attribute__((visibility("default"))) #else // Just do nothing? #define EXPORT #endif /* MAIN FUNCTIONS */ EXPORT void calc_potential(int nR, double *R, int nz, double *z, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double *out, int * err){ int ii, jj, tid, nthreads; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif double * row= (double *) malloc ( nthreads * nz * ( sizeof ( double ) ) ); //Set up the potentials struct potentialArg * potentialArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,potentialArgs,&pot_type,&pot_args,&pot_tfuncs); //Run through the grid and calculate UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) private(ii,tid,jj) \ shared(row,npot,potentialArgs,R,z,nR,nz) for (ii=0; ii < nR; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif for (jj=0; jj < nz; jj++){ *(row+jj+tid*nz)= evaluatePotentials(*(R+ii),*(z+jj),npot,potentialArgs); } put_row(out,ii,row+tid*nz,nz); } free_potentialArgs(npot,potentialArgs); free(potentialArgs); free(row); } EXPORT void calc_rforce(int nR, double *R, int nz, double *z, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double *out, int * err){ int ii, jj, tid, nthreads; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif double * row= (double *) malloc ( nthreads * nz * ( sizeof ( double ) ) ); //Set up the potentials struct potentialArg * potentialArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,potentialArgs,&pot_type,&pot_args,&pot_tfuncs); //Run through the grid and calculate UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) private(ii,tid,jj) \ shared(row,npot,potentialArgs,R,z,nR,nz) for (ii=0; ii < nR; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif for (jj=0; jj < nz; jj++){ *(row+jj+tid*nz)= calcRforce(*(R+ii),*(z+jj),0.,0.,npot,potentialArgs); } put_row(out,ii,row+tid*nz,nz); } free_potentialArgs(npot,potentialArgs); free(potentialArgs); free(row); } EXPORT void calc_zforce(int nR, double *R, int nz, double *z, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double *out, int * err){ int ii, jj, tid, nthreads; #ifdef _OPENMP nthreads = omp_get_max_threads(); #else nthreads = 1; #endif double * row= (double *) malloc ( nthreads * nz * ( sizeof ( double ) ) ); //Set up the potentials struct potentialArg * potentialArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,potentialArgs,&pot_type,&pot_args,&pot_tfuncs); //Run through the grid and calculate UNUSED int chunk= CHUNKSIZE; #pragma omp parallel for schedule(static,chunk) private(ii,tid,jj) \ shared(row,npot,potentialArgs,R,z,nR,nz) for (ii=0; ii < nR; ii++){ #ifdef _OPENMP tid= omp_get_thread_num(); #else tid = 0; #endif for (jj=0; jj < nz; jj++){ *(row+jj+tid*nz)= calczforce(*(R+ii),*(z+jj),0.,0.,npot,potentialArgs); } put_row(out,ii,row+tid*nz,nz); } free_potentialArgs(npot,potentialArgs); free(potentialArgs); free(row); } EXPORT void eval_potential(int nR, double *R, double *z, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double *out, int * err){ int ii; //Set up the potentials struct potentialArg * potentialArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,potentialArgs,&pot_type,&pot_args,&pot_tfuncs); //Run through and evaluate for (ii=0; ii < nR; ii++){ *(out+ii)= evaluatePotentials(*(R+ii),*(z+ii),npot,potentialArgs); } free_potentialArgs(npot,potentialArgs); free(potentialArgs); } EXPORT void eval_rforce(int nR, double *R, double *z, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double *out, int * err){ int ii; //Set up the potentials struct potentialArg * potentialArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,potentialArgs,&pot_type,&pot_args,&pot_tfuncs); //Run through and evaluate for (ii=0; ii < nR; ii++){ *(out+ii)= calcRforce(*(R+ii),*(z+ii),0.,0.,npot,potentialArgs); } free_potentialArgs(npot,potentialArgs); free(potentialArgs); } EXPORT void eval_zforce(int nR, double *R, double *z, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double *out, int * err){ int ii; //Set up the potentials struct potentialArg * potentialArgs= (struct potentialArg *) malloc ( npot * sizeof (struct potentialArg) ); parse_leapFuncArgs_Full(npot,potentialArgs,&pot_type,&pot_args,&pot_tfuncs); //Run through and evaluate for (ii=0; ii < nR; ii++){ *(out+ii)= calczforce(*(R+ii),*(z+ii),0.,0.,npot,potentialArgs); } free_potentialArgs(npot,potentialArgs); free(potentialArgs); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/linearPotential.py0000644000175100001710000002526114327773303021072 0ustar00runnerdockerimport copy import os import os.path import pickle import numpy from ..util import config, conversion, plot from ..util.conversion import (physical_compatible, physical_conversion, potential_physical_input) from .Potential import PotentialError, flatten class linearPotential: """Class representing 1D potentials""" def __init__(self,amp=1.,ro=None,vo=None): self._amp= amp self.dim= 1 self.isRZ= False self.hasC= False self.hasC_dxdv= False self.hasC_dens= False # Parse ro and vo if ro is None: self._ro= config.__config__.getfloat('normalization','ro') self._roSet= False else: ro= conversion.parse_length_kpc(ro) self._ro= ro self._roSet= True if vo is None: self._vo= config.__config__.getfloat('normalization','vo') self._voSet= False else: vo= conversion.parse_velocity_kms(vo) self._vo= vo self._voSet= True return None def __mul__(self,b): """ NAME: __mul__ PURPOSE: Multiply a linearPotential's amplitude by a number INPUT: b - number OUTPUT: New instance with amplitude = (old amplitude) x b HISTORY: 2019-01-27 - Written - Bovy (UofT) """ if not isinstance(b,(int,float)): raise TypeError("Can only multiply a planarPotential instance with a number") out= copy.deepcopy(self) out._amp*= b return out # Similar functions __rmul__= __mul__ def __div__(self,b): return self.__mul__(1./b) __truediv__= __div__ def __add__(self,b): """ NAME: __add__ PURPOSE: Add linearPotential instances together to create a multi-component potential (e.g., pot= pot1+pot2+pot3) INPUT: b - linearPotential instance or a list thereof OUTPUT: List of linearPotential instances that represents the combined potential HISTORY: 2019-01-27 - Written - Bovy (UofT) """ from ..potential import flatten as flatten_pot if not isinstance(flatten_pot([b])[0],linearPotential): raise TypeError("""Can only combine galpy linearPotential""" """ objects with """ """other such objects or lists thereof""") assert physical_compatible(self,b), \ """Physical unit conversion parameters (ro,vo) are not """\ """compatible between potentials to be combined""" if isinstance(b,list): return [self]+b else: return [self,b] # Define separately to keep order def __radd__(self,b): from ..potential import flatten as flatten_pot if not isinstance(flatten_pot([b])[0],linearPotential): raise TypeError("""Can only combine galpy linearPotential""" """ objects with """ """other such objects or lists thereof""") assert physical_compatible(self,b), \ """Physical unit conversion parameters (ro,vo) are not """\ """compatible between potentials to be combined""" # If we get here, b has to be a list return b+[self] def turn_physical_off(self): """ NAME: turn_physical_off PURPOSE: turn off automatic returning of outputs in physical units INPUT: (none) OUTPUT: (none) HISTORY: 2016-01-30 - Written - Bovy (UofT) """ self._roSet= False self._voSet= False return None def turn_physical_on(self,ro=None,vo=None): """ NAME: turn_physical_on PURPOSE: turn on automatic returning of outputs in physical units INPUT: ro= reference distance (kpc; can be Quantity) vo= reference velocity (km/s; can be Quantity) OUTPUT: (none) HISTORY: 2016-01-30 - Written - Bovy (UofT) 2020-04-22 - Don't turn on a parameter when it is False - Bovy (UofT) """ if not ro is False: self._roSet= True if not vo is False: self._voSet= True if not ro is None and ro: self._ro= conversion.parse_length_kpc(ro) if not vo is None and vo: self._vo= conversion.parse_velocity_kms(vo) return None @potential_physical_input @physical_conversion('energy',pop=True) def __call__(self,x,t=0.): """ NAME: __call__ PURPOSE: evaluate the potential INPUT: x - position (can be Quantity) t= time (optional; can be Quantity) OUTPUT: Phi(x,t) HISTORY: 2010-07-12 - Written - Bovy (NYU) """ return self._call_nodecorator(x,t=t) def _call_nodecorator(self,x,t=0.): # Separate, so it can be used during orbit integration try: return self._amp*self._evaluate(x,t=t) except AttributeError: #pragma: no cover raise PotentialError("'_evaluate' function not implemented for this potential") @potential_physical_input @physical_conversion('force',pop=True) def force(self,x,t=0.): """ NAME: force PURPOSE: evaluate the force INPUT: x - position (can be Quantity) t= time (optional; can be Quantity) OUTPUT: F(x,t) HISTORY: 2010-07-12 - Written - Bovy (NYU) """ return self._force_nodecorator(x,t=t) def _force_nodecorator(self,x,t=0.): # Separate, so it can be used during orbit integration try: return self._amp*self._force(x,t=t) except AttributeError: #pragma: no cover raise PotentialError("'_force' function not implemented for this potential") def plot(self,t=0.,min=-15.,max=15,ns=21,savefilename=None): """ NAME: plot PURPOSE: plot the potential INPUT: t - time to evaluate the potential at min - minimum x max - maximum x ns - grid in x savefilename - save to or restore from this savefile (pickle) OUTPUT: plot to output device HISTORY: 2010-07-13 - Written - Bovy (NYU) """ if not savefilename == None and os.path.exists(savefilename): print("Restoring savefile "+savefilename+" ...") savefile= open(savefilename,'rb') potx= pickle.load(savefile) xs= pickle.load(savefile) savefile.close() else: xs= numpy.linspace(min,max,ns) potx= numpy.zeros(ns) for ii in range(ns): potx[ii]= self._evaluate(xs[ii],t=t) if not savefilename == None: print("Writing savefile "+savefilename+" ...") savefile= open(savefilename,'wb') pickle.dump(potx,savefile) pickle.dump(xs,savefile) savefile.close() return plot.plot(xs,potx, xlabel=r"$x/x_0$",ylabel=r"$\Phi(x)$", xrange=[min,max]) @potential_physical_input @physical_conversion('energy',pop=True) def evaluatelinearPotentials(Pot,x,t=0.): """ NAME: evaluatelinearPotentials PURPOSE: evaluate the sum of a list of potentials INPUT: Pot - (list of) linearPotential instance(s) x - evaluate potentials at this position (can be Quantity) t - time to evaluate at (can be Quantity) OUTPUT: pot(x,t) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return _evaluatelinearPotentials(Pot,x,t=t) def _evaluatelinearPotentials(Pot,x,t=0.): """Raw, undecorated function for internal use""" if isinstance(Pot,list): sum= 0. for pot in Pot: sum+= pot._call_nodecorator(x,t=t) return sum elif isinstance(Pot,linearPotential): return Pot._call_nodecorator(x,t=t) else: #pragma: no cover raise PotentialError("Input to 'evaluatelinearPotentials' is neither a linearPotential-instance or a list of such instances") @potential_physical_input @physical_conversion('force',pop=True) def evaluatelinearForces(Pot,x,t=0.): """ NAME: evaluatelinearForces PURPOSE: evaluate the forces due to a list of potentials INPUT: Pot - (list of) linearPotential instance(s) x - evaluate forces at this position (can be Quantity) t - time to evaluate at (can be Quantity) OUTPUT: force(x,t) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return _evaluatelinearForces(Pot,x,t=t) def _evaluatelinearForces(Pot,x,t=0.): """Raw, undecorated function for internal use""" if isinstance(Pot,list): sum= 0. for pot in Pot: sum+= pot._force_nodecorator(x,t=t) return sum elif isinstance(Pot,linearPotential): return Pot._force_nodecorator(x,t=t) else: #pragma: no cover raise PotentialError("Input to 'evaluateForces' is neither a linearPotential-instance or a list of such instances") def plotlinearPotentials(Pot,t=0.,min=-15.,max=15,ns=21,savefilename=None): """ NAME: plotlinearPotentials PURPOSE: plot a combination of potentials INPUT: t - time to evaluate potential at min - minimum x max - maximum x ns - grid in x savefilename - save to or restore from this savefile (pickle) OUTPUT: plot to output device HISTORY: 2010-07-13 - Written - Bovy (NYU) """ Pot= flatten(Pot) if not savefilename == None and os.path.exists(savefilename): print("Restoring savefile "+savefilename+" ...") savefile= open(savefilename,'rb') potx= pickle.load(savefile) xs= pickle.load(savefile) savefile.close() else: xs= numpy.linspace(min,max,ns) potx= numpy.zeros(ns) for ii in range(ns): potx[ii]= evaluatelinearPotentials(Pot,xs[ii],t=t) if not savefilename == None: print("Writing savefile "+savefilename+" ...") savefile= open(savefilename,'wb') pickle.dump(potx,savefile) pickle.dump(xs,savefile) savefile.close() return plot.plot(xs,potx, xlabel=r"$x/x_0$",ylabel=r"$\Phi(x)$", xrange=[min,max]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/mwpot_helpers.py0000644000175100001710000000242014327773303020620 0ustar00runnerdocker# mwpot_helpers.py: auxiliary functions to help in setting up various # Milky-Way-like potentials # (for now, functions required to setup the Dehnen & Binney (1998), # Binney & Tremaine (2008), and McMillan (2017) potentials) import numpy def expexp_dens(R,z,Rd,zd,Sigma0): """rho(R,z) = Sigma_0/(2zd) exp(-|z|/zd-R/Rd)""" return Sigma0/(2*zd)*numpy.exp(-numpy.fabs(z)/zd-R/Rd) def expexp_dens_with_hole(R,z,Rd,Rm,zd,Sigma0): """rho(R,z) = Sigma0 / (4zd) exp(-Rm/R-R/Rd-|z|/zd)""" if R == 0.: return 0. return Sigma0/(2*zd)*numpy.exp(-Rm/R-R/Rd-numpy.fabs(z)/zd) def expsech2_dens_with_hole(R,z,Rd,Rm,zd,Sigma0): """rho(R,z) = Sigma0 / (4zd) exp(-Rm/R-R/Rd)*sech(z/[2zd])^2""" if R == 0.: return 0. return Sigma0/(4*zd)*numpy.exp(-Rm/R-R/Rd)/numpy.cosh(z/(2*zd))**2 def core_pow_dens_with_cut(R,z,alpha,r0,rcut,rho0,q): """rho(R,z) = rho0(1+r'/r0)^-alpha exp(-[r'/rcut]^2) r' = sqrt(R^2+z^2/q^2""" rdash= numpy.sqrt(R**2+(z/q)**2) return rho0/(1+rdash/r0)**alpha*numpy.exp(-(rdash/rcut)**2) def pow_dens_with_cut(R,z,alpha,r0,rcut,rho0,q): """rho(R,z) = rho0(1+r'/r0)^-alpha exp(-[r'/rcut]^2) r' = sqrt(R^2+z^2/q^2""" rdash= numpy.sqrt(R**2+(z/q)**2) return rho0/(rdash/r0)**alpha*numpy.exp(-(rdash/rcut)**2) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/mwpotentials.py0000644000175100001710000001331014327773303020456 0ustar00runnerdocker# galpy.potential.mwpotentials: Milky-Way-like potentials and tools for # working with MW-like potentials (bars, spirals, ...) import copy import os import sys from . import (HernquistPotential, MiyamotoNagaiPotential, NFWPotential, PowerSphericalPotentialwCutoff) ############################ MILKY WAY MODELS ################################# # galpy's first version of a MW-like potential, kept for backwards # compatibility and reproducibility-of-results-in-the-literature reasons, # underscore it here to avoid use _MWPotential= [MiyamotoNagaiPotential(a=0.5,b=0.0375,normalize=.6), NFWPotential(a=4.5,normalize=.35), HernquistPotential(a=0.6/8,normalize=0.05)] # See Table 1 in galpy paper: Bovy (2014) MWPotential2014= [PowerSphericalPotentialwCutoff(normalize=0.05,alpha=1.8, rc=1.9/8.), MiyamotoNagaiPotential(a=3./8.,b=0.28/8.,normalize=0.6), NFWPotential(a=2.,normalize=0.35)] # Following class allows potentials that are expensive to setup to be # lazily-loaded # (see https://stackoverflow.com/questions/1462986/lazy-module-variables-can-it-be-done) def _setup_globals(): # this func necessary to transfer *all* globals in Py2 out= copy.copy(globals()) out['__path__']= [os.path.dirname(__file__)] return out class _ExpensivePotentials: def __init__(self): # Initialize all expensive potentials as None, filled in when loaded self._mcmillan17= None self._cautun20= None self._irrgang13i= None self._irrgang13ii= None self._irrgang13iii= None self._dehnenbinney98i= None self._dehnenbinney98ii= None self._dehnenbinney98iii= None self._dehnenbinney98iv= None # This is necessary to transfer *all* globals in Py2 self.__globals__= _setup_globals() return None # For tab completion def __dir__(self): # pragma: no cover return ['McMillan17','Irrgang13I','Irrgang13II','Irrgang13III', 'Cautun20','DehnenBinney98I','DehnenBinney98II', 'DehnenBinney98III','DehnenBinney98IV'] @property def McMillan17(self): if not self._mcmillan17: # In python 3 this can be a relative import, but for some reason # in python 2 it cannot from galpy.potential.McMillan17 import McMillan17 as _McMillan17 self._mcmillan17= _McMillan17 return self._mcmillan17 @property def Cautun20(self): if not self._cautun20: # In python 3 this can be a relative import, but for some reason # in python 2 it cannot from galpy.potential.Cautun20 import Cautun20 as _Cautun20 self._cautun20= _Cautun20 return self._cautun20 @property def Irrgang13I(self): if not self._irrgang13i: # In python 3 this can be a relative import, but for some reason # in python 2 it cannot from galpy.potential.Irrgang13 import Irrgang13I as _Irrgang13I self._irrgang13i= _Irrgang13I return self._irrgang13i @property def Irrgang13II(self): if not self._irrgang13ii: # In python 3 this can be a relative import, but for some reason # in python 2 it cannot from galpy.potential.Irrgang13 import Irrgang13II as _Irrgang13II self._irrgang13ii= _Irrgang13II return self._irrgang13ii @property def Irrgang13III(self): if not self._irrgang13iii: # In python 3 this can be a relative import, but for some reason # in python 2 it cannot from galpy.potential.Irrgang13 import Irrgang13III as _Irrgang13III self._irrgang13iii= _Irrgang13III return self._irrgang13iii @property def DehnenBinney98I(self): # In python 3 this can be a relative import, but for some reason # in python 2 it cannot from galpy.potential.DehnenBinney98 import define_dehnenbinney98_models if not self._dehnenbinney98i: self._dehnenbinney98i= define_dehnenbinney98_models(model=1) return self._dehnenbinney98i @property def DehnenBinney98II(self): # In python 3 this can be a relative import, but for some reason # in python 2 it cannot from galpy.potential.DehnenBinney98 import define_dehnenbinney98_models if not self._dehnenbinney98ii: self._dehnenbinney98ii= define_dehnenbinney98_models(model=2) return self._dehnenbinney98ii @property def DehnenBinney98III(self): # In python 3 this can be a relative import, but for some reason # in python 2 it cannot from galpy.potential.DehnenBinney98 import define_dehnenbinney98_models if not self._dehnenbinney98iii: self._dehnenbinney98iii= define_dehnenbinney98_models(model=3) return self._dehnenbinney98iii @property def DehnenBinney98IV(self): # In python 3 this can be a relative import, but for some reason # in python 2 it cannot from galpy.potential.DehnenBinney98 import define_dehnenbinney98_models if not self._dehnenbinney98iv: self._dehnenbinney98iv= define_dehnenbinney98_models(model=4) return self._dehnenbinney98iv def __getattr__(self,name): try: #In Py3 you can just do 'return globals()[name]', but not in Py2 return self.__globals__[name] except: # pragma: no cover raise AttributeError(f"'module' object has no attribute '{name}'") __all__= ['MWPotential2014'] # The magic to make lazy-loading of expensive potentials possible sys.modules[__name__] = _ExpensivePotentials() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/planarPotential.py0000644000175100001710000011274014327773303021074 0ustar00runnerdockerimport copy import os import pickle import warnings import numpy from scipy import integrate from ..util import config, conversion, plot from ..util.conversion import (physical_compatible, physical_conversion, potential_physical_input) from .DissipativeForce import _isDissipative from .plotEscapecurve import _INF, plotEscapecurve from .plotRotcurve import plotRotcurve from .Potential import Potential, PotentialError, flatten, lindbladR class planarPotential: r"""Class representing 2D (R,\phi) potentials""" def __init__(self,amp=1.,ro=None,vo=None): self._amp= amp self.dim= 2 self.isNonAxi= True #Gets reset by planarAxiPotential self.isRZ= False self.hasC= False self.hasC_dxdv= False self.hasC_dens= False # Parse ro and vo if ro is None: self._ro= config.__config__.getfloat('normalization','ro') self._roSet= False else: self._ro= conversion.parse_length_kpc(ro) self._roSet= True if vo is None: self._vo= config.__config__.getfloat('normalization','vo') self._voSet= False else: self._vo= conversion.parse_velocity_kms(vo) self._voSet= True return None def __mul__(self,b): """ NAME: __mul__ PURPOSE: Multiply a planarPotential's amplitude by a number INPUT: b - number OUTPUT: New instance with amplitude = (old amplitude) x b HISTORY: 2019-01-27 - Written - Bovy (UofT) """ if not isinstance(b,(int,float)): raise TypeError("Can only multiply a planarPotential instance with a number") out= copy.deepcopy(self) out._amp*= b return out # Similar functions __rmul__= __mul__ def __div__(self,b): return self.__mul__(1./b) __truediv__= __div__ def __add__(self,b): """ NAME: __add__ PURPOSE: Add planarPotential instances together to create a multi-component potential (e.g., pot= pot1+pot2+pot3) INPUT: b - planarPotential instance or a list thereof OUTPUT: List of planarPotential instances that represents the combined potential HISTORY: 2019-01-27 - Written - Bovy (UofT) """ from ..potential import flatten as flatten_pot if not isinstance(flatten_pot([b])[0],(Potential,planarPotential)): raise TypeError("""Can only combine galpy Potential""" """/planarPotential objects with """ """other such objects or lists thereof""") assert physical_compatible(self,b), \ """Physical unit conversion parameters (ro,vo) are not """\ """compatible between potentials to be combined""" if isinstance(b,list): return [self]+b else: return [self,b] # Define separately to keep order def __radd__(self,b): from ..potential import flatten as flatten_pot if not isinstance(flatten_pot([b])[0],(Potential,planarPotential)): raise TypeError("""Can only combine galpy Force objects with """ """other Force objects or lists thereof""") assert physical_compatible(self,b), \ """Physical unit conversion parameters (ro,vo) are not """\ """compatible between potentials to be combined""" # If we get here, b has to be a list return b+[self] def turn_physical_off(self): """ NAME: turn_physical_off PURPOSE: turn off automatic returning of outputs in physical units INPUT: (none) OUTPUT: (none) HISTORY: 2016-01-30 - Written - Bovy (UofT) """ self._roSet= False self._voSet= False return None def turn_physical_on(self,ro=None,vo=None): """ NAME: turn_physical_on PURPOSE: turn on automatic returning of outputs in physical units INPUT: ro= reference distance (kpc; can be Quantity) vo= reference velocity (km/s; can be Quantity) OUTPUT: (none) HISTORY: 2016-01-30 - Written - Bovy (UofT) 2020-04-22 - Don't turn on a parameter when it is False - Bovy (UofT) """ if not ro is False: self._roSet= True if not vo is False: self._voSet= True if not ro is None and ro: self._ro= conversion.parse_length_kpc(ro) if not vo is None and vo: self._vo= conversion.parse_velocity_kms(vo) return None @potential_physical_input @physical_conversion('energy',pop=True) def __call__(self,R,phi=0.,t=0.,dR=0,dphi=0): """ NAME: __call__ PURPOSE: evaluate the potential INPUT: R - Cylindrica radius (can be Quantity) phi= azimuth (optional; can be Quantity) t= time (optional; can be Quantity) OUTPUT: Phi(R(,phi,t))) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return self._call_nodecorator(R,phi=phi,t=t,dR=dR,dphi=dphi) def _call_nodecorator(self,R,phi=0.,t=0.,dR=0,dphi=0): # Separate, so it can be used during orbit integration if dR == 0 and dphi == 0: try: return self._amp*self._evaluate(R,phi=phi,t=t) except AttributeError: #pragma: no cover raise PotentialError("'_evaluate' function not implemented for this potential") elif dR == 1 and dphi == 0: return -self.Rforce(R,phi=phi,t=t,use_physical=False) elif dR == 0 and dphi == 1: return -self.phitorque(R,phi=phi,t=t,use_physical=False) elif dR == 2 and dphi == 0: return self.R2deriv(R,phi=phi,t=t,use_physical=False) elif dR == 0 and dphi == 2: return self.phi2deriv(R,phi=phi,t=t,use_physical=False) elif dR == 1 and dphi == 1: return self.Rphideriv(R,phi=phi,t=t,use_physical=False) elif dR != 0 or dphi != 0: raise NotImplementedError('Higher-order derivatives not implemented for this potential') @potential_physical_input @physical_conversion('force',pop=True) def Rforce(self,R,phi=0.,t=0.): r""" NAME: Rforce PURPOSE: evaluate the radial force INPUT: R - Cylindrical radius (can be Quantity) phi= azimuth (optional; can be Quantity) t= time (optional; can be Quantity) OUTPUT: F_R(R,(\phi,t))) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return self._Rforce_nodecorator(R,phi=phi,t=t) def _Rforce_nodecorator(self,R,phi=0.,t=0.): # Separate, so it can be used during orbit integration try: return self._amp*self._Rforce(R,phi=phi,t=t) except AttributeError: #pragma: no cover raise PotentialError("'_Rforce' function not implemented for this potential") def phiforce(self,R,phi=0.,t=0.): warnings.warn('phiforce has been renamed phitorque, because it has always really been a torque (per unit mass); please switch to the new method name, because the old name will be removed in v1.9 and may be re-used for the actual phi force component',FutureWarning) return self.phitorque(R,phi=phi,t=t) @potential_physical_input @physical_conversion('energy',pop=True) def phitorque(self,R,phi=0.,t=0.): """ NAME: phitorque PURPOSE: evaluate the azimuthal torque = - d Phi / d phi INPUT: R - Cylindrical radius (can be Quantity) phi= azimuth (optional; can be Quantity) t= time (optional; can be Quantity) OUTPUT: tau_phi(R,(phi,t))) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return self._phitorque_nodecorator(R,phi=phi,t=t) def _phitorque_nodecorator(self,R,phi=0.,t=0.): # Separate, so it can be used during orbit integration try: return self._amp*self._phitorque(R,phi=phi,t=t) except AttributeError: #pragma: no cover raise PotentialError("'_phitorque' function not implemented for this potential") @potential_physical_input @physical_conversion('forcederivative',pop=True) def R2deriv(self,R,phi=0.,t=0.): """ NAME: R2deriv PURPOSE: evaluate the second radial derivative INPUT: R - Cylindrical radius (can be Quantity) phi= azimuth (optional; can be Quantity) t= time (optional; can be Quantity) OUTPUT: d2phi/dR2 HISTORY: 2011-10-09 - Written - Bovy (IAS) """ try: return self._amp*self._R2deriv(R,phi=phi,t=t) except AttributeError: #pragma: no cover raise PotentialError("'_R2deriv' function not implemented for this potential") @potential_physical_input @physical_conversion('energy',pop=True) def phi2deriv(self,R,phi=0.,t=0.): """ NAME: phi2deriv PURPOSE: evaluate the second azimuthal derivative INPUT: R - Cylindrical radius (can be Quantity) phi= azimuth (optional; can be Quantity) t= time (optional; can be Quantity) OUTPUT: d2phi/daz2 HISTORY: 2014-04-06 - Written - Bovy (IAS) """ try: return self._amp*self._phi2deriv(R,phi=phi,t=t) except AttributeError: #pragma: no cover raise PotentialError("'_phi2deriv' function not implemented for this potential") @potential_physical_input @physical_conversion('force',pop=True) def Rphideriv(self,R,phi=0.,t=0.): """ NAME: Rphideriv PURPOSE: evaluate the mixed radial and azimuthal derivative INPUT: R - Cylindrical radius (can be Quantity) phi= azimuth (optional can be Quantity) t= time (optional; can be Quantity) OUTPUT: d2phi/dR d az HISTORY: 2014-05-21 - Written - Bovy (IAS) """ try: return self._amp*self._Rphideriv(R,phi=phi,t=t) except AttributeError: #pragma: no cover raise PotentialError("'_Rphideriv' function not implemented for this potential") def plot(self,*args,**kwargs): """ NAME: plot PURPOSE: plot the potential INPUT: Rrange - range (can be Quantity) grid - number of points to plot savefilename - save to or restore from this savefile (pickle) +galpy.util.plot.plot(*args,**kwargs) OUTPUT: plot to output device HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return plotplanarPotentials(self,*args,**kwargs) class planarAxiPotential(planarPotential): """Class representing axisymmetric planar potentials""" def __init__(self,amp=1.,ro=None,vo=None): planarPotential.__init__(self,amp=amp,ro=ro,vo=vo) self.isNonAxi= False return None def _phitorque(self,R,phi=0.,t=0.): return 0. def _phi2deriv(self,R,phi=0.,t=0.): #pragma: no cover """ NAME: _phi2deriv PURPOSE: evaluate the second azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the second azimuthal derivative HISTORY: 2011-10-17 - Written - Bovy (IAS) """ return 0. def _Rphideriv(self,R,phi=0.,t=0.): #pragma: no cover """ NAME: _Rphideriv PURPOSE: evaluate the radial+azimuthal derivative for this potential INPUT: R - Galactocentric cylindrical radius z - vertical height phi - azimuth t - time OUTPUT: the radial+azimuthal derivative HISTORY: 2011-10-17 - Written - Bovy (IAS) """ return 0. @potential_physical_input @physical_conversion('velocity',pop=True) def vcirc(self,R,phi=None,t=0.): """ NAME: vcirc PURPOSE: calculate the circular velocity at R in potential Pot INPUT: Pot - Potential instance or list of such instances R - Galactocentric radius (can be Quantity) phi= (None) azimuth to use for non-axisymmetric potentials t - time (optional; can be Quantity) OUTPUT: circular rotation velocity HISTORY: 2011-10-09 - Written - Bovy (IAS) 2016-06-15 - Added phi= keyword for non-axisymmetric potential - Bovy (UofT) """ return numpy.sqrt(R*-self.Rforce(R,phi=phi,t=t,use_physical=False)) @potential_physical_input @physical_conversion('frequency',pop=True) def omegac(self,R,t=0.): """ NAME: omegac PURPOSE: calculate the circular angular speed at R in potential Pot INPUT: Pot - Potential instance or list of such instances R - Galactocentric radius (can be Quantity) t - time (optional; can be Quantity) OUTPUT: circular angular speed HISTORY: 2011-10-09 - Written - Bovy (IAS) """ return numpy.sqrt(-self.Rforce(R,t=t,use_physical=False)/R) @potential_physical_input @physical_conversion('frequency',pop=True) def epifreq(self,R,t=0.): """ NAME: epifreq PURPOSE: calculate the epicycle frequency at R in this potential INPUT: R - Galactocentric radius (can be Quantity) t - time (optional; can be Quantity) OUTPUT: epicycle frequency HISTORY: 2011-10-09 - Written - Bovy (IAS) """ return numpy.sqrt(self.R2deriv(R,t=t,use_physical=False) -3./R*self.Rforce(R,t=t,use_physical=False)) @physical_conversion('position',pop=True) def lindbladR(self,OmegaP,m=2,t=0.,**kwargs): """ NAME: lindbladR PURPOSE: calculate the radius of a Lindblad resonance INPUT: OmegaP - pattern speed (can be Quantity) m= order of the resonance (as in m(O-Op)=kappa (negative m for outer) use m='corotation' for corotation +scipy.optimize.brentq xtol,rtol,maxiter kwargs t - time (optional; can be Quantity) OUTPUT: radius of Linblad resonance, None if there is no resonance HISTORY: 2011-10-09 - Written - Bovy (IAS) """ OmegaP= conversion.parse_frequency(OmegaP,ro=self._ro,vo=self._vo) return lindbladR(self,OmegaP,m=m,t=t,use_physical=False,**kwargs) @potential_physical_input @physical_conversion('velocity',pop=True) def vesc(self,R,t=0.): """ NAME: vesc PURPOSE: calculate the escape velocity at R for potential Pot INPUT: Pot - Potential instances or list thereof R - Galactocentric radius (can be Quantity) t - time (optional; can be Quantity) OUTPUT: escape velocity HISTORY: 2011-10-09 - Written - Bovy (IAS) """ return numpy.sqrt(2.*(self(_INF,t=t,use_physical=False) -self(R,t=t,use_physical=False))) def plotRotcurve(self,*args,**kwargs): """ NAME: plotRotcurve PURPOSE: plot the rotation curve for this potential INPUT: Rrange - range (can be Quantity) grid - number of points to plot savefilename - save to or restore from this savefile (pickle) +galpy.util.plot.plot(*args,**kwargs) OUTPUT: plot to output device HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return plotRotcurve(self,*args,**kwargs) def plotEscapecurve(self,*args,**kwargs): """ NAME: plotEscapecurve PURPOSE: plot the escape velocity curve for this potential INPUT: Rrange - range (can be Quantity) grid - number of points to plot savefilename - save to or restore from this savefile (pickle) +galpy.util.plot.plot(*args,**kwargs) OUTPUT: plot to output device HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return plotEscapecurve(self,*args,**kwargs) class planarPotentialFromRZPotential(planarAxiPotential): """Class that represents an axisymmetic planar potential derived from a RZPotential""" def __init__(self,RZPot): """ NAME: __init__ PURPOSE: Initialize INPUT: RZPot - RZPotential instance OUTPUT: planarAxiPotential instance HISTORY: 2010-07-13 - Written - Bovy (NYU) """ planarAxiPotential.__init__(self,amp=1.,ro=RZPot._ro,vo=RZPot._vo) # Also transfer roSet and voSet self._roSet= RZPot._roSet self._voSet= RZPot._voSet self._Pot= RZPot self.hasC= RZPot.hasC self.hasC_dxdv= RZPot.hasC_dxdv self.hasC_dens= RZPot.hasC_dens return None def _evaluate(self,R,phi=0.,t=0.): r""" NAME: _evaluate PURPOSE: evaluate the potential INPUT: R phi t OUTPUT: Pot(R(,\phi,t)) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return self._Pot(R,0.,t=t,use_physical=False) def _Rforce(self,R,phi=0.,t=0.): r""" NAME: _Rforce PURPOSE: evaluate the radial force INPUT: R phi t OUTPUT: F_R(R(,\phi,t)) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return self._Pot.Rforce(R,0.,t=t,use_physical=False) def _R2deriv(self,R,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative INPUT: R phi t OUTPUT: d2phi/dR2 HISTORY: 2011-10-09 - Written - Bovy (IAS) """ return self._Pot.R2deriv(R,0.,t=t,use_physical=False) def RZToplanarPotential(RZPot): """ NAME: RZToplanarPotential PURPOSE: convert an RZPotential to a planarPotential in the mid-plane (z=0) INPUT: RZPot - RZPotential instance or list of such instances (existing planarPotential instances are just copied to the output) OUTPUT: planarPotential instance(s) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ RZPot= flatten(RZPot) if _isDissipative(RZPot): raise NotImplementedError("Converting dissipative forces to 2D potentials is currently not supported") if isinstance(RZPot,list): out= [] for pot in RZPot: if isinstance(pot,planarPotential) and not pot.isNonAxi: out.append(pot) elif isinstance(pot,Potential) and not pot.isNonAxi: out.append(planarPotentialFromRZPotential(pot)) else: raise PotentialError("Input to 'RZToplanarPotential' is neither an RZPotential-instance or a list of such instances") return out elif isinstance(RZPot,Potential) and not RZPot.isNonAxi: return planarPotentialFromRZPotential(RZPot) elif isinstance(RZPot,planarPotential) and not RZPot.isNonAxi: return RZPot else: raise PotentialError("Input to 'RZToplanarPotential' is neither an RZPotential-instance or a list of such instances") class planarPotentialFromFullPotential(planarPotential): """Class that represents a planar potential derived from a non-axisymmetric 3D potential""" def __init__(self,Pot): """ NAME: __init__ PURPOSE: Initialize INPUT: Pot - Potential instance OUTPUT: planarPotential instance HISTORY: 2016-06-02 - Written - Bovy (UofT) """ planarPotential.__init__(self,amp=1.,ro=Pot._ro,vo=Pot._vo) # Also transfer roSet and voSet self._roSet= Pot._roSet self._voSet= Pot._voSet self._Pot= Pot self.hasC= Pot.hasC self.hasC_dxdv= Pot.hasC_dxdv self.hasC_dens= Pot.hasC_dens return None def _evaluate(self,R,phi=0.,t=0.): r""" NAME: _evaluate PURPOSE: evaluate the potential INPUT: R phi t OUTPUT: Pot(R(,\phi,t)) HISTORY: 2016-06-02 - Written - Bovy (UofT) """ return self._Pot(R,0.,phi=phi,t=t,use_physical=False) def _Rforce(self,R,phi=0.,t=0.): r""" NAME: _Rforce PURPOSE: evaluate the radial force INPUT: R phi t OUTPUT: F_R(R(,\phi,t)) HISTORY: 2016-06-02 - Written - Bovy (UofT) """ return self._Pot.Rforce(R,0.,phi=phi,t=t,use_physical=False) def _phitorque(self,R,phi=0.,t=0.): r""" NAME: _phitorque PURPOSE: evaluate the azimuthal torque INPUT: R phi t OUTPUT: tau_phi(R(,\phi,t)) HISTORY: 2016-06-02 - Written - Bovy (UofT) """ return self._Pot.phitorque(R,0.,phi=phi,t=t,use_physical=False) def _R2deriv(self,R,phi=0.,t=0.): """ NAME: _R2deriv PURPOSE: evaluate the second radial derivative INPUT: R phi t OUTPUT: d2phi/dR2 HISTORY: 2016-06-02 - Written - Bovy (UofT) """ return self._Pot.R2deriv(R,0.,phi=phi,t=t,use_physical=False) def _phi2deriv(self,R,phi=0.,t=0.): """ NAME: _phi2deriv PURPOSE: evaluate the second azimuthal derivative INPUT: R phi t OUTPUT: d2phi/dphi2 HISTORY: 2016-06-02 - Written - Bovy (UofT) """ return self._Pot.phi2deriv(R,0.,phi=phi,t=t,use_physical=False) def _Rphideriv(self,R,phi=0.,t=0.): """ NAME: _Rphideriv PURPOSE: evaluate the mixed radial-azimuthal derivative INPUT: R phi t OUTPUT: d2phi/dRdphi HISTORY: 2016-06-02 - Written - Bovy (UofT) """ return self._Pot.Rphideriv(R,0.,phi=phi,t=t,use_physical=False) def OmegaP(self): """ NAME: OmegaP PURPOSE: return the pattern speed INPUT: (none) OUTPUT: pattern speed HISTORY: 2016-05-31 - Written - Bovy (UofT) """ return self._Pot.OmegaP() def toPlanarPotential(Pot): """ NAME: toPlanarPotential PURPOSE: convert an Potential to a planarPotential in the mid-plane (z=0) INPUT: Pot - Potential instance or list of such instances (existing planarPotential instances are just copied to the output) OUTPUT: planarPotential instance(s) HISTORY: 2016-06-11 - Written - Bovy (UofT) """ Pot= flatten(Pot) if _isDissipative(Pot): raise NotImplementedError("Converting dissipative forces to 2D potentials is currently not supported") elif isinstance(Pot,list): out= [] for pot in Pot: if isinstance(pot,planarPotential): out.append(pot) elif isinstance(pot,Potential) and pot.isNonAxi: out.append(planarPotentialFromFullPotential(pot)) elif isinstance(pot,Potential): out.append(planarPotentialFromRZPotential(pot)) else: raise PotentialError("Input to 'toPlanarPotential' is neither an Potential-instance or a list of such instances") return out elif isinstance(Pot,Potential) and Pot.isNonAxi: return planarPotentialFromFullPotential(Pot) elif isinstance(Pot,Potential): return planarPotentialFromRZPotential(Pot) elif isinstance(Pot,planarPotential): return Pot else: raise PotentialError("Input to 'toPlanarPotential' is neither an Potential-instance or a list of such instances") @potential_physical_input @physical_conversion('energy',pop=True) def evaluateplanarPotentials(Pot,R,phi=None,t=0.,dR=0,dphi=0): """ NAME: evaluateplanarPotentials PURPOSE: evaluate a (list of) planarPotential instance(s) INPUT: Pot - (list of) planarPotential instance(s) R - Cylindrical radius (can be Quantity) phi= azimuth (optional; can be Quantity) t= time (optional; can be Quantity) dR=, dphi= if set to non-zero integers, return the dR,dphi't derivative instead OUTPUT: Phi(R(,phi,t)) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return _evaluateplanarPotentials(Pot,R,phi=phi,t=t,dR=dR,dphi=dphi) def _evaluateplanarPotentials(Pot,R,phi=None,t=0.,dR=0,dphi=0): from .Potential import _isNonAxi isList= isinstance(Pot,list) nonAxi= _isNonAxi(Pot) if nonAxi and phi is None: raise PotentialError("The (list of) planarPotential instances is non-axisymmetric, but you did not provide phi") if isList and numpy.all([isinstance(p,planarPotential) for p in Pot]): sum= 0. for pot in Pot: if nonAxi: sum+= pot._call_nodecorator(R,phi=phi,t=t,dR=dR,dphi=dphi) else: sum+= pot._call_nodecorator(R,t=t,dR=dR,dphi=dphi) return sum elif isinstance(Pot,planarPotential): if nonAxi: return Pot._call_nodecorator(R,phi=phi,t=t,dR=dR,dphi=dphi) else: return Pot._call_nodecorator(R,t=t,dR=dR,dphi=dphi) else: #pragma: no cover raise PotentialError("Input to 'evaluatePotentials' is neither a Potential-instance or a list of such instances") @potential_physical_input @physical_conversion('force',pop=True) def evaluateplanarRforces(Pot,R,phi=None,t=0.): """ NAME: evaluateplanarRforces PURPOSE: evaluate the Rforce of a (list of) planarPotential instance(s) INPUT: Pot - (list of) planarPotential instance(s) R - Cylindrical radius (can be Quantity) phi= azimuth (optional can be Quantity) t= time (optional; can be Quantity) OUTPUT: F_R(R(,phi,t)) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return _evaluateplanarRforces(Pot,R,phi=phi,t=t) def _evaluateplanarRforces(Pot,R,phi=None,t=0.): """Raw, undecorated function for internal use""" from .Potential import _isNonAxi isList= isinstance(Pot,list) nonAxi= _isNonAxi(Pot) if nonAxi and phi is None: raise PotentialError("The (list of) planarPotential instances is non-axisymmetric, but you did not provide phi") if isinstance(Pot,list) \ and numpy.all([isinstance(p,planarPotential) for p in Pot]): sum= 0. for pot in Pot: if nonAxi: sum+= pot._Rforce_nodecorator(R,phi=phi,t=t) else: sum+= pot._Rforce_nodecorator(R,t=t) return sum elif isinstance(Pot,planarPotential): if nonAxi: return Pot._Rforce_nodecorator(R,phi=phi,t=t) else: return Pot._Rforce_nodecorator(R,t=t) else: #pragma: no cover raise PotentialError("Input to 'evaluatePotentials' is neither a Potential-instance or a list of such instances") def evaluateplanarphiforces(Pot,R,phi=None,t=0.): warnings.warn('evaluateplanarphiforces has been renamed evaluateplanarphitorques, because it has always really been a torque (per unit mass); please switch to the new method name, because the old name will be removed in v1.9 and may be re-used for the actual phi force component',FutureWarning) return evaluateplanarphitorques(Pot,R,phi=phi,t=t) @potential_physical_input @physical_conversion('energy',pop=True) def evaluateplanarphitorques(Pot,R,phi=None,t=0.): """ NAME: evaluateplanarphitorques PURPOSE: evaluate the phitorque of a (list of) planarPotential instance(s) INPUT: Pot - (list of) planarPotential instance(s) R - Cylindrical radius (can be Quantity) phi= azimuth (optional; can be Quantity) t= time (optional; can be Quantity) OUTPUT: tau_phi(R(,phi,t)) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ return _evaluateplanarphitorques(Pot,R,phi=phi,t=t) def _evaluateplanarphitorques(Pot,R,phi=None,t=0.): from .Potential import _isNonAxi isList= isinstance(Pot,list) nonAxi= _isNonAxi(Pot) if nonAxi and phi is None: raise PotentialError("The (list of) planarPotential instances is non-axisymmetric, but you did not provide phi") if isinstance(Pot,list) \ and numpy.all([isinstance(p,planarPotential) for p in Pot]): sum= 0. for pot in Pot: if nonAxi: sum+= pot._phitorque_nodecorator(R,phi=phi,t=t) else: sum+= pot._phitorque_nodecorator(R,t=t) return sum elif isinstance(Pot,planarPotential): if nonAxi: return Pot._phitorque_nodecorator(R,phi=phi,t=t) else: return Pot._phitorque_nodecorator(R,t=t) else: #pragma: no cover raise PotentialError("Input to 'evaluatePotentials' is neither a Potential-instance or a list of such instances") @potential_physical_input @physical_conversion('forcederivative',pop=True) def evaluateplanarR2derivs(Pot,R,phi=None,t=0.): """ NAME: evaluateplanarR2derivs PURPOSE: evaluate the second radial derivative of a (list of) planarPotential instance(s) INPUT: Pot - (list of) planarPotential instance(s) R - Cylindrical radius (can be Quantity) phi= azimuth (optional; can be Quantity) t= time (optional; can be Quantity) OUTPUT: F_R(R(,phi,t)) HISTORY: 2010-10-09 - Written - Bovy (IAS) """ from .Potential import _isNonAxi isList= isinstance(Pot,list) nonAxi= _isNonAxi(Pot) if nonAxi and phi is None: raise PotentialError("The (list of) planarPotential instances is non-axisymmetric, but you did not provide phi") if isinstance(Pot,list) \ and numpy.all([isinstance(p,planarPotential) for p in Pot]): sum= 0. for pot in Pot: if nonAxi: sum+= pot.R2deriv(R,phi=phi,t=t,use_physical=False) else: sum+= pot.R2deriv(R,t=t,use_physical=False) return sum elif isinstance(Pot,planarPotential): if nonAxi: return Pot.R2deriv(R,phi=phi,t=t,use_physical=False) else: return Pot.R2deriv(R,t=t,use_physical=False) else: #pragma: no cover raise PotentialError("Input to 'evaluatePotentials' is neither a Potential-instance or a list of such instances") def LinShuReductionFactor(axiPot,R,sigmar,nonaxiPot=None, k=None,m=None,OmegaP=None): """ NAME: LinShuReductionFactor PURPOSE: Calculate the Lin & Shu (1966) reduction factor: the reduced linear response of a kinematically-warm stellar disk to a perturbation INPUT: axiPot - The background, axisymmetric potential R - Cylindrical radius (can be Quantity) sigmar - radial velocity dispersion of the population (can be Quantity) Then either provide: 1) m= m in the perturbation's m x phi (number of arms for a spiral) k= wavenumber (see Binney & Tremaine 2008) OmegaP= pattern speed (can be Quantity) 2) nonaxiPot= a non-axisymmetric Potential instance (such as SteadyLogSpiralPotential) that has functions that return OmegaP, m, and wavenumber OUTPUT: reduction factor HISTORY: 2014-08-23 - Written - Bovy (IAS) """ axiPot= flatten(axiPot) from ..potential import epifreq, omegac if nonaxiPot is None and (OmegaP is None or k is None or m is None): raise OSError("Need to specify either nonaxiPot= or m=, k=, OmegaP= for LinShuReductionFactor") elif not nonaxiPot is None: OmegaP= nonaxiPot.OmegaP() k= nonaxiPot.wavenumber(R) m= nonaxiPot.m() tepif= epifreq(axiPot,R) s= m*(OmegaP-omegac(axiPot,R))/tepif chi= sigmar**2.*k**2./tepif**2. return (1.-s**2.)/numpy.sin(numpy.pi*s)\ *integrate.quad(lambda t: numpy.exp(-chi*(1.+numpy.cos(t)))\ *numpy.sin(s*t)*numpy.sin(t), 0.,numpy.pi)[0] def plotplanarPotentials(Pot,*args,**kwargs): """ NAME: plotplanarPotentials PURPOSE: plot a planar potential INPUT: Rrange - range (can be Quantity) xrange, yrange - if relevant (can be Quantity) grid, gridx, gridy - number of points to plot savefilename - save to or restore from this savefile (pickle) ncontours - number of contours to plot (if applicable) +galpy.util.plot.plot(*args,**kwargs) or galpy.util.plot.dens2d(**kwargs) OUTPUT: plot to output device HISTORY: 2010-07-13 - Written - Bovy (NYU) """ Pot= flatten(Pot) Rrange= kwargs.pop('Rrange',[0.01,5.]) xrange= kwargs.pop('xrange',[-5.,5.]) yrange= kwargs.pop('yrange',[-5.,5.]) if hasattr(Pot,'_ro'): tro= Pot._ro else: tro= Pot[0]._ro Rrange[0]= conversion.parse_length(Rrange[0],ro=tro) Rrange[1]= conversion.parse_length(Rrange[1],ro=tro) xrange[0]= conversion.parse_length(xrange[0],ro=tro) xrange[1]= conversion.parse_length(xrange[1],ro=tro) yrange[0]= conversion.parse_length(yrange[0],ro=tro) yrange[1]= conversion.parse_length(yrange[1],ro=tro) grid= kwargs.pop('grid',100) gridx= kwargs.pop('gridx',100) gridy= kwargs.pop('gridy',gridx) savefilename= kwargs.pop('savefilename',None) isList= isinstance(Pot,list) nonAxi= ((isList and Pot[0].isNonAxi) or (not isList and Pot.isNonAxi)) if not savefilename is None and os.path.exists(savefilename): print("Restoring savefile "+savefilename+" ...") savefile= open(savefilename,'rb') potR= pickle.load(savefile) if nonAxi: xs= pickle.load(savefile) ys= pickle.load(savefile) else: Rs= pickle.load(savefile) savefile.close() else: if nonAxi: xs= numpy.linspace(xrange[0],xrange[1],gridx) ys= numpy.linspace(yrange[0],yrange[1],gridy) potR= numpy.zeros((gridx,gridy)) for ii in range(gridx): for jj in range(gridy): thisR= numpy.sqrt(xs[ii]**2.+ys[jj]**2.) if xs[ii] >= 0.: thisphi= numpy.arcsin(ys[jj]/thisR) else: thisphi= -numpy.arcsin(ys[jj]/thisR)+numpy.pi potR[ii,jj]= evaluateplanarPotentials(Pot,thisR, phi=thisphi, use_physical=False) else: Rs= numpy.linspace(Rrange[0],Rrange[1],grid) potR= numpy.zeros(grid) for ii in range(grid): potR[ii]= evaluateplanarPotentials(Pot,Rs[ii], use_physical=False) if not savefilename is None: print("Writing planar savefile "+savefilename+" ...") savefile= open(savefilename,'wb') pickle.dump(potR,savefile) if nonAxi: pickle.dump(xs,savefile) pickle.dump(ys,savefile) else: pickle.dump(Rs,savefile) savefile.close() if nonAxi: if not 'orogin' in kwargs: kwargs['origin']= 'lower' if not 'cmap' in kwargs: kwargs['cmap']= 'gist_yarg' if not 'contours' in kwargs: kwargs['contours']= True if not 'xlabel' in kwargs: kwargs['xlabel']= r"$x / R_0$" if not 'ylabel' in kwargs: kwargs['ylabel']= "$y / R_0$" if not 'aspect' in kwargs: kwargs['aspect']= 1. if not 'cntrls' in kwargs: kwargs['cntrls']= '-' ncontours= kwargs.pop('ncontours',10) if not 'levels' in kwargs: kwargs['levels']= numpy.linspace(numpy.nanmin(potR),numpy.nanmax(potR),ncontours) return plot.dens2d(potR.T, xrange=xrange, yrange=yrange,**kwargs) else: kwargs['xlabel']=r"$R/R_0$" kwargs['ylabel']=r"$\Phi(R)$" kwargs['xrange']=Rrange return plot.plot(Rs,potR,*args,**kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/plotEscapecurve.py0000644000175100001710000001176414327773303021107 0ustar00runnerdockerimport os import pickle import numpy from ..util import conversion, plot from ..util.conversion import physical_conversion, potential_physical_input _INF= 10**12. def plotEscapecurve(Pot,*args,**kwargs): """ NAME: plotEscapecurve PURPOSE: plot the escape velocity curve for this potential (in the z=0 plane for non-spherical potentials) INPUT: Pot - Potential or list of Potential instances Rrange= Range in R to consider (can be Quantity) grid= grid in R savefilename= save to or restore from this savefile (pickle) +galpy.util.plot.plot args and kwargs OUTPUT: plot to output device HISTORY: 2010-08-08 - Written - Bovy (NYU) """ # Using physical units or not? if isinstance(Pot,list): potro= Pot[0]._ro roSet= Pot[0]._roSet potvo= Pot[0]._vo voSet= Pot[0]._voSet else: potro= Pot._ro roSet= Pot._roSet potvo= Pot._vo voSet= Pot._voSet if (kwargs.get('use_physical',False) \ and kwargs.get('ro',roSet) and kwargs.get('vo',voSet)) or \ (not 'use_physical' in kwargs \ and kwargs.get('ro',roSet) and kwargs.get('vo',voSet)): use_physical= True potro= kwargs.get('ro',potro) potvo= kwargs.get('vo',potvo) xlabel= r'$R\,(\mathrm{kpc})$' ylabel= r"$v_e(R)\,(\mathrm{km\,s}^{-1})$" Rrange= kwargs.pop('Rrange',[0.01*potro,5.*potro]) else: use_physical= False xlabel= r"$R/R_0$" ylabel= r"$v_e(R)/v_c(R_0)$" Rrange= kwargs.pop('Rrange',[0.01,5.]) # Parse ro potro= conversion.parse_length_kpc(potro) potvo= conversion.parse_velocity_kms(potvo) Rrange[0]= conversion.parse_length_kpc(Rrange[0]) Rrange[1]= conversion.parse_length_kpc(Rrange[1]) if use_physical: Rrange[0]/= potro Rrange[1]/= potro grid= kwargs.pop('grid',1001) savefilename= kwargs.pop('savefilename',None) if not savefilename == None and os.path.exists(savefilename): print("Restoring savefile "+savefilename+" ...") savefile= open(savefilename,'rb') esccurve= pickle.load(savefile) Rs= pickle.load(savefile) savefile.close() else: Rs= numpy.linspace(Rrange[0],Rrange[1],grid) esccurve= calcEscapecurve(Pot,Rs) if not savefilename == None: print("Writing savefile "+savefilename+" ...") savefile= open(savefilename,'wb') pickle.dump(esccurve,savefile) pickle.dump(Rs,savefile) savefile.close() if use_physical: Rs*= potro esccurve*= potvo Rrange[0]*= potro Rrange[1]*= potro if not 'xlabel' in kwargs: kwargs['xlabel']= xlabel if not 'ylabel' in kwargs: kwargs['ylabel']= ylabel if not 'xrange' in kwargs: kwargs['xrange']= Rrange if not 'yrange' in kwargs: kwargs['yrange']=\ [0.,1.2*numpy.amax(esccurve[True^numpy.isnan(esccurve)])] kwargs.pop('ro',None) kwargs.pop('vo',None) kwargs.pop('use_physical',None) return plot.plot(Rs,esccurve,*args,**kwargs) def calcEscapecurve(Pot,Rs,t=0.): """ NAME: calcEscapecurve PURPOSE: calculate the escape velocity curve for this potential (in the z=0 plane for non-spherical potentials) INPUT: Pot - Potential or list of Potential instances Rs - (array of) radius(i) t - instantaneous time (optional) OUTPUT: array of v_esc HISTORY: 2011-04-16 - Written - Bovy (NYU) """ isList= isinstance(Pot,list) isNonAxi= ((isList and Pot[0].isNonAxi) or (not isList and Pot.isNonAxi)) if isNonAxi: raise AttributeError("Escape velocity curve plotting for non-axisymmetric potentials is not currently supported") try: grid= len(Rs) except TypeError: grid=1 Rs= numpy.array([Rs]) esccurve= numpy.zeros(grid) for ii in range(grid): esccurve[ii]= vesc(Pot,Rs[ii],t=t,use_physical=False) return esccurve @potential_physical_input @physical_conversion('velocity',pop=True) def vesc(Pot,R,t=0.): """ NAME: vesc PURPOSE: calculate the escape velocity at R for potential Pot INPUT: Pot - Potential instances or list thereof R - Galactocentric radius (can be Quantity) t - time (optional; can be Quantity) OUTPUT: escape velocity HISTORY: 2011-10-09 - Written - Bovy (IAS) """ from ..potential import PotentialError, evaluateplanarPotentials try: return numpy.sqrt(2.*(evaluateplanarPotentials(Pot,_INF,t=t,use_physical=False)-evaluateplanarPotentials(Pot,R,t=t,use_physical=False))) except PotentialError: from ..potential import RZToplanarPotential Pot= RZToplanarPotential(Pot) return numpy.sqrt(2.*(evaluateplanarPotentials(Pot,_INF,t=t,use_physical=False)-evaluateplanarPotentials(Pot,R,t=t,use_physical=False))) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/plotRotcurve.py0000644000175100001710000001477314327773303020456 0ustar00runnerdockerimport os import pickle import numpy from ..util import conversion, plot from ..util.conversion import physical_conversion, potential_physical_input def plotRotcurve(Pot,*args,**kwargs): """ NAME: plotRotcurve PURPOSE: plot the rotation curve for this potential (in the z=0 plane for non-spherical potentials) INPUT: Pot - Potential or list of Potential instances Rrange - Range in R to consider (needs to be in the units that you are plotting; can be Quantity) grid= grid in R phi= (None) azimuth to use for non-axisymmetric potentials savefilename= save to or restore from this savefile (pickle) +galpy.util.plot.plot args and kwargs OUTPUT: plot to output device HISTORY: 2010-07-10 - Written - Bovy (NYU) 2016-06-15 - Added phi= keyword for non-axisymmetric potential - Bovy (UofT) """ # Using physical units or not? if isinstance(Pot,list): potro= Pot[0]._ro roSet= Pot[0]._roSet potvo= Pot[0]._vo voSet= Pot[0]._voSet else: potro= Pot._ro roSet= Pot._roSet potvo= Pot._vo voSet= Pot._voSet if (kwargs.get('use_physical',False) \ and kwargs.get('ro',roSet) and kwargs.get('vo',voSet)) or \ (not 'use_physical' in kwargs \ and kwargs.get('ro',roSet) and kwargs.get('vo',voSet)): use_physical= True potro= kwargs.get('ro',potro) potvo= kwargs.get('vo',potvo) xlabel= r'$R\,(\mathrm{kpc})$' ylabel= r"$v_c(R)\,(\mathrm{km\,s}^{-1})$" Rrange= kwargs.pop('Rrange',[0.01*potro,5.*potro]) else: use_physical= False xlabel= r"$R/R_0$" ylabel= r"$v_c(R)/v_c(R_0)$" Rrange= kwargs.pop('Rrange',[0.01,5.]) # Parse ro potro= conversion.parse_length_kpc(potro) potvo= conversion.parse_velocity_kms(potvo) Rrange[0]= conversion.parse_length_kpc(Rrange[0]) Rrange[1]= conversion.parse_length_kpc(Rrange[1]) if use_physical: Rrange[0]/= potro Rrange[1]/= potro grid= kwargs.pop('grid',1001) savefilename= kwargs.pop('savefilename',None) phi= kwargs.pop('phi',None) if not savefilename is None and os.path.exists(savefilename): print("Restoring savefile "+savefilename+" ...") savefile= open(savefilename,'rb') rotcurve= pickle.load(savefile) Rs= pickle.load(savefile) savefile.close() else: Rs= numpy.linspace(Rrange[0],Rrange[1],grid) rotcurve= calcRotcurve(Pot,Rs,phi=phi) if not savefilename == None: print("Writing savefile "+savefilename+" ...") savefile= open(savefilename,'wb') pickle.dump(rotcurve,savefile) pickle.dump(Rs,savefile) savefile.close() if use_physical: Rs*= potro rotcurve*= potvo Rrange[0]*= potro Rrange[1]*= potro if not 'xlabel' in kwargs: kwargs['xlabel']= xlabel if not 'ylabel' in kwargs: kwargs['ylabel']= ylabel if not 'xrange' in kwargs: kwargs['xrange']= Rrange if not 'yrange' in kwargs: kwargs['yrange']= [0.,1.2*numpy.amax(rotcurve)] kwargs.pop('ro',None) kwargs.pop('vo',None) kwargs.pop('use_physical',None) return plot.plot(Rs,rotcurve,*args,**kwargs) def calcRotcurve(Pot,Rs,phi=None,t=0.): """ NAME: calcRotcurve PURPOSE: calculate the rotation curve for this potential (in the z=0 plane for non-spherical potentials) INPUT: Pot - Potential or list of Potential instances Rs - (array of) radius(i) phi= (None) azimuth to use for non-axisymmetric potentials ts - instantaneous time (optional) OUTPUT: array of vc HISTORY: 2011-04-13 - Written - Bovy (NYU) 2016-06-15 - Added phi= keyword for non-axisymmetric potential - Bovy (UofT) """ try: grid= len(Rs) except TypeError: grid=1 Rs= numpy.array([Rs]) rotcurve= numpy.zeros(grid) for ii in range(grid): rotcurve[ii]= vcirc(Pot,Rs[ii],phi=phi,t=t,use_physical=False) return rotcurve @potential_physical_input @physical_conversion('velocity',pop=True) def vcirc(Pot,R,phi=None,t=0.): """ NAME: vcirc PURPOSE: calculate the circular velocity at R in potential Pot INPUT: Pot - Potential instance or list of such instances R - Galactocentric radius (can be Quantity) phi= (None) azimuth to use for non-axisymmetric potentials t= time (optional; can be Quantity) OUTPUT: circular rotation velocity HISTORY: 2011-10-09 - Written - Bovy (IAS) 2016-06-15 - Added phi= keyword for non-axisymmetric potential - Bovy (UofT) """ from ..potential import PotentialError, evaluateplanarRforces try: return numpy.sqrt(-R*evaluateplanarRforces(Pot,R,phi=phi,t=t, use_physical=False)) except PotentialError: from ..potential import toPlanarPotential Pot= toPlanarPotential(Pot) return numpy.sqrt(-R*evaluateplanarRforces(Pot,R,phi=phi,t=t, use_physical=False)) @potential_physical_input @physical_conversion('frequency',pop=True) def dvcircdR(Pot,R,phi=None,t=0.): """ NAME: dvcircdR PURPOSE: calculate the derivative of the circular velocity wrt R at R in potential Pot INPUT: Pot - Potential instance or list of such instances R - Galactocentric radius (can be Quantity) phi= (None) azimuth to use for non-axisymmetric potentials t= time (optional; can be Quantity) OUTPUT: derivative of the circular rotation velocity wrt R HISTORY: 2013-01-08 - Written - Bovy (IAS) 2016-06-28 - Added phi= keyword for non-axisymmetric potential - Bovy (UofT) """ from ..potential import (PotentialError, evaluateplanarR2derivs, evaluateplanarRforces) tvc= vcirc(Pot,R,phi=phi,t=t,use_physical=False) try: return 0.5*(-evaluateplanarRforces(Pot,R,phi=phi,t=t,use_physical=False)+R*evaluateplanarR2derivs(Pot,R,phi=phi,t=t,use_physical=False))/tvc except PotentialError: from ..potential import RZToplanarPotential Pot= RZToplanarPotential(Pot) return 0.5*(-evaluateplanarRforces(Pot,R,phi=phi,t=t,use_physical=False)+R*evaluateplanarR2derivs(Pot,R,phi=phi,t=t,use_physical=False))/tvc ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667233486.717467 galpy-1.8.1/galpy/potential/potential_c_ext/0000755000175100001710000000000014327773317020546 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/BurkertPotential.c0000644000175100001710000000475214327773303024213 0ustar00runnerdocker#include #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif //BurkertPotential // 2 arguments: amp, a double BurkertPotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double a= *(args+1); //Calculate potential double x= sqrt( R*R + Z*Z) / a; return -amp * a * a * M_PI / x * ( -M_PI + 2. * ( 1. + x ) * atan( 1 / x ) + 2. * ( 1. + x ) * log ( 1. + x ) + ( 1. - x ) * log ( 1. + x * x )); } double BurkertPotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double a= *(args+1); //Calculate Rforce double r= sqrt( R*R + Z*Z); double x= r / a; return amp * a * M_PI / x / x * ( M_PI - 2. * atan ( 1. / x ) - 2. * log ( 1. + x ) - log ( 1. + x * x)) * R / r; } double BurkertPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double a= *(args+1); //Calculate Rforce double x= R / a; return amp * a * M_PI / x / x * ( M_PI - 2. * atan ( 1. / x ) - 2. * log ( 1. + x ) - log ( 1. + x * x)); } double BurkertPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double a= *(args+1); //Calculate zforce double r= sqrt( R*R + z*z); double x= r / a; return amp * a * M_PI / x / x * ( M_PI - 2. * atan ( 1. / x ) - 2. * log ( 1. + x ) - log ( 1. + x * x)) * z / r; } double BurkertPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double a= *(args+1); //Calculate R2deriv double x= R / a; double R5= pow(R,5); return -amp * M_PI * pow(a,3) / R5 * (-4. * R5 / ( a * a + R * R ) / ( a + R ) - 2. * R * R * ( M_PI - 2. * atan ( 1. / x ) - 2. * log( 1. + x ) - log( 1. + x * x ))); } double BurkertPotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double a= *(args+1); //Calculate potential double x= sqrt( R*R + Z*Z) / a; return amp / ( 1. + x ) / ( 1. + x * x ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/ChandrasekharDynamicalFrictionForce.c0000644000175100001710000001112414327773303027741 0ustar00runnerdocker#include // Constants not defined in MSVC's math.h #ifndef M_SQRT1_2 #define M_SQRT1_2 0.70710678118654746172 #endif #ifndef M_2_SQRTPI #define M_2_SQRTPI 1.12837916709551255856 #endif #include #include // ChandrasekharDynamicalFrictionForce: 8 arguments: amp,ms,rhm,gamma^2, // lnLambda, minr^2, ro, rf double ChandrasekharDynamicalFrictionForceAmplitude(double R,double z, double phi,double t, double r2, struct potentialArg * potentialArgs, double vR,double vT, double vz){ double sr,X,Xfactor,d_ind,forceAmplitude; double * args= potentialArgs->args; //Get args double amp= *args; double ms= *(args+9); double rhm= *(args+10); double gamma2= *(args+11); double lnLambda= *(args+12); double ro= *(args+14); double rf= *(args+15); double GMvs; double r= sqrt( r2 ); double v2= vR * vR + vT * vT + vz * vz; double v= sqrt( v2 ); // Constant or variable Lambda if ( lnLambda < 0 ) { GMvs= ms/v/v; if ( GMvs < rhm ) lnLambda= 0.5 * log ( 1. + r2 / gamma2 / rhm / rhm ); else lnLambda= 0.5 * log ( 1. + r2 / gamma2 / GMvs / GMvs ); } d_ind= (r-ro)/(rf-ro); d_ind= d_ind < 0 ? 0. : ( d_ind > 1 ? 1. : d_ind); sr= gsl_spline_eval(*potentialArgs->spline1d,d_ind,*potentialArgs->acc1d); X= M_SQRT1_2 * v / sr; Xfactor= erf ( X ) - M_2_SQRTPI * X * exp ( - X * X ); forceAmplitude= - amp * Xfactor * lnLambda / v2 / v \ * calcDensity(R,z,phi,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); // Caching *(args + 1)= R; *(args + 2)= z; *(args + 3)= phi; *(args + 4)= t; *(args + 5)= vR; *(args + 6)= vT; *(args + 7)= vz; *(args + 8)= forceAmplitude; return forceAmplitude; } double ChandrasekharDynamicalFrictionForceRforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs, double vR,double vT, double vz){ double forceAmplitude; double * args= potentialArgs->args; double r2= R * R + z * z; if ( r2 < *(args+13) ) // r < minr, don't bother caching return 0.; //Get args double cached_R= *(args + 1); double cached_z= *(args + 2); double cached_phi= *(args + 3); double cached_t= *(args + 4); double cached_vR= *(args + 5); double cached_vT= *(args + 6); double cached_vz= *(args + 7); if ( R != cached_R || phi != cached_phi || z != cached_z || t != cached_t \ || vR != cached_vR || vT != cached_vT || vz != cached_vz ) forceAmplitude= ChandrasekharDynamicalFrictionForceAmplitude(R,z,phi,t,r2, potentialArgs, vR,vT,vz); else forceAmplitude= *(args + 8); return forceAmplitude * vR; } double ChandrasekharDynamicalFrictionForcezforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs, double vR,double vT, double vz){ double forceAmplitude; double * args= potentialArgs->args; double r2= R * R + z * z; if ( r2 < *(args+13) ) // r < minr, don't bother caching return 0.; //Get args double cached_R= *(args + 1); double cached_z= *(args + 2); double cached_phi= *(args + 3); double cached_t= *(args + 4); double cached_vR= *(args + 5); double cached_vT= *(args + 6); double cached_vz= *(args + 7); if ( R != cached_R || phi != cached_phi || z != cached_z || t != cached_t \ || vR != cached_vR || vT != cached_vT || vz != cached_vz ) // LCOV_EXCL_START forceAmplitude= ChandrasekharDynamicalFrictionForceAmplitude(R,z,phi,t,r2, potentialArgs, vR,vT,vz); // LCOV_EXCL_STOP else forceAmplitude= *(args + 8); return forceAmplitude * vz; } double ChandrasekharDynamicalFrictionForcephitorque(double R,double z, double phi,double t, struct potentialArg * potentialArgs, double vR,double vT, double vz){ double forceAmplitude; double * args= potentialArgs->args; double r2= R * R + z * z; if ( r2 < *(args+13) ) // r < minr, don't bother caching return 0.; //Get args double cached_R= *(args + 1); double cached_z= *(args + 2); double cached_phi= *(args + 3); double cached_t= *(args + 4); double cached_vR= *(args + 5); double cached_vT= *(args + 6); double cached_vz= *(args + 7); if ( R != cached_R || phi != cached_phi || z != cached_z || t != cached_t \ || vR != cached_vR || vT != cached_vT || vz != cached_vz ) // LCOV_EXCL_START forceAmplitude= ChandrasekharDynamicalFrictionForceAmplitude(R,z,phi,t,r2, potentialArgs, vR,vT,vz); // LCOV_EXCL_STOP else forceAmplitude= *(args + 8); return forceAmplitude * vT * R; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/CorotatingRotationWrapperPotential.c0000644000175100001710000001055714327773303027767 0ustar00runnerdocker#include #include //CorotatingRotationWrapperPotential // 5 arguments: amp, vpo, beta, pa, to double CorotatingRotationWrapperPotentialRforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rforce double phi_new= phi-*(args+1) * pow(R,*(args+2)-1) * (t-*(args+4))\ - *(args+3); return *args * ( calcRforce(R,z,phi_new,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg) \ - calcphitorque(R,z,phi_new,t, potentialArgs->nwrapped,potentialArgs->wrappedPotentialArg)\ * *(args+1) * ( *(args+2) - 1 ) * pow(R,*(args+2)-2) * (t-*(args+4))); } double CorotatingRotationWrapperPotentialphitorque(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phitorque return *args * calcphitorque(R,z, phi-*(args+1) * pow(R,*(args+2)-1) * (t-*(args+4)) \ - *(args+3),t, potentialArgs->nwrapped,potentialArgs->wrappedPotentialArg); } double CorotatingRotationWrapperPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate zforce return *args * calczforce(R,z, phi-*(args+1) * pow(R,*(args+2)-1) * (t-*(args+4)) \ - *(args+3),t, potentialArgs->nwrapped,potentialArgs->wrappedPotentialArg); } double CorotatingRotationWrapperPotentialPlanarRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rforce double phi_new= phi-*(args+1) * pow(R,*(args+2)-1) * (t-*(args+4))\ - *(args+3); return *args * ( calcPlanarRforce(R,phi_new,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg) \ - calcPlanarphitorque(R,phi_new,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg) \ * *(args+1) * ( *(args+2) - 1 ) * pow(R,*(args+2)-2) * (t-*(args+4))); } double CorotatingRotationWrapperPotentialPlanarphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phitorque return *args * calcPlanarphitorque(R, phi-*(args+1) * pow(R,*(args+2)-1) * (t-*(args+4)) \ - *(args+3),t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double CorotatingRotationWrapperPotentialPlanarR2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate R2deriv double phi_new= phi-*(args+1) * pow(R,*(args+2)-1) * (t-*(args+4))\ - *(args+3); double phiRderiv= -*(args+1) * (*(args+2)-1) * pow(R,*(args+2)-2) \ * (t-*(args+4)); return *args * (calcPlanarR2deriv(R,phi_new,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg) + 2. * phiRderiv * calcPlanarRphideriv(R,phi_new,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg) + phiRderiv * phiRderiv * calcPlanarphi2deriv(R,phi_new,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg) + calcPlanarphitorque(R,phi_new,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg) * *(args+1) * (*(args+2)-1) * (*(args+2)-2) * pow(R,*(args+2)-3) * (t-*(args+4))); } double CorotatingRotationWrapperPotentialPlanarphi2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phi2deriv return *args * calcPlanarphi2deriv(R, phi-*(args+1) * pow(R,*(args+2)-1) * (t-*(args+4)) \ - *(args+3),t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double CorotatingRotationWrapperPotentialPlanarRphideriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rphideriv double phi_new= phi-*(args+1) * pow(R,*(args+2)-1) * (t-*(args+4))\ - *(args+3); return *args * ( calcPlanarRphideriv(R,phi_new,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg) - calcPlanarphi2deriv(R,phi_new,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg) * *(args+1) * (*(args+2)-1) * pow(R,*(args+2)-2) \ * (t-*(args+4))); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/CosmphiDiskPotential.c0000644000175100001710000000516514327773303025011 0ustar00runnerdocker#include #include //CosmphiDiskPotential double CosmphiDiskPotentialRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double mphio= *args++; double p= *args++; double mphib= *args++; int m= (int) *args++; double rb= *args++; double rb2p= *(args+1); if ( R <= rb ) return -amp * p * mphio / m * rb2p / pow(R,p+1.) * cos( m * phi - mphib); else return -amp * p * mphio / m * pow(R,p-1.) * cos( m * phi - mphib); } double CosmphiDiskPotentialphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double mphio= *args++; double p= *args++; double mphib= *args++; int m= (int) *args++; double rb= *args++; double rbp= *args++; double r1p= *(args+1); if ( R <= rb ) return amp * mphio * rbp * ( 2. * r1p - rbp / pow(R,p) )\ * sin( m * phi-mphib); else return amp * mphio * pow(R,p) * sin( m * phi-mphib); } double CosmphiDiskPotentialR2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double mphio= *args++; double p= *args++; double mphib= *args++; int m= (int) *args++; double rb= *args++; double rb2p= *(args+1); if ( R <= rb ) return -amp * p * ( p + 1 ) * mphio / m * rb2p / pow(R,p+2.) \ * cos( m * phi - mphib); else return amp * p * ( p - 1) * mphio / m * pow(R,p-2.) * cos(m * phi - mphib); } double CosmphiDiskPotentialphi2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double mphio= *args++; double p= *args++; double mphib= *args++; int m= (int) *args++; double rb= *args++; double rbp= *args++; double r1p= *(args+1); if ( R <= rb ) return - amp * m * mphio * rbp * ( 2. * r1p - rbp / pow(R,p) )\ * cos( m * phi-mphib); else return - amp * m * mphio * pow(R,p) * cos( m * phi - mphib ); } double CosmphiDiskPotentialRphideriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double mphio= *args++; double p= *args++; double mphib= *args++; int m= (int) *args++; double rb= *args++; double rb2p= *(args+1); if ( R <= rb ) return - amp * p * mphio / m * rb2p / pow(R,p+1) * sin( m * phi-mphib); else return - amp * p * mphio * pow(R,p-1.) * sin( m * phi - mphib ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/DehnenBarPotential.c0000644000175100001710000001217314327773303024417 0ustar00runnerdocker#include #include //DehnenBarPotential // double dehnenBarSmooth(double t,double tform, double tsteady){ double smooth, xi,deltat; if ( t < tform ) smooth= 0.; else if ( t < tsteady ) { deltat= t-tform; xi= 2.*deltat/(tsteady-tform)-1.; smooth= (3./16.*pow(xi,5.)-5./8.*pow(xi,3.)+15./16.*xi+.5); } else smooth= 1.; return smooth; } double DehnenBarPotentialRforce(double R,double z,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; double r; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double rb= *args++; double omegab= *args++; double barphi= *args++; //Calculate Rforce smooth= dehnenBarSmooth(t,tform,tsteady); r= sqrt( R * R + z * z ); if (r <= rb ) return -amp*smooth*cos(2.*(phi-omegab*t-barphi))*\ (pow(r/rb,3.)*R*(3.*R*R+2.*z*z)-4.*R*z*z)/pow(r,4.); else return -amp*smooth*cos(2.*(phi-omegab*t-barphi))\ *pow(rb/r,3.)*R/pow(r,4)*(3.*R*R-2.*z*z); } double DehnenBarPotentialPlanarRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double rb= *args++; double omegab= *args++; double barphi= *args++; //Calculate Rforce smooth= dehnenBarSmooth(t,tform,tsteady); if (R <= rb ) return -3.*amp*smooth*cos(2.*(phi-omegab*t-barphi))*pow(R/rb,3.)/R; else return -3.*amp*smooth*cos(2.*(phi-omegab*t-barphi))*pow(rb/R,3.)/R; } double DehnenBarPotentialphitorque(double R,double z,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; double r, r2; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double rb= *args++; double omegab= *args++; double barphi= *args++; //Calculate phitorque smooth= dehnenBarSmooth(t,tform,tsteady); r2= R * R + z * z; r= sqrt( r2 ); if ( R <= rb ) return 2.*amp*smooth*sin(2.*(phi-omegab*t-barphi))*(pow(r/rb,3.)-2.)\ *R*R/r2; else return -2.*amp*smooth*sin(2.*(phi-omegab*t-barphi))*pow(rb/r,3.)*R*R/r2; } double DehnenBarPotentialPlanarphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double rb= *args++; double omegab= *args++; double barphi= *args++; //Calculate phitorque smooth= dehnenBarSmooth(t,tform,tsteady); if ( R <= rb ) return 2.*amp*smooth*sin(2.*(phi-omegab*t-barphi))*(pow(R/rb,3.)-2.); else return -2.*amp*smooth*sin(2.*(phi-omegab*t-barphi))*pow(rb/R,3.); } double DehnenBarPotentialzforce(double R,double z,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; double r; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double rb= *args++; double omegab= *args++; double barphi= *args++; //Calculate Rforce smooth= dehnenBarSmooth(t,tform,tsteady); r= sqrt( R * R + z * z ); if (r <= rb ) return -amp*smooth*cos(2.*(phi-omegab*t-barphi))*\ (pow(r/rb,3.)+4.)*R*R*z/pow(r,4.); else return -5.*amp*smooth*cos(2.*(phi-omegab*t-barphi))\ *pow(rb/r,3.)*R*R*z/pow(r,4); } double DehnenBarPotentialPlanarR2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double rb= *args++; double omegab= *args++; double barphi= *args++; smooth= dehnenBarSmooth(t,tform,tsteady); if (R <= rb ) return 6.*amp*smooth*cos(2.*(phi-omegab*t-barphi))*pow(R/rb,3.)/R/R; else return -12.*amp*smooth*cos(2.*(phi-omegab*t-barphi))*pow(rb/R,3.)/R/R; } double DehnenBarPotentialPlanarphi2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double rb= *args++; double omegab= *args++; double barphi= *args++; smooth= dehnenBarSmooth(t,tform,tsteady); if (R <= rb ) return -4.*amp*smooth*cos(2.*(phi-omegab*t-barphi))*(pow(R/rb,3.)-2.); else return 4.*amp*smooth*cos(2.*(phi-omegab*t-barphi))*pow(rb/R,3.); } double DehnenBarPotentialPlanarRphideriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double rb= *args++; double omegab= *args++; double barphi= *args++; smooth= dehnenBarSmooth(t,tform,tsteady); if (R <= rb ) return -6.*amp*smooth*sin(2.*(phi-omegab*t-barphi))*pow(R/rb,3.)/R; else return -6.*amp*smooth*sin(2.*(phi-omegab*t-barphi))*pow(rb/R,3.)/R; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/DehnenCorePotential.c0000644000175100001710000000373414327773303024606 0ustar00runnerdocker#include #include "galpy_potentials.h" //CoreDehnenPotential //2 arguments: amp, a double DehnenCoreSphericalPotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args++; double a= *args++; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); return -amp * (1. - pow(sqrtRz/(sqrtRz+a), 2.)) / (6. * a); } double DehnenCoreSphericalPotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args++; double a= *args++; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); return - amp * (R / pow(a+sqrtRz,3.) / 3.); } double DehnenCoreSphericalPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; //Calculate Rforce return - amp * (R / pow(a+R, 3.) / 3.); } double DehnenCoreSphericalPotentialzforce(double R,double Z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); return - amp * (Z / pow(a+sqrtRz, 3.) / 3.); } double DehnenCoreSphericalPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; // return return -amp * (pow(a+R, -4.) * (2.*R - a)) / 3.; } double DehnenCoreSphericalPotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args++; double a= *args++; //Calculate Rforce double r= sqrt ( R * R + Z * Z ); return amp * M_1_PI / 4. * pow ( 1. + r / a, -4.) * pow (a, - 3.); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/DehnenPotential.c0000644000175100001710000000462314327773303023773 0ustar00runnerdocker#include #include "galpy_potentials.h" //DehnenPotential //3 arguments: amp, a, alpha double DehnenSphericalPotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args++; double a= *args++; double alpha= *args; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); // if (alpha == 2.) {return -amp * R * pow( sqrtRz , -3. ) / ( 1. + a / sqrtRz );} // not needed b/c Jaffe return -amp * (1. - pow(sqrtRz/(sqrtRz+a), 2.-alpha)) / (a * (2. - alpha) * (3. - alpha)); } double DehnenSphericalPotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args++; double a= *args++; double alpha= *args; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); return - amp * (R * pow(1.+a/sqrtRz, alpha) / pow(a+sqrtRz,3.) / (3.-alpha)); } double DehnenSphericalPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; double alpha= *args; //Calculate Rforce return - amp * (R * pow(1.+a/R, alpha) / pow(a+R, 3.) / (3.-alpha)); } double DehnenSphericalPotentialzforce(double R,double Z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; double alpha= *args; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); return - amp * (Z * pow(1. + a/sqrtRz, alpha) / pow(a+sqrtRz, 3.) / (3.-alpha)); } double DehnenSphericalPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; double alpha= *args; // return return -amp * (pow(1.+a/R, alpha) * pow(a+R, -4.) * (2.*R + a*(alpha-1.))) / (3.-alpha); } double DehnenSphericalPotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args++; double a= *args++; double alpha= *args; //Calculate density double r= sqrt ( R * R + Z * Z ); return amp * M_1_PI / 4. * pow (r,-alpha ) * pow ( 1. + r / a, alpha-4.) \ * pow (a, alpha - 3.); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/DehnenSmoothWrapperPotential.c0000644000175100001710000000735414327773303026532 0ustar00runnerdocker#include #include #include #include //DehnenSmoothWrapperPotential double dehnenSmooth(double t,double tform, double tsteady,bool grow){ double smooth, xi,deltat; if ( t < tform ) smooth= 0.; else if ( t < tsteady ) { deltat= t-tform; xi= 2.*deltat/(tsteady-tform)-1.; smooth= (3./16.*pow(xi,5.)-5./8.*pow(xi,3.)+15./16.*xi+.5); } else smooth= 1.; return grow ? smooth: 1.-smooth; } double DehnenSmoothWrapperPotentialEval(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate potential, only used in actionAngle, so phi=0, t=0 return *args * dehnenSmooth(t,*(args+1),*(args+2),(bool) *(args+3)) \ * evaluatePotentials(R,z, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double DehnenSmoothWrapperPotentialRforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rforce return *args * dehnenSmooth(t,*(args+1),*(args+2),(bool) *(args+3)) \ * calcRforce(R,z,phi,t, potentialArgs->nwrapped,potentialArgs->wrappedPotentialArg); } double DehnenSmoothWrapperPotentialphitorque(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phitorque return *args * dehnenSmooth(t,*(args+1),*(args+2),(bool) *(args+3)) \ * calcphitorque(R,z,phi,t, potentialArgs->nwrapped,potentialArgs->wrappedPotentialArg); } double DehnenSmoothWrapperPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate zforce return *args * dehnenSmooth(t,*(args+1),*(args+2),(bool) *(args+3)) \ * calczforce(R,z,phi,t, potentialArgs->nwrapped,potentialArgs->wrappedPotentialArg); } double DehnenSmoothWrapperPotentialPlanarRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rforce return *args * dehnenSmooth(t,*(args+1),*(args+2),(bool) *(args+3)) \ * calcPlanarRforce(R,phi,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double DehnenSmoothWrapperPotentialPlanarphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phitorque return *args * dehnenSmooth(t,*(args+1),*(args+2),(bool) *(args+3)) \ * calcPlanarphitorque(R,phi,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double DehnenSmoothWrapperPotentialPlanarR2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate R2deriv return *args * dehnenSmooth(t,*(args+1),*(args+2),(bool) *(args+3)) \ * calcPlanarR2deriv(R,phi,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double DehnenSmoothWrapperPotentialPlanarphi2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phi2deriv return *args * dehnenSmooth(t,*(args+1),*(args+2),(bool) *(args+3)) \ * calcPlanarphi2deriv(R,phi,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double DehnenSmoothWrapperPotentialPlanarRphideriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rphideriv return *args * dehnenSmooth(t,*(args+1),*(args+2),(bool) *(args+3)) \ * calcPlanarRphideriv(R,phi,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/DiskSCFPotential.c0000644000175100001710000001354414327773303024022 0ustar00runnerdocker/////////////////////////////////////////////////////////////////////////////// // // DiskSCFPotential.c: // // The C implementation of DiskSCFPotential uses the fact that // (a) the SCF part of the potential can be gotten directly from the SCF // implementation // (b) the approximation part can be written as a sum over pairs // [Sigma_i(R),h_i(z)] // Thus, this file only implements the potential and forces coming from // a single of the approximation pairs; the entire potential is obtained // by summing all of these (by treating them as separate instances of the // potential) and adding the SCF; this is all handled through the parsing // of the potential in the Python code. // // Surface-density profile is passed by type: // // 0= exponential: amp x exp(-R/h) // 1= exponential w/ hole: amp x exp(-Rhole/R-R/h) // // Vertical profile is passed by type: // // 0= exponential: exp(-|z|/h)/[2h] // 1= sech2: sech^2(z/[2h])/[4h] // /////////////////////////////////////////////////////////////////////////////// #include #include #ifndef M_LN2 #define M_LN2 0.693147180559945309417232121 #endif //DiskSCFPotential //Only the part coming from a single approximation pair // Arguments: nsigma_args,sigma_type,sigma_amp,sigma_h[,sigma_rhole], // hz_type,hz_h double Sigma(double R,double * Sigma_args){ int Sigma_type= (int) *Sigma_args; switch ( Sigma_type ) { case 0: // Pure exponential return *(Sigma_args+1) * exp(-R / *(Sigma_args+2) ); case 1: // Exponential with central hole return *(Sigma_args+1) * exp(- *(Sigma_args+3) / R - R / *(Sigma_args+2) ); } return -1; // LCOV_EXCL_LINE } double dSigmadR(double R,double * Sigma_args){ int Sigma_type= (int) *Sigma_args; switch ( Sigma_type ) { case 0: // Pure exponential return - *(Sigma_args+1) * exp(-R / *(Sigma_args+2) ) / *(Sigma_args+2); case 1: // Exponential with central hole return *(Sigma_args+1) * ( *(Sigma_args+3) / R / R - 1. / *(Sigma_args+2))\ * exp(- *(Sigma_args+3) / R - R / *(Sigma_args+2) ); } return -1; // LCOV_EXCL_LINE } double d2SigmadR2(double R,double * Sigma_args){ int Sigma_type= (int) *Sigma_args; switch ( Sigma_type ) { case 0: // Pure exponential return *(Sigma_args+1) * exp(-R / *(Sigma_args+2) ) \ / *(Sigma_args+2) / *(Sigma_args+2); case 1: // Exponential with central hole return *(Sigma_args+1) * ( pow( *(Sigma_args+3) / R / R \ - 1. / *(Sigma_args+2) , 2 ) \ -2. * *(Sigma_args+3) / R / R / R )\ * exp(- *(Sigma_args+3) / R - R / *(Sigma_args+2) ); } return -1; // LCOV_EXCL_LINE } double hz(double z,double * hz_args){ int hz_type= (int) *hz_args; double fz; switch ( hz_type ) { case 0: // exponential fz= fabs(z); return 0.5 * exp ( - fz / *(hz_args+1) ) / *(hz_args+1); case 1: // sech2 return 0.25 * pow ( cosh ( 0.5 * z / *(hz_args+1) ) , -2 ) \ / *(hz_args+1); } return -1; // LCOV_EXCL_LINE } double Hz(double z,double * hz_args){ int hz_type= (int) *hz_args; double fz= fabs(z); switch ( hz_type ) { case 0: // exponential return 0.5 * ( exp ( - fz / *(hz_args+1) ) - 1. + fz / *(hz_args+1) ) \ * *(hz_args+1); case 1: // sech2 return *(hz_args+1) * ( log ( 1. + exp ( - fz / *(hz_args+1) ) ) \ + 0.5 * fz / *(hz_args+1) - M_LN2 ); } return -1; // LCOV_EXCL_LINE } double dHzdz(double z,double * hz_args){ int hz_type= (int) *hz_args; double fz; switch ( hz_type ) { case 0: // exponential fz= fabs(z); return 0.5 * copysign ( 1. - exp ( - fz / *(hz_args+1) ) , z); case 1: // sech2 return 0.5 * tanh ( 0.5 * z / *(hz_args+1) ); } return -1; // LCOV_EXCL_LINE } double DiskSCFPotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args int nsigma_args= (int) *args; double * Sigma_args= args+1; double * hz_args= args+1+nsigma_args; //Calculate Rforce double r= sqrt( R * R + Z * Z ); return Sigma(r,Sigma_args) * Hz(Z,hz_args); } double DiskSCFPotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args int nsigma_args= (int) *args; double * Sigma_args= args+1; double * hz_args= args+1+nsigma_args; //Calculate Rforce double r= sqrt( R * R + Z * Z ); return -dSigmadR(r,Sigma_args) * Hz(Z,hz_args) * R / r; } double DiskSCFPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ //Supposed to be zero (bc H(0) supposed to be zero), but just to make sure double * args= potentialArgs->args; //Get args int nsigma_args= (int) *args; double * Sigma_args= args+1; double * hz_args= args+1+nsigma_args; //Calculate Rforce return -dSigmadR(R,Sigma_args) * Hz(0.,hz_args); } double DiskSCFPotentialzforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args int nsigma_args= (int) *args; double * Sigma_args= args+1; double * hz_args= args+1+nsigma_args; //Calculate Rforce double r= sqrt( R * R + Z * Z ); return -dSigmadR(r,Sigma_args) * Hz(Z,hz_args) * Z / r \ -Sigma(r,Sigma_args) * dHzdz(Z,hz_args); } double DiskSCFPotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args int nsigma_args= (int) *args; double * Sigma_args= args+1; double * hz_args= args+1+nsigma_args; //Calculate Rforce double r= sqrt( R * R + Z * Z ); return M_1_PI / 4. * (Sigma(r,Sigma_args) * hz(Z,hz_args) + d2SigmadR2(r,Sigma_args) * Hz(Z,hz_args) + 2. / r * dSigmadR(r,Sigma_args) \ * ( Hz(Z,hz_args) + Z * dHzdz(Z,hz_args) ) ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/DoubleExponentialDiskPotential.c0000644000175100001710000000730514327773303027026 0ustar00runnerdocker#include #include //Double exponential disk potential double DoubleExponentialDiskPotentialEval(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double x; double * args= potentialArgs->args; //Get args double amp= *(args+1); double alpha= *(args+2); double beta= *(args+3); int de_n= (int) *(args+4); double * de_j0_xs= args + 5; double * de_j0_ws= args + 5 + 2 * de_n; double alpha2= alpha * alpha; double beta2= beta * beta; double fz= fabs(z); double ebetafz= exp( - beta * fz ); double out= 0; double prev_term= 1; int ii= 0; while ( fabs(prev_term) > 1e-15 && ii < de_n ) { x= *(de_j0_xs+ii) / R; prev_term= *(de_j0_ws+ii) * pow( alpha2 + x * x , -1.5 ) \ * ( beta * exp( -x * fz ) - x * ebetafz ) \ / ( beta2 - x * x ); out+= prev_term; prev_term/= out; ii+= 1; } return amp * out / R; } double DoubleExponentialDiskPotentialRforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double x; double * args= potentialArgs->args; //Get args double amp= *(args+1); double alpha= *(args+2); double beta= *(args+3); int de_n= (int) *(args+4); double * de_j1_xs= args + 5 + de_n; double * de_j1_ws= args + 5 + 3 * de_n; double alpha2= alpha * alpha; double beta2= beta * beta; double fz= fabs(z); double ebetafz= exp( - beta * fz ); double out= 0; double prev_term= 1; int ii= 0; while ( fabs(prev_term) > 1e-15 && ii < de_n ) { x= *(de_j1_xs+ii) / R; prev_term= *(de_j1_ws+ii) * x * pow( alpha2 + x * x , -1.5) \ * ( beta * exp(-x * fz )-x * ebetafz )\ / ( beta2 - x * x); out+= prev_term; prev_term/= out; ii+= 1; } return amp * out / R; } double DoubleExponentialDiskPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double x; double * args= potentialArgs->args; //Get args double amp= *(args+1); double alpha= *(args+2); double beta= *(args+3); int de_n= (int) *(args+4); double * de_j1_xs= args + 5 + de_n; double * de_j1_ws= args + 5 + 3 * de_n; double alpha2= alpha * alpha; double out= 0; double prev_term= 1; int ii= 0; while ( fabs(prev_term) > 1e-15 && ii < de_n ) { x= *(de_j1_xs+ii) / R; prev_term= *(de_j1_ws+ii) * x * pow( alpha2 + x * x , -1.5) / ( beta + x ); out+= prev_term; prev_term/= out; ii+= 1; } return amp * out / R; } double DoubleExponentialDiskPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double x; double * args= potentialArgs->args; //Get args double amp= *(args+1); double alpha= *(args+2); double beta= *(args+3); int de_n= (int) *(args+4); double * de_j0_xs= args + 5; double * de_j0_ws= args + 5 + 2 * de_n; double alpha2= alpha * alpha; double beta2= beta * beta; double fz= fabs(z); double ebetafz= exp(-beta * fabs(z) ); double out= 0; double prev_term= 1; int ii= 0; while ( fabs(prev_term) > 1e-15 && ii < de_n ) { x= *(de_j0_xs+ii) / R; prev_term= *(de_j0_ws+ii) * pow( alpha2 + x * x , -1.5) \ * x * ( exp(-x * fz ) - ebetafz )\ /( beta2 - x * x ); out+= prev_term; prev_term/= out; ii+= 1; } if ( z > 0. ) return amp * out * beta / R; else return -amp * out * beta / R; } double DoubleExponentialDiskPotentialDens(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double alpha= *(args+2); double beta= *(args+3); // calculate density return amp * exp ( - alpha * R - beta * fabs ( z ) ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/EllipsoidalPotential.c0000644000175100001710000001774614327773303025045 0ustar00runnerdocker#include #include #include #include //General routines for EllipsoidalPotentials double EllipsoidalPotentialEval(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ int ii; double s; //Get args double * args= potentialArgs->args; double amp= *args; double * ellipargs= args + 8 + (int) *(args+7); // *(args+7) = num. arguments psi double b2= *ellipargs++; double c2= *ellipargs++; bool aligned= (bool) *ellipargs++; double * rot= ellipargs; ellipargs+= 9; int glorder= (int) *ellipargs++; double * glx= ellipargs; double * glw= ellipargs + glorder; //Calculate potential double x, y; double out= 0.; cyl_to_rect(R,phi,&x,&y); if ( !aligned ) rotate(&x,&y,&z,rot); for (ii=0; ii < glorder; ii++) { s= 1. / *(glx+ii) / *(glx+ii) - 1.; out+= *(glw+ii) * potentialArgs->psi ( sqrt ( x * x / ( 1. + s ) + y * y / ( b2 + s ) + z * z / ( c2 + s ) ), args+8); } return -0.5 * amp * out; } void EllipsoidalPotentialxyzforces_xyz(double (*dens)(double m, double * args), double x,double y, double z, double * Fx, double * Fy, double * Fz,double * args){ int ii; double t; double td; //Get args double * ellipargs= args + 8 + (int) *(args+7); // *(args+7) = num. arguments dens double b2= *ellipargs++; double c2= *ellipargs++; bool aligned= (bool) *ellipargs++; double * rot= ellipargs; ellipargs+= 9; int glorder= (int) *ellipargs++; double * glx= ellipargs; double * glw= ellipargs + glorder; //Setup caching *(args + 1)= x; *(args + 2)= y; *(args + 3)= z; if ( !aligned ) rotate(&x,&y,&z,rot); *Fx= 0.; *Fy= 0.; *Fz= 0.; for (ii=0; ii < glorder; ii++) { t= 1. / *(glx+ii) / *(glx+ii) - 1.; td= *(glw+ii) * dens( sqrt ( x * x / ( 1. + t ) + y * y / ( b2 + t ) \ + z * z / ( c2 + t ) ),args+8); *Fx+= td * x / ( 1. + t ); *Fy+= td * y / ( b2 + t ); *Fz+= td * z / ( c2 + t ); } if ( !aligned ) rotate_force(Fx,Fy,Fz,rot); *(args + 4)= *Fx; *(args + 5)= *Fy; *(args + 6)= *Fz; } double EllipsoidalPotentialRforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args; // Get caching args: amp = 0, x,y,z,Fx,Fy,Fz double cached_x= *(args + 1); double cached_y= *(args + 2); double cached_z= *(args + 3); //Calculate potential double x, y; double Fx, Fy, Fz; cyl_to_rect(R,phi,&x,&y); if ( x == cached_x && y == cached_y && z == cached_z ){ // LCOV_EXCL_START Fx= *(args + 4); Fy= *(args + 5); Fz= *(args + 6); // LCOV_EXCL_STOP } else EllipsoidalPotentialxyzforces_xyz(potentialArgs->mdens, x,y,z,&Fx,&Fy,&Fz,args); return amp * ( cos ( phi ) * Fx + sin( phi ) * Fy ); } double EllipsoidalPotentialphitorque(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args; // Get caching args: amp = 0, x,y,z,Fx,Fy,Fz double cached_x= *(args + 1); double cached_y= *(args + 2); double cached_z= *(args + 3); //Calculate potential double x, y; double Fx, Fy, Fz; cyl_to_rect(R,phi,&x,&y); if ( x == cached_x && y == cached_y && z == cached_z ){ Fx= *(args + 4); Fy= *(args + 5); Fz= *(args + 6); } else // LCOV_EXCL_START EllipsoidalPotentialxyzforces_xyz(potentialArgs->mdens, x,y,z,&Fx,&Fy,&Fz,args); // LCOV_EXCL_STOP return amp * R * ( -sin ( phi ) * Fx + cos( phi ) * Fy ); } double EllipsoidalPotentialzforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args; // Get caching args: amp = 0, x,y,z,Fx,Fy,Fz double cached_x= *(args + 1); double cached_y= *(args + 2); double cached_z= *(args + 3); //Calculate potential double x, y; double Fx, Fy, Fz; cyl_to_rect(R,phi,&x,&y); if ( x == cached_x && y == cached_y && z == cached_z ){ Fx= *(args + + 4); Fy= *(args + + 5); Fz= *(args + + 6); } else // LCOV_EXCL_START EllipsoidalPotentialxyzforces_xyz(potentialArgs->mdens, x,y,z,&Fx,&Fy,&Fz,args); // LCOV_EXCL_STOP return amp * Fz; } double EllipsoidalPotentialPlanarRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ return EllipsoidalPotentialRforce(R,0.,phi,t,potentialArgs); } double EllipsoidalPotentialPlanarphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ return EllipsoidalPotentialphitorque(R,0.,phi,t,potentialArgs); } double EllipsoidalPotentialDens(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ //Get args double * args= potentialArgs->args; double amp= *args; double * ellipargs= args + 8 + (int) *(args+7); // *(args+7) = num. arguments psi double b2= *ellipargs++; double c2= *ellipargs++; bool aligned= (bool) *ellipargs++; double * rot= ellipargs; ellipargs+= 9; //Calculate density double x, y; cyl_to_rect(R,phi,&x,&y); if ( !aligned ) rotate(&x,&y,&z,rot); return amp * potentialArgs->mdens ( sqrt (x * x + y * y / b2 + z * z / c2 ), args+8); } /* // Implement the potentials separately //NFW static inline double TriaxialNFWPotential_psi(double x){ return 1. / ( 1. + x ); } double TriaxialNFWPotentialpotential_xyz_integrand(double s, double x,double y, double z,double a, double b2,double c2){ double t= 1. / s / s - 1.; return TriaxialNFWPotential_psi( sqrt ( x * x / ( 1. + t ) \ + y * y / ( b2 + t ) \ + z * z / ( c2 + t ) ) / a ); } //Hernquist static inline double TriaxialHernquistPotential_psi(double x){ return 0.5 / ( 1. + x ) / ( 1. + x ); } double TriaxialHernquistPotentialpotential_xyz_integrand(double s, double x,double y, double z,double a, double b2,double c2){ double t= 1. / s / s - 1.; return TriaxialHernquistPotential_psi( sqrt ( x * x / ( 1. + t ) \ + y * y / ( b2 + t ) \ + z * z / ( c2 + t ) ) / a ); } double TriaxialHernquistPotentialEval(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ int ii; double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; double b2= *args++; double c2= *args++; bool aligned= (bool) *args++; double * rot= args; args+= 9; int glorder= (int) *args++; double * glx= args; double * glw= args + glorder; //Calculate potential double x, y; double out= 0.; cyl_to_rect(R,phi,&x,&y); if ( !aligned ) rotate(&x,&y,&z,rot); for (ii=0; ii < glorder; ii++) out+= *(glw+ii) * a * a \ * TriaxialHernquistPotentialpotential_xyz_integrand(*(glx+ii),x,y,z, a,b2,c2); return amp * out; } //Jaffe static inline double TriaxialJaffePotential_psi(double x){ return - 1. / ( 1. + x ) - log ( x / ( 1. + x ) ) ; } double TriaxialJaffePotentialpotential_xyz_integrand(double s, double x,double y, double z,double a, double b2,double c2){ double t= 1. / s / s - 1.; return TriaxialJaffePotential_psi( sqrt ( x * x / ( 1. + t ) \ + y * y / ( b2 + t ) \ + z * z / ( c2 + t ) ) / a ); } double TriaxialJaffePotentialEval(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ int ii; double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; double b2= *args++; double c2= *args++; bool aligned= (bool) *args++; double * rot= args; args+= 9; int glorder= (int) *args++; double * glx= args; double * glw= args + glorder; //Calculate potential double x, y; double out= 0.; cyl_to_rect(R,phi,&x,&y); if ( !aligned ) rotate(&x,&y,&z,rot); for (ii=0; ii < glorder; ii++) out+= *(glw+ii) * a * a \ * TriaxialJaffePotentialpotential_xyz_integrand(*(glx+ii),x,y,z, a,b2,c2); return amp * out; } */ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/EllipticalDiskPotential.c0000644000175100001710000000566214327773303025473 0ustar00runnerdocker#include #include //EllipticalDiskPotential // double EllipticalDiskSmooth(double t,double tform, double tsteady){ double smooth, xi,deltat; //Slightly different smoothing if ( ! isnan(tform) ) if ( t < tform ) smooth= 0.; else if ( t < tsteady ) { deltat= t-tform; xi= 2.*deltat/(tsteady-tform)-1.; smooth= (3./16.*pow(xi,5.)-5./8.*pow(xi,3.)+15./16.*xi+.5); } else smooth= 1.; else smooth=1.; return smooth; } double EllipticalDiskPotentialRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double twophio= *args++; double p= *args++; double phib= *args; //Calculate Rforce smooth= EllipticalDiskSmooth(t,tform,tsteady); return -amp * smooth * p * twophio / 2. * pow(R,p-1.) * cos( 2. * (phi - phib)); } double EllipticalDiskPotentialphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double twophio= *args++; double p= *args++; double phib= *args; //Calculate phitorque smooth= EllipticalDiskSmooth(t,tform,tsteady); return amp * smooth * twophio * pow(R,p) * sin( 2. * (phi-phib)); } double EllipticalDiskPotentialR2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double twophio= *args++; double p= *args++; double phib= *args; //Calculate Rforce smooth= EllipticalDiskSmooth(t,tform,tsteady); return amp * smooth * p * ( p - 1) * twophio / 2. * pow(R,p-2.) * cos( 2. * ( phi - phib ) ); } double EllipticalDiskPotentialphi2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double twophio= *args++; double p= *args++; double phib= *args; //Calculate Rforce smooth= EllipticalDiskSmooth(t,tform,tsteady); return - 2. * amp * smooth * twophio * pow(R,p) * cos( 2. * ( phi - phib ) ); } double EllipticalDiskPotentialRphideriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double twophio= *args++; double p= *args++; double phib= *args; //Calculate Rforce smooth= EllipticalDiskSmooth(t,tform,tsteady); return - amp * smooth * p * twophio * pow(R,p-1.) * sin( 2. * ( phi - phib ) ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/FlattenedPowerPotential.c0000644000175100001710000000615014327773303025512 0ustar00runnerdocker#include #include //FlattenedPowerPotential //4 arguments: amp, alpha, q^2, and core^2 double FlattenedPowerPotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double alpha= *(args+1); double q2= *(args+2); double core2= *(args+3); double m2; //Calculate potential if ( alpha == 0. ) return 0.5 * amp * log(R*R+Z*Z/q2+core2); else { m2= core2+R*R+Z*Z/q2; return - amp * pow(m2,-0.5 * alpha) / alpha; } } double FlattenedPowerPotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double alpha= *(args+1); double q2= *(args+2); double core2= *(args+3); double m2; //Calculate potential if ( alpha == 0. ) return - amp * R/(R*R+Z*Z/q2+core2); else { m2= core2+R*R+Z*Z/q2; return - amp * pow(m2,-0.5 * alpha-1.) * R; } } double FlattenedPowerPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double alpha= *(args+1); double core2= *(args+2); double m2; //Calculate potential if ( alpha == 0. ) return - amp * R/(R*R+core2); else { m2= core2+R*R; return - amp * pow(m2,-0.5 * alpha - 1.) * R; } } double FlattenedPowerPotentialzforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double alpha= *(args+1); double q2= *(args+2); double core2= *(args+3); double m2; //Calculate potential if ( alpha == 0. ) return -amp * Z/q2/(R*R+Z*Z/q2+core2); else { m2= core2+R*R+Z*Z/q2; return - amp * pow(m2,-0.5 * alpha - 1.) * Z / q2; } } double FlattenedPowerPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double alpha= *(args+1); double core2= *(args+2); double m2; //Calculate potential if ( alpha == 0. ) return amp * (1.- 2.*R*R/(R*R+core2))/(R*R+core2); else { m2= core2+R*R; return - amp * pow(m2,-0.5 * alpha - 1.) * ( (alpha + 1.) * R*R/m2 -1.); } } double FlattenedPowerPotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double alpha= *(args+1); double q2= *(args+2); double core2= *(args+3); //Calculate density if ( alpha == 0. ) return amp * M_1_PI / 4. / q2 * ( ( 2. * q2 + 1. ) * core2 + R * R \ + ( 2. - 1. / q2 ) * Z * Z ) \ * pow ( R * R + Z * Z / q2 + core2 ,-2.); else { return amp * M_1_PI / 4. / q2 * ( ( 2. * q2 + 1. ) * core2 + R * R \ * ( 1. - alpha * q2 ) + Z * Z \ * ( 2. - ( 1. +alpha ) / q2 ) ) \ * pow( R * R + Z * Z / q2 + core2, -alpha / 2. - 2. ); } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/GaussianAmplitudeWrapperPotential.c0000644000175100001710000000657314327773303027560 0ustar00runnerdocker#include #include //GaussianAmplitudeWrapperPotential //3 parameters: amp, to, sigma^2 double gaussSmooth(double t,double to, double sigma2){ return exp(-0.5*(t-to)*(t-to)/sigma2); } double GaussianAmplitudeWrapperPotentialEval(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate potential, only used in actionAngle, so phi=0, t=0 return *args * gaussSmooth(t,*(args+1),*(args+2)) \ * evaluatePotentials(R,z, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double GaussianAmplitudeWrapperPotentialRforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rforce return *args * gaussSmooth(t,*(args+1),*(args+2)) \ * calcRforce(R,z,phi,t, potentialArgs->nwrapped,potentialArgs->wrappedPotentialArg); } double GaussianAmplitudeWrapperPotentialphitorque(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phitorque return *args * gaussSmooth(t,*(args+1),*(args+2)) \ * calcphitorque(R,z,phi,t, potentialArgs->nwrapped,potentialArgs->wrappedPotentialArg); } double GaussianAmplitudeWrapperPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate zforce return *args * gaussSmooth(t,*(args+1),*(args+2)) \ * calczforce(R,z,phi,t, potentialArgs->nwrapped,potentialArgs->wrappedPotentialArg); } double GaussianAmplitudeWrapperPotentialPlanarRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rforce return *args * gaussSmooth(t,*(args+1),*(args+2)) \ * calcPlanarRforce(R,phi,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double GaussianAmplitudeWrapperPotentialPlanarphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phitorque return *args * gaussSmooth(t,*(args+1),*(args+2)) \ * calcPlanarphitorque(R,phi,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double GaussianAmplitudeWrapperPotentialPlanarR2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate R2deriv return *args * gaussSmooth(t,*(args+1),*(args+2)) \ * calcPlanarR2deriv(R,phi,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double GaussianAmplitudeWrapperPotentialPlanarphi2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phi2deriv return *args * gaussSmooth(t,*(args+1),*(args+2)) \ * calcPlanarphi2deriv(R,phi,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double GaussianAmplitudeWrapperPotentialPlanarRphideriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rphideriv return *args * gaussSmooth(t,*(args+1),*(args+2)) \ * calcPlanarRphideriv(R,phi,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/HenonHeilesPotential.c0000644000175100001710000000212014327773303024761 0ustar00runnerdocker#include #include //HenonHeilesPotential double HenonHeilesPotentialRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; return - *args * R * (1. + R * sin( 3. * phi ) ); } double HenonHeilesPotentialphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phitorque return - *args * pow(R,3.) * cos( 3. * phi ); } double HenonHeilesPotentialR2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; return *args * ( 1. + 2. * R * sin( 3. * phi ) ); } double HenonHeilesPotentialphi2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; return - 3. * *args * pow(R,3.) * sin( 3. * phi ); } double HenonHeilesPotentialRphideriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; return 3. * *args * R * R * cos( 3. * phi ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/HernquistPotential.c0000644000175100001710000000373414327773303024556 0ustar00runnerdocker#include #include //HernquistPotential //2 arguments: amp, a double HernquistPotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args++; double a= *args; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); return - amp / (1. + sqrtRz / a ) / 2. / a; } double HernquistPotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); return - amp * R / a / sqrtRz * pow(1. + sqrtRz / a , -2. ) / 2. / a; } double HernquistPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Rforce return - amp / a * pow(1. + R / a , -2. ) / 2. / a; } double HernquistPotentialzforce(double R,double Z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); return - amp * Z / a / sqrtRz * pow(1. + sqrtRz / a , -2. ) / 2. / a; } double HernquistPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate R2deriv return -amp / a / a / a * pow(1. + R / a, -3. ); } double HernquistPotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args++; double a= *args; //Calculate density double r= sqrt ( R * R + Z * Z ); return amp * M_1_PI / 4. / a / a / r * pow ( 1. + r / a , -3. ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/HomogeneousSpherePotential.c0000644000175100001710000000461614327773303026233 0ustar00runnerdocker#include #include //HomogeneousSpherePotential //3 arguments: amp, R2, R3 double HomogeneousSpherePotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double R2= *(args+1); double R3= *(args+2); //Calculate potential double r2= R * R + Z * Z; if ( r2 < R2 ) return amp * ( r2 - 3. * R2 ); else return -2. * amp * R3 / sqrt( r2 ); } double HomogeneousSpherePotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double R2= *(args+1); double R3= *(args+2); //Calculate Rforce double r2= R * R + Z * Z; if ( r2 < R2 ) return -2. * amp * R; else return -2. * amp * R3 * R / pow( r2 , 1.5 ); } double HomogeneousSpherePotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double R2= *(args+1); double R3= *(args+2); //Calculate Rforce double r2= R * R; if ( r2 < R2 ) return -2. * amp * R; else return -2. * amp * R3 / r2; } double HomogeneousSpherePotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double R2= *(args+1); double R3= *(args+2); //Calculate zforce double r2= R * R + z * z; if ( r2 < R2 ) return -2. * amp * z; else return -2. * amp * R3 * z / pow ( r2 , 1.5 ); } double HomogeneousSpherePotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double R2= *(args+1); double R3= *(args+2); //Calculate R2deriv double r2= R * R; if ( r2 < R2 ) return 2. * amp; else return -4. * amp * R3 / pow ( r2 , 1.5 ); } double HomogeneousSpherePotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double R2= *(args+1); //Calculate potential double r2= R * R + Z * Z; if ( r2 < R2 ) return 1.5 * amp * M_1_PI; else return 0.; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/IsochronePotential.c0000644000175100001710000000431614327773303024522 0ustar00runnerdocker#include #include //IsochronePotential //2 arguments: amp, b double IsochronePotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double b= *(args+1); //Calculate potential double r2= R*R+Z*Z; return -amp / ( b + sqrt(r2 + b * b) ); } double IsochronePotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double b= *(args+1); //Calculate Rforce double r2= R*R+Z*Z; double rb= sqrt(r2 + b * b); return - amp * R / rb * pow(b + rb,-2.); } double IsochronePotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double b= *(args+1); //Calculate Rforce double r2= R*R; double rb= sqrt(r2 + b * b); return - amp * R / rb * pow(b + rb,-2.); } double IsochronePotentialzforce(double R,double Z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double b= *(args+1); //Calculate zforce double r2= R*R+Z*Z; double rb= sqrt(r2 + b * b); return - amp * Z / rb * pow(b + rb,-2.); } double IsochronePotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double b= *(args+1); //Calculate Rforce double r2= R*R; double rb= sqrt(r2 + b * b); return - amp * ( -pow(b,3.) - b * b * rb + 2. * r2 * rb ) * pow(rb * ( b + rb ),-3.); } double IsochronePotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double b= *(args+1); //Calculate potential double r2= R * R + Z * Z; double rb= sqrt ( r2 + b * b ); double brbrb= ( b + rb ) * rb; return amp * M_1_PI / 4. * ( 3. * brbrb * rb \ - r2 * ( b + 3. * rb ) ) \ * pow ( brbrb , -3.); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/IsothermalDiskPotential.c0000644000175100001710000000047114327773303025511 0ustar00runnerdocker#include #include //IsothermalDiskPotential: 2 parameters: amp = (real amp) * sigma2 / H, 2H double IsothermalDiskPotentialLinearForce(double x, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; return - *args * tanh ( x / *(args+1) ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/JaffePotential.c0000644000175100001710000000367214327773303023610 0ustar00runnerdocker#include #include //JaffePotential //2 arguments: amp, a double JaffePotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); return - amp * log ( 1. + a / sqrtRz ) / a; } double JaffePotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); return - amp * R * pow( sqrtRz , -3. ) / ( 1. + a / sqrtRz ); } double JaffePotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Rforce return - amp * pow(R,-2.) / ( 1. + a / R ); } double JaffePotentialzforce(double R,double Z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); return - amp * Z * pow( sqrtRz , -3. ) / ( 1. + a / sqrtRz ); } double JaffePotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate R2deriv return - amp * (a + 2. * R) * pow(R,-4.) * pow(1.+a/R,-2.); } double JaffePotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate density double r= sqrt ( R * R + Z * Z ); return amp * M_1_PI / 4. / a * pow ( r * ( 1. + r / a ), -2. ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/KGPotential.c0000644000175100001710000000105514327773303023067 0ustar00runnerdocker#include #include //KGPotential: 4 parameters: amp, K, D^2, 2F //Routines to evaluate forces etc. for a verticalPotential, i.e., a 1D //potential derived from a 3D potential as the z potential at a given (R,phi) double KGPotentialLinearForce(double x, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //double amp= *args; //double K= *(args+1); //double D2= *(args+2); //double F= *(args+3); return - *args * x * ( *(args+1) / sqrt ( x * x + *(args+2) ) + *(args+3) ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/KuzminDiskPotential.c0000644000175100001710000000320414327773303024654 0ustar00runnerdocker#include #include //Kuzmin Disk potential //2 arguments: amp, a double KuzminDiskPotentialEval(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Potential return - amp * pow(R*R+pow(a+fabs(z),2.),-0.5); } double KuzminDiskPotentialRforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Rforce return - amp * R * pow(R*R+pow(a+fabs(z),2.),-1.5); } double KuzminDiskPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate planarRforce return - amp * R * pow(R*R+a*a,-1.5); } double KuzminDiskPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate zforce double zsign= (z > 0 ) - (z < 0); //Gets the sign of z return -zsign* amp * pow(R*R+pow(a+fabs(z),2.),-1.5) * (a + fabs(z)); } double KuzminDiskPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //calculate R2deriv double denom=R*R+a*a; return amp * (-pow(denom,-1.5) + 3*R*R*pow(denom, -2.5)); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/KuzminKutuzovStaeckelPotential.c0000644000175100001710000000750114327773303027131 0ustar00runnerdocker#include #include //KuzminKutuzovStaeckelPotential //3 arguments: amp, ac, Delta double KuzminKutuzovStaeckelPotentialEval(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp = *args; double ac = *(args+1); double Delta = *(args+2); //Coordinate transformation double gamma = Delta*Delta / (1.-ac*ac); double alpha = gamma - Delta*Delta; double term = R*R + z*z - alpha - gamma; double discr = pow(R*R + z*z - Delta*Delta, 2.) + (4. * Delta*Delta * R*R); double l = 0.5 * (term + sqrt(discr)); double n = 0.5 * (term - sqrt(discr)); n= ((n > 0.) ? n: 0.); //Calculate potential return -amp /(sqrt(l) + sqrt(n)); } double KuzminKutuzovStaeckelPotentialRforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp = *args; double ac = *(args+1); double Delta = *(args+2); //Coordinate transformation double gamma = Delta*Delta / (1.-ac*ac); double alpha = gamma - Delta*Delta; double term = R*R + z*z - alpha - gamma; double discr = pow(R*R + z*z - Delta*Delta, 2.) + (4. * Delta*Delta * R*R); double l = 0.5 * (term + sqrt(discr)); double n = 0.5 * (term - sqrt(discr)); double dldR = R * (1. + (R*R + z*z + Delta*Delta) / sqrt(discr)); double dndR = R * (1. - (R*R + z*z + Delta*Delta) / sqrt(discr)); //Calculate Rforce double dVdl = 0.5/sqrt(l)/pow(sqrt(l)+sqrt(n),2.); double dVdn = 0.5/sqrt(n)/pow(sqrt(l)+sqrt(n),2.); return -amp * (dldR * dVdl + dndR * dVdn); } double KuzminKutuzovStaeckelPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp = *args; double ac = *(args+1); double Delta = *(args+2); //Coordinate transformation (for z=0) double gamma = Delta*Delta / (1.-ac*ac); double alpha = gamma - Delta*Delta; double l = R*R - alpha; double n = -gamma; double dldR = 2.*R; //Calculate Rforce double dVdl = 0.5/sqrt(l)/pow(sqrt(l)+sqrt(n),2.); return -amp * dldR * dVdl; } double KuzminKutuzovStaeckelPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp = *args; double ac = *(args+1); double Delta = *(args+2); //Coordinate transformation double gamma = Delta*Delta / (1.-ac*ac); double alpha = gamma - Delta*Delta; double term = R*R + z*z - alpha - gamma; double discr = pow(R*R + z*z - Delta*Delta, 2.) + (4. * Delta*Delta * R*R); double l = 0.5 * (term + sqrt(discr)); double n = 0.5 * (term - sqrt(discr)); double dldz = z * (1. + (R*R + z*z - Delta*Delta) / sqrt(discr)); double dndz = z * (1. - (R*R + z*z - Delta*Delta) / sqrt(discr)); //Calculate zforce double dVdl = 0.5/sqrt(l)/pow(sqrt(l)+sqrt(n),2.); double dVdn = 0.5/sqrt(n)/pow(sqrt(l)+sqrt(n),2.); return -amp * (dldz * dVdl + dndz * dVdn); } double KuzminKutuzovStaeckelPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp = *args; double ac = *(args+1); double Delta = *(args+2); //Coordinate transformation (for z=0) double gamma = Delta*Delta / (1.-ac*ac); double alpha = gamma - Delta*Delta; double l = R*R - alpha; double n = -gamma; double dldR = 2.*R; double d2ldR2 = 2.; //Calculate R2deriv double dVdl = 0.5/sqrt(l)/pow(sqrt(l)+sqrt(n),2.); double d2Vdl2 = (-3.*sqrt(l)-sqrt(n)) / (4. * pow(l,1.5) * pow(sqrt(l)+sqrt(n),3.)); return amp * (d2ldR2 * dVdl + dldR*dldR*d2Vdl2); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/LogarithmicHaloPotential.c0000644000175100001710000001273314327773303025641 0ustar00runnerdocker#include #include //LogarithmicHaloPotential //4 (3) arguments: amp, c2, (and q), now also 1-1/b^2 for triaxial! double LogarithmicHaloPotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double q= *(args+1); double c= *(args+2); double onem1overb2= *(args+3); //Calculate potential double zq= Z/q; if ( onem1overb2 < 1 ) return 0.5 * amp * log(R*R * (1. - onem1overb2 * pow(sin(phi),2))+zq*zq+c); else return 0.5 * amp * log(R*R+zq*zq+c); } double LogarithmicHaloPotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double q= *(args+1); double c= *(args+2); double onem1overb2= *(args+3); //Calculate Rforce double zq= Z/q; double Rt2; if ( onem1overb2 < 1 ) { Rt2= R*R * (1. - onem1overb2 * pow(sin(phi),2)); return - amp * Rt2/R/(Rt2+zq*zq+c); } else return - amp * R/(R*R+zq*zq+c); } double LogarithmicHaloPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double c= *(args+2); // skip q double onem1overb2= *(args+3); //Calculate Rforce double Rt2; if ( onem1overb2 < 1 ) { Rt2= R*R * (1. - onem1overb2 * pow(sin(phi),2)); return -amp * Rt2/R/(Rt2+c); } else return -amp * R/(R*R+c); } double LogarithmicHaloPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double q= *(args+1); double c= *(args+2); double onem1overb2= *(args+3); //Calculate zforce double zq= z/q; double Rt2; if ( onem1overb2 < 1 ) { Rt2= R*R * (1. - onem1overb2 * pow(sin(phi),2)); return -amp * zq/q/(Rt2+zq*zq+c); } else return -amp * zq/q/(R*R+zq*zq+c); } double LogarithmicHaloPotentialphitorque(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double q= *(args+1); double c= *(args+2); double onem1overb2= *(args+3); //Calculate phitorque double zq; double Rt2; if ( onem1overb2 < 1 ) { zq= z/q; Rt2= R*R * (1. - onem1overb2 * pow(sin(phi),2)); return amp * R*R / (Rt2+zq*zq+c) * sin(2*phi) * onem1overb2 / 2.; } else return 0.; } double LogarithmicHaloPotentialPlanarphitorque(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double c= *(args+2); // skip q double onem1overb2= *(args+3); //Calculate phitorque double Rt2; if ( onem1overb2 < 1 ) { Rt2= R*R * (1. - onem1overb2 * pow(sin(phi),2)); return amp * R*R / (Rt2+c) * sin(2*phi) * onem1overb2 / 2.; } else return 0.; } double LogarithmicHaloPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double c= *(args+2); // skip q double onem1overb2= *(args+3); //Calculate Rforce double Rt2; if ( onem1overb2 < 1 ) { Rt2= R*R * (1. - onem1overb2 * pow(sin(phi),2)); return amp * (1.- 2.*Rt2/(Rt2+c))/(Rt2+c)*Rt2/R/R; } else return amp * (1.- 2.*R*R/(R*R+c))/(R*R+c); } double LogarithmicHaloPotentialPlanarphi2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double c= *(args+2); // skip q double onem1overb2= *(args+3); //Calculate Rforce double Rt2; if ( onem1overb2 < 1 ) { Rt2= R*R * (1. - onem1overb2 * pow(sin(phi),2)); return - amp * onem1overb2 * (0.5 * pow(R*R*sin(2.*phi),2.) * onem1overb2 \ /(Rt2+c)/(Rt2+c)+R*R/(Rt2+c)*cos(2.*phi)); } else return 0.; } double LogarithmicHaloPotentialPlanarRphideriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double c= *(args+2); // skip q double onem1overb2= *(args+3); //Calculate Rforce double Rt2; if ( onem1overb2 < 1 ) { Rt2= R*R * (1. - onem1overb2 * pow(sin(phi),2)); return - amp * c / (Rt2+c) / (Rt2+c) * R * sin(2.*phi) * onem1overb2; } else return 0.; } double LogarithmicHaloPotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double q= *(args+1); double c= *(args+2); double onem1overb2= *(args+3); //Calculate density double zq= Z/q; double R2, Rt2, denom, denom2; double q2= q*q; if ( onem1overb2 < 1 ) { R2= R * R; Rt2= R2 * (1. - onem1overb2 * pow(sin(phi),2)); denom= 1. / ( Rt2 + zq * zq + c ); denom2= denom * denom; return amp * M_1_PI / 4. * ( 2. * Rt2 / R2 * ( denom - Rt2 * denom2 )\ + denom / q2 - 2. * zq * zq * denom2 / q2 \ - onem1overb2 \ * ( 2. * R2 * pow ( sin ( 2. * phi ),2) / 4. * onem1overb2 \ * denom2 + denom * cos( 2. * phi ) ) ); } else return amp * M_1_PI / 4. / q2 * ( ( 2. * q2 + 1. ) * c + R * R \ + ( 2. - 1. / q2 ) * Z * Z )/ \ pow( R * R + zq * zq + c ,2.); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/LopsidedDiskPotential.c0000644000175100001710000000333314327773303025145 0ustar00runnerdocker#include #include //LopsidedDiskPotential double LopsidedDiskPotentialRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double phio= *args++; double p= *args++; double phib= *args; //Calculate Rforce return -amp * p * phio * pow(R,p-1.) * cos( phi - phib); } double LopsidedDiskPotentialphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double phio= *args++; double p= *args++; double phib= *args; //Calculate phitorque return amp * phio * pow(R,p) * sin( phi-phib); } double LopsidedDiskPotentialR2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double phio= *args++; double p= *args++; double phib= *args; //Calculate Rforce return amp * p * ( p - 1) * phio * pow(R,p-2.) * cos(phi - phib); } double LopsidedDiskPotentialphi2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double phio= *args++; double p= *args++; double phib= *args; //Calculate Rforce return - amp * phio * pow(R,p) * cos( phi - phib ); } double LopsidedDiskPotentialRphideriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double phio= *args++; double p= *args++; double phib= *args; //Calculate Rforce return - amp * p * phio * pow(R,p-1.) * sin( phi - phib ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/MiyamotoNagaiPotential.c0000644000175100001710000000474514327773303025335 0ustar00runnerdocker#include #include //Miyamoto-Nagai potential //3 arguments: amp, a, b double MiyamotoNagaiPotentialEval(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; double b= *args; //Calculate Rforce return - amp * pow(R*R+pow(a+pow(z*z+b*b,0.5),2.),-0.5); } double MiyamotoNagaiPotentialRforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; double b= *args; //Calculate Rforce return - amp * R * pow(R*R+pow(a+pow(z*z+b*b,0.5),2.),-1.5); } double MiyamotoNagaiPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; double b= *args; //Calculate Rforce return - amp * R * pow(R*R+pow(a+b,2.),-1.5); } double MiyamotoNagaiPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; double b= *args; //Calculate zforce double sqrtbz= pow(b*b+z*z,.5); double asqrtbz= a+sqrtbz; if ( a == 0. ) return amp * ( -z * pow(R*R+asqrtbz*asqrtbz,-1.5) ); else return amp * ( -z * asqrtbz / sqrtbz * pow(R*R+asqrtbz*asqrtbz,-1.5) ); } double MiyamotoNagaiPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; double b= *args; //calculate R2deriv double denom= R*R+pow(a+b,2.); return amp * (pow(denom,-1.5) - 3. * R * R * pow(denom,-2.5)); } double MiyamotoNagaiPotentialDens(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args++; double b= *args; //Calculate density double b2= b*b; double sqrtbz= sqrt ( b2 + z * z ); double asqrtbz= a+sqrtbz; asqrtbz*= asqrtbz; if ( a == 0. ) return 3. * amp * M_1_PI / 4. * b2 * pow ( R * R + sqrtbz * sqrtbz, -2.5); else return amp * M_1_PI / 4. * b2 \ * ( a * R * R + ( a + 3. * sqrtbz ) * asqrtbz ) \ * pow ( R * R + asqrtbz,-2.5 ) * pow ( sqrtbz,-3.); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/MovingObjectPotential.c0000644000175100001710000001076414327773303025163 0ustar00runnerdocker#include #include #include #include // MovingObjectPotential // 3 arguments: amp, t0, tf void constrain_range(double * d) { // Constrains index to be within interpolation range if (*d < 0) *d = 0.0; if (*d > 1) *d = 1.0; } double MovingObjectPotentialRforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double amp,t0,tf,d_ind,x,y,obj_x,obj_y,obj_z, Rdist,RF; double * args= potentialArgs->args; //Get args amp= *args; t0= *(args+1); tf= *(args+2); d_ind= (t-t0)/(tf-t0); x= R*cos(phi); y= R*sin(phi); constrain_range(&d_ind); // Interpolate x, y, z obj_x= gsl_spline_eval(*potentialArgs->spline1d,d_ind,*potentialArgs->acc1d); obj_y= gsl_spline_eval(*(potentialArgs->spline1d+1),d_ind, *(potentialArgs->acc1d+1)); obj_z= gsl_spline_eval(*(potentialArgs->spline1d+2),d_ind, *(potentialArgs->acc1d+2)); Rdist= pow(pow(x-obj_x, 2)+pow(y-obj_y, 2), 0.5); // Calculate R force RF= calcRforce(Rdist,(obj_z-z),phi,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); return -amp*RF*(cos(phi)*(obj_x-x)+sin(phi)*(obj_y-y))/Rdist; } double MovingObjectPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double amp,t0,tf,d_ind,x,y,obj_x,obj_y,obj_z, Rdist; double * args= potentialArgs->args; //Get args amp= *args; t0= *(args+1); tf= *(args+2); d_ind= (t-t0)/(tf-t0); x= R*cos(phi); y= R*sin(phi); constrain_range(&d_ind); // Interpolate x, y, z obj_x= gsl_spline_eval(*potentialArgs->spline1d,d_ind,*potentialArgs->acc1d); obj_y= gsl_spline_eval(*(potentialArgs->spline1d+1),d_ind, *(potentialArgs->acc1d+1)); obj_z= gsl_spline_eval(*(potentialArgs->spline1d+2),d_ind, *(potentialArgs->acc1d+2)); Rdist= pow(pow(x-obj_x, 2)+pow(y-obj_y, 2), 0.5); // Calculate z force return -amp * calczforce(Rdist,(obj_z-z),phi,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double MovingObjectPotentialphitorque(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double amp,t0,tf,d_ind,x,y,obj_x,obj_y,obj_z, Rdist,RF; double * args= potentialArgs->args; //Get args amp= *args; t0= *(args+1); tf= *(args+2); d_ind= (t-t0)/(tf-t0); x= R*cos(phi); y= R*sin(phi); constrain_range(&d_ind); // Interpolate x, y, z obj_x= gsl_spline_eval(*potentialArgs->spline1d,d_ind,*potentialArgs->acc1d); obj_y= gsl_spline_eval(*(potentialArgs->spline1d+1),d_ind, *(potentialArgs->acc1d+1)); obj_z= gsl_spline_eval(*(potentialArgs->spline1d+2),d_ind, *(potentialArgs->acc1d+2)); Rdist= pow(pow(x-obj_x, 2)+pow(y-obj_y, 2), 0.5); // Calculate phitorque RF= calcRforce(Rdist,(obj_z-z),phi,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); return -amp*RF*R*(cos(phi)*(obj_y-y)-sin(phi)*(obj_x-x))/Rdist; } double MovingObjectPotentialPlanarRforce(double R, double phi, double t, struct potentialArg * potentialArgs){ double amp,t0,tf,d_ind,x,y,obj_x,obj_y,Rdist,RF; double * args= potentialArgs->args; //Get args amp= *args; t0= *(args+1); tf= *(args+2); d_ind= (t-t0)/(tf-t0); x= R*cos(phi); y= R*sin(phi); constrain_range(&d_ind); // Interpolate x, y obj_x= gsl_spline_eval(*potentialArgs->spline1d,d_ind,*potentialArgs->acc1d); obj_y= gsl_spline_eval(*(potentialArgs->spline1d+1),d_ind, *(potentialArgs->acc1d+1)); Rdist= pow(pow(x-obj_x, 2)+pow(y-obj_y, 2), 0.5); // Calculate R force RF= calcPlanarRforce(Rdist, phi, t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); return -amp*RF*(cos(phi)*(obj_x-x)+sin(phi)*(obj_y-y))/Rdist; } double MovingObjectPotentialPlanarphitorque(double R, double phi, double t, struct potentialArg * potentialArgs){ double amp,t0,tf,d_ind,x,y,obj_x,obj_y,Rdist,RF; double * args= potentialArgs->args; // Get args amp= *args; t0= *(args+1); tf= *(args+2); d_ind= (t-t0)/(tf-t0); x= R*cos(phi); y= R*sin(phi); constrain_range(&d_ind); // Interpolate x, y obj_x= gsl_spline_eval(*potentialArgs->spline1d,d_ind,*potentialArgs->acc1d); obj_y= gsl_spline_eval(*(potentialArgs->spline1d+1),d_ind, *(potentialArgs->acc1d+1)); Rdist= pow(pow(x-obj_x, 2)+pow(y-obj_y, 2), 0.5); // Calculate phitorque RF= calcPlanarRforce(Rdist, phi, t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); return -amp*RF*R*(cos(phi)*(obj_y-y)-sin(phi)*(obj_x-x))/Rdist; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/NFWPotential.c0000644000175100001710000000405114327773303023217 0ustar00runnerdocker#include #include //NFWPotential //2 arguments: amp, a double NFWPotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Rforce double sqrtRz= pow(R*R+Z*Z,0.5); return - amp * log ( 1. + sqrtRz / a ) / sqrtRz; } double NFWPotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Rforce double Rz= R*R+Z*Z; double sqrtRz= pow(Rz,0.5); return amp * R * (1. / Rz / (a + sqrtRz)-log(1.+sqrtRz / a)/sqrtRz/Rz); } double NFWPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Rforce return amp / R * (1. / (a + R)-log(1.+ R / a)/ R); } double NFWPotentialzforce(double R,double Z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate Rforce double Rz= R*R+Z*Z; double sqrtRz= pow(Rz,0.5); return amp * Z * (1. / Rz / (a + sqrtRz)-log(1.+sqrtRz / a)/sqrtRz/Rz); } double NFWPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate R2deriv double aR= a+R; double aR2= aR*aR; return amp * (((R*(2.*a+3.*R))-2.*aR2*log(1.+R/a))/R/R/R/aR2); } double NFWPotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double a= *args; //Calculate density double sqrtRz= sqrt ( R * R + Z * Z ); return amp * M_1_PI / 4. / a / a \ / ( 1. + sqrtRz / a ) / ( 1. + sqrtRz / a ) / sqrtRz; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/NonInertialFrameForce.c0000644000175100001710000001775314327773303025076 0ustar00runnerdocker#include #include //NonInertialFrameForce //arguments: amp void NonInertialFrameForcexyzforces_xyz(double R,double z,double phi,double t, double vR,double vT,double vz, double * Fx, double * Fy, double * Fz, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double x, y, vx, vy; bool rot_acc, lin_acc, omegaz_only, const_freq, Omega_as_func; double Omegax, Omegay, Omegaz; double Omega2, Omegatimesvecx; double Omegadotx, Omegadoty, Omegadotz; double x0x, x0y, x0z, v0x, v0y, v0z; cyl_to_rect(R,phi,&x,&y); cyl_to_rect_vec(vR,vT,phi,&vx,&vy); //Setup caching *(args + 1)= R; *(args + 2)= z; *(args + 3)= phi; *(args + 4)= t; *(args + 5)= vR; *(args + 6)= vT; *(args + 7)= vz; // Evaluate force *Fx= 0.; *Fy= 0.; *Fz= 0.; // Rotational acceleration part rot_acc= (bool) *(args + 11); lin_acc= (bool) *(args + 12); Omega_as_func= (bool) *(args + 15); if ( rot_acc ) { omegaz_only= (bool) *(args + 13); const_freq= (bool) *(args + 14); if ( omegaz_only ) { if ( Omega_as_func ) { Omegaz= (*(*(potentialArgs->tfuncs+9*lin_acc)))(t); Omega2= Omegaz * Omegaz; } else { Omegaz= *(args + 18); if ( !const_freq ) { Omegaz+= *(args + 22) * t; Omega2= Omegaz * Omegaz; } else { Omega2= *(args + 19); } } *Fx+= 2. * Omegaz * vy + Omega2 * x; *Fy+= -2. * Omegaz * vx + Omega2 * y; if ( !const_freq ) { if ( Omega_as_func ) { Omegadotz= (*(*(potentialArgs->tfuncs+9*lin_acc+1)))(t); } else { Omegadotz= *(args + 22); } *Fx+= Omegadotz * y; *Fy-= Omegadotz * x; } if ( lin_acc ) { x0x= (*(*(potentialArgs->tfuncs+3)))(t); x0y= (*(*(potentialArgs->tfuncs+4)))(t); v0x= (*(*(potentialArgs->tfuncs+6)))(t); v0y= (*(*(potentialArgs->tfuncs+7)))(t); *Fx+= 2. * Omegaz * v0y + Omega2 * x0x; *Fy+= -2. * Omegaz * v0x + Omega2 * x0y; if ( !const_freq ) { *Fx+= Omegadotz * x0y; *Fy-= Omegadotz * x0x; } } } else { if ( Omega_as_func ) { Omegax= (*(*(potentialArgs->tfuncs+9*lin_acc )))(t); Omegay= (*(*(potentialArgs->tfuncs+9*lin_acc+1)))(t); Omegaz= (*(*(potentialArgs->tfuncs+9*lin_acc+2)))(t); Omega2= Omegax * Omegax + Omegay * Omegay + Omegaz * Omegaz; } else { Omegax= *(args + 16); Omegay= *(args + 17); Omegaz= *(args + 18); if ( !const_freq ) { Omegax+= *(args + 20) * t; Omegay+= *(args + 21) * t; Omegaz+= *(args + 22) * t; Omega2= Omegax * Omegax + Omegay * Omegay + Omegaz * Omegaz; } else { Omega2= *(args + 19); } } Omegatimesvecx= Omegax * x + Omegay * y + Omegaz * z; *Fx+= 2. * ( Omegaz * vy - Omegay * vz ) + Omega2 * x - Omegax * Omegatimesvecx; *Fy+= -2. * ( Omegaz * vx - Omegax * vz ) + Omega2 * y - Omegay * Omegatimesvecx; *Fz+= 2. * ( Omegay * vx - Omegax * vy ) + Omega2 * z - Omegaz * Omegatimesvecx; if ( !const_freq ) { if ( Omega_as_func ) { Omegadotx= (*(*(potentialArgs->tfuncs+9*lin_acc+3)))(t); Omegadoty= (*(*(potentialArgs->tfuncs+9*lin_acc+4)))(t); Omegadotz= (*(*(potentialArgs->tfuncs+9*lin_acc+5)))(t); } else { Omegadotx= *(args + 20); Omegadoty= *(args + 21); Omegadotz= *(args + 22); } *Fx-= -Omegadotz * y + Omegadoty * z; *Fy-= Omegadotz * x - Omegadotx * z; *Fz-= -Omegadoty * x + Omegadotx * y; } if ( lin_acc ) { x0x= (*(*(potentialArgs->tfuncs+3)))(t); x0y= (*(*(potentialArgs->tfuncs+4)))(t); x0z= (*(*(potentialArgs->tfuncs+5)))(t); v0x= (*(*(potentialArgs->tfuncs+6)))(t); v0y= (*(*(potentialArgs->tfuncs+7)))(t); v0z= (*(*(potentialArgs->tfuncs+8)))(t); // Re-use variable Omegatimesvecx= Omegax * x0x + Omegay * x0y + Omegaz * x0z; *Fx+= 2. * ( Omegaz * v0y - Omegay * v0z ) + Omega2 * x0x - Omegax * Omegatimesvecx; *Fy+= -2. * ( Omegaz * v0x - Omegax * v0z ) + Omega2 * x0y - Omegay * Omegatimesvecx; *Fz+= 2. * ( Omegay * v0x - Omegax * v0y ) + Omega2 * x0z - Omegaz * Omegatimesvecx; if ( !const_freq ) { *Fx-= -Omegadotz * x0y + Omegadoty * x0z; *Fy-= Omegadotz * x0x - Omegadotx * x0z; *Fz-= -Omegadoty * x0x + Omegadotx * x0y; } } } } // Linear acceleration part if ( lin_acc ) { *Fx-= (*(*(potentialArgs->tfuncs )))(t); *Fy-= (*(*(potentialArgs->tfuncs+1)))(t); *Fz-= (*(*(potentialArgs->tfuncs+2)))(t); } // Caching *(args + 8)= *Fx; *(args + 9)= *Fy; *(args + 10)= *Fz; } double NonInertialFrameForceRforce(double R,double z,double phi,double t, struct potentialArg * potentialArgs, double vR,double vT,double vz){ double * args= potentialArgs->args; double amp= *args; // Get caching args: amp = 0, R,z,phi,t,vR,vT,vz,Fx,Fy,Fz double cached_R= *(args + 1); double cached_z= *(args + 2); double cached_phi= *(args + 3); double cached_t= *(args + 4); double cached_vR= *(args + 5); double cached_vT= *(args + 6); double cached_vz= *(args + 7); //Calculate potential double Fx, Fy, Fz; if ( R != cached_R || phi != cached_phi || z != cached_z || t != cached_t \ || vR != cached_vR || vT != cached_vT || vz != cached_vz ) // LCOV_EXCL_LINE NonInertialFrameForcexyzforces_xyz(R,z,phi,t,vR,vT,vz, &Fx,&Fy,&Fz,potentialArgs); else { // LCOV_EXCL_START Fx= *(args + 8); Fy= *(args + 9); Fz= *(args + 10); // LCOV_EXCL_STOP } return amp * ( cos ( phi ) * Fx + sin( phi ) * Fy ); } double NonInertialFrameForcephitorque(double R,double z,double phi,double t, struct potentialArg * potentialArgs, double vR,double vT,double vz){ double * args= potentialArgs->args; double amp= *args; // Get caching args: amp = 0, R,z,phi,t,vR,vT,vz,Fx,Fy,Fz double cached_R= *(args + 1); double cached_z= *(args + 2); double cached_phi= *(args + 3); double cached_t= *(args + 4); double cached_vR= *(args + 5); double cached_vT= *(args + 6); double cached_vz= *(args + 7); //Calculate potential double Fx, Fy, Fz; if ( R != cached_R || phi != cached_phi || z != cached_z || t != cached_t \ || vR != cached_vR || vT != cached_vT || vz != cached_vz ) // LCOV_EXCL_START NonInertialFrameForcexyzforces_xyz(R,z,phi,t,vR,vT,vz, &Fx,&Fy,&Fz,potentialArgs); // LCOV_EXCL_STOP else { Fx= *(args + 8); Fy= *(args + 9); Fz= *(args + 10); } return amp * R * ( -sin ( phi ) * Fx + cos( phi ) * Fy ); } double NonInertialFrameForcezforce(double R,double z,double phi,double t, struct potentialArg * potentialArgs, double vR,double vT,double vz){ double * args= potentialArgs->args; double amp= *args; // Get caching args: amp = 0, R,z,phi,t,vR,vT,vz,Fx,Fy,Fz double cached_R= *(args + 1); double cached_z= *(args + 2); double cached_phi= *(args + 3); double cached_t= *(args + 4); double cached_vR= *(args + 5); double cached_vT= *(args + 6); double cached_vz= *(args + 7); //Calculate potential double Fx, Fy, Fz; if ( R != cached_R || phi != cached_phi || z != cached_z || t != cached_t \ || vR != cached_vR || vT != cached_vT || vz != cached_vz ) // LCOV_EXCL_START NonInertialFrameForcexyzforces_xyz(R,z,phi,t,vR,vT,vz, &Fx,&Fy,&Fz,potentialArgs); // LCOV_EXCL_STOP else { Fx= *(args + 8); Fy= *(args + 9); Fz= *(args + 10); } return amp * Fz; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/PerfectEllipsoidPotential.c0000644000175100001710000000110714327773303026021 0ustar00runnerdocker#include #include // Special case of EllipsoidalPotential, only need to define three functions double PerfectEllipsoidPotentialpsi(double m,double * args){ double a2= *args; double m2= m*m; return -1. / ( a2 + m2 ); } double PerfectEllipsoidPotentialmdens(double m,double * args){ double a2= *args; double m2= m*m; return 1. / ( a2 + m2 ) / ( a2 + m2 ); } // LCOV_EXCL_START double PerfectEllipsoidPotentialmdensDeriv(double m,double * args){ double a2= *args; double m2= m*m; return -4. * m * pow ( a2 + m2 ,-3 ); } // LCOV_EXCL_STOP ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/PlummerPotential.c0000644000175100001710000000360014327773303024205 0ustar00runnerdocker#include #include //PlummerPotential //2 arguments: amp, b double PlummerPotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double b2= *(args+1) * *(args+1); //Calculate potential return - amp / sqrt( R * R + Z * Z + b2 ); } double PlummerPotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double b2= *(args+1) * *(args+1); //Calculate Rforce return - amp * R * pow(R*R+Z*Z+b2,-1.5); } double PlummerPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double b2= *(args+1) * *(args+1); //Calculate Rforce return - amp * R * pow(R*R+b2,-1.5); } double PlummerPotentialzforce(double R,double Z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double b2= *(args+1) * *(args+1); //Calculate zforce return - amp * Z * pow(R*R+Z*Z+b2,-1.5); } double PlummerPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double b2= *(args+1) * *(args+1); //Calculate Rforce return amp * (b2 - 2.*R*R)*pow(R*R+b2,-2.5); } double PlummerPotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double b2= *(args+1) * *(args+1); //Calculate density return 3. * amp *M_1_PI / 4. * b2 * pow ( R * R + Z * Z + b2 , -2.5 ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/PowerSphericalPotential.c0000644000175100001710000000370314327773303025517 0ustar00runnerdocker#include #include //PowerSphericalPotential //2 arguments: amp, alpha double PowerSphericalPotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double alpha= *args; //Calculate Rforce if ( alpha == 2. ) return 0.5 * amp * log ( R*R+Z*Z); else return - amp * pow(R*R+Z*Z,1.-0.5*alpha) / (alpha - 2.); } double PowerSphericalPotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double alpha= *args; //Calculate Rforce return - amp * R * pow(R*R+Z*Z,-0.5*alpha); } double PowerSphericalPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double alpha= *args; //Calculate Rforce return - amp * pow(R,-alpha + 1.); } double PowerSphericalPotentialzforce(double R,double Z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double alpha= *args; //Calculate zforce return - amp * Z * pow(R*R+Z*Z,-0.5*alpha); } double PowerSphericalPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double alpha= *args; //Calculate R2deriv return amp * (1. - alpha ) * pow(R,-alpha); } double PowerSphericalPotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double alpha= *args; //Calculate density return amp * M_1_PI / 4. * ( 3. - alpha ) * pow (R*R + Z*Z, -0.5 * alpha); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/PowerSphericalPotentialwCutoff.c0000644000175100001710000000542114327773303027054 0ustar00runnerdocker#include #include #include #ifndef M_PI #define M_PI 3.14159265358979323846 #endif //PowerSphericalPotentialwCutoff //3 arguments: amp, alpha, rc double mass(double r2,double alpha, double rc){ return 2. * M_PI * pow ( rc , 3. - alpha ) * ( gsl_sf_gamma ( 1.5 - 0.5 * alpha ) - gsl_sf_gamma_inc ( 1.5 - 0.5 * alpha , r2 / rc / rc ) ); } double PowerSphericalPotentialwCutoffEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double alpha= *args++; double rc= *args; //Radius double r2= R*R+Z*Z; double r= sqrt(r2); return amp * 2. * M_PI * pow(rc,3.-alpha) / r * ( r / rc * ( gsl_sf_gamma ( 1. - 0.5 * alpha ) - gsl_sf_gamma_inc ( 1. - 0.5 * alpha , r2 / rc / rc ) ) - ( gsl_sf_gamma ( 1.5 - 0.5 * alpha ) - gsl_sf_gamma_inc ( 1.5 - 0.5 * alpha , r2 / rc / rc) ) ); } double PowerSphericalPotentialwCutoffRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double alpha= *args++; double rc= *args; //Radius double r2= R*R+Z*Z; //Calculate Rforce return - amp * mass (r2,alpha,rc) * R / pow(r2,1.5); } double PowerSphericalPotentialwCutoffPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double alpha= *args++; double rc= *args; //Radius double r2= R*R; //Calculate Rforce return - amp * mass (r2,alpha,rc) / r2; } double PowerSphericalPotentialwCutoffzforce(double R,double Z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double alpha= *args++; double rc= *args; //Radius double r2= R*R+Z*Z; //Calculate Rforce return - amp * mass (r2,alpha,rc) * Z / pow(r2,1.5); } double PowerSphericalPotentialwCutoffPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double alpha= *args++; double rc= *args; //Radius double r2= R*R; //Calculate R2deriv return amp * ( 4. * M_PI * pow(r2,- 0.5 * alpha) * exp(-r2/rc/rc) - 2. * mass(r2,alpha,rc)/pow(r2,1.5) ); } double PowerSphericalPotentialwCutoffDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double alpha= *args++; double rc= *args; //Radius double r2= R * R + Z * Z; double r= sqrt(r2); return amp * pow(r,-alpha) * exp ( -r2 / rc / rc ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/PowerTriaxialPotential.c0000644000175100001710000000116114327773303025356 0ustar00runnerdocker#include #include // Special case of EllipsoidalPotential, only need to define three functions double PowerTriaxialPotentialpsi(double m,double * args){ double alpha= *args; double m2= m*m; return 2. / ( 2. - alpha ) * pow ( m2 , 1. - alpha / 2. ); } double PowerTriaxialPotentialmdens(double m,double * args){ double alpha= *args; double m2= m*m; return pow ( m2 , - alpha / 2. ); } // LCOV_EXCL_START double PowerTriaxialPotentialmdensDeriv(double m,double * args){ double alpha= *args; double m2= m*m; return - alpha * pow ( m2 , ( -alpha - 1. ) / 2. ); } // LCOV_EXCL_STOP ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/PseudoIsothermalPotential.c0000644000175100001710000000431014327773303026052 0ustar00runnerdocker#include #include //PseudoIsothermalPotential //2 arguments: amp, a double PseudoIsothermalPotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double a= *(args+1); double a2= a*a; //Calculate potential double r2= R*R+Z*Z; double r= sqrt(r2); return amp * (0.5 * log(1 + r2 / a2) + a / r * atan(r / a)) / a; } double PseudoIsothermalPotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double a= *(args+1); //Calculate potential double r2= R*R+Z*Z; double r= sqrt(r2); return - amp * (1. / r - a / r2 * atan(r / a)) / a * R / r; } double PseudoIsothermalPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double a= *(args+1); //Calculate potential return - amp * (1. / R - a / R / R * atan(R / a)) / a; } double PseudoIsothermalPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double a= *(args+1); //Calculate potential double r2= R*R+z*z; double r= sqrt(r2); return - amp * (1. / r - a / r2 * atan(r / a)) / a * z / r; } double PseudoIsothermalPotentialPlanarR2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double a= *(args+1); double a2= a*a; //Calculate potential double R2= R*R; return amp / R2 * (2. * a / R * atan(R / a) - ( 2. * a2 + R2)/(a2 + R2) ) / a; } double PseudoIsothermalPotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args; double a= *(args+1); double a2= a*a; //Calculate potential double r2= R*R+Z*Z; return amp * M_1_PI / 4. / ( 1. + r2 / a2 ) / a2 / a; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/RotateAndTiltWrapperPotential.c0000644000175100001710000000736514327773303026657 0ustar00runnerdocker#include #include #include #include //RotateAndTiltWrapperPotential void RotateAndTiltWrapperPotentialxyzforces(double R, double z, double phi, double t, double * Fx, double * Fy, double * Fz, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double * rot= args+7; bool rotSet= (bool) *(args+16); bool offsetSet= (bool) *(args+17); double * offset= args+18; double x, y; double Rforce, phitorque; cyl_to_rect(R, phi, &x, &y); //caching *(args + 1)= x; *(args + 2)= y; *(args + 3)= z; //now get the forces in R, phi, z in the aligned frame if (rotSet) { rotate(&x,&y,&z,rot); } if (offsetSet) { x += *(offset); y += *(offset+1); z += *(offset+2); } rect_to_cyl(x,y,&R,&phi); Rforce= calcRforce(R, z, phi, t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); phitorque= calcphitorque(R, z, phi, t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); *Fz= calczforce(R, z, phi, t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); //back to rectangular *Fx= cos( phi )*Rforce - sin( phi )*phitorque / R; *Fy= sin( phi )*Rforce + cos( phi )*phitorque / R; //rotate back if (rotSet) { rotate_force(Fx,Fy,Fz,rot); } //cache *(args + 4)= *Fx; *(args + 5)= *Fy; *(args + 6)= *Fz; } double RotateAndTiltWrapperPotentialRforce(double R, double z, double phi, double t, struct potentialArg * potentialArgs){ double * args = potentialArgs->args; //get cached xyz double cached_x = *(args + 1); double cached_y = *(args + 2); double cached_z = *(args + 3); // change the zvector, calculate Rforce double Fx, Fy, Fz; double x, y; cyl_to_rect(R, phi, &x, &y); if ( x == cached_x && y == cached_y && z == cached_z ){ Fx = *(args + 4); Fy = *(args + 5); Fz = *(args + 6); } else RotateAndTiltWrapperPotentialxyzforces(R, z, phi, t, &Fx, &Fy, &Fz, potentialArgs); return *args * ( cos ( phi ) * Fx + sin ( phi ) * Fy ); } double RotateAndTiltWrapperPotentialphitorque(double R, double z, double phi, double t, struct potentialArg * potentialArgs){ double * args = potentialArgs->args; //get cached xyz double cached_x = *(args + 1); double cached_y = *(args + 2); double cached_z = *(args + 3); // change the zvector, calculate Rforce double Fx, Fy, Fz; double x, y; cyl_to_rect(R, phi, &x, &y); if ( x == cached_x && y == cached_y && z == cached_z ){ Fx = *(args + 4); Fy = *(args + 5); Fz = *(args + 6); } else // LCOV_EXCL_START RotateAndTiltWrapperPotentialxyzforces(R, z, phi, t, &Fx, &Fy, &Fz, potentialArgs); // LCOV_EXCL_STOP return *args * R * ( -sin ( phi ) * Fx + cos ( phi ) * Fy ); } double RotateAndTiltWrapperPotentialzforce(double R, double z, double phi, double t, struct potentialArg * potentialArgs){ double * args = potentialArgs->args; //get cached xyz double cached_x = *(args + 1); double cached_y = *(args + 2); double cached_z = *(args + 3); // change the zvector, calculate Rforce double Fx, Fy, Fz; double x, y; cyl_to_rect(R, phi, &x, &y); if ( x == cached_x && y == cached_y && z == cached_z ){ Fx = *(args + 4); Fy = *(args + 5); Fz = *(args + 6); } else // LCOV_EXCL_START RotateAndTiltWrapperPotentialxyzforces(R, z, phi, t, &Fx, &Fy, &Fz, potentialArgs); // LCOV_EXCL_STOP return *args * Fz; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/SCFPotential.c0000644000175100001710000005633714327773303023216 0ustar00runnerdocker#include #include #include #include #include #ifndef GSL_MAJOR_VERSION #define GSL_MAJOR_VERSION 1 #endif #ifndef M_PI #define M_PI 3.14159265358979323846 #endif //SCF Disk potential //4 arguments: amp, Acos, Asin, a const int FORCE =1; const int DERIV =2; //Useful Functions //Converts from cylindrical coordinates to spherical static inline void cyl_to_spher(double R, double Z,double *r, double *theta) { *r = sqrt(R*R + Z*Z); *theta = atan2(R, Z); } //Integer power double power(double x, int i) { if (i==0) return 1; return x*power(x,i - 1); } //Calculates xi static inline void calculateXi(double r, double a, double *xi) { *xi = (r - a)/(r + a); } //Potentials, forces, and derivative functions // LCOV_EXCL_START // Also used for density, just with rhoTilde double computePhi(double Acos_val, double Asin_val, double mCos, double mSin, double P, double phiTilde, int m) { return (Acos_val*mCos + Asin_val*mSin)*P*phiTilde; } // LCOV_EXCL_STOP double computeAxiPhi(double Acos_val, double P, double phiTilde) { return Acos_val*P*phiTilde; } double computeF_r(double Acos_val, double Asin_val, double mCos, double mSin, double P, double dphiTilde, int m) { return -(Acos_val*mCos + Asin_val*mSin)*P*dphiTilde; } double computeAxiF_r(double Acos_val, double P, double dphiTilde) { return -Acos_val*P*dphiTilde; } double computeF_theta(double Acos_val, double Asin_val, double mCos, double mSin, double dP, double phiTilde, int m) { return -(Acos_val*mCos + Asin_val*mSin)*dP*phiTilde; } double computeAxiF_theta(double Acos_val, double dP, double phiTilde) { return -Acos_val*dP*phiTilde; } double computeF_phi(double Acos_val, double Asin_val, double mCos, double mSin, double P, double phiTilde, int m) { return m*(Acos_val*mSin - Asin_val*mCos)*P*phiTilde; } double computeAxiF_phi(double Acos_val, double P, double phiTilde) { return 0.; } double computeF_rr(double Acos_val, double Asin_val, double mCos, double mSin, double P, double d2phiTilde, int m) { return -(Acos_val*mCos + Asin_val*mSin)*P*d2phiTilde; } double computeAxiF_rr(double Acos_val, double P, double d2phiTilde) { return -Acos_val*P*d2phiTilde; } double computeF_rphi(double Acos_val, double Asin_val, double mCos, double mSin, double P, double dphiTilde, int m) { return m*(Acos_val*mSin - Asin_val*mCos)*P*dphiTilde; } double computeAxiF_rphi(double Acos_val, double P, double d2phiTilde) { return 0.; } double computeF_phiphi(double Acos_val, double Asin_val, double mCos, double mSin, double P, double phiTilde, int m) { return m*m*(Acos_val*mCos + Asin_val*mSin)*P*phiTilde; } double computeAxiF_phiphi(double Acos_val, double P, double d2phiTilde) { return 0.; } //Calculates the Gegenbauer polynomials void compute_C(double xi, int N, int L, double * C_array) { int l; for (l = 0; l < L; l++) { gsl_sf_gegenpoly_array(N - 1, 3./2 + 2*l, xi, C_array + l*N); } } //Calculates the derivative of the Gegenbauer polynomials void compute_dC(double xi, int N, int L, double * dC_array) { int n,l; for (l = 0; l < L; l++) { *(dC_array +l*N) = 0; if (N != 1) gsl_sf_gegenpoly_array(N - 2, 5./2 + 2*l, xi, dC_array + l*N + 1); for (n = 0; n1) *(d2C_array +l*N + 1) = 0; if (N > 2) gsl_sf_gegenpoly_array(N - 3, 7./2 + 2*l, xi, d2C_array + l*N + 2); for (n = 0; nargs; //Get args double a = *args++; int isNonAxi = (int)*args++; int N = (int)*args++; int L = (int)*args++; int M = (int)*args++; double* Acos = args; double* caching_i = (args + (isNonAxi + 1)*N*L*M); double *Asin; if (isNonAxi == 1) { Asin = args + N*L*M; } double *cached_type = caching_i; double * cached_coords = (caching_i+ 1); double * cached_values = (caching_i + 4); if ((int)*cached_type==FORCE) { if (*cached_coords == R && *(cached_coords + 1) == Z && *(cached_coords + 2) == phi) { *F = *cached_values; *(F + 1) = *(cached_values + 1); *(F + 2) = *(cached_values + 2); return; } } double r; double theta; cyl_to_spher(R, Z, &r, &theta); double xi; calculateXi(r, a, &xi); //Compute the gegenbauer polynomials and its derivative. double *C= (double *) malloc ( N*L * sizeof(double) ); double *dC= (double *) malloc ( N*L * sizeof(double) ); double *phiTilde= (double *) malloc ( N*L * sizeof(double) ); double *dphiTilde= (double *) malloc ( N*L * sizeof(double) ); compute_C(xi, N, L, C); compute_dC(xi, N, L, dC); //Compute phiTilde and its derivative compute_phiTilde(r, a, N, L, C, phiTilde); compute_dphiTilde(r, a, N, L, C, dC, dphiTilde); //Compute Associated Legendre Polynomials int M_eff = M; int size = 0; if (isNonAxi==0) { M_eff = 1; size = L; } else{ size = L*L - L*(L-1)/2; } double *P= (double *) malloc ( size * sizeof(double) ); double *dP= (double *) malloc ( size * sizeof(double) ); compute_P_dP(cos(theta), L, M_eff, P, dP); double (*PhiTilde_Pointer[3]) = {dphiTilde,phiTilde,phiTilde}; double (*P_Pointer[3]) = {P, dP, P}; double Constant[3] = {1., -sin(theta), 1.}; if (isNonAxi == 1) { double (*Eq[3])(double, double, double, double, double, double, int) = {&computeF_r, &computeF_theta, &computeF_phi}; equations e = {Eq,&PhiTilde_Pointer[0], &P_Pointer[0], &Constant[0]}; computeNonAxi(a, N, L, M,r, theta, phi, Acos, Asin, 3, e, F); } else { double (*Eq[3])(double, double, double) = {&computeAxiF_r, &computeAxiF_theta, &computeAxiF_phi}; axi_equations e = {Eq,&PhiTilde_Pointer[0], &P_Pointer[0], &Constant[0]}; compute(a, N, L, M,r, theta, phi, Acos, 3, e, F); } //Caching *cached_type = (double)FORCE; * cached_coords = R; * (cached_coords + 1) = Z; * (cached_coords + 2) = phi; * (cached_values) = *F; * (cached_values + 1) = *(F + 1); * (cached_values + 2) = *(F + 2); // Free memory free(C); free(dC); free(phiTilde); free(dphiTilde); free(P); free(dP); } //Compute the Derivatives void computeDeriv(double R,double Z, double phi, double t, struct potentialArg * potentialArgs, double * F) { double * args= potentialArgs->args; //Get args double a = *args++; int isNonAxi = (int)*args++; int N = (int) *args++; int L = (int) *args++; int M = (int) *args++; double* Acos = args; double * caching_i = (args + (isNonAxi + 1)*N*L*M); double *Asin; if (isNonAxi == 1) { Asin = args + N*L*M; } double *cached_type = caching_i; double * cached_coords = (caching_i+ 1); double * cached_values = (caching_i + 4); if ((int)*cached_type==DERIV) { if (*cached_coords == R && *(cached_coords + 1) == Z && *(cached_coords + 2) == phi) { *F = *cached_values; *(F + 1) = *(cached_values + 1); *(F + 2) = *(cached_values + 2); return; } } double r; double theta; cyl_to_spher(R, Z, &r, &theta); double xi; calculateXi(r, a, &xi); //Compute the gegenbauer polynomials and its derivative. double *C= (double *) malloc ( N*L * sizeof(double) ); double *dC= (double *) malloc ( N*L * sizeof(double) ); double *d2C= (double *) malloc ( N*L * sizeof(double) ); double *phiTilde= (double *) malloc ( N*L * sizeof(double) ); double *dphiTilde= (double *) malloc ( N*L * sizeof(double) ); double *d2phiTilde= (double *) malloc ( N*L * sizeof(double) ); compute_C(xi, N, L, C); compute_dC(xi, N, L, dC); compute_d2C(xi, N, L, d2C); //Compute phiTilde and its derivative compute_phiTilde(r, a, N, L, C, phiTilde); compute_dphiTilde(r, a, N, L, C, dC, dphiTilde); compute_d2phiTilde(r, a, N, L, C, dC, d2C, d2phiTilde); //Compute Associated Legendre Polynomials int M_eff = M; int size = 0; if (isNonAxi==0) { M_eff = 1; size = L; } else{ size = L*L - L*(L-1)/2; } double *P= (double *) malloc ( size * sizeof(double) ); compute_P(cos(theta), L,M_eff, P); double (*PhiTilde_Pointer[3])= {d2phiTilde,phiTilde,dphiTilde}; double (*P_Pointer[3]) = {P, P, P}; double Constant[3] = {1., 1., 1.}; if (isNonAxi==1) { double (*Eq[3])(double, double, double, double, double, double, int) = {&computeF_rr, &computeF_phiphi, &computeF_rphi}; equations e = {Eq,&PhiTilde_Pointer[0],&P_Pointer[0],&Constant[0]}; computeNonAxi(a, N, L, M,r, theta, phi, Acos, Asin, 3, e, F); } else { double (*Eq[3])(double, double, double) = {&computeAxiF_rr, &computeAxiF_phiphi, &computeAxiF_rphi}; axi_equations e = {Eq,&PhiTilde_Pointer[0],&P_Pointer[0],&Constant[0]}; compute(a, N, L, M,r, theta, phi, Acos, 3, e, F); } //Caching *cached_type = (double)DERIV; * cached_coords = R; * (cached_coords + 1) = Z; * (cached_coords + 2) = phi; * (cached_values) = *F; * (cached_values + 1) = *(F + 1); * (cached_values + 2) = *(F + 2); //Free memory free(C); free(dC); free(d2C); free(phiTilde); free(dphiTilde); free(d2phiTilde); free(P); } //Compute the Potential double SCFPotentialEval(double R,double Z, double phi, double t, struct potentialArg * potentialArgs) { double * args= potentialArgs->args; //Get args double a = *args++; int isNonAxi = (int)*args++; int N = (int) *args++; int L = (int) *args++; int M = (int) *args++; double* Acos = args; double* Asin; if (isNonAxi==1) // LCOV_EXCL_START { Asin = args + N*L*M; } // LCOV_EXCL_STOP //convert R,Z to r, theta double r; double theta; cyl_to_spher(R, Z,&r, &theta); double xi; calculateXi(r, a, &xi); //Compute the gegenbauer polynomials and its derivative. double *C= (double *) malloc ( N*L * sizeof(double) ); double *phiTilde= (double *) malloc ( N*L * sizeof(double) ); compute_C(xi, N, L, C); //Compute phiTilde and its derivative compute_phiTilde(r, a, N, L, C, phiTilde); //Compute Associated Legendre Polynomials int M_eff = M; int size = 0; if (isNonAxi==0) { M_eff = 1; size = L; } else{ // LCOV_EXCL_START size = L*L - L*(L-1)/2; } // LCOV_EXCL_STOP double *P= (double *) malloc ( size * sizeof(double) ); compute_P(cos(theta), L,M_eff, P); double potential; double (*PhiTilde_Pointer[1]) = {phiTilde}; double (*P_Pointer[1]) = {P}; double Constant[1] = {1.}; if (isNonAxi==1) // LCOV_EXCL_START { double (*Eq[1])(double, double, double, double, double, double, int) = {&computePhi}; equations e = {Eq,&PhiTilde_Pointer[0],&P_Pointer[0],&Constant[0]}; computeNonAxi(a, N, L, M,r, theta, phi, Acos, Asin, 1, e, &potential); } // LCOV_EXCL_STOP else { double (*Eq[1])(double, double, double) = {&computeAxiPhi}; axi_equations e = {Eq,&PhiTilde_Pointer[0],&P_Pointer[0],&Constant[0]}; compute(a, N, L, M,r, theta, phi, Acos, 1, e, &potential); } //Free memory free(C); free(phiTilde); free(P); return potential; } //Compute the force in the R direction double SCFPotentialRforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs) { double r; double theta; cyl_to_spher(R, Z, &r, &theta); // The derivatives double dr_dR = R/r; double dtheta_dR = Z/(r*r); double dphi_dR = 0; double F[3]; computeForce(R, Z, phi, t,potentialArgs, &F[0]) ; return *(F + 0)*dr_dR + *(F + 1)*dtheta_dR + *(F + 2)*dphi_dR; } //Compute the force in the z direction double SCFPotentialzforce(double R,double Z, double phi, double t, struct potentialArg * potentialArgs) { double r; double theta; cyl_to_spher(R, Z,&r, &theta); double dr_dz = Z/r; double dtheta_dz = -R/(r*r); double dphi_dz = 0; double F[3]; computeForce(R, Z, phi, t,potentialArgs, &F[0]) ; return *(F + 0)*dr_dz + *(F + 1)*dtheta_dz + *(F + 2)*dphi_dz; } //Compute the force in the phi direction double SCFPotentialphitorque(double R,double Z, double phi, double t, struct potentialArg * potentialArgs) { double r; double theta; cyl_to_spher(R, Z, &r, &theta); double dr_dphi = 0; double dtheta_dphi = 0; double dphi_dphi = 1; double F[3]; computeForce(R, Z, phi, t,potentialArgs, &F[0]) ; return *(F + 0)*dr_dphi + *(F + 1)*dtheta_dphi + *(F + 2)*dphi_dphi; } //Compute the planar force in the R direction double SCFPotentialPlanarRforce(double R,double phi, double t, struct potentialArg * potentialArgs) { return SCFPotentialRforce(R,0., phi,t,potentialArgs); } //Compute the planar force in the phi direction double SCFPotentialPlanarphitorque(double R,double phi, double t, struct potentialArg * potentialArgs) { return SCFPotentialphitorque(R,0., phi,t,potentialArgs); } //Compute the planar double derivative of the potential with respect to R double SCFPotentialPlanarR2deriv(double R, double phi, double t, struct potentialArg * potentialArgs) { double Farray[3]; computeDeriv(R, 0, phi, t,potentialArgs, &Farray[0]) ; return *Farray; } //Compute the planar double derivative of the potential with respect to phi double SCFPotentialPlanarphi2deriv(double R, double phi, double t, struct potentialArg * potentialArgs) { double Farray[3]; computeDeriv(R, 0, phi, t,potentialArgs, &Farray[0]) ; return *(Farray + 1); } //Compute the planar double derivative of the potential with respect to R, Phi double SCFPotentialPlanarRphideriv(double R, double phi, double t, struct potentialArg * potentialArgs) { double Farray[3]; computeDeriv(R, 0, phi, t,potentialArgs, &Farray[0]) ; return *(Farray + 2); } //Compute the density double SCFPotentialDens(double R,double Z, double phi, double t, struct potentialArg * potentialArgs) { double * args= potentialArgs->args; //Get args double a = *args++; int isNonAxi = (int)*args++; int N = (int) *args++; int L = (int) *args++; int M = (int) *args++; double* Acos = args; double* Asin; if (isNonAxi==1) { Asin = args + N*L*M; } //convert R,Z to r, theta double r; double theta; cyl_to_spher(R, Z,&r, &theta); double xi; calculateXi(r, a, &xi); //Compute the gegenbauer polynomials and its derivative. double *C= (double *) malloc ( N*L * sizeof(double) ); double *rhoTilde= (double *) malloc ( N*L * sizeof(double) ); compute_C(xi, N, L, C); //Compute rhoTilde and its derivative compute_rhoTilde(r, a, N, L, C, rhoTilde); //Compute Associated Legendre Polynomials int M_eff = M; int size = 0; if (isNonAxi==0) { M_eff = 1; size = L; } else size = L*L - L*(L-1)/2; double *P= (double *) malloc ( size * sizeof(double) ); compute_P(cos(theta), L,M_eff, P); double density; double (*RhoTilde_Pointer[1]) = {rhoTilde}; double (*P_Pointer[1]) = {P}; double Constant[1] = {1.}; if (isNonAxi==1) { double (*Eq[1])(double, double, double, double, double, double, int) = {&computePhi}; equations e = {Eq,&RhoTilde_Pointer[0],&P_Pointer[0],&Constant[0]}; computeNonAxi(a, N, L, M,r, theta, phi, Acos, Asin, 1, e, &density); } else { double (*Eq[1])(double, double, double) = {&computeAxiPhi}; axi_equations e = {Eq,&RhoTilde_Pointer[0],&P_Pointer[0],&Constant[0]}; compute(a, N, L, M,r, theta, phi, Acos, 1, e, &density); } //Free memory free(C); free(rhoTilde); free(P); return density / 2. / M_PI; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/SoftenedNeedleBarPotential.c0000644000175100001710000001062314327773303026100 0ustar00runnerdocker#include #include #include //SoftenedNeedleBarPotentials static inline void compute_TpTm(double x, double y, double z, double *Tp, double *Tm, double a, double b, double c2){ double secondpart= y * y + pow( b + sqrt ( z * z + c2 ) , 2); *Tp= sqrt ( pow ( a + x , 2) + secondpart ); *Tm= sqrt ( pow ( a - x , 2) + secondpart ); } double SoftenedNeedleBarPotentialEval(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args: amp, a, b, c2, pa, omegab double amp= *args++; double a= *args++; double b= *args++; double c2= *args++; double pa= *args++; double omegab= *args++; double x,y; double Tp,Tm; //Calculate potential cyl_to_rect(R,phi-pa-omegab*t,&x,&y); compute_TpTm(x,y,z,&Tp,&Tm,a,b,c2); return 0.5 * amp * log( ( x - a + Tm ) / ( x + a + Tp ) ) / a; } void SoftenedNeedleBarPotentialxyzforces_xyz(double R,double z, double phi, double t,double * args, double a,double b, double c2, double pa, double omegab, double cached_R, double cached_z, double cached_phi, double cached_t){ double x,y; double Tp,Tm; double Fx, Fy, Fz; double zc; double cp, sp; if ( R != cached_R || phi != cached_phi || z != cached_z || t != cached_t){ // Set up cache *args= R; *(args + 1)= z; *(args + 2)= phi; *(args + 3)= t; // Compute forces in rectangular, aligned frame cyl_to_rect(R,phi-pa-omegab*t,&x,&y); compute_TpTm(x,y,z,&Tp,&Tm,a,b,c2); Fx= -2. * x / Tp / Tm / (Tp+Tm); Fy= -y / 2. / Tp /Tm * ( Tp + Tm -4. * x * x / (Tp+Tm) ) \ / ( y * y + pow( b + sqrt ( z * z + c2), 2)); zc= sqrt ( z * z + c2 ); Fz= Fy * z / y * ( b + zc ) / zc; cp= cos ( pa + omegab * t ); sp= sin ( pa + omegab * t ); // Rotate to rectangular, correct frame *(args + 4)= cp * Fx - sp * Fy; *(args + 5)= sp * Fx + cp * Fy; *(args + 6)= Fz; } } double SoftenedNeedleBarPotentialRforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args: amp, a, b, c2, pa, omegab double amp= *args++; double a= *args++; double b= *args++; double c2= *args++; double pa= *args++; double omegab= *args++; double cached_R= *args; double cached_z= *(args + 1); double cached_phi= *(args + 2); double cached_t= *(args + 3); //Calculate potential SoftenedNeedleBarPotentialxyzforces_xyz(R,z,phi,t,args,a,b,c2,pa,omegab, cached_R,cached_z, cached_phi,cached_t); return amp * ( cos ( phi ) * *(args + 4) + sin( phi ) * *(args + 5) ); } double SoftenedNeedleBarPotentialPlanarRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ return SoftenedNeedleBarPotentialRforce(R,0.,phi,t,potentialArgs); } double SoftenedNeedleBarPotentialphitorque(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args: amp, a, b, c2, pa, omegab double amp= *args++; double a= *args++; double b= *args++; double c2= *args++; double pa= *args++; double omegab= *args++; double cached_R= *args; double cached_z= *(args + 1); double cached_phi= *(args + 2); double cached_t= *(args + 3); //Calculate potential SoftenedNeedleBarPotentialxyzforces_xyz(R,z,phi,t,args,a,b,c2,pa,omegab, cached_R,cached_z, cached_phi,cached_t); return amp * R * ( -sin ( phi ) * *(args + 4) + cos( phi ) * *(args + 5) ); } double SoftenedNeedleBarPotentialPlanarphitorque(double R, double phi,double t, struct potentialArg * potentialArgs){ return SoftenedNeedleBarPotentialphitorque(R,0.,phi,t,potentialArgs); } double SoftenedNeedleBarPotentialzforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args: amp, a, b, c2, pa, omegab double amp= *args++; double a= *args++; double b= *args++; double c2= *args++; double pa= *args++; double omegab= *args++; double cached_R= *args; double cached_z= *(args + 1); double cached_phi= *(args + 2); double cached_t= *(args + 3); //Calculate potential SoftenedNeedleBarPotentialxyzforces_xyz(R,z,phi,t,args,a,b,c2,pa,omegab, cached_R,cached_z, cached_phi,cached_t); return amp * *(args + 6); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/SolidBodyRotationWrapperPotential.c0000644000175100001710000000541314327773303027541 0ustar00runnerdocker#include #include #include //SolidBodyRotationWrapperPotential double SolidBodyRotationWrapperPotentialRforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rforce return *args * calcRforce(R,z,phi - *(args+1) * t - *(args+2),t, potentialArgs->nwrapped,potentialArgs->wrappedPotentialArg); } double SolidBodyRotationWrapperPotentialphitorque(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phitorque return *args * calcphitorque(R,z,phi - *(args+1) * t - *(args+2),t, potentialArgs->nwrapped,potentialArgs->wrappedPotentialArg); } double SolidBodyRotationWrapperPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate zforce return *args * calczforce(R,z,phi - *(args+1) * t - *(args+2),t, potentialArgs->nwrapped,potentialArgs->wrappedPotentialArg); } double SolidBodyRotationWrapperPotentialPlanarRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rforce return *args * calcPlanarRforce(R,phi - *(args+1) * t - *(args+2),t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double SolidBodyRotationWrapperPotentialPlanarphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phitorque return *args * calcPlanarphitorque(R,phi - *(args+1) * t - *(args+2),t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double SolidBodyRotationWrapperPotentialPlanarR2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate R2deriv return *args * calcPlanarR2deriv(R,phi - *(args+1) * t - *(args+2),t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double SolidBodyRotationWrapperPotentialPlanarphi2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phi2deriv return *args * calcPlanarphi2deriv(R,phi - *(args+1) * t - *(args+2),t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double SolidBodyRotationWrapperPotentialPlanarRphideriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rphideriv return *args * calcPlanarRphideriv(R,phi - *(args+1) * t - *(args+2),t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/SphericalPotential.c0000644000175100001710000000426014327773303024501 0ustar00runnerdocker#include #include //General routines for SphericalPotentials double SphericalPotentialEval(double R,double z,double phi,double t, struct potentialArg * potentialArgs){ //Get args double * args= potentialArgs->args; double amp= *args; //Calculate potential double r= sqrt(R*R+z*z); return amp * potentialArgs->revaluate(r,t,potentialArgs); } double SphericalPotentialRforce(double R,double z,double phi,double t, struct potentialArg * potentialArgs){ //Get args double * args= potentialArgs->args; double amp= *args; //Calculate Rforce double r= sqrt(R*R+z*z); return amp * potentialArgs->rforce(r,t,potentialArgs)*R/r; } double SphericalPotentialPlanarRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ //Get args double * args= potentialArgs->args; double amp= *args; //Calculate planar Rforce return amp * potentialArgs->rforce(R,t,potentialArgs); } double SphericalPotentialzforce(double R,double z,double phi,double t, struct potentialArg * potentialArgs){ //Get args double * args= potentialArgs->args; double amp= *args; //Calculate zforce double r= sqrt(R*R+z*z); return amp * potentialArgs->rforce(r,t,potentialArgs)*z/r; } double SphericalPotentialPlanarR2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ //Get args double * args= potentialArgs->args; double amp= *args; //Calculate planar R2deriv return amp * potentialArgs->r2deriv(R,t,potentialArgs); } double SphericalPotentialDens(double R,double z,double phi,double t, struct potentialArg * potentialArgs){ //Get args double * args= potentialArgs->args; double amp= *args; //Calculate density through the Poisson equation double r= sqrt(R*R+z*z); /* Uncomment next few commented-out lines if you ever want to automatically use the Poisson equation to calculate the density rather than implement rdens */ // if ( potentialArgs->rdens ) return amp * potentialArgs->rdens(r,t,potentialArgs); // else // return amp * M_1_PI / 4. * ( potentialArgs->r2deriv(r,t,potentialArgs) // - 2. * potentialArgs->rforce(r,t,potentialArgs)/r); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/SpiralArmsPotential.c0000644000175100001710000006653514327773303024661 0ustar00runnerdocker#include #include double gam(double R, double phi, double N, double phi_ref, double r_ref, double tan_alpha); double dgam_dR(double R, double N, double tan_alpha); double K(double R, double n, double N, double sin_alpha); double B(double R, double H, double n, double N, double sin_alpha); double D(double R, double H, double n, double N, double sin_alpha); double dK_dR(double R, double n, double N, double sin_alpha); double dB_dR(double R, double H, double n, double N, double sin_alpha); double dD_dR(double R, double H, double n, double N, double sin_alpha); // LCOV_EXCL_START double SpiralArmsPotentialEval(double R, double z, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); // Return value of the potential. double sum = 0; int n; double Cn; double Kn; double Bn; double Dn; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Bn = B(R, H, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); sum += Cn / Kn / Dn * cos(n * g) / pow(cosh(Kn * z / Bn), Bn); } return -amp * H * exp(-(R - r_ref) / Rs) * sum; } // LCOV_EXCL_STOP double SpiralArmsPotentialRforce(double R, double z, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); double dg_dR = dgam_dR(R, N, tan_alpha); // Return the Rforce (-dPhi / dR) double sum = 0; int n; double Cn; double Kn; double Bn; double Dn; double dKn_dR; double dBn_dR; double dDn_dR; double cos_ng; double sin_ng; double zKB; double sechzKB; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Bn = B(R, H, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); dKn_dR = dK_dR(R, n, N, sin_alpha); dBn_dR = dB_dR(R, H, n, N, sin_alpha); dDn_dR = dD_dR(R, H, n, N, sin_alpha); cos_ng = cos(n * g); sin_ng = sin(n * g); zKB = z * Kn / Bn; sechzKB = 1 / cosh(zKB); sum += Cn * pow(sechzKB, Bn) / Dn * ((n * dg_dR / Kn * sin_ng + cos_ng * (z * tanh(zKB) * (dKn_dR / Kn - dBn_dR / Bn) - dBn_dR / Kn * log(sechzKB) + dKn_dR / Kn / Kn + dDn_dR / Dn / Kn)) + cos_ng / Kn / Rs); } return -amp * H * exp(-(R - r_ref) / Rs) * sum; } double SpiralArmsPotentialzforce(double R, double z, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); // Return the zforce (-dPhi / dz) double sum = 0; int n; double Cn; double Kn; double Bn; double Dn; double zKn_Bn; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Bn = B(R, H, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); zKn_Bn = z * Kn / Bn; sum += Cn / Dn * cos(n * g) * tanh(zKn_Bn) / pow(cosh(zKn_Bn), Bn); } return -amp * H * exp(-(R - r_ref) / Rs) * sum; } double SpiralArmsPotentialphitorque(double R, double z, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); // Return the phitorque (-dPhi / dphi) double sum = 0; int n; double Cn; double Kn; double Bn; double Dn; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Bn = B(R, H, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); sum += N * n * Cn / Dn / Kn / pow(cosh(z * Kn / Bn), Bn) * sin(n * g); } return -amp * H * exp(-(R - r_ref) / Rs) * sum; } // LCOV_EXCL_START double SpiralArmsPotentialR2deriv(double R, double z, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); double dg_dR = dgam_dR(R, N, tan_alpha); double d2g_dR2 = N / R / R / tan_alpha; // Return the second derivative of the potential wrt R (d2Phi / dR2) double sum = 0; int n; double Cn; double Kn; double Bn; double Dn; double dKn_dR; double dBn_dR; double dDn_dR; double HNn; double HNn_R; double R_sina; double HNn_R_sina; double HNn_R_sina_2; double x; double d2Kn_dR2; double d2Bn_dR2; double d2Dn_dR2; double cos_ng; double sin_ng; double zKB; double sechzKB; double sechzKB_B; double log_sechzKB; double tanhzKB; double ztanhzKB; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Bn = B(R, H, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); dKn_dR = dK_dR(R, n, N, sin_alpha); dBn_dR = dB_dR(R, H, n, N, sin_alpha); dDn_dR = dD_dR(R, H, n, N, sin_alpha); HNn = H * N * n; HNn_R = HNn / R; R_sina = R * sin_alpha; HNn_R_sina = HNn / R_sina; HNn_R_sina_2 = HNn_R_sina * HNn_R_sina; x = R * (0.3 * HNn_R_sina + 1) * sin_alpha; d2Kn_dR2 = 2 * N * n / R / R / R / sin_alpha; d2Bn_dR2 = HNn_R / R / R / sin_alpha * (2.4 * HNn_R / sin_alpha + 2); d2Dn_dR2 = sin_alpha / R / x * (HNn * (0.18 * HNn * (HNn_R_sina + 0.3 * HNn_R_sina_2 + 1) / x / x + 2 / R_sina - 0.6 * HNn_R_sina * (1 + 0.6 * HNn_R_sina) / x - 0.6 * (HNn_R_sina + 0.3 * HNn_R_sina_2 + 1) / x + 1.8 * HNn / R_sina / R_sina)); cos_ng = cos(n * g); sin_ng = sin(n * g); zKB = z * Kn / Bn; sechzKB = 1 / cosh(zKB); sechzKB_B = pow(sechzKB, Bn); log_sechzKB = log(sechzKB); tanhzKB = tanh(zKB); ztanhzKB = z * tanhzKB; sum += (Cn * sechzKB_B / Dn * ((n * dg_dR / Kn * sin_ng + cos_ng * (ztanhzKB * (dKn_dR / Kn - dBn_dR / Bn) - dBn_dR / Kn * log_sechzKB + dKn_dR / Kn / Kn + dDn_dR / Dn / Kn)) - (Rs * (1 / Kn * ((ztanhzKB * (dBn_dR / Bn * Kn - dKn_dR) + log_sechzKB * dBn_dR) - dDn_dR / Dn) * (n * dg_dR * sin_ng + cos_ng * (ztanhzKB * Kn * (dKn_dR / Kn - dBn_dR / Bn) - dBn_dR * log_sechzKB + dKn_dR / Kn / Kn + dDn_dR / Dn)) + (n * (sin_ng * (d2g_dR2 / Kn - dg_dR / Kn / Kn * dKn_dR) + dg_dR * dg_dR / Kn * cos_ng * n) + z * (-sin_ng * n * dg_dR * tanhzKB * (dKn_dR / Kn - dBn_dR / Bn) + cos_ng * (z * (dKn_dR / Bn - dBn_dR / Bn / Bn * Kn) * (1 - tanhzKB * tanhzKB) * (dKn_dR / Kn - dBn_dR / Bn) + tanhzKB * (d2Kn_dR2 / Kn - (dKn_dR / Kn) * (dKn_dR / Kn) - d2Bn_dR2 / Bn + (dBn_dR / Bn) * (dBn_dR / Bn)))) + (cos_ng * (dBn_dR / Kn * ztanhzKB * (dKn_dR / Bn - dBn_dR / Bn / Bn * Kn) - (d2Bn_dR2 / Kn - dBn_dR * dKn_dR / Kn / Kn) * log_sechzKB) + dBn_dR / Kn * log_sechzKB * sin_ng * n * dg_dR) + ((cos_ng * (d2Kn_dR2 / Kn / Kn - 2 * dKn_dR * dKn_dR / Kn / Kn / Kn) - dKn_dR / Kn / Kn * sin_ng * n * dg_dR) + (cos_ng * (d2Dn_dR2 / Dn / Kn - (dDn_dR / Dn) * (dDn_dR / Dn) / Kn - dDn_dR / Dn / Kn / Kn * dKn_dR) - sin_ng * n * dg_dR * dDn_dR / Dn / Kn)))) - 1 / Kn * (cos_ng / Rs + (cos_ng * ((dDn_dR * Kn + Dn * dKn_dR) / (Dn * Kn) - (ztanhzKB * (dBn_dR / Bn * Kn - dKn_dR) + log_sechzKB * dBn_dR)) + sin_ng * n * dg_dR))))); } return -amp * H * exp(-(R - r_ref) / Rs) / Rs * sum; } // LCOV_EXCL_STOP // LCOV_EXCL_START double SpiralArmsPotentialz2deriv(double R, double z, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); // Return the second derivative of the potential wrt z (d2Phi / dz2) double sum = 0; int n; double Cn; double Kn; double Bn; double Dn; double zKB; double tanh2_zKB; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Bn = B(R, H, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); zKB = z * Kn / Bn; tanh2_zKB = tanh(zKB) * tanh(zKB); sum += Cn * Kn / Dn * ((tanh2_zKB - 1) / Bn + tanh2_zKB) * cos(n * g) / pow(cosh(zKB), Bn); } return -amp * H * exp(-(R - r_ref) / Rs) * sum; } // LCOV_EXCL_STOP // LCOV_EXCL_START double SpiralArmsPotentialphi2deriv(double R, double z, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); // Return the second derivative of the potential wrt phi (d2Phi / dphi2) double sum = 0; int n; double Cn; double Kn; double Bn; double Dn; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Bn = B(R, H, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); sum += Cn * N * N * n * n / Dn / Kn / pow(cosh(z * Kn / Bn), Bn) * cos(n * g); } return amp * H * exp(-(R - r_ref) / Rs) * sum; } // LCOV_EXCL_STOP // LCOV_EXCL_START double SpiralArmsPotentialRzderiv(double R, double z, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); double dg_dR = dgam_dR(R, N, tan_alpha); // Return the mixed (cylindrical) radial and vertical derivative of the potential (d^2 potential / dR dz). double sum = 0; int n; double Cn; double Kn; double Bn; double Dn; double dKn_dR; double dBn_dR; double dDn_dR; double cos_ng; double sin_ng; double zKB; double sechzKB; double sechzKB_B; double log_sechzKB; double tanhzKB; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Bn = B(R, H, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); dKn_dR = dK_dR(R, n, N, sin_alpha); dBn_dR = dB_dR(R, H, n, N, sin_alpha); dDn_dR = dD_dR(R, H, n, N, sin_alpha); cos_ng = cos(n * g); sin_ng = sin(n * g); zKB = z * Kn / Bn; sechzKB = 1 / cosh(zKB); sechzKB_B = pow(sechzKB, Bn); log_sechzKB = log(sechzKB); tanhzKB = tanh(zKB); sum += sechzKB_B * Cn / Dn * (Kn * tanhzKB * (n * dg_dR / Kn * sin_ng + cos_ng * (z * tanhzKB * (dKn_dR / Kn - dBn_dR / Bn) - dBn_dR / Kn * log_sechzKB + dKn_dR / Kn / Kn + dDn_dR / Dn / Kn)) - cos_ng * ((zKB * (dKn_dR / Kn - dBn_dR / Bn) * (1 - tanhzKB * tanhzKB) + tanhzKB * (dKn_dR / Kn - dBn_dR / Bn) + dBn_dR / Bn * tanhzKB) - tanhzKB / Rs)); } return -amp * H * exp(-(R - r_ref) / Rs) * sum; } // LCOV_EXCL_STOP // LCOV_EXCL_START double SpiralArmsPotentialRphideriv(double R, double z, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); double dg_dR = dgam_dR(R, N, tan_alpha); // Return the mixed (cylindrical) radial and azimuthal derivative of the potential (d^2 potential / dR dphi). double sum = 0; int n; double Cn; double Kn; double Bn; double Dn; double dKn_dR; double dBn_dR; double dDn_dR; double cos_ng; double sin_ng; double zKB; double sechzKB; double sechzKB_B; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Bn = B(R, H, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); dKn_dR = dK_dR(R, n, N, sin_alpha); dBn_dR = dB_dR(R, H, n, N, sin_alpha); dDn_dR = dD_dR(R, H, n, N, sin_alpha); cos_ng = cos(n * g); sin_ng = sin(n * g); zKB = z * Kn / Bn; sechzKB = 1 / cosh(zKB); sechzKB_B = pow(sechzKB, Bn); sum += Cn * sechzKB_B / Dn * n * N * (-n * dg_dR / Kn * cos_ng + sin_ng * (z * tanh(zKB) * (dKn_dR / Kn - dBn_dR / Bn) + 1 / Kn * (-dBn_dR * log(sechzKB) + dKn_dR / Kn + dDn_dR / Dn + 1 / Rs))); } return -amp * H * exp(-(R - r_ref) / Rs) * sum; } // LCOV_EXCL_STOP double SpiralArmsPotentialPlanarRforce(double R, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); double dg_dR = dgam_dR(R, N, tan_alpha); // Return the planar Rforce (-dPhi / dR) double sum = 0; int n; double Cn; double Kn; double Dn; double dKn_dR; double dDn_dR; double cos_ng; double sin_ng; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); dKn_dR = dK_dR(R, n, N, sin_alpha); dDn_dR = dD_dR(R, H, n, N, sin_alpha); cos_ng = cos(n * g); sin_ng = sin(n * g); sum += Cn / Dn * ((n * dg_dR / Kn * sin_ng + cos_ng * (dKn_dR / Kn / Kn + dDn_dR / Dn / Kn)) + cos_ng / Kn / Rs); } return -amp * H * exp(-(R - r_ref) / Rs) * sum; } double SpiralArmsPotentialPlanarphitorque(double R, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); // Return the planar phitorque (-dPhi / dphi) double sum = 0; int n; double Cn; double Kn; double Dn; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); sum += N * n * Cn / Dn / Kn * sin(n * g); } return -amp * H * exp(-(R - r_ref) / Rs) * sum; } double SpiralArmsPotentialPlanarR2deriv(double R, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); double dg_dR = dgam_dR(R, N, tan_alpha); double d2g_dR2 = N / R / R / tan_alpha; // Return the planar second derivative of the potential wrt R (d2Phi / dR2) double sum = 0; int n; double Cn; double Kn; double Dn; double dKn_dR; double dDn_dR; double HNn; double R_sina; double HNn_R_sina; double HNn_R_sina_2; double x; double d2Kn_dR2; double d2Dn_dR2; double cos_ng; double sin_ng; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); dKn_dR = dK_dR(R, n, N, sin_alpha); dDn_dR = dD_dR(R, H, n, N, sin_alpha); HNn = H * N * n; R_sina = R * sin_alpha; HNn_R_sina = HNn / R_sina; HNn_R_sina_2 = HNn_R_sina * HNn_R_sina; x = R * (0.3 * HNn_R_sina + 1) * sin_alpha; d2Kn_dR2 = 2 * N * n / R / R / R / sin_alpha; d2Dn_dR2 = sin_alpha / R / x * (HNn * (0.18 * HNn * (HNn_R_sina + 0.3 * HNn_R_sina_2 + 1) / x / x + 2 / R_sina - 0.6 * HNn_R_sina * (1 + 0.6 * HNn_R_sina) / x - 0.6 * (HNn_R_sina + 0.3 * HNn_R_sina_2 + 1) / x + 1.8 * HNn / R_sina / R_sina)); cos_ng = cos(n * g); sin_ng = sin(n * g); sum += (Cn / Dn * ((n * dg_dR / Kn * sin_ng + cos_ng * (dKn_dR / Kn / Kn + dDn_dR / Dn / Kn)) - (Rs * (1 / Kn * (-dDn_dR / Dn) * (n * dg_dR * sin_ng + cos_ng * (dKn_dR / Kn / Kn + dDn_dR / Dn)) + (n * (sin_ng * (d2g_dR2 / Kn - dg_dR / Kn / Kn * dKn_dR) + dg_dR * dg_dR / Kn * cos_ng * n) + ((cos_ng * (d2Kn_dR2 / Kn / Kn - 2 * dKn_dR * dKn_dR / Kn / Kn / Kn) - dKn_dR / Kn / Kn * sin_ng * n * dg_dR) + (cos_ng * (d2Dn_dR2 / Dn / Kn - (dDn_dR / Dn) * (dDn_dR / Dn) / Kn - dDn_dR / Dn / Kn / Kn * dKn_dR) - sin_ng * n * dg_dR * dDn_dR / Dn / Kn)))) - 1 / Kn * (cos_ng / Rs + (cos_ng * ((dDn_dR * Kn + Dn * dKn_dR) / (Dn * Kn)) + sin_ng * n * dg_dR))))); } return -amp * H * exp(-(R - r_ref) / Rs) / Rs * sum; } double SpiralArmsPotentialPlanarphi2deriv(double R, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); // Return the second derivative of the potential wrt phi (d2Phi / dphi2) double sum = 0; int n; double Cn; double Kn; double Dn; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); sum += Cn * N * N * n * n / Dn / Kn * cos(n * g); } return amp * H * exp(-(R - r_ref) / Rs) * sum; } double SpiralArmsPotentialPlanarRphideriv(double R, double phi, double t, struct potentialArg *potentialArgs) { // Get args double *args = potentialArgs->args; int nCs = (int) *args++; double amp = *args++; double N = *args++; double sin_alpha = *args++; double tan_alpha = *args++; double r_ref = *args++; double phi_ref = *args++; double Rs = *args++; double H = *args++; double omega = *args++; double g = gam(R, phi-omega*t, N, phi_ref, r_ref, tan_alpha); double dg_dR = dgam_dR(R, N, tan_alpha); // Return the mixed (cylindrical) radial and azimuthal derivative of the potential (d^2 potential / dR dphi). double sum = 0; int n; double Cn; double Kn; double Dn; double dKn_dR; double dDn_dR; double cos_ng; double sin_ng; for (n = 1; n <= nCs; n++) { Cn = *args++; Kn = K(R, n, N, sin_alpha); Dn = D(R, H, n, N, sin_alpha); dKn_dR = dK_dR(R, n, N, sin_alpha); dDn_dR = dD_dR(R, H, n, N, sin_alpha); cos_ng = cos(n * g); sin_ng = sin(n * g); sum += Cn / Dn * n * N * (-n * dg_dR / Kn * cos_ng + sin_ng * (1 / Kn * (dKn_dR / Kn + dDn_dR / Dn + 1 / Rs))); } return -amp * H * exp(-(R - r_ref) / Rs) * sum; } double gam(double R, double phi, double N, double phi_ref, double r_ref, double tan_alpha) { return N * (phi - phi_ref - log(R / r_ref) / tan_alpha); } double dgam_dR(double R, double N, double tan_alpha) { return -N / R / tan_alpha; } double K(double R, double n, double N, double sin_alpha) { return n * N / R / sin_alpha; } double B(double R, double H, double n, double N, double sin_alpha) { double HNn_R = H * N * n / R; return HNn_R / sin_alpha * (0.4 * HNn_R / sin_alpha + 1); } double D(double R, double H, double n, double N, double sin_alpha) { double HNn = H * N * n; return (0.3 * HNn * HNn / sin_alpha / R + HNn + R * sin_alpha) / (0.3 * HNn + R * sin_alpha); } double dK_dR(double R, double n, double N, double sin_alpha) { return -n * N / (R * R) / sin_alpha; } double dB_dR(double R, double H, double n, double N, double sin_alpha) { double HNn = H * N * n; return -HNn / R / R / R / sin_alpha / sin_alpha * (0.8 * HNn + R * sin_alpha); } double dD_dR(double R, double H, double n, double N, double sin_alpha) { double HNn_R_sina = H * N * n / R / sin_alpha; return HNn_R_sina * (0.3 * (HNn_R_sina + 0.3 * HNn_R_sina * HNn_R_sina + 1) / R / pow((0.3 * HNn_R_sina + 1), 2) - (1 / R * (1 + 0.6 * HNn_R_sina) / (0.3 * HNn_R_sina + 1))); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/SteadyLogSpiralPotential.c0000644000175100001710000000306614327773303025640 0ustar00runnerdocker#include #include //SteadyLogSpiralPotential // double dehnenSpiralSmooth(double t,double tform, double tsteady){ double smooth, xi,deltat; //Slightly different smoothing if ( ! isnan(tform) ) if ( t < tform ) smooth= 0.; else if ( t < tsteady ) { deltat= t-tform; xi= 2.*deltat/(tsteady-tform)-1.; smooth= (3./16.*pow(xi,5.)-5./8.*pow(xi,3.)+15./16.*xi+.5); } else smooth= 1.; else smooth=1.; return smooth; } double SteadyLogSpiralPotentialRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double A= *args++; double alpha= *args++; double m= *args++; double omegas= *args++; double gamma= *args++; //Calculate Rforce smooth= dehnenSpiralSmooth(t,tform,tsteady); return amp * smooth * A / R * sin(alpha * log(R) - m * (phi-omegas*t-gamma)); } double SteadyLogSpiralPotentialphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //declare double smooth; //Get args double amp= *args++; double tform= *args++; double tsteady= *args++; double A= *args++; double alpha= *args++; double m= *args++; double omegas= *args++; double gamma= *args++; //Calculate Rforce smooth= dehnenSpiralSmooth(t,tform,tsteady); return -amp * smooth * A / alpha * m * sin(alpha * log(R) - m * (phi-omegas*t-gamma)); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/TimeDependentAmplitudeWrapperPotential.c0000644000175100001710000000663314327773303030530 0ustar00runnerdocker#include //TimeDependentAmplitudeWrapperPotential: 1 argument, 1 tfunc double TimeDependentAmplitudeWrapperPotentialEval(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate potential, only used in actionAngle, so phi=0, t=0 return *args * (*(*(potentialArgs->tfuncs)))(t) \ * evaluatePotentials(R,z,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double TimeDependentAmplitudeWrapperPotentialRforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rforce return *args * (*(*(potentialArgs->tfuncs)))(t) \ * calcRforce(R,z,phi,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double TimeDependentAmplitudeWrapperPotentialphitorque(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phitorque return *args * (*(*(potentialArgs->tfuncs)))(t) \ * calcphitorque(R,z,phi,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double TimeDependentAmplitudeWrapperPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate zforce return *args * (*(*(potentialArgs->tfuncs)))(t) \ * calczforce(R,z,phi,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double TimeDependentAmplitudeWrapperPotentialPlanarRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rforce return *args * (*(*(potentialArgs->tfuncs)))(t) \ * calcPlanarRforce(R,phi,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double TimeDependentAmplitudeWrapperPotentialPlanarphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phitorque return *args * (*(*(potentialArgs->tfuncs)))(t) \ * calcPlanarphitorque(R,phi,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double TimeDependentAmplitudeWrapperPotentialPlanarR2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate R2deriv return *args * (*(*(potentialArgs->tfuncs)))(t) \ * calcPlanarR2deriv(R,phi,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double TimeDependentAmplitudeWrapperPotentialPlanarphi2deriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate phi2deriv return *args * (*(*(potentialArgs->tfuncs)))(t) \ * calcPlanarphi2deriv(R,phi,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } double TimeDependentAmplitudeWrapperPotentialPlanarRphideriv(double R,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Calculate Rphideriv return *args * (*(*(potentialArgs->tfuncs)))(t) \ * calcPlanarRphideriv(R,phi,t,potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/TransientLogSpiralPotential.c0000644000175100001710000000203614327773303026352 0ustar00runnerdocker#include #include //TransientLogSpiralPotential // double TransientLogSpiralPotentialRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double A= *args++; double to= *args++; double sigma2= *args++; double alpha= *args++; double m= *args++; double omegas= *args++; double gamma= *args++; //Calculate Rforce return amp * A * exp(-pow(t-to,2.)/2./sigma2) / R * sin(alpha*log(R)-m*(phi-omegas*t-gamma)); } double TransientLogSpiralPotentialphitorque(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double amp= *args++; double A= *args++; double to= *args++; double sigma2= *args++; double alpha= *args++; double m= *args++; double omegas= *args++; double gamma= *args++; //Calculate phitorque return -amp * A * exp(-pow(t-to,2.)/2./sigma2) / alpha * m * sin(alpha*log(R)-m*(phi-omegas*t-gamma)); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/TriaxialGaussianPotential.c0000644000175100001710000000115614327773303026040 0ustar00runnerdocker#include #include // Special case of EllipsoidalPotential, only need to define three functions double TriaxialGaussianPotentialpsi(double m,double * args){ double minustwosigma2= *args; return minustwosigma2 * exp ( m * m / minustwosigma2 ); } double TriaxialGaussianPotentialmdens(double m,double * args){ double minustwosigma2= *args; return exp ( m * m / minustwosigma2 ); } // LCOV_EXCL_START double TriaxialGaussianPotentialmdensDeriv(double m,double * args){ double minustwosigma2= *args; return 2. * m / minustwosigma2 * exp ( m * m / minustwosigma2 ); } // LCOV_EXCL_STOP ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/TwoPowerTriaxialPotential.c0000644000175100001710000000371514327773303026057 0ustar00runnerdocker#include #include #include //TriaxialHernquistPotential // Special case of EllipsoidalPotential, only need to define three functions double TriaxialHernquistPotentialpsi(double m,double * args){ double a= *args++; double a4= *args; double mpa= m+a; return - a4 / mpa / mpa; } double TriaxialHernquistPotentialmdens(double m,double * args){ double a= *args++; double a4= *args; double mpa= m+a; return a4 * pow ( mpa , -3 ) / m; } // LCOV_EXCL_START double TriaxialHernquistPotentialmdensDeriv(double m,double * args){ double a= *args++; double a4= *args; double mpa= m+a; return - a4 * ( a + 4. * m ) / m / m * pow ( mpa , -4 ) ; } // LCOV_EXCL_STOP //TriaxialJaffePotential // Special case of EllipsoidalPotential, only need to define three functions double TriaxialJaffePotentialpsi(double m,double * args){ double a= *args++; double a2= *args; double mpa= m+a; return 2. * a2 * ( a / mpa + log ( m / mpa ) ); } double TriaxialJaffePotentialmdens(double m,double * args){ double a= *args++; double a2= *args; double mpa= m+a; return a2 * a2 * pow ( m * mpa , -2 ); } // LCOV_EXCL_START double TriaxialJaffePotentialmdensDeriv(double m,double * args){ double a= *args++; double a2= *args; double mpa= m+a; return - 2. * a2 * a2 * ( a + 2. * m ) * pow ( m * mpa , -3 ); } // LCOV_EXCL_STOP //TriaxialNFWPotential // Special case of EllipsoidalPotential, only need to define three functions double TriaxialNFWPotentialpsi(double m,double * args){ double a= *args++; double a3= *args; double mpa= m+a; return - 2. * a3 / mpa; } double TriaxialNFWPotentialmdens(double m,double * args){ double a= *args++; double a3= *args; double mpa= m+a; return a3 / m / mpa / mpa; } // LCOV_EXCL_START double TriaxialNFWPotentialmdensDeriv(double m,double * args){ double a= *args++; double a3= *args; double mpa= m+a; return - a3 * ( a + 3. * m ) / m / m * pow ( mpa , -3 ) ; } // LCOV_EXCL_STOP ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/ZeroPotential.c0000644000175100001710000000043014327773303023501 0ustar00runnerdocker#include //Dummy zero forces double ZeroPlanarForce(double R, double phi,double t, struct potentialArg * potentialArgs){ return 0.; } double ZeroForce(double R, double Z, double phi,double t, struct potentialArg * potentialArgs){ return 0.; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/galpy_potentials.c0000644000175100001710000001762714327773303024300 0ustar00runnerdocker#include void init_potentialArgs(int npot, struct potentialArg * potentialArgs){ int ii; for (ii=0; ii < npot; ii++) { (potentialArgs+ii)->i2d= NULL; (potentialArgs+ii)->accx= NULL; (potentialArgs+ii)->accy= NULL; (potentialArgs+ii)->i2drforce= NULL; (potentialArgs+ii)->accxrforce= NULL; (potentialArgs+ii)->accyrforce= NULL; (potentialArgs+ii)->i2dzforce= NULL; (potentialArgs+ii)->accxzforce= NULL; (potentialArgs+ii)->accyzforce= NULL; (potentialArgs+ii)->wrappedPotentialArg= NULL; (potentialArgs+ii)->spline1d= NULL; (potentialArgs+ii)->acc1d= NULL; (potentialArgs+ii)->tfuncs= NULL; } } void free_potentialArgs(int npot, struct potentialArg * potentialArgs){ int ii, jj; for (ii=0; ii < npot; ii++) { if ( (potentialArgs+ii)->i2d ) interp_2d_free((potentialArgs+ii)->i2d) ; if ( (potentialArgs+ii)->accx ) gsl_interp_accel_free ((potentialArgs+ii)->accx); if ( (potentialArgs+ii)->accy ) gsl_interp_accel_free ((potentialArgs+ii)->accy); if ( (potentialArgs+ii)->i2drforce ) interp_2d_free((potentialArgs+ii)->i2drforce) ; if ( (potentialArgs+ii)->accxrforce ) gsl_interp_accel_free ((potentialArgs+ii)->accxrforce); if ( (potentialArgs+ii)->accyrforce ) gsl_interp_accel_free ((potentialArgs+ii)->accyrforce); if ( (potentialArgs+ii)->i2dzforce ) interp_2d_free((potentialArgs+ii)->i2dzforce) ; if ( (potentialArgs+ii)->accxzforce ) gsl_interp_accel_free ((potentialArgs+ii)->accxzforce); if ( (potentialArgs+ii)->accyzforce ) gsl_interp_accel_free ((potentialArgs+ii)->accyzforce); if ( (potentialArgs+ii)->wrappedPotentialArg ) { free_potentialArgs((potentialArgs+ii)->nwrapped, (potentialArgs+ii)->wrappedPotentialArg); free((potentialArgs+ii)->wrappedPotentialArg); } if ( (potentialArgs+ii)->spline1d ) { for (jj=0; jj < (potentialArgs+ii)->nspline1d; jj++) gsl_spline_free(*((potentialArgs+ii)->spline1d+jj)); free((potentialArgs+ii)->spline1d); } if ( (potentialArgs+ii)->acc1d ) { for (jj=0; jj < (potentialArgs+ii)->nspline1d; jj++) gsl_interp_accel_free (*((potentialArgs+ii)->acc1d+jj)); free((potentialArgs+ii)->acc1d); } free((potentialArgs+ii)->args); } } double evaluatePotentials(double R, double Z, int nargs, struct potentialArg * potentialArgs){ int ii; double pot= 0.; for (ii=0; ii < nargs; ii++){ pot+= potentialArgs->potentialEval(R,Z,0.,0., potentialArgs); potentialArgs++; } potentialArgs-= nargs; return pot; } // function name in parentheses, because actual function defined by macro // in galpy_potentials.h and parentheses are necessary to avoid macro expansion double (calcRforce)(double R, double Z, double phi, double t, int nargs, struct potentialArg * potentialArgs, double vR, double vT, double vZ){ int ii; double Rforce= 0.; for (ii=0; ii < nargs; ii++){ if ( potentialArgs->requiresVelocity ) Rforce+= potentialArgs->RforceVelocity(R,Z,phi,t,potentialArgs,vR,vT,vZ); else Rforce+= potentialArgs->Rforce(R,Z,phi,t, potentialArgs); potentialArgs++; } potentialArgs-= nargs; return Rforce; } double (calczforce)(double R, double Z, double phi, double t, int nargs, struct potentialArg * potentialArgs, double vR, double vT, double vZ){ int ii; double zforce= 0.; for (ii=0; ii < nargs; ii++){ if ( potentialArgs->requiresVelocity ) zforce+= potentialArgs->zforceVelocity(R,Z,phi,t,potentialArgs,vR,vT,vZ); else zforce+= potentialArgs->zforce(R,Z,phi,t,potentialArgs); potentialArgs++; } potentialArgs-= nargs; return zforce; } double (calcphitorque)(double R, double Z, double phi, double t, int nargs, struct potentialArg * potentialArgs, double vR, double vT, double vZ){ int ii; double phitorque= 0.; for (ii=0; ii < nargs; ii++){ if ( potentialArgs->requiresVelocity ) phitorque+= potentialArgs->phitorqueVelocity(R,Z,phi,t,potentialArgs, vR,vT,vZ); else phitorque+= potentialArgs->phitorque(R,Z,phi,t,potentialArgs); potentialArgs++; } potentialArgs-= nargs; return phitorque; } double calcPlanarRforce(double R, double phi, double t, int nargs, struct potentialArg * potentialArgs){ int ii; double Rforce= 0.; for (ii=0; ii < nargs; ii++){ Rforce+= potentialArgs->planarRforce(R,phi,t, potentialArgs); potentialArgs++; } potentialArgs-= nargs; return Rforce; } double calcPlanarphitorque(double R, double phi, double t, int nargs, struct potentialArg * potentialArgs){ int ii; double phitorque= 0.; for (ii=0; ii < nargs; ii++){ phitorque+= potentialArgs->planarphitorque(R,phi,t, potentialArgs); potentialArgs++; } potentialArgs-= nargs; return phitorque; } // LCOV_EXCL_START double calcR2deriv(double R, double Z, double phi, double t, int nargs, struct potentialArg * potentialArgs){ int ii; double R2deriv= 0.; for (ii=0; ii < nargs; ii++){ R2deriv+= potentialArgs->R2deriv(R,Z,phi,t, potentialArgs); potentialArgs++; } potentialArgs-= nargs; return R2deriv; } double calcphi2deriv(double R, double Z, double phi, double t, int nargs, struct potentialArg * potentialArgs){ int ii; double phi2deriv= 0.; for (ii=0; ii < nargs; ii++){ phi2deriv+= potentialArgs->phi2deriv(R,Z,phi,t, potentialArgs); potentialArgs++; } potentialArgs-= nargs; return phi2deriv; } double calcRphideriv(double R, double Z, double phi, double t, int nargs, struct potentialArg * potentialArgs){ int ii; double Rphideriv= 0.; for (ii=0; ii < nargs; ii++){ Rphideriv+= potentialArgs->Rphideriv(R,Z,phi,t, potentialArgs); potentialArgs++; } potentialArgs-= nargs; return Rphideriv; } // LCOV_EXCL_STOP double calcPlanarR2deriv(double R, double phi, double t, int nargs, struct potentialArg * potentialArgs){ int ii; double R2deriv= 0.; for (ii=0; ii < nargs; ii++){ R2deriv+= potentialArgs->planarR2deriv(R,phi,t, potentialArgs); potentialArgs++; } potentialArgs-= nargs; return R2deriv; } double calcPlanarphi2deriv(double R, double phi, double t, int nargs, struct potentialArg * potentialArgs){ int ii; double phi2deriv= 0.; for (ii=0; ii < nargs; ii++){ phi2deriv+= potentialArgs->planarphi2deriv(R,phi,t, potentialArgs); potentialArgs++; } potentialArgs-= nargs; return phi2deriv; } double calcPlanarRphideriv(double R, double phi, double t, int nargs, struct potentialArg * potentialArgs){ int ii; double Rphideriv= 0.; for (ii=0; ii < nargs; ii++){ Rphideriv+= potentialArgs->planarRphideriv(R,phi,t, potentialArgs); potentialArgs++; } potentialArgs-= nargs; return Rphideriv; } double calcLinearForce(double x, double t, int nargs, struct potentialArg * potentialArgs){ int ii; double force= 0.; for (ii=0; ii < nargs; ii++){ force+= potentialArgs->linearForce(x,t,potentialArgs); potentialArgs++; } potentialArgs-= nargs; return force; } double calcDensity(double R, double Z, double phi, double t, int nargs, struct potentialArg * potentialArgs){ int ii; double dens= 0.; for (ii=0; ii < nargs; ii++){ dens+= potentialArgs->dens(R,Z,phi,t,potentialArgs); potentialArgs++; } potentialArgs-= nargs; return dens; } void rotate(double *x, double *y, double *z, double *rot){ double xp,yp,zp; xp= *(rot) * *x + *(rot+1) * *y + *(rot+2) * *z; yp= *(rot+3) * *x + *(rot+4) * *y + *(rot+5) * *z; zp= *(rot+6) * *x + *(rot+7) * *y + *(rot+8) * *z; *x= xp; *y= yp; *z= zp; } void rotate_force(double *Fx, double *Fy, double *Fz, double *rot){ double Fxp,Fyp,Fzp; Fxp= *(rot) * *Fx + *(rot+3) * *Fy + *(rot+6) * *Fz; Fyp= *(rot+1) * *Fx + *(rot+4) * *Fy + *(rot+7) * *Fz; Fzp= *(rot+2) * *Fx + *(rot+5) * *Fy + *(rot+8) * *Fz; *Fx= Fxp; *Fy= Fyp; *Fz= Fzp; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/galpy_potentials.h0000644000175100001710000010517314327773303024277 0ustar00runnerdocker/* C implementations of galpy potentials */ /* Structure declarations */ #ifndef __GALPY_POTENTIALS_H__ #define __GALPY_POTENTIALS_H__ #ifdef __cplusplus extern "C" { #endif #include #include #ifndef M_1_PI #define M_1_PI 0.31830988618379069122 #endif typedef double (**tfuncs_type_arr)(double t); // array of functions of time struct potentialArg{ double (*potentialEval)(double R, double Z, double phi, double t, struct potentialArg *); double (*Rforce)(double R, double Z, double phi, double t, struct potentialArg *); double (*zforce)(double R, double Z, double phi, double t, struct potentialArg *); double (*phitorque)(double R, double Z, double phi, double t, struct potentialArg *); double (*planarRforce)(double R,double phi, double t, struct potentialArg *); double (*planarphitorque)(double R,double phi, double t, struct potentialArg *); double (*R2deriv)(double R,double Z,double phi, double t, struct potentialArg *); double (*phi2deriv)(double R,double Z,double phi, double t, struct potentialArg *); double (*Rphideriv)(double R,double Z,double phi, double t, struct potentialArg *); double (*planarR2deriv)(double R,double phi, double t, struct potentialArg *); double (*planarphi2deriv)(double R,double phi, double t, struct potentialArg *); double (*planarRphideriv)(double R,double phi, double t, struct potentialArg *); double (*linearForce)(double x, double t, struct potentialArg *); double (*dens)(double R, double Z, double phi, double t, struct potentialArg *); // For forces that require velocity input (e.g., dynam fric) bool requiresVelocity; double (*RforceVelocity)(double R, double Z, double phi, double t, struct potentialArg *,double,double,double); double (*zforceVelocity)(double R, double Z, double phi, double t, struct potentialArg *,double,double,double); double (*phitorqueVelocity)(double R, double Z, double phi, double t, struct potentialArg *,double,double,double); int nargs; double * args; // To allow 1D interpolation for an arbitrary number of splines int nspline1d; gsl_interp_accel ** acc1d; gsl_spline ** spline1d; // 2D interpolation interp_2d * i2d; gsl_interp_accel * accx; gsl_interp_accel * accy; interp_2d * i2drforce; gsl_interp_accel * accxrforce; gsl_interp_accel * accyrforce; interp_2d * i2dzforce; gsl_interp_accel * accxzforce; gsl_interp_accel * accyzforce; // To allow an arbitrary number of functions of time int ntfuncs; tfuncs_type_arr tfuncs; // see typedef above // Wrappers int nwrapped; struct potentialArg * wrappedPotentialArg; // For EllipsoidalPotentials double (*psi)(double m,double * args); double (*mdens)(double m,double * args); double (*mdensDeriv)(double m,double * args); // For SphericalPotentials double (*revaluate)(double r,double t,struct potentialArg *); double (*rforce)(double r,double t,struct potentialArg *); double (*r2deriv)(double r,double t,struct potentialArg *); double (*rdens)(double r,double t,struct potentialArg *); }; /* Function declarations */ //Dealing with potentialArg void init_potentialArgs(int,struct potentialArg *); void free_potentialArgs(int,struct potentialArg *); //Potential and force evaluation double evaluatePotentials(double,double,int, struct potentialArg *); // Hack to allow optional velocity for dissipative forces // https://stackoverflow.com/a/52610204/10195320 // Reason to use ##__VA_ARGS__ is that when no optional velocity is supplied, // there is a comma left in the argument list and ## absorbs that for gcc/icc // MSVC supposedly does this automatically for regular __VA_ARGS__, at least // for now, but I can't get this to work locally or on AppVeyor // https://docs.microsoft.com/en-us/cpp/preprocessor/preprocessor-experimental-overview?view=vs-2019#comma-elision-in-variadic-macros // Therefore, I use an alternative where all arguments are variadic, such // that there should always be many. Final subtlety is that we have to define // the EXPAND macro to expand the __VA_ARGS__ into multiple arguments, // otherwise it's treated as a single argument in the next function call // calll (e.g., CALCRFORCE would get R = __VA_ARGS = R,Z,phi,...) #ifdef _MSC_VER #define EXPAND(x) x #define calcRforce(...) EXPAND(CALCRFORCE(__VA_ARGS__,0.,0.,0.)) #define calczforce(...) EXPAND(CALCZFORCE(__VA_ARGS__,0.,0.,0.)) #define calcphitorque(...) EXPAND(CALCphitorque(__VA_ARGS__,0.,0.,0.)) #else #define calcRforce(R,Z,phi,t,nargs,potentialArgs,...) CALCRFORCE(R,Z,phi,t,nargs,potentialArgs,##__VA_ARGS__,0.,0.,0.) #define calczforce(R,Z,phi,t,nargs,potentialArgs,...) CALCZFORCE(R,Z,phi,t,nargs,potentialArgs,##__VA_ARGS__,0.,0.,0.) #define calcphitorque(R,Z,phi,t,nargs,potentialArgs,...) CALCphitorque(R,Z,phi,t,nargs,potentialArgs,##__VA_ARGS__,0.,0.,0.) #endif #define CALCRFORCE(R,Z,phi,t,nargs,potentialArgs,vR,vT,vZ,...) calcRforce(R,Z,phi,t,nargs,potentialArgs,vR,vT,vZ) #define CALCZFORCE(R,Z,phi,t,nargs,potentialArgs,vR,vT,vZ,...) calczforce(R,Z,phi,t,nargs,potentialArgs,vR,vT,vZ) #define CALCphitorque(R,Z,phi,t,nargs,potentialArgs,vR,vT,vZ,...) calcphitorque(R,Z,phi,t,nargs,potentialArgs,vR,vT,vZ) double (calcRforce)(double,double,double,double,int,struct potentialArg *, double,double,double); double (calczforce)(double,double,double,double,int,struct potentialArg *, double,double,double); double (calcphitorque)(double, double,double, double, int, struct potentialArg *, double,double,double); // end hack double calcR2deriv(double, double, double,double, int, struct potentialArg *); double calcphi2deriv(double, double, double,double, int, struct potentialArg *); double calcRphideriv(double, double, double,double, int, struct potentialArg *); double calcPlanarRforce(double, double, double, int, struct potentialArg *); double calcPlanarphitorque(double, double, double, int, struct potentialArg *); double calcPlanarR2deriv(double, double, double, int, struct potentialArg *); double calcPlanarphi2deriv(double, double, double, int, struct potentialArg *); double calcPlanarRphideriv(double, double, double, int, struct potentialArg *); double calcLinearForce(double, double, int, struct potentialArg *); double calcDensity(double, double, double,double, int, struct potentialArg *); void rotate(double *, double *, double *, double *); void rotate_force(double *, double *, double *,double *); //ZeroForce double ZeroPlanarForce(double,double,double, struct potentialArg *); double ZeroForce(double,double,double,double, struct potentialArg *); //verticalPotential double verticalPotentialLinearForce(double,double,struct potentialArg *); //LogarithmicHaloPotential double LogarithmicHaloPotentialEval(double ,double , double, double, struct potentialArg *); double LogarithmicHaloPotentialRforce(double ,double , double, double, struct potentialArg *); double LogarithmicHaloPotentialPlanarRforce(double ,double, double, struct potentialArg *); double LogarithmicHaloPotentialzforce(double,double,double,double, struct potentialArg *); double LogarithmicHaloPotentialphitorque(double,double,double,double, struct potentialArg *); double LogarithmicHaloPotentialPlanarphitorque(double,double,double, struct potentialArg *); double LogarithmicHaloPotentialPlanarR2deriv(double ,double, double, struct potentialArg *); double LogarithmicHaloPotentialPlanarphi2deriv(double ,double, double, struct potentialArg *); double LogarithmicHaloPotentialPlanarRphideriv(double ,double, double, struct potentialArg *); double LogarithmicHaloPotentialDens(double ,double , double, double, struct potentialArg *); //DehnenBarPotential double DehnenBarPotentialRforce(double,double,double,double, struct potentialArg *); double DehnenBarPotentialphitorque(double,double,double,double, struct potentialArg *); double DehnenBarPotentialzforce(double,double,double,double, struct potentialArg *); double DehnenBarPotentialPlanarRforce(double,double,double, struct potentialArg *); double DehnenBarPotentialPlanarphitorque(double,double,double, struct potentialArg *); double DehnenBarPotentialPlanarR2deriv(double,double,double, struct potentialArg *); double DehnenBarPotentialPlanarphi2deriv(double,double,double, struct potentialArg *); double DehnenBarPotentialPlanarRphideriv(double,double,double, struct potentialArg *); //TransientLogSpiralPotential double TransientLogSpiralPotentialRforce(double,double,double, struct potentialArg *); double TransientLogSpiralPotentialphitorque(double,double,double, struct potentialArg *); //SteadyLogSpiralPotential double SteadyLogSpiralPotentialRforce(double,double,double, struct potentialArg *); double SteadyLogSpiralPotentialphitorque(double,double,double, struct potentialArg *); //EllipticalDiskPotential double EllipticalDiskPotentialRforce(double,double,double, struct potentialArg *); double EllipticalDiskPotentialphitorque(double,double,double, struct potentialArg *); double EllipticalDiskPotentialR2deriv(double,double,double, struct potentialArg *); double EllipticalDiskPotentialphi2deriv(double,double,double, struct potentialArg *); double EllipticalDiskPotentialRphideriv(double,double,double, struct potentialArg *); //Miyamoto-Nagai Potential double MiyamotoNagaiPotentialEval(double ,double , double, double, struct potentialArg *); double MiyamotoNagaiPotentialRforce(double ,double , double, double, struct potentialArg *); double MiyamotoNagaiPotentialPlanarRforce(double ,double, double, struct potentialArg *); double MiyamotoNagaiPotentialzforce(double,double,double,double, struct potentialArg *); double MiyamotoNagaiPotentialPlanarR2deriv(double ,double, double, struct potentialArg *); double MiyamotoNagaiPotentialDens(double ,double , double, double, struct potentialArg *); //LopsidedDiskPotential double LopsidedDiskPotentialRforce(double,double,double, struct potentialArg *); double LopsidedDiskPotentialphitorque(double,double,double, struct potentialArg *); double LopsidedDiskPotentialR2deriv(double,double,double, struct potentialArg *); double LopsidedDiskPotentialphi2deriv(double,double,double, struct potentialArg *); double LopsidedDiskPotentialRphideriv(double,double,double, struct potentialArg *); //PowerSphericalPotential double PowerSphericalPotentialEval(double ,double , double, double, struct potentialArg *); double PowerSphericalPotentialRforce(double ,double , double, double, struct potentialArg *); double PowerSphericalPotentialPlanarRforce(double ,double, double, struct potentialArg *); double PowerSphericalPotentialzforce(double,double,double,double, struct potentialArg *); double PowerSphericalPotentialPlanarR2deriv(double ,double, double, struct potentialArg *); double PowerSphericalPotentialDens(double ,double , double, double, struct potentialArg *); //HernquistPotential double HernquistPotentialEval(double ,double , double, double, struct potentialArg *); double HernquistPotentialRforce(double ,double , double, double, struct potentialArg *); double HernquistPotentialPlanarRforce(double ,double, double, struct potentialArg *); double HernquistPotentialzforce(double,double,double,double, struct potentialArg *); double HernquistPotentialPlanarR2deriv(double ,double, double, struct potentialArg *); double HernquistPotentialDens(double ,double , double, double, struct potentialArg *); //NFWPotential double NFWPotentialEval(double ,double , double, double, struct potentialArg *); double NFWPotentialRforce(double ,double , double, double, struct potentialArg *); double NFWPotentialPlanarRforce(double ,double, double, struct potentialArg *); double NFWPotentialzforce(double,double,double,double, struct potentialArg *); double NFWPotentialPlanarR2deriv(double ,double, double, struct potentialArg *); double NFWPotentialDens(double ,double , double, double, struct potentialArg *); //JaffePotential double JaffePotentialEval(double ,double , double, double, struct potentialArg *); double JaffePotentialRforce(double ,double , double, double, struct potentialArg *); double JaffePotentialPlanarRforce(double ,double, double, struct potentialArg *); double JaffePotentialzforce(double,double,double,double, struct potentialArg *); double JaffePotentialPlanarR2deriv(double ,double, double, struct potentialArg *); double JaffePotentialDens(double ,double , double, double, struct potentialArg *); //DoubleExponentialDiskPotential double DoubleExponentialDiskPotentialEval(double ,double , double, double, struct potentialArg *); double DoubleExponentialDiskPotentialRforce(double,double, double,double, struct potentialArg *); double DoubleExponentialDiskPotentialPlanarRforce(double,double,double, struct potentialArg *); double DoubleExponentialDiskPotentialzforce(double,double, double,double, struct potentialArg *); double DoubleExponentialDiskPotentialDens(double ,double , double, double, struct potentialArg *); //FlattenedPowerPotential double FlattenedPowerPotentialEval(double,double,double,double, struct potentialArg *); double FlattenedPowerPotentialRforce(double,double,double,double, struct potentialArg *); double FlattenedPowerPotentialPlanarRforce(double,double,double, struct potentialArg *); double FlattenedPowerPotentialzforce(double,double,double,double, struct potentialArg *); double FlattenedPowerPotentialPlanarR2deriv(double,double,double, struct potentialArg *); double FlattenedPowerPotentialDens(double,double,double,double, struct potentialArg *); //interpRZPotential double interpRZPotentialEval(double ,double , double, double, struct potentialArg *); double interpRZPotentialRforce(double ,double , double, double, struct potentialArg *); double interpRZPotentialzforce(double ,double , double, double, struct potentialArg *); //IsochronePotential double IsochronePotentialEval(double ,double , double, double, struct potentialArg *); double IsochronePotentialRforce(double ,double , double, double, struct potentialArg *); double IsochronePotentialPlanarRforce(double ,double, double, struct potentialArg *); double IsochronePotentialzforce(double,double,double,double, struct potentialArg *); double IsochronePotentialPlanarR2deriv(double ,double, double, struct potentialArg *); double IsochronePotentialDens(double ,double , double, double, struct potentialArg *); //PowerSphericalPotentialwCutoff double PowerSphericalPotentialwCutoffEval(double ,double , double, double, struct potentialArg *); double PowerSphericalPotentialwCutoffRforce(double ,double , double, double, struct potentialArg *); double PowerSphericalPotentialwCutoffPlanarRforce(double ,double, double, struct potentialArg *); double PowerSphericalPotentialwCutoffzforce(double,double,double,double, struct potentialArg *); double PowerSphericalPotentialwCutoffPlanarR2deriv(double ,double, double, struct potentialArg *); double PowerSphericalPotentialwCutoffDens(double ,double , double, double, struct potentialArg *); //KuzminKutuzovStaeckelPotential double KuzminKutuzovStaeckelPotentialEval(double,double,double,double, struct potentialArg *); double KuzminKutuzovStaeckelPotentialRforce(double,double,double,double, struct potentialArg *); double KuzminKutuzovStaeckelPotentialPlanarRforce(double,double,double, struct potentialArg *); double KuzminKutuzovStaeckelPotentialzforce(double,double,double,double, struct potentialArg *); double KuzminKutuzovStaeckelPotentialPlanarR2deriv(double,double,double, struct potentialArg *); //KuzminDiskPotential double KuzminDiskPotentialEval(double,double,double,double, struct potentialArg *); double KuzminDiskPotentialRforce(double,double,double,double, struct potentialArg *); double KuzminDiskPotentialPlanarRforce(double,double,double, struct potentialArg *); double KuzminDiskPotentialzforce(double,double,double,double, struct potentialArg *); double KuzminDiskPotentialPlanarR2deriv(double,double,double, struct potentialArg *); //PlummerPotential double PlummerPotentialEval(double,double,double,double, struct potentialArg *); double PlummerPotentialRforce(double,double,double,double, struct potentialArg *); double PlummerPotentialPlanarRforce(double,double,double, struct potentialArg *); double PlummerPotentialzforce(double,double,double,double, struct potentialArg *); double PlummerPotentialPlanarR2deriv(double,double,double, struct potentialArg *); double PlummerPotentialDens(double,double,double,double, struct potentialArg *); //PseudoIsothermalPotential double PseudoIsothermalPotentialEval(double,double,double,double, struct potentialArg *); double PseudoIsothermalPotentialRforce(double,double,double,double, struct potentialArg *); double PseudoIsothermalPotentialPlanarRforce(double,double,double, struct potentialArg *); double PseudoIsothermalPotentialzforce(double,double,double,double, struct potentialArg *); double PseudoIsothermalPotentialPlanarR2deriv(double,double,double, struct potentialArg *); double PseudoIsothermalPotentialDens(double,double,double,double, struct potentialArg *); //BurkertPotential double BurkertPotentialEval(double,double,double,double, struct potentialArg *); double BurkertPotentialRforce(double,double,double,double, struct potentialArg *); double BurkertPotentialPlanarRforce(double,double,double, struct potentialArg *); double BurkertPotentialzforce(double,double,double,double, struct potentialArg *); double BurkertPotentialPlanarR2deriv(double,double,double, struct potentialArg *); double BurkertPotentialDens(double,double,double,double, struct potentialArg *); //EllipsoidalPotential double EllipsoidalPotentialEval(double,double,double,double, struct potentialArg *); double EllipsoidalPotentialRforce(double,double,double,double, struct potentialArg *); double EllipsoidalPotentialPlanarRforce(double,double,double, struct potentialArg *); double EllipsoidalPotentialphitorque(double,double,double,double, struct potentialArg *); double EllipsoidalPotentialPlanarphitorque(double,double,double, struct potentialArg *); double EllipsoidalPotentialzforce(double,double,double,double, struct potentialArg *); double EllipsoidalPotentialDens(double,double,double,double, struct potentialArg *); //TriaxialHernquistPotential: uses EllipsoidalPotential, only need psi, dens, densDeriv double TriaxialHernquistPotentialpsi(double,double *); double TriaxialHernquistPotentialmdens(double,double *); double TriaxialHernquistPotentialmdensDeriv(double,double *); //TriaxialJaffePotential: uses EllipsoidalPotential, only need psi, dens, densDeriv double TriaxialJaffePotentialpsi(double,double *); double TriaxialJaffePotentialmdens(double,double *); double TriaxialJaffePotentialmdensDeriv(double,double *); //TriaxialNFWPotential: uses EllipsoidalPotential, only need psi, dens, densDeriv double TriaxialNFWPotentialpsi(double,double *); double TriaxialNFWPotentialmdens(double,double *); double TriaxialNFWPotentialmdensDeriv(double,double *); //SCFPotential double SCFPotentialEval(double,double,double,double, struct potentialArg *); double SCFPotentialRforce(double,double,double,double, struct potentialArg *); double SCFPotentialzforce(double,double,double,double, struct potentialArg *); double SCFPotentialphitorque(double,double,double,double, struct potentialArg *); double SCFPotentialPlanarRforce(double,double,double, struct potentialArg *); double SCFPotentialPlanarphitorque(double,double,double, struct potentialArg *); double SCFPotentialPlanarR2deriv(double,double,double, struct potentialArg *); double SCFPotentialPlanarphi2deriv(double,double,double, struct potentialArg *); double SCFPotentialPlanarRphideriv(double,double,double, struct potentialArg *); double SCFPotentialDens(double,double,double,double, struct potentialArg *); //SoftenedNeedleBarPotential double SoftenedNeedleBarPotentialEval(double,double,double,double, struct potentialArg *); double SoftenedNeedleBarPotentialRforce(double,double,double,double, struct potentialArg *); double SoftenedNeedleBarPotentialzforce(double,double,double,double, struct potentialArg *); double SoftenedNeedleBarPotentialphitorque(double,double,double,double, struct potentialArg *); double SoftenedNeedleBarPotentialPlanarRforce(double,double,double, struct potentialArg *); double SoftenedNeedleBarPotentialPlanarphitorque(double,double,double, struct potentialArg *); //DiskSCFPotential double DiskSCFPotentialEval(double,double,double,double, struct potentialArg *); double DiskSCFPotentialRforce(double,double,double,double, struct potentialArg *); double DiskSCFPotentialzforce(double,double,double,double, struct potentialArg *); double DiskSCFPotentialPlanarRforce(double,double,double, struct potentialArg *); double DiskSCFPotentialDens(double,double,double,double, struct potentialArg *); // SpiralArmsPotential double SpiralArmsPotentialEval(double, double, double, double, struct potentialArg*); double SpiralArmsPotentialRforce(double, double, double, double, struct potentialArg*); double SpiralArmsPotentialzforce(double, double, double, double, struct potentialArg*); double SpiralArmsPotentialphitorque(double, double, double, double, struct potentialArg*); double SpiralArmsPotentialR2deriv(double R, double z, double phi, double t, struct potentialArg* potentialArgs); double SpiralArmsPotentialz2deriv(double R, double z, double phi, double t, struct potentialArg* potentialArgs); double SpiralArmsPotentialphi2deriv(double R, double z, double phi, double t, struct potentialArg* potentialArgs); double SpiralArmsPotentialRzderiv(double R, double z, double phi, double t, struct potentialArg* potentialArgs); double SpiralArmsPotentialRphideriv(double R, double z, double phi, double t, struct potentialArg* potentialArgs); double SpiralArmsPotentialPlanarRforce(double, double, double, struct potentialArg*); double SpiralArmsPotentialPlanarphitorque(double, double, double, struct potentialArg*); double SpiralArmsPotentialPlanarR2deriv(double, double, double, struct potentialArg*); double SpiralArmsPotentialPlanarphi2deriv(double, double, double, struct potentialArg*); double SpiralArmsPotentialPlanarRphideriv(double, double, double, struct potentialArg*); //CosmphiDiskPotential double CosmphiDiskPotentialRforce(double,double,double, struct potentialArg *); double CosmphiDiskPotentialphitorque(double,double,double, struct potentialArg *); double CosmphiDiskPotentialR2deriv(double,double,double, struct potentialArg *); double CosmphiDiskPotentialphi2deriv(double,double,double, struct potentialArg *); double CosmphiDiskPotentialRphideriv(double,double,double, struct potentialArg *); //HenonHeilesPotential double HenonHeilesPotentialRforce(double,double,double, struct potentialArg *); double HenonHeilesPotentialphitorque(double,double,double, struct potentialArg *); double HenonHeilesPotentialR2deriv(double,double,double, struct potentialArg *); double HenonHeilesPotentialphi2deriv(double,double,double, struct potentialArg *); double HenonHeilesPotentialRphideriv(double,double,double, struct potentialArg *); //PerfectEllipsoid: uses EllipsoidalPotential, only need psi, dens, densDeriv double PerfectEllipsoidPotentialpsi(double,double *); double PerfectEllipsoidPotentialmdens(double,double *); double PerfectEllipsoidPotentialmdensDeriv(double,double *); //KGPotential double KGPotentialLinearForce(double,double,struct potentialArg *); //IsothermalDiskPotential double IsothermalDiskPotentialLinearForce(double,double,struct potentialArg *); //DehnenSphericalPotential double DehnenSphericalPotentialEval(double ,double , double, double, struct potentialArg *); double DehnenSphericalPotentialRforce(double ,double , double, double, struct potentialArg *); double DehnenSphericalPotentialPlanarRforce(double ,double, double, struct potentialArg *); double DehnenSphericalPotentialzforce(double,double,double,double, struct potentialArg *); double DehnenSphericalPotentialPlanarR2deriv(double ,double, double, struct potentialArg *); double DehnenSphericalPotentialDens(double ,double , double, double, struct potentialArg *); //DehnenCoreSphericalPotential double DehnenCoreSphericalPotentialEval(double ,double , double, double, struct potentialArg *); double DehnenCoreSphericalPotentialRforce(double ,double , double, double, struct potentialArg *); double DehnenCoreSphericalPotentialPlanarRforce(double ,double, double, struct potentialArg *); double DehnenCoreSphericalPotentialzforce(double,double,double,double, struct potentialArg *); double DehnenCoreSphericalPotentialPlanarR2deriv(double ,double, double, struct potentialArg *); double DehnenCoreSphericalPotentialDens(double ,double , double, double, struct potentialArg *); //HomogeneousSpherePotential double HomogeneousSpherePotentialEval(double ,double , double, double, struct potentialArg *); double HomogeneousSpherePotentialRforce(double ,double , double, double, struct potentialArg *); double HomogeneousSpherePotentialPlanarRforce(double ,double, double, struct potentialArg *); double HomogeneousSpherePotentialzforce(double,double,double,double, struct potentialArg *); double HomogeneousSpherePotentialPlanarR2deriv(double ,double, double, struct potentialArg *); double HomogeneousSpherePotentialDens(double ,double , double, double, struct potentialArg *); //SphericalPotential double SphericalPotentialEval(double,double,double,double, struct potentialArg *); double SphericalPotentialRforce(double,double,double,double, struct potentialArg *); double SphericalPotentialPlanarRforce(double,double,double, struct potentialArg *); double SphericalPotentialzforce(double,double,double,double, struct potentialArg *); double SphericalPotentialPlanarR2deriv(double ,double, double, struct potentialArg *); double SphericalPotentialDens(double,double,double,double, struct potentialArg *); //interpSphericalPotential: uses SphericalPotential, only need revaluate, rforce, r2deriv double interpSphericalPotentialrevaluate(double,double,struct potentialArg *); double interpSphericalPotentialrforce(double,double,struct potentialArg *); double interpSphericalPotentialr2deriv(double,double,struct potentialArg *); double interpSphericalPotentialrdens(double,double,struct potentialArg *); //TriaxialGaussian: uses EllipsoidalPotential, only need psi, dens, densDeriv double TriaxialGaussianPotentialpsi(double,double *); double TriaxialGaussianPotentialmdens(double,double *); double TriaxialGaussianPotentialmdensDeriv(double,double *); //PowerTriaxial: uses EllipsoidalPotential, only need psi, dens, densDeriv double PowerTriaxialPotentialpsi(double,double *); double PowerTriaxialPotentialmdens(double,double *); double PowerTriaxialPotentialmdensDeriv(double,double *); //NonInertialFrameForce, takes vR,vT,vZ double NonInertialFrameForceRforce(double,double,double,double, struct potentialArg *, double,double,double); double NonInertialFrameForcephitorque(double,double,double,double, struct potentialArg *, double,double,double); double NonInertialFrameForcezforce(double,double,double,double, struct potentialArg *, double,double,double); //////////////////////////////// WRAPPERS ///////////////////////////////////// //DehnenSmoothWrapperPotential double DehnenSmoothWrapperPotentialEval(double,double,double,double, struct potentialArg *); double DehnenSmoothWrapperPotentialRforce(double,double,double,double, struct potentialArg *); double DehnenSmoothWrapperPotentialphitorque(double,double,double,double, struct potentialArg *); double DehnenSmoothWrapperPotentialzforce(double,double,double,double, struct potentialArg *); double DehnenSmoothWrapperPotentialPlanarRforce(double,double,double, struct potentialArg *); double DehnenSmoothWrapperPotentialPlanarphitorque(double,double,double, struct potentialArg *); double DehnenSmoothWrapperPotentialPlanarR2deriv(double,double,double, struct potentialArg *); double DehnenSmoothWrapperPotentialPlanarphi2deriv(double,double,double, struct potentialArg *); double DehnenSmoothWrapperPotentialPlanarRphideriv(double,double,double, struct potentialArg *); //SolidBodyRotationWrapperPotential double SolidBodyRotationWrapperPotentialRforce(double,double,double,double, struct potentialArg *); double SolidBodyRotationWrapperPotentialphitorque(double,double,double,double, struct potentialArg *); double SolidBodyRotationWrapperPotentialzforce(double,double,double,double, struct potentialArg *); double SolidBodyRotationWrapperPotentialPlanarRforce(double,double,double, struct potentialArg *); double SolidBodyRotationWrapperPotentialPlanarphitorque(double,double,double, struct potentialArg *); double SolidBodyRotationWrapperPotentialPlanarR2deriv(double,double,double, struct potentialArg *); double SolidBodyRotationWrapperPotentialPlanarphi2deriv(double,double,double, struct potentialArg *); double SolidBodyRotationWrapperPotentialPlanarRphideriv(double,double,double, struct potentialArg *); //CorotatingRotationWrapperPotential double CorotatingRotationWrapperPotentialRforce(double,double,double,double, struct potentialArg *); double CorotatingRotationWrapperPotentialphitorque(double,double,double,double, struct potentialArg *); double CorotatingRotationWrapperPotentialzforce(double,double,double,double, struct potentialArg *); double CorotatingRotationWrapperPotentialPlanarRforce(double,double,double, struct potentialArg *); double CorotatingRotationWrapperPotentialPlanarphitorque(double,double,double, struct potentialArg *); double CorotatingRotationWrapperPotentialPlanarR2deriv(double,double,double, struct potentialArg *); double CorotatingRotationWrapperPotentialPlanarphi2deriv(double,double,double, struct potentialArg *); double CorotatingRotationWrapperPotentialPlanarRphideriv(double,double,double, struct potentialArg *); //GaussianAmplitudeWrapperPotential double GaussianAmplitudeWrapperPotentialEval(double,double,double,double, struct potentialArg *); double GaussianAmplitudeWrapperPotentialRforce(double,double,double,double, struct potentialArg *); double GaussianAmplitudeWrapperPotentialphitorque(double,double,double,double, struct potentialArg *); double GaussianAmplitudeWrapperPotentialzforce(double,double,double,double, struct potentialArg *); double GaussianAmplitudeWrapperPotentialPlanarRforce(double,double,double, struct potentialArg *); double GaussianAmplitudeWrapperPotentialPlanarphitorque(double,double,double, struct potentialArg *); double GaussianAmplitudeWrapperPotentialPlanarR2deriv(double,double,double, struct potentialArg *); double GaussianAmplitudeWrapperPotentialPlanarphi2deriv(double,double,double, struct potentialArg *); double GaussianAmplitudeWrapperPotentialPlanarRphideriv(double,double,double, struct potentialArg *); //MovingObjectPotential double MovingObjectPotentialRforce(double,double,double,double, struct potentialArg *); double MovingObjectPotentialphitorque(double,double,double,double, struct potentialArg *); double MovingObjectPotentialzforce(double,double,double,double, struct potentialArg *); double MovingObjectPotentialPlanarRforce(double,double,double, struct potentialArg *); double MovingObjectPotentialPlanarphitorque(double,double,double, struct potentialArg *); //RotateAndTiltWrapperPotential double RotateAndTiltWrapperPotentialRforce(double,double,double,double, struct potentialArg *); double RotateAndTiltWrapperPotentialphitorque(double,double,double,double, struct potentialArg *); double RotateAndTiltWrapperPotentialzforce(double,double,double,double, struct potentialArg *); //ChandrasekharDynamicalFrictionForce, takes vR,vT,vZ double ChandrasekharDynamicalFrictionForceRforce(double,double,double,double, struct potentialArg *, double,double,double); double ChandrasekharDynamicalFrictionForcephitorque(double,double,double,double, struct potentialArg *, double,double,double); double ChandrasekharDynamicalFrictionForcezforce(double,double,double,double, struct potentialArg *, double,double,double); //TimeDependentAmplitudeWrapperPotential double TimeDependentAmplitudeWrapperPotentialEval(double,double,double,double, struct potentialArg *); double TimeDependentAmplitudeWrapperPotentialRforce(double,double,double,double, struct potentialArg *); double TimeDependentAmplitudeWrapperPotentialphitorque(double,double,double,double, struct potentialArg *); double TimeDependentAmplitudeWrapperPotentialzforce(double,double,double,double, struct potentialArg *); double TimeDependentAmplitudeWrapperPotentialPlanarRforce(double,double,double, struct potentialArg *); double TimeDependentAmplitudeWrapperPotentialPlanarphitorque(double,double,double, struct potentialArg *); double TimeDependentAmplitudeWrapperPotentialPlanarR2deriv(double,double,double, struct potentialArg *); double TimeDependentAmplitudeWrapperPotentialPlanarphi2deriv(double,double,double, struct potentialArg *); double TimeDependentAmplitudeWrapperPotentialPlanarRphideriv(double,double,double, struct potentialArg *); #ifdef __cplusplus } #endif #endif /* galpy_potentials.h */ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/interpRZPotential.c0000644000175100001710000000351114327773303024342 0ustar00runnerdocker#include #include #include //interpRZpotential //2 remaining arguments: amp, logR double interpRZPotentialEval(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double y; double amp= *args++; int logR= (int) *args; if ( logR == 1) y= ( R > 0. ) ? log(R): -20.72326583694641; else y= R; //Calculate potential through interpolation return amp * interp_2d_eval_cubic_bspline(potentialArgs->i2d,y,fabs(z), potentialArgs->accx, potentialArgs->accy); } double interpRZPotentialRforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double y; double amp= *args++; int logR= (int) *args; if ( logR == 1) y= ( R > 0. ) ? log(R): -20.72326583694641; else y= R; //Calculate potential through interpolation return amp * interp_2d_eval_cubic_bspline(potentialArgs->i2drforce,y,fabs(z), potentialArgs->accxrforce, potentialArgs->accyrforce); } double interpRZPotentialzforce(double R,double z, double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double y; double amp= *args++; int logR= (int) *args; if ( logR == 1) y= ( R > 0. ) ? log(R): -20.72326583694641; else y= R; //Calculate potential through interpolation if ( z < 0. ) return - amp * interp_2d_eval_cubic_bspline(potentialArgs->i2dzforce,y, -z, potentialArgs->accxzforce, potentialArgs->accyzforce); else return amp * interp_2d_eval_cubic_bspline(potentialArgs->i2dzforce,y, z, potentialArgs->accxzforce, potentialArgs->accyzforce); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/interpSphericalPotential.c0000644000175100001710000000354214327773303025725 0ustar00runnerdocker#include #include // interpSphericalPotential: 6 arguments: amp (not used here), rmin, rmax, // M(args; //Get args double rmin= *(args+1); double rmax= *(args+2); double Mmax= *(args+3); double Phi0= *(args+4); double Phimax= *(args+5); if ( r >= rmax ) { return -Mmax/r+Phimax; } else { return r < rmin ? 0. : \ -gsl_spline_eval_integ(*potentialArgs->spline1d, rmin,r,*potentialArgs->acc1d) + Phi0; } } double interpSphericalPotentialrforce(double r,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double rmin= *(args+1); double rmax= *(args+2); double Mmax= *(args+3); if ( r >= rmax ) { return -Mmax/r/r; } else { return r < rmin ? 0. : gsl_spline_eval(*potentialArgs->spline1d, r,*potentialArgs->acc1d); } } double interpSphericalPotentialr2deriv(double r,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double rmin= *(args+1); double rmax= *(args+2); double Mmax= *(args+3); if ( r >= rmax ) { return -2. * Mmax / r / r / r; } else { return r < rmin ? 0. : -gsl_spline_eval_deriv(*potentialArgs->spline1d, r,*potentialArgs->acc1d); } } double interpSphericalPotentialrdens(double r,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; //Get args double rmin= *(args+1); double rmax= *(args+2); if ( r >= rmax ) { return 0.; } else { return r < rmin ? 0. : M_1_PI / 4. \ * ( interpSphericalPotentialr2deriv(r,t,potentialArgs) - 2. * interpSphericalPotentialrforce(r,t,potentialArgs)/r); } } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/potential_c_ext/verticalPotential.c0000644000175100001710000000071614327773303024402 0ustar00runnerdocker#include //Routines to evaluate forces etc. for a verticalPotential, i.e., a 1D //potential derived from a 3D potential as the z potential at a given (R,phi) double verticalPotentialLinearForce(double x, double t, struct potentialArg * potentialArgs){ double R= *potentialArgs->args; double phi= *(potentialArgs->args+1); return calczforce(R,x,phi,t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/potential/verticalPotential.py0000644000175100001710000001637314327773303021435 0ustar00runnerdockerimport numpy from ..util import conversion from .DissipativeForce import _isDissipative from .linearPotential import linearPotential from .planarPotential import planarPotential from .Potential import Potential, PotentialError, flatten class verticalPotential(linearPotential): """Class that represents a vertical potential derived from a 3D Potential: phi(z,t;R,phi)= phi(R,z,phi,t)-phi(R,0.,phi,t0)""" def __init__(self,Pot,R=1.,phi=None,t0=0.): """ NAME: __init__ PURPOSE: Initialize INPUT: Pot - Potential instance R - Galactocentric radius at which to create the vertical potential phi= (None) Galactocentric azimuth at which to create the vertical potential (rad); necessary for t0= (0.) time at which to create the vertical potential OUTPUT: verticalPotential instance HISTORY: 2010-07-13 - Written - Bovy (NYU) 2018-10-07 - Added support for non-axi potentials - Bovy (UofT) 2019-08-19 - Added support for time-dependent potentials - Bovy (UofT) """ linearPotential.__init__(self,amp=1.,ro=Pot._ro,vo=Pot._vo) self._Pot= Pot self._R= R if phi is None: if Pot.isNonAxi: raise PotentialError("The Potential instance to convert to a verticalPotential is non-axisymmetric, but you did not provide phi") self._phi= 0. else: self._phi= phi self._midplanePot= self._Pot(self._R,0.,phi=self._phi, t=t0,use_physical=False) self.hasC= Pot.hasC # Also transfer roSet and voSet self._roSet= Pot._roSet self._voSet= Pot._voSet return None def _evaluate(self,z,t=0.): """ NAME: _evaluate PURPOSE: evaluate the potential INPUT: z t OUTPUT: Pot(z,t;R,phi) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ tR= self._R if not hasattr(z,'__len__') else self._R*numpy.ones_like(z) tphi= self._phi if not hasattr(z,'__len__') else self._phi*numpy.ones_like(z) return self._Pot(tR,z,phi=tphi,t=t,use_physical=False)\ -self._midplanePot def _force(self,z,t=0.): """ NAME: _force PURPOSE: evaluate the force INPUT: z t OUTPUT: F_z(z,t;R,phi) HISTORY: 2010-07-13 - Written - Bovy (NYU) """ tR= self._R if not hasattr(z,'__len__') else self._R*numpy.ones_like(z) tphi= self._phi if not hasattr(z,'__len__') else self._phi*numpy.ones_like(z) return self._Pot.zforce(tR,z,phi=tphi,t=t,use_physical=False) def RZToverticalPotential(RZPot,R): """ NAME: RZToverticalPotential PURPOSE: convert a RZPotential to a vertical potential at a given R INPUT: RZPot - RZPotential instance or list of such instances R - Galactocentric radius at which to evaluate the vertical potential (can be Quantity) OUTPUT: (list of) linearPotential instance(s) HISTORY: 2010-07-21 - Written - Bovy (NYU) """ RZPot= flatten(RZPot) if _isDissipative(RZPot): raise NotImplementedError("Converting dissipative forces to 1D vertical potentials is currently not supported") try: conversion.get_physical(RZPot) except: raise PotentialError("Input to 'RZToverticalPotential' is neither an RZPotential-instance or a list of such instances") R= conversion.parse_length(R,**conversion.get_physical(RZPot)) if isinstance(RZPot,list): out= [] for pot in RZPot: if isinstance(pot,linearPotential): out.append(pot) elif isinstance(pot,Potential): out.append(verticalPotential(pot,R)) elif isinstance(pot,planarPotential): raise PotentialError("Input to 'RZToverticalPotential' cannot be a planarPotential") else: raise PotentialError("Input to 'RZToverticalPotential' is neither an RZPotential-instance or a list of such instances") return out elif isinstance(RZPot,Potential): return verticalPotential(RZPot,R) elif isinstance(RZPot,linearPotential): return RZPot elif isinstance(RZPot,planarPotential): raise PotentialError("Input to 'RZToverticalPotential' cannot be a planarPotential") else: # pragma: no cover # All other cases should have been caught by the # conversion.get_physical test above raise PotentialError("Input to 'RZToverticalPotential' is neither an RZPotential-instance or a list of such instances") def toVerticalPotential(Pot,R,phi=None,t0=0.): """ NAME: toVerticalPotential PURPOSE: convert a Potential to a vertical potential at a given R: Phi(z,phi,t) = Phi(R,z,phi,t)-Phi(R,0.,phi0,t0) where phi0 and t0 are the phi and t inputs INPUT: Pot - Potential instance or list of such instances R - Galactocentric radius at which to evaluate the vertical potential (can be Quantity) phi= (None) Galactocentric azimuth at which to evaluate the vertical potential (can be Quantity); required if Pot is non-axisymmetric t0= (0.) time at which to evaluate the vertical potential (can be Quantity) OUTPUT: (list of) linearPotential instance(s) HISTORY: 2018-10-07 - Written - Bovy (UofT) """ Pot= flatten(Pot) if _isDissipative(Pot): raise NotImplementedError("Converting dissipative forces to 1D vertical potentials is currently not supported") try: conversion.get_physical(Pot) except: raise PotentialError("Input to 'toVerticalPotential' is neither an Potential-instance or a list of such instances") R= conversion.parse_length(R,**conversion.get_physical(Pot)) phi= conversion.parse_angle(phi) t0= conversion.parse_time(t0,**conversion.get_physical(Pot)) if isinstance(Pot,list): out= [] for pot in Pot: if isinstance(pot,linearPotential): out.append(pot) elif isinstance(pot,Potential): out.append(verticalPotential(pot,R,phi=phi,t0=t0)) elif isinstance(pot,planarPotential): raise PotentialError("Input to 'toVerticalPotential' cannot be a planarPotential") else: # pragma: no cover # All other cases should have been caught by the # conversion.get_physical test above raise PotentialError("Input to 'toVerticalPotential' is neither an RZPotential-instance or a list of such instances") return out elif isinstance(Pot,Potential): return verticalPotential(Pot,R,phi=phi,t0=t0) elif isinstance(Pot,linearPotential): return Pot elif isinstance(Pot,planarPotential): raise PotentialError("Input to 'toVerticalPotential' cannot be a planarPotential") else: # pragma: no cover # All other cases should have been caught by the # conversion.get_physical test above raise PotentialError("Input to 'toVerticalPotential' is neither an Potential-instance or a list of such instances") ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667233486.717467 galpy-1.8.1/galpy/snapshot/0000755000175100001710000000000014327773317015225 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/snapshot/GadgetSnapshot.py0000644000175100001710000000423614327773303020512 0ustar00runnerdockertry: import pynbody _PYNBODYENABLED= True except ImportError: _PYNBODYENABLED= False class GadgetSnapshot: """Snapshot coming out of gadget""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize a Gadget snapshot object INPUT: Initialize using: 1) filename OUTPUT: HISTORY: 2011-08-15 - Started - Bovy """ if not _PYNBODYENABLED: raise ImportError("pynbody could not be loaded to read the gadget snapshot") filename= args[0] self._sim = pynbody.load(filename) self._sim.physical_units() self._star= self._sim.star return None def __getattr__(self,i): if i == 'x': return self.pos[0] elif i =='pos': return self._star['pos'] def plot(self,*args,**kwargs): """ NAME: plot PURPOSE: plot the snapshot INPUT: OUTPUT: HISTORY: 2011-08-15 - Started - Bovy (NYU) """ labeldict= {'t':r'$t$','R':r'$R$','vR':r'$v_R$','vT':r'$v_T$', 'z':r'$z$','vz':r'$v_z$','phi':r'$\phi$', 'x':r'$x$','y':r'$y$','vx':r'$v_x$','vy':r'$v_y$'} #Defaults if not kwargs.has_key('d1') and not kwargs.has_key('d2'): if len(self.orbits[0].vxvv) == 3: d1= 'R' d2= 'vR' elif len(self.orbits[0].vxvv) == 4: d1= 'x' d2= 'y' elif len(self.orbits[0].vxvv) == 2: d1= 'x' d2= 'vx' elif len(self.orbits[0].vxvv) == 5 \ or len(self.orbits[0].vxvv) == 6: d1= 'R' d2= 'z' elif not kwargs.has_key('d1'): d2= kwargs['d2'] kwargs.pop('d2') d1= 't' elif not kwargs.has_key('d2'): d1= kwargs['d1'] kwargs.pop('d1') d2= 't' else: d1= kwargs['d1'] kwargs.pop('d1') d2= kwargs['d2'] kwargs.pop('d2') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/snapshot/Snapshot.py0000644000175100001710000003231314327773303017373 0ustar00runnerdockerimport numpy as nu from directnbody import direct_nbody from galpy.orbit import Orbit from galpy.potential.planarPotential import RZToplanarPotential from galpy.util import plot class Snapshot: """General snapshot = collection of particles class""" def __init__(self,*args,**kwargs): """ NAME: __init__ PURPOSE: initialize a snapshot object INPUT: Initialize using: 1) list of orbits, list of masses (masses=) Coming soon: 2) observations 3) DFs to draw from OUTPUT: HISTORY: 2011-02-02 - Started - Bovy """ if isinstance(args[0],list) and isinstance(args[0][0],Orbit): self.orbits= args[0] if kwargs.has_key('masses'): self.masses= kwargs['masses'] else: self.masses= nu.ones(len(self.orbits)) return None def integrate(self,t,pot=None,method='test-particle', **kwargs): """ NAME: integrate PURPOSE: integrate the snapshot in time INPUT: t - numpy.array of times to save the snapshots at (must start at 0) pot= potential object or list of such objects (default=None) method= method to use ('test-particle' or 'direct-python' for now) OUTPUT: list of snapshots at times t HISTORY: 2011-02-02 - Written - Bovy (NYU) """ if method.lower() == 'test-particle': return self._integrate_test_particle(t,pot) elif method.lower() == 'direct-python': return self._integrate_direct_python(t,pot,**kwargs) def _integrate_test_particle(self,t,pot): """Integrate the snapshot as a set of test particles in an external \ potential""" #Integrate all the orbits for o in self.orbits: o.integrate(t,pot) #Return them as a set of snapshots out= [] for ii in range(len(t)): outOrbits= [] for o in self.orbits: outOrbits.append(o(t[ii])) out.append(Snapshot(outOrbits,self.masses)) return out def _integrate_direct_python(self,t,pot,**kwargs): """Integrate the snapshot using a direct force summation method \ written entirely in python""" #Prepare input for direct_nbody q= [] p= [] nq= len(self.orbits) dim= self.orbits[0].dim() if pot is None: thispot= None elif dim == 2: thispot= RZToplanarPotential(pot) else: thispot= pot for ii in range(nq): #Transform to rectangular frame if dim == 1: thisq= nu.array([self.orbits[ii].x()]).flatten() thisp= nu.array([self.orbits[ii].vx()]).flatten() elif dim == 2: thisq= nu.array([self.orbits[ii].x(), self.orbits[ii].y()]).flatten() thisp= nu.array([self.orbits[ii].vx(), self.orbits[ii].vy()]).flatten() elif dim == 3: thisq= nu.array([self.orbits[ii].x(), self.orbits[ii].y(), self.orbits[ii].z()]).flatten() thisp= nu.array([self.orbits[ii].vx(), self.orbits[ii].vy(), self.orbits[ii].vz()]).flatten() q.append(thisq) p.append(thisp) #Run simulation nbody_out= direct_nbody(q,p,self.masses,t,pot=thispot,**kwargs) #Post-process output nt= len(nbody_out) out= [] for ii in range(nt): snap_orbits= [] for jj in range(nq): if dim == 3: #go back to the cylindrical frame R= nu.sqrt(nbody_out[ii][0][jj][0]**2. +nbody_out[ii][0][jj][1]**2.) phi= nu.arccos(nbody_out[ii][0][jj][0]/R) if nbody_out[ii][0][jj][1] < 0.: phi= 2.*nu.pi-phi vR= nbody_out[ii][1][jj][0]*nu.cos(phi)\ +nbody_out[ii][1][jj][1]*nu.sin(phi) vT= nbody_out[ii][1][jj][1]*nu.cos(phi)\ -nbody_out[ii][1][jj][0]*nu.sin(phi) vxvv= nu.zeros(dim*2) vxvv[3]= nbody_out[ii][0][jj][2] vxvv[4]= nbody_out[ii][1][jj][2] vxvv[0]= R vxvv[1]= vR vxvv[2]= vT vxvv[5]= phi if dim == 2: #go back to the cylindrical frame R= nu.sqrt(nbody_out[ii][0][jj][0]**2. +nbody_out[ii][0][jj][1]**2.) phi= nu.arccos(nbody_out[ii][0][jj][0]/R) if nbody_out[ii][0][jj][1] < 0.: phi= 2.*nu.pi-phi vR= nbody_out[ii][1][jj][0]*nu.cos(phi)\ +nbody_out[ii][1][jj][1]*nu.sin(phi) vT= nbody_out[ii][1][jj][1]*nu.cos(phi)\ -nbody_out[ii][1][jj][0]*nu.sin(phi) vxvv= nu.zeros(dim*2) vxvv[0]= R vxvv[1]= vR vxvv[2]= vT vxvv[3]= phi if dim == 1: vxvv= [nbody_out[ii][0][jj],nbody_out[ii][1][jj]] snap_orbits.append(Orbit(vxvv)) out.append(Snapshot(snap_orbits,self.masses)) return out #Plotting def plot(self,*args,**kwargs): """ NAME: plot PURPOSE: plot the snapshot (with reasonable defaults) INPUT: d1= first dimension to plot ('x', 'y', 'R', 'vR', 'vT', 'z', 'vz', ...) d2= second dimension to plot matplotlib.plot inputs+galpy.util.plot.plot inputs OUTPUT: sends plot to output device HISTORY: 2011-02-06 - Written based on Orbit's plot """ labeldict= {'t':r'$t$','R':r'$R$','vR':r'$v_R$','vT':r'$v_T$', 'z':r'$z$','vz':r'$v_z$','phi':r'$\phi$', 'x':r'$x$','y':r'$y$','vx':r'$v_x$','vy':r'$v_y$'} #Defaults if not kwargs.has_key('d1') and not kwargs.has_key('d2'): if len(self.orbits[0].vxvv) == 3: d1= 'R' d2= 'vR' elif len(self.orbits[0].vxvv) == 4: d1= 'x' d2= 'y' elif len(self.orbits[0].vxvv) == 2: d1= 'x' d2= 'vx' elif len(self.orbits[0].vxvv) == 5 \ or len(self.orbits[0].vxvv) == 6: d1= 'R' d2= 'z' elif not kwargs.has_key('d1'): d2= kwargs['d2'] kwargs.pop('d2') d1= 't' elif not kwargs.has_key('d2'): d1= kwargs['d1'] kwargs.pop('d1') d2= 't' else: d1= kwargs['d1'] kwargs.pop('d1') d2= kwargs['d2'] kwargs.pop('d2') #Get x and y if d1 == 'R': x= [o.R() for o in self.orbits] elif d1 == 'z': x= [o.z() for o in self.orbits] elif d1 == 'vz': x= [o.vz() for o in self.orbits] elif d1 == 'vR': x= [o.vR() for o in self.orbits] elif d1 == 'vT': x= [o.vT() for o in self.orbits] elif d1 == 'x': x= [o.x() for o in self.orbits] elif d1 == 'y': x= [o.y() for o in self.orbits] elif d1 == 'vx': x= [o.vx() for o in self.orbits] elif d1 == 'vy': x= [o.vy() for o in self.orbits] elif d1 == 'phi': x= [o.phi() for o in self.orbits] if d2 == 'R': y= [o.R() for o in self.orbits] elif d2 == 'z': y= [o.z() for o in self.orbits] elif d2 == 'vz': y= [o.vz() for o in self.orbits] elif d2 == 'vR': y= [o.vR() for o in self.orbits] elif d2 == 'vT': y= [o.vT() for o in self.orbits] elif d2 == 'x': y= [o.x() for o in self.orbits] elif d2 == 'y': y= [o.y() for o in self.orbits] elif d2 == 'vx': y= [o.vx() for o in self.orbits] elif d2 == 'vy': y= [o.vy() for o in self.orbits] elif d2 == 'phi': y= [o.phi() for o in self.orbits] #Plot if not kwargs.has_key('xlabel'): kwargs['xlabel']= labeldict[d1] if not kwargs.has_key('ylabel'): kwargs['ylabel']= labeldict[d2] if len(args) == 0: args= (',',) plot.plot(x,y,*args,**kwargs) def plot3d(self,*args,**kwargs): """ NAME: plot3d PURPOSE: plot the snapshot in 3D (with reasonable defaults) INPUT: d1= first dimension to plot ('x', 'y', 'R', 'vR', 'vT', 'z', 'vz', ...) d2= second dimension to plot d3= third dimension to plot matplotlib.plot inputs+galpy.util.plot.plot3d inputs OUTPUT: sends plot to output device HISTORY: 2011-02-06 - Written based on Orbit's plot3d """ labeldict= {'t':r'$t$','R':r'$R$','vR':r'$v_R$','vT':r'$v_T$', 'z':r'$z$','vz':r'$v_z$','phi':r'$\phi$', 'x':r'$x$','y':r'$y$','vx':r'$v_x$','vy':r'$v_y$'} #Defaults if not kwargs.has_key('d1') and not kwargs.has_key('d2') \ and not kwargs.has_key('d3'): if len(self.orbits[0].vxvv) == 3: d1= 'R' d2= 'vR' d3= 'vT' elif len(self.orbits[0].vxvv) == 4: d1= 'x' d2= 'y' d3= 'vR' elif len(self.orbits[0].vxvv) == 2: raise AttributeError("Cannot plot 3D aspects of 1D orbits") elif len(self.orbits[0].vxvv) == 5: d1= 'R' d2= 'vR' d3= 'z' elif len(self.orbits[0].vxvv) == 6: d1= 'x' d2= 'y' d3= 'z' elif not (kwargs.has_key('d1') and kwargs.has_key('d2') \ and kwargs.has_key('d3')): raise AttributeError("Please provide 'd1', 'd2', and 'd3'") else: d1= kwargs['d1'] kwargs.pop('d1') d2= kwargs['d2'] kwargs.pop('d2') d3= kwargs['d3'] kwargs.pop('d3') #Get x, y, and z if d1 == 'R': x= [o.R() for o in self.orbits] elif d1 == 'z': x= [o.z() for o in self.orbits] elif d1 == 'vz': x= [o.vz() for o in self.orbits] elif d1 == 'vR': x= [o.vR() for o in self.orbits] elif d1 == 'vT': x= [o.vT() for o in self.orbits] elif d1 == 'x': x= [o.x() for o in self.orbits] elif d1 == 'y': x= [o.y() for o in self.orbits] elif d1 == 'vx': x= [o.vx() for o in self.orbits] elif d1 == 'vy': x= [o.vy() for o in self.orbits] elif d1 == 'phi': x= [o.phi() for o in self.orbits] if d2 == 'R': y= [o.R() for o in self.orbits] elif d2 == 'z': y= [o.z() for o in self.orbits] elif d2 == 'vz': y= [o.vz() for o in self.orbits] elif d2 == 'vR': y= [o.vR() for o in self.orbits] elif d2 == 'vT': y= [o.vT() for o in self.orbits] elif d2 == 'x': y= [o.x() for o in self.orbits] elif d2 == 'y': y= [o.y() for o in self.orbits] elif d2 == 'vx': y= [o.vx() for o in self.orbits] elif d2 == 'vy': y= [o.vy() for o in self.orbits] elif d2 == 'phi': y= [o.phi() for o in self.orbits] if d3 == 'R': z= [o.R() for o in self.orbits] elif d3 == 'z': z= [o.z() for o in self.orbits] elif d3 == 'vz': z= [o.vz() for o in self.orbits] elif d3 == 'vR': z= [o.vR() for o in self.orbits] elif d3 == 'vT': z= [o.vT() for o in self.orbits] elif d3 == 'x': z= [o.x() for o in self.orbits] elif d3 == 'y': z= [o.y() for o in self.orbits] elif d3 == 'vx': z= [o.vx() for o in self.orbits] elif d3 == 'vy': z= [o.vy() for o in self.orbits] elif d3 == 'phi': z= [o.phi() for o in self.orbits] #Plot if not kwargs.has_key('xlabel'): kwargs['xlabel']= labeldict[d1] if not kwargs.has_key('ylabel'): kwargs['ylabel']= labeldict[d2] if not kwargs.has_key('zlabel'): kwargs['zlabel']= labeldict[d3] if len(args) == 0: args= (',',) plot.plot3d(x,y,z,*args,**kwargs) #Pickling def __getstate__(self): return (self.orbits, self.masses) def __setstate__(self,state): self.orbits= state[0] self.masses= state[1] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/snapshot/__init__.py0000644000175100001710000000022314327773303017326 0ustar00runnerdockerfrom . import Snapshot, snapshotMovies # # Functions # snapshotToMovie= snapshotMovies.snapshotToMovie # # Classes # Snapshot= Snapshot.Snapshot ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/snapshot/directnbody.py0000644000175100001710000001102614327773303020100 0ustar00runnerdocker#Direct force summation N-body code import numpy as nu from numpy import linalg from galpy.potential.linearPotential import evaluatelinearForces from galpy.potential.planarPotential import (evaluateplanarphitorques, evaluateplanarRforces) from galpy.potential.Potential import (evaluatephitorques, evaluateRforces, evaluatezforces) from galpy.util import symplecticode def direct_nbody(q,p,m,t,pot=None,softening_model='plummer', softening_length=None, atol=None,rtol=None): """ NAME: direct_nbody PURPOSE: N-body code using direct summation for force evaluation INPUT: q - list of initial positions (numpy.ndarrays) p - list of initial momenta (numpy.ndarrays) m - list of masses t - times at which output is desired pot= external potential (galpy.potential or list of galpy.potentials) softening_model= type of softening to use ('plummer') softening_length= (optional) OUTPUT: list of [q,p] at times t HISTORY: 2011-02-03 - Written - Bovy (NYU) """ #Set up everything if softening_model.lower() == 'plummer': softening= _plummer_soft out= [] out.append([q,p]) qo= q po= p #Determine appropriate stepsize dt= t[1]-t[0] ndt= 1 to= t[0] #determine appropriate softening length if not given if softening_length is None: softening_length= 0.01 #Run simulation for ii in range(1,len(t)): print(ii) for jj in range(ndt): #loop over number of sub-intervals (qo,po)= _direct_nbody_step(qo,po,m,to,dt,pot, softening,(softening_length,)) #print(qo) to+= dt out.append([qo,po]) #Return output return out def _direct_nbody_step(q,p,m,t,dt,pot,softening,softening_args): """One N-body step: drift-kick-drift""" #drift q12= [symplecticode.leapfrog_leapq(q[ii],p[ii],dt/2.) \ for ii in range(len(q))] #kick force= _direct_nbody_force(q12,m,t+dt/2.,pot,softening,softening_args) #print(force) p= [symplecticode.leapfrog_leapp(p[ii],dt,force[ii]) \ for ii in range(len(p))] #drift q= [symplecticode.leapfrog_leapq(q12[ii],p[ii],dt/2.) \ for ii in range(len(q12))] return (q,p) def _direct_nbody_force(q,m,t,pot,softening,softening_args): """Calculate the force""" #First do the particles #Calculate all the distances nq= len(q) dim= len(q[0]) dist_vec= nu.zeros((nq,nq,dim)) dist= nu.zeros((nq,nq)) for ii in range(nq): for jj in range(ii+1,nq): dist_vec[ii,jj,:]= q[jj]-q[ii] dist_vec[jj,ii,:]= -dist_vec[ii,jj,:] dist[ii,jj]= linalg.norm(dist_vec[ii,jj,:]) dist[jj,ii]= dist[ii,jj] #Calculate all the forces force= [] for ii in range(nq): thisforce= nu.zeros(dim) for jj in range(nq): if ii == jj: continue thisforce+= m[jj]*softening(dist[ii,jj],*softening_args)\ /dist[ii,jj]*dist_vec[ii,jj,:] force.append(thisforce) #Then add the external force if pot is None: return force for ii in range(nq): force[ii]+= _external_force(q[ii],t,pot) return force def _external_force(x,t,pot): dim= len(x) if dim == 3: #x is rectangular so calculate R and phi R= nu.sqrt(x[0]**2.+x[1]**2.) phi= nu.arccos(x[0]/R) sinphi= x[1]/R cosphi= x[0]/R if x[1] < 0.: phi= 2.*nu.pi-phi #calculate forces Rforce= evaluateRforces(R,x[2],pot,phi=phi,t=t) phitorque= evaluatephitorques(R,x[2],pot,phi=phi,t=t) return nu.array([cosphi*Rforce-1./R*sinphi*phitorque, sinphi*Rforce+1./R*cosphi*phitorque, evaluatezforces(R,x[2],pot,phi=phi,t=t)]) elif dim == 2: #x is rectangular so calculate R and phi R= nu.sqrt(x[0]**2.+x[1]**2.) phi= nu.arccos(x[0]/R) sinphi= x[1]/R cosphi= x[0]/R if x[1] < 0.: phi= 2.*nu.pi-phi #calculate forces Rforce= evaluateplanarRforces(R,pot,phi=phi,t=t) phitorque= evaluateplanarphitorques(R,pot,phi=phi,t=t) return nu.array([cosphi*Rforce-1./R*sinphi*phitorque, sinphi*Rforce+1./R*cosphi*phitorque]) elif dim == 1: return evaluatelinearForces(x,pot,t=t) def _plummer_soft(d,eps): return d/(d**2.+eps**2.)**1.5 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/snapshot/nemo_util.py0000644000175100001710000000366414327773303017576 0ustar00runnerdocker############################################################################### # nemo_util.py: some utilities for handling NEMO snapshots ############################################################################### import os import subprocess import tempfile import numpy def read(filename,ext=None,swapyz=False): """ NAME: read PURPOSE: read a NEMO snapshot file consisting of mass,position,velocity INPUT: filename - name of the file ext= if set, 'nemo' for NEMO binary format, otherwise assumed ASCII; if not set, gleaned from extension swapyz= (False) if True, swap the y and z axes in the output (only for position and velocity) OUTPUT: snapshots [nbody,ndim,nt] HISTORY: 2015-11-18 - Written - Bovy (UofT) """ if ext is None and filename.split('.')[-1] == 'nemo': ext= 'nemo' elif ext is None: ext= 'dat' # Convert to ASCII if necessary if ext.lower() == 'nemo': file_handle, asciifilename= tempfile.mkstemp() os.close(file_handle) stderr= open('/dev/null','w') try: subprocess.check_call(['s2a',filename,asciifilename])#,stderr=stderr) except subprocess.CalledProcessError: os.remove(asciifilename) finally: stderr.close() else: asciifilename= filename # Now read out= numpy.loadtxt(asciifilename,comments='#') if ext.lower() == 'nemo': os.remove(asciifilename) if swapyz: out[:,[2,3]]= out[:,[3,2]] out[:,[5,6]]= out[:,[6,5]] # Get the number of snapshots nt= (_wc(asciifilename)-out.shape[0])//13 # 13 comments/snapshot out= numpy.reshape(out,(nt,out.shape[0]//nt,out.shape[1])) return numpy.swapaxes(numpy.swapaxes(out,0,1),1,2) def _wc(filename): try: return int(subprocess.check_output(['wc','-l',filename]).split()[0]) except subprocess.CalledProcessError: return numpy.nan ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/snapshot/snapshotMovies.py0000644000175100001710000001031714327773303020616 0ustar00runnerdocker#Functions to turn snapshots into movies import math as m import os import os.path import re import shutil import subprocess import tempfile from Snapshot import * import galpy.util.plot as galpy_plot def snapshotToMovie(snap,filename,*args,**kwargs): """ NAME: snapshotToMovie PURPOSE: turn a list of snapshots into a movie INPUT: snap - the snapshots (list) filename - name of the file to save the movie to framerate= in fps bitrate= ? thumbnail=False : create thumbnail image (filename-extension+.jpg) thumbsize= size of thumbnail +Snapshot.plot args and kwargs OUTPUT: movie is saved to file DEPENDENCIES: this procedure uses ffmpeg and convert BUGS: matplotlib's 'Agg' backend has a memory leak that prevents it from creating hundred's of figures. It is recommended to call import matplotlib matplotlib.use('PDF') at the beginning of the movie creating script as the PDF backend does not have the same memory leak. HISTORY: 2011-02-06 - Written - Bovy (NYU) """ if kwargs.has_key('tmpdir'): tmpdir= kwargs['tmpdir'] kwargs.pop('tmpdir') else: tmpdir= '/tmp' if kwargs.has_key('framerate'): framerate= kwargs['framerate'] kwargs.pop('framerate') else: framerate= 25 if kwargs.has_key('bitrate'): bitrate= kwargs['bitrate'] kwargs.pop('bitrate') else: bitrate= 1000 if kwargs.has_key('thumbnail') and kwargs['thumbnail']: thumbnail= True kwargs.pop('thumbnail') elif kwargs.has_key('thumbnail'): kwargs.pop('thumbnail') thumbnail= False else: thumbnail= False if kwargs.has_key('thumbsize'): thumbsize= kwargs['thumbsize'] else: thumbsize= 300 #Create all of the files tempdir= tempfile.mkdtemp(dir=tmpdir) #Temporary directory tmpfiles= [] nsnap= len(snap) file_length= int(m.ceil(m.log10(nsnap))) #Determine good xrange BOVY TO DO if not kwargs.has_key('xrange'): pass if not kwargs.has_key('yrange'): pass for ii in range(nsnap): tmpfiles.append(os.path.join(tempdir, str(ii).zfill(file_length))) galpy_plot.print() snap[ii].plot(*args,**kwargs) galpy_plot.end_print(tmpfiles[ii]+'.pdf') #Convert to jpeg try: subprocess.check_call(['convert', tmpfiles[ii]+'.pdf', tmpfiles[ii]+'.jpg']) except subprocess.CalledProcessError: print("'convert' failed") raise subprocess.CalledProcessError #turn them into a movie try: subprocess.check_call(['ffmpeg', '-r',str(framerate), '-b', str(bitrate), '-i', os.path.join(tempdir, '%'+'0%id.jpg' % file_length), '-y', filename]) if thumbnail: thumbnameTemp= re.split(r'\.',filename) thumbnameTemp= thumbnameTemp[0:len(thumbnameTemp)-1] thumbname= '' for t in thumbnameTemp: thumbname+= t thumbname+= '.jpg' subprocess.check_call(['ffmpeg', '-itsoffset','-4','-y', '-i',filename, '-vcodec', 'mjpeg', '-vframes','1', '-an', '-f', 'rawvideo', '-s', '%ix%i' % (thumbsize,thumbsize), thumbname]) except subprocess.CalledProcessError: print("'ffmpeg' failed") _cleanupMovieTempdir(tempdir) raise subprocess.CalledProcessError finally: _cleanupMovieTempdir(tempdir) def _cleanupMovieTempdir(tempdir): shutil.rmtree(tempdir) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667233486.721467 galpy-1.8.1/galpy/util/0000755000175100001710000000000014327773317014343 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/__init__.py0000644000175100001710000001225614327773303016455 0ustar00runnerdockerimport os import pickle import shutil import tempfile import warnings import numpy import scipy.linalg as linalg from ..util.config import __config__ _SHOW_WARNINGS= __config__.getboolean('warnings','verbose') class galpyWarning(Warning): pass # galpy warnings only shown if verbose = True in the configuration class galpyWarningVerbose(galpyWarning): pass def _warning( message, category=galpyWarning, filename='', lineno=-1, file=None, line=None): if issubclass(category,galpyWarning): if not issubclass(category,galpyWarningVerbose) or _SHOW_WARNINGS: print("galpyWarning: "+str(message)) else: print(warnings.formatwarning(message,category,filename,lineno)) warnings.showwarning = _warning def save_pickles(savefilename,*args,**kwargs): """ NAME: save_pickles PURPOSE: relatively safely save things to a pickle INPUT: savefilename - name of the file to save to; actual save operation will be performed on a temporary file and then that file will be shell mv'ed to savefilename +things to pickle (as many as you want!) OUTPUT: none HISTORY: 2010-? - Written - Bovy (NYU) 2011-08-23 - generalized and added to galpy.util - Bovy (NYU) """ saving= True interrupted= False file, tmp_savefilename= tempfile.mkstemp() #savefilename+'.tmp' os.close(file) #Easier this way while saving: try: savefile= open(tmp_savefilename,'wb') file_open= True if kwargs.get('testKeyboardInterrupt',False) and not interrupted: raise KeyboardInterrupt for f in args: pickle.dump(f,savefile,pickle.HIGHEST_PROTOCOL) savefile.close() file_open= False shutil.move(tmp_savefilename,savefilename) saving= False if interrupted: raise KeyboardInterrupt except KeyboardInterrupt: if not saving: raise print("KeyboardInterrupt ignored while saving pickle ...") interrupted= True finally: if file_open: savefile.close() def logsumexp(arr,axis=0): """Faster logsumexp?""" minarr= numpy.amax(arr,axis=axis) if axis == 1: minarr= numpy.reshape(minarr,(arr.shape[0],1)) if axis == 0: minminarr= numpy.tile(minarr,(arr.shape[0],1)) elif axis == 1: minminarr= numpy.tile(minarr,(1,arr.shape[1])) elif axis == None: minminarr= numpy.tile(minarr,arr.shape) else: raise NotImplementedError("'galpy.util.logsumexp' not implemented for axis > 2") if axis == 1: minarr= numpy.reshape(minarr,(arr.shape[0])) return minarr+numpy.log(numpy.sum(numpy.exp(arr-minminarr),axis=axis)) _TINY= 0.000000001 def stable_cho_factor(x,tiny=_TINY): """ NAME: stable_cho_factor PURPOSE: Stable version of the cholesky decomposition INPUT: x - (sc.array) positive definite matrix tiny - (double) tiny number to add to the covariance matrix to make the decomposition stable (has a default) OUTPUT: (L,lowerFlag) - output from scipy.linalg.cho_factor for lower=True REVISION HISTORY: 2009-09-25 - Written - Bovy (NYU) """ return linalg.cho_factor(x+numpy.sum(numpy.diag(x))*tiny*numpy.eye(x.shape[0]),lower=True) def fast_cholesky_invert(A,logdet=False,tiny=_TINY): """ NAME: fast_cholesky_invert PURPOSE: invert a positive definite matrix by using its Cholesky decomposition INPUT: A - matrix to be inverted logdet - (Bool) if True, return the logarithm of the determinant as well tiny - (double) tiny number to add to the covariance matrix to make the decomposition stable (has a default) OUTPUT: A^{-1} REVISION HISTORY: 2009-10-07 - Written - Bovy (NYU) """ L= stable_cho_factor(A,tiny=tiny) if logdet: return (linalg.cho_solve(L,numpy.eye(A.shape[0])), 2.*numpy.sum(numpy.log(numpy.diag(L[0])))) else: return linalg.cho_solve(L,numpy.eye(A.shape[0])) def _rotate_to_arbitrary_vector(v,a,inv=False,_dontcutsmall=False): r""" Return a rotation matrix that rotates v to align with unit vector a i.e. R . v = |v|\hat{a} """ normv= v/numpy.tile(numpy.sqrt(numpy.sum(v**2.,axis=1)),(3,1)).T rotaxis= numpy.cross(normv,a) rotaxis/= numpy.tile(numpy.sqrt(numpy.sum(rotaxis**2.,axis=1)),(3,1)).T crossmatrix= numpy.empty((len(v),3,3)) crossmatrix[:,0,:]= numpy.cross(rotaxis,[1,0,0]) crossmatrix[:,1,:]= numpy.cross(rotaxis,[0,1,0]) crossmatrix[:,2,:]= numpy.cross(rotaxis,[0,0,1]) costheta= numpy.dot(normv,a) sintheta= numpy.sqrt(1.-costheta**2.) if inv: sgn= 1. else: sgn= -1. out= numpy.tile(costheta,(3,3,1)).T*numpy.tile(numpy.eye(3),(len(v),1,1))\ +sgn*numpy.tile(sintheta,(3,3,1)).T*crossmatrix\ +numpy.tile(1.-costheta,(3,3,1)).T\ *(rotaxis[:,:,numpy.newaxis]*rotaxis[:,numpy.newaxis,:]) if not _dontcutsmall: out[numpy.fabs(costheta-1.) < 10.**-10.]= numpy.eye(3) out[numpy.fabs(costheta+1.) < 10.**-10.]= -numpy.eye(3) return out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/_load_extension_libs.py0000644000175100001710000000662614327773303021105 0ustar00runnerdocker# _load_extension_libs.py: centralized place to load the C extensions import ctypes import os import sys import sysconfig import warnings from ..util import galpyWarning, galpyWarningVerbose PY3= sys.version > '3' if PY3: _ext_suffix= sysconfig.get_config_var('EXT_SUFFIX') else: #pragma: no cover _ext_suffix= '.so' _libgalpy= None _libgalpy_loaded= None _libgalpy_actionAngleTorus= None _libgalpy_actionAngleTorus_loaded= None def load_libgalpy(): global _libgalpy global _libgalpy_loaded if _libgalpy_loaded is False or not _libgalpy is None: return (_libgalpy,_libgalpy_loaded) outerr= None for path in sys.path: if not os.path.isdir(path): continue try: if sys.platform == 'win32' and sys.version_info >= (3,8): # pragma: no cover # winmode=0x008 is easy-going way to call LoadLibraryExA _lib = ctypes.CDLL(os.path.join(path,'libgalpy%s' % _ext_suffix),winmode=0x008) else: _lib = ctypes.CDLL(os.path.join(path,'libgalpy%s' % _ext_suffix)) except OSError as e: if os.path.exists(os.path.join(path,'libgalpy%s' % _ext_suffix)): #pragma: no cover outerr= e _lib = None else: break if _lib is None: #pragma: no cover if not outerr is None: warnings.warn("libgalpy C extension module not loaded, because of error '%s' " % outerr, galpyWarning) else: warnings.warn("libgalpy C extension module not loaded, because libgalpy%s image was not found" % _ext_suffix, galpyWarning) _libgalpy_loaded= False else: _libgalpy_loaded= True _libgalpy= _lib return (_libgalpy,_libgalpy_loaded) def load_libgalpy_actionAngleTorus(): global _libgalpy_actionAngleTorus global _libgalpy_actionAngleTorus_loaded if _libgalpy_actionAngleTorus_loaded is False \ or not _libgalpy_actionAngleTorus is None: return (_libgalpy_actionAngleTorus,_libgalpy_actionAngleTorus_loaded) outerr= None for path in sys.path: if not os.path.isdir(path): continue try: if sys.platform == 'win32' and sys.version_info >= (3,8): # pragma: no cover # winmode=0x008 is easy-going way to call LoadLibraryExA _lib = ctypes.CDLL(os.path.join(path,'libgalpy_actionAngleTorus%s' % _ext_suffix),winmode=0x008) else: _lib = ctypes.CDLL(os.path.join(path,'libgalpy_actionAngleTorus%s' % _ext_suffix)) except OSError as e: if os.path.exists(os.path.join(path,'libgalpy_actionAngleTorus%s' % _ext_suffix)): #pragma: no cover outerr= e _lib = None else: break if _lib is None: #pragma: no cover if not outerr is None: warnings.warn("libgalpy_actionAngleTorus C extension module not loaded, because of error '%s' " % outerr, galpyWarningVerbose) else: warnings.warn("libgalpy_actionAngleTorus C extension module not loaded, because libgalpy%s image was not found" % _ext_suffix, galpyWarningVerbose) _libgalpy_actionAngleTorus_loaded= False else: _libgalpy_actionAngleTorus_loaded= True _libgalpy_actionAngleTorus= _lib return (_libgalpy_actionAngleTorus,_libgalpy_actionAngleTorus_loaded) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/_optional_deps.py0000644000175100001710000000255414327773303017715 0ustar00runnerdocker# Central place to process optional dependencies from packaging.version import parse as parse_version # astropy from ..util.config import __config__ _APY_UNITS= __config__.getboolean('astropy','astropy-units') _APY_LOADED= True _APY3= None _APY_GE_31= None try: from astropy import constants, units except ImportError: _APY_UNITS= False _APY_LOADED= False else: import astropy _APY3= parse_version(astropy.__version__) > parse_version('3') _APY_GE_31= parse_version(astropy.__version__) > parse_version('3.0.5') _APY_COORD_LOADED= True try: from astropy.coordinates import SkyCoord except ImportError: SkyCoord = None _APY_COORD_LOADED= False # astroquery _ASTROQUERY_LOADED= True try: from astroquery.simbad import Simbad except ImportError: _ASTROQUERY_LOADED= False # numexpr _NUMEXPR_LOADED= True try: import numexpr except ImportError: # pragma: no cover _NUMEXPR_LOADED= True # tqdm _TQDM_LOADED= True try: import tqdm except ImportError: #pragma: no cover _TQDM_LOADED= False # numba _NUMBA_LOADED= True try: from numba import cfunc, types except ImportError: _NUMBA_LOADED= False # jax _JAX_LOADED= True try: from jax import grad, vmap except ImportError: _JAX_LOADED= False # pynbody _PYNBODY_LOADED= True try: import pynbody except ImportError: #pragma: no cover _PYNBODY_LOADED= False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/ars.py0000644000175100001710000003337414327773303015507 0ustar00runnerdocker############################################################################# #Copyright (c) 2011, Jo Bovy #All rights reserved. # #Redistribution and use in source and binary forms, with or without #modification, are permitted provided that the following conditions are met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # #THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS #"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT #LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR #A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT #HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, #INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, #BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS #OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED #AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT #LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY #WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE #POSSIBILITY OF SUCH DAMAGE. ############################################################################# import numpy import scipy.stats as stats #TO DO: #Throw errors in the sample_hull routine def ars(domain,isDomainFinite,abcissae,hx,hpx,nsamples=1, hxparams=(),maxn=100): """ars: Implementation of the Adaptive-Rejection Sampling algorithm by Gilks & Wild (1992): Adaptive Rejection Sampling for Gibbs Sampling, Applied Statistics, 41, 337 Based on Wild & Gilks (1993), Algorithm AS 287: Adaptive Rejection Sampling from Log-concave Density Functions, Applied Statistics, 42, 701 Input: domain - [.,.] upper and lower limit to the domain isDomainFinite - [.,.] is there a lower/upper limit to the domain? abcissae - initial list of abcissae (must lie on either side of the peak in hx if the domain is unbounded hx - function that evaluates h(x) = ln g(x) hpx - function that evaluates hp(x) = d h(x) / d x nsamples - (optional) number of desired samples (default=1) hxparams - (optional) a tuple of parameters for h(x) and h'(x) maxn - (optional) maximum number of updates to the hull (default=100) Output: list with nsamples of samples from exp(h(x)) External dependencies: math scipy scipy.stats History: 2009-05-21 - Written - Bovy (NYU) """ #First set-up the upper and lower hulls hull=setup_hull(domain,isDomainFinite,abcissae,hx,hpx,hxparams) #Then start sampling: call sampleone repeatedly out= [] nupdates= 0 for ii in range(int(nsamples)): thissample, hull, nupdates= sampleone(hull,hx,hpx,domain,isDomainFinite,maxn,nupdates,hxparams) out.append(thissample) return out def setup_hull(domain,isDomainFinite,abcissae,hx,hpx,hxparams): """setup_hull: set up the upper and lower hull and everything that comes with that Input: domain - [.,.] upper and lower limit to the domain isDomainFinite - [.,.] is there a lower/upper limit to the domain? abcissae - initial list of abcissae (must lie on either side of the peak in hx if the domain is unbounded hx - function that evaluates h(x) hpx - function that evaluates hp(x) hxparams - tuple of parameters for h(x) and h'(x) Output: list with: [0]= c_u [1]= xs [2]= h(xs) [3]= hp(xs) [4]= zs [5]= s_cum [6]= hu(zi) History: 2009-05-21 - Written - Bovy (NYU) """ nx= len(abcissae) #Create the output arrays xs= numpy.zeros(nx) hxs= numpy.zeros(nx) hpxs= numpy.zeros(nx) zs= numpy.zeros(nx-1) scum= numpy.zeros(nx-1) hus= numpy.zeros(nx-1) #Function evaluations xs= numpy.sort(abcissae) for ii in range(nx): hxs[ii]= hx(xs[ii],hxparams) hpxs[ii]= hpx(xs[ii],hxparams) #THERE IS NO CHECKING HERE TO SEE WHETHER IN THE INFINITE DOMAIN CASE #WE HAVE ABCISSAE ON BOTH SIDES OF THE PEAK #zi for jj in range(nx-1): zs[jj]= (hxs[jj+1]-hxs[jj]-xs[jj+1]*hpxs[jj+1]+xs[jj]*hpxs[jj])/( hpxs[jj]-hpxs[jj+1]) #hu for jj in range(nx-1): hus[jj]= hpxs[jj]*(zs[jj]-xs[jj])+hxs[jj] #Calculate cu and scum if isDomainFinite[0]: scum[0]= 1./hpxs[0]*(numpy.exp(hus[0])-numpy.exp( hpxs[0]*(domain[0]-xs[0])+hxs[0])) else: scum[0]= 1./hpxs[0]*numpy.exp(hus[0]) if nx > 2: for jj in range(nx-2): if hpxs[jj+1] == 0.: scum[jj+1]= (zs[jj+1]-zs[jj])*numpy.exp(hxs[jj+1]) else: scum[jj+1]=1./hpxs[jj+1]*(numpy.exp(hus[jj+1])-numpy.exp(hus[jj])) if isDomainFinite[1]: cu=1./hpxs[nx-1]*(numpy.exp(hpxs[nx-1]*( domain[1]-xs[nx-1])+hxs[nx-1]) - numpy.exp(hus[nx-2])) else: cu=- 1./hpxs[nx-1]*numpy.exp(hus[nx-2]) cu= cu+numpy.sum(scum) scum= numpy.cumsum(scum)/cu out=[] out.append(cu) out.append(xs) out.append(hxs) out.append(hpxs) out.append(zs) out.append(scum) out.append(hus) return out def sampleone(hull,hx,hpx,domain,isDomainFinite,maxn,nupdates,hxparams): """sampleone: sample one point by ars Input: hull - the hull (see doc of setup_hull for definition) hx - function that evaluates h(x) hpx - function that evaluates hp(x) domain - [.,.] upper and lower limit to the domain isDomainFinite - [.,.] is there a lower/upper limit to the domain? maxn - maximum number of updates to the hull nupdates - number of updates to the hull that have occurred hxparams - tuple of parameters for h(x) and h'(x) Output: a sample a new hull nupdates History: 2009-05-21 - Written - Bovy (NYU) """ thishull= hull noSampleYet= True while noSampleYet: #Sample a candidate from the upper hull candidate= sample_hull(thishull,domain,isDomainFinite) thishux, thishlx= evaluate_hull(candidate,thishull) u= stats.uniform.rvs() if u < numpy.exp(thishlx-thishux): thissample= candidate noSampleYet= False else: thishx= hx(candidate,hxparams) if u < numpy.exp(thishx-thishux): thissample= candidate noSampleYet= False if nupdates < maxn: thishpx= hpx(candidate,hxparams) thishull= update_hull(thishull,candidate,thishx,thishpx, domain,isDomainFinite) nupdates= nupdates+1 return thissample, thishull, nupdates def sample_hull(hull,domain,isDomainFinite): """sample_hull: Sample the upper hull Input: hull - hull structure (see setup_hull for a definition of this) domain - [.,.] upper and lower limit to the domain isDomainFinite - [.,.] is there a lower/upper limit to the domain? Output: a sample from the hull History: 2009-05-21 - Written - Bovy """ u= stats.uniform.rvs() #Find largest zs[jj] such that scum[jj] < u #The first bin is a special case if hull[5][0] >= u: if hull[3][0] == 0: if isDomainFinite[0]: thissample= domain[0]+u/hull[5][0]*(hull[4][0]-domain[0]) else: thissample= 100000000 #Throw some kind of error else: thissample= hull[4][0]+1./hull[3][0]*numpy.log(1.-hull[3][0]*hull[0]*(hull[5][0]-u)/numpy.exp(hull[6][0])) else: if len(hull[5]) == 1: indx= 0 else: indx= 1 while indx < len(hull[5]) and hull[5][indx] < u: indx= indx+1 indx= indx-1 if numpy.fabs(hull[3][indx+1]) == 0: if indx != (len(hull[5])-1): thissample= hull[4][indx]+(u-hull[5][indx])/(hull[5][indx+1]-hull[5][indx])*(hull[4][indx+1]-hull[4][indx]) else: if isDomainFinite[1]: thissample= hull[4][indx]+(u-hull[5][indx])/(1.-hull[5][indx])*(domain[1]-hull[4][indx]) else: thissample= 100000 #Throw some kind of error else: thissample= hull[4][indx]+1./hull[3][indx+1]*numpy.log(1.+hull[3][indx+1]*hull[0]*(u-hull[5][indx])/numpy.exp(hull[6][indx])) return thissample def evaluate_hull(x,hull): """evaluate_hull: evaluate h_u(x) and (optional) h_l(x) Input: x - abcissa hull - the hull (see setup_hull for a definition) Output: hu(x) (optional), hl(x) History: 2009-05-21 - Written - Bovy (NYU) """ #Find in which [z_{i-1},z_i] interval x lies if x < hull[4][0]: #x lies in the first interval hux= hull[3][0]*(x-hull[1][0])+hull[2][0] indx= 0 else: if len(hull[5]) == 1: #There are only two intervals indx= 1 else: indx= 1 while indx < len(hull[4]) and hull[4][indx] < x: indx= indx+1 indx= indx-1 hux= hull[3][indx]*(x-hull[1][indx])+hull[2][indx] #Now evaluate hlx neginf= numpy.finfo(numpy.dtype(numpy.float64)).min if x < hull[1][0] or x > hull[1][-1]: hlx= neginf else: if indx == 0: hlx= ((hull[1][1]-x)*hull[2][0]+(x-hull[1][0])*hull[2][1])/(hull[1][1]-hull[1][0]) elif indx == len(hull[4]): hlx= ((hull[1][-1]-x)*hull[2][-2]+(x-hull[1][-2])*hull[2][-1])/(hull[1][-1]-hull[1][-2]) elif x < hull[1][indx+1]: hlx= ((hull[1][indx+1]-x)*hull[2][indx]+(x-hull[1][indx])*hull[2][indx+1])/(hull[1][indx+1]-hull[1][indx]) else: hlx= ((hull[1][indx+2]-x)*hull[2][indx+1]+(x-hull[1][indx+1])*hull[2][indx+2])/(hull[1][indx+2]-hull[1][indx+1]) return hux, hlx def update_hull(hull,newx,newhx,newhpx,domain,isDomainFinite): """update_hull: update the hull with a new function evaluation Input: hull - the current hull (see setup_hull for a definition) newx - a new abcissa newhx - h(newx) newhpx - hp(newx) domain - [.,.] upper and lower limit to the domain isDomainFinite - [.,.] is there a lower/upper limit to the domain? Output: newhull History: 2009-05-21 - Written - Bovy (NYU) """ #BOVY: Perhaps add a check that newx is sufficiently far from any existing point #Find where newx fits in with the other xs if newx > hull[1][-1]: newxs= numpy.append(hull[1],newx) newhxs= numpy.append(hull[2],newhx) newhpxs= numpy.append(hull[3],newhpx) #new z newz= ( newhx - hull[2][-1] - newx*newhpx + hull[1][-1]*hull[3][-1])/( hull[3][-1] - newhpx) newzs= numpy.append(hull[4],newz) #New hu newhu= hull[3][-1]*(newz-hull[1][-1]) + hull[2][-1] newhus= numpy.append(hull[6],newhu) else: indx= 0 while newx > hull[1][indx]: indx=indx+1 newxs= numpy.insert(hull[1],indx,newx) newhxs= numpy.insert(hull[2],indx,newhx) newhpxs= numpy.insert(hull[3],indx,newhpx) #Replace old z with new zs if newx < hull[1][0]: newz= (hull[2][0]-newhx-hull[1][0]*hull[3][0]+newx*newhpx)/(newhpx-hull[3][0]) newzs= numpy.insert(hull[4],0,newz) #Also add the new hu newhu= newhpx*(newz-newx)+newhx newhus= numpy.insert(hull[6],0,newhu) else: newz1= (newhx-hull[2][indx-1] - newx*newhpx+hull[1][indx-1]*hull[3][indx-1])/(hull[3][indx-1]-newhpx) newz2= (hull[2][indx]-newhx - hull[1][indx]*hull[3][indx]+newx*newhpx)/(newhpx-hull[3][indx]) #Insert newz1 and replace z_old newzs= numpy.insert(hull[4],indx-1,newz1) newzs[indx]= newz2 #Update the hus newhu1= hull[3][indx-1]*(newz1-hull[1][indx-1])+hull[2][indx-1] newhu2= newhpx*(newz2-newx)+newhx newhus= numpy.insert(hull[6],indx-1,newhu1) newhus[indx]= newhu2 #Recalculate the cumulative sum nx= len(newxs) newscum= numpy.zeros(nx-1) if isDomainFinite[0]: newscum[0]= 1./newhpxs[0]*(numpy.exp(newhus[0])-numpy.exp( newhpxs[0]*(domain[0]-newxs[0])+newhxs[0])) else: newscum[0]= 1./newhpxs[0]*numpy.exp(newhus[0]) if nx > 2: for jj in range(nx-2): if newhpxs[jj+1] == 0.: newscum[jj+1]= (newzs[jj+1]-newzs[jj])*numpy.exp(newhxs[jj+1]) else: newscum[jj+1]=1./newhpxs[jj+1]*(numpy.exp(newhus[jj+1])-numpy.exp(newhus[jj])) if isDomainFinite[1]: newcu=1./newhpxs[nx-1]*(numpy.exp(newhpxs[nx-1]*( domain[1]-newxs[nx-1])+newhxs[nx-1]) - numpy.exp(newhus[nx-2])) else: newcu=- 1./newhpxs[nx-1]*numpy.exp(newhus[nx-2]) newcu= newcu+numpy.sum(newscum) newscum= numpy.cumsum(newscum)/newcu newhull=[] newhull.append(newcu) newhull.append(newxs) newhull.append(newhxs) newhull.append(newhpxs) newhull.append(newzs) newhull.append(newscum) newhull.append(newhus) return newhull ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/bovy_conversion.py0000644000175100001710000000071014327773303020132 0ustar00runnerdocker# renamed to conversion.py import warnings from .conversion import * from .conversion import (_APY_LOADED, _APY_UNITS, _G, _TWOPI, _CIN10p5KMS, _EVIN10m19J, _kmsInPcMyr, _MyrIn1013Sec, _PCIN10p18CM) warnings.warn('galpy.util.bovy_conversion is being deprecated in favor of galpy.util.conversion; all functions in there are the same; please switch to the new import, because the old import will be removed in v1.9',FutureWarning) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/bovy_coords.c0000644000175100001710000000652214327773303017037 0ustar00runnerdocker#include #include /* NAME: cyl_to_rect PURPOSE: convert 2D (R,phi) to (x,y) [mainly used in the context of cylindrical coordinates, hence the name) INPUT: double R - cylindrical radius double phi - azimuth (rad) OUTPUT (as arguments): double *x - x double *y - y */ void cyl_to_rect(double R, double phi,double *x, double *y){ *x= R * cos ( phi ); *y= R * sin ( phi ); } /* NAME: rect_to_cyl PURPOSE: convert 2D (x,y) to (R,phi) [mainly used in the context of cylindrical coordinates, hence the name) INPUT: double x - x double y - y OUTPUT (as arguments): double *R - cylindrical radius double *phi - azimuth (rad) */ void rect_to_cyl(double x, double y,double *R, double *phi){ *R = sqrt (x*x+y*y); *phi = atan2 ( y , x ); } /* NAME: cyl_to_rect_vec PURPOSE: convert 2D (vR,vT) to (vx,vy) [mainly used in the context of cylindrical coordinates, hence the name) INPUT: double vR - cylindrical radial velocity double vT - cylindrical rotational velocity double phi - azimuth (rad) OUTPUT (as arguments): double *vx - vx double *vy - vy */ void cyl_to_rect_vec(double vR, double vT, double phi,double * vx, double * vy){ double cp= cos(phi); double sp= sin(phi); *vx= vR * cp - vT * sp; *vy= vR * sp + vT * cp; } /* NAME: polar_to_rect_galpy PURPOSE: convert (R,vR,vT,phi) to (x,y,vx,vy) INPUT: double * vxvv - (R,vR,vT,phi) OUTPUT: performed in-place HISTORY: 2012-12-24 - Written - Bovy (UofT) */ void polar_to_rect_galpy(double *vxvv){ double R,phi,cp,sp,vR,vT; R = *vxvv; phi= *(vxvv+3); cp = cos ( phi ); sp = sin ( phi ); vR = *(vxvv+1); vT = *(vxvv+2); *vxvv = R * cp; *(vxvv+1)= R * sp; *(vxvv+2)= vR * cp - vT * sp; *(vxvv+3)= vR * sp + vT * cp; } /* NAME: rect_to_polar_galpy PURPOSE: convert (x,y,vx,vy) to (R,vR,vT,phi) INPUT: double * vxvv - (x,y,vx,vy) OUTPUT (as arguments): performed in-place HISTORY: 2012-12-24 - Written - Bovy (UofT) */ void rect_to_polar_galpy(double *vxvv){ double x,y,vx,vy,cp,sp; x = *vxvv; y = *(vxvv+1); vx= *(vxvv+2); vy= *(vxvv+3); *(vxvv+3)= atan2( y , x ); cp = cos ( *(vxvv+3) ); sp = sin ( *(vxvv+3) ); *vxvv = sqrt ( x * x + y * y ); *(vxvv+1)= vx * cp + vy * sp; *(vxvv+2)= -vx * sp + vy * cp; } /* NAME: cyl_to_rect_galpy PURPOSE: convert (R,vR,vT,z,vz,phi) to (x,y,z,vx,vy,vz) INPUT: double * vxvv - (R,vR,vT,z,vz,phi) OUTPUT: performed in-place HISTORY: 2012-12-24 - Written - Bovy (UofT) */ void cyl_to_rect_galpy(double *vxvv){ double R,phi,cp,sp,vR,vT; R = *vxvv; phi= *(vxvv+5); cp = cos ( phi ); sp = sin ( phi ); vR = *(vxvv+1); vT = *(vxvv+2); *vxvv = R * cp; *(vxvv+1)= R * sp; *(vxvv+2)= *(vxvv+3); *(vxvv+5)= *(vxvv+4); *(vxvv+3)= vR * cp - vT * sp; *(vxvv+4)= vR * sp + vT * cp; } /* NAME: rect_to_cyl_galpy PURPOSE: convert (x,y,z,vx,vy,vz) to (R,vR,vT,z,vzphi) INPUT: double * vxvv - (x,y,z,vx,vy,vz) OUTPUT (as arguments): performed in-place HISTORY: 2012-12-24 - Written - Bovy (UofT) */ void rect_to_cyl_galpy(double *vxvv){ double x,y,vx,vy,cp,sp; x = *vxvv; y = *(vxvv+1); vx= *(vxvv+3); vy= *(vxvv+4); *(vxvv+3)= *(vxvv+2); *(vxvv+4)= *(vxvv+5); *(vxvv+5)= atan2( y , x ); cp = cos ( *(vxvv+5) ); sp = sin ( *(vxvv+5) ); *vxvv = sqrt ( x * x + y * y ); *(vxvv+1)= vx * cp + vy * sp; *(vxvv+2)= -vx * sp + vy * cp; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/bovy_coords.h0000644000175100001710000000375514327773303017051 0ustar00runnerdocker/* Some coordinate transformations for use in galpy's C code */ /* Copyright (c) 2018, Jo Bovy All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef __BOVY_COORDS_H__ #define __BOVY_COORDS_H__ #ifdef __cplusplus extern "C" { #endif /* Global variables */ /* Function declarations */ void cyl_to_rect(double,double,double *,double *); void rect_to_cyl(double,double,double *,double *); void cyl_to_rect_vec(double,double,double,double *, double *); void polar_to_rect_galpy(double *); void rect_to_polar_galpy(double *); void cyl_to_rect_galpy(double *); void rect_to_cyl_galpy(double *); #ifdef __cplusplus } #endif #endif /* bovy_coords.h */ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/bovy_coords.py0000644000175100001710000000061014327773303017235 0ustar00runnerdocker# renamed to coords.py import warnings from .coords import * from .coords import (_APY_COORDS, _APY_LOADED, _DEGTORAD, _K, _parse_epoch_frame_apy) warnings.warn('galpy.util.bovy_coords is being deprecated in favor of galpy.util.coords; all functions in there are the same; please switch to the new import, because the old import will be removed in v1.9',FutureWarning) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/bovy_plot.py0000644000175100001710000000112214327773303016721 0ustar00runnerdocker# renamed to plot.py import warnings from .plot import * from .plot import _DEFAULTNCNTR, _MPL_VERSION, _add_axislabels, _add_ticks warnings.warn('galpy.util.bovy_plot is being deprecated in favor of galpy.util.plot; functions inside of this module have also changed name, but all functions still exist; please switch to the new import and new function names, because the old import and function names will be removed in v1.9',FutureWarning) # Old names bovy_end_print= end_print bovy_hist= hist bovy_plot= plot bovy_plot3d= plot3d bovy_dens2d= dens2d bovy_print= start_print bovy_text= text ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/bovy_rk.c0000644000175100001710000006174514327773303016172 0ustar00runnerdocker/* C implementations of Runge-Kutta integrators */ /* Copyright (c) 2011, Jo Bovy All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include "signal.h" #define _MAX_STEPCHANGE_POWERTWO 3. #define _MIN_STEPCHANGE_POWERTWO -3. #define _MAX_STEPREDUCE 10000. #define _MAX_DT_REDUCE 10000. /* Runge-Kutta 4 integrator Usage: Provide the acceleration function func with calling sequence func (t,q,a,nargs,args) where double t: time double * q: current value (dimension: dim) double * a: will be set to the derivative by func int nargs: number of arguments the function takes double *args: arguments Other arguments are: int dim: dimension double *yo: initial value, dimension: dim int nt: number of times at which the output is wanted double dt: (optional) stepsize to use, must be an integer divisor of time difference between output steps (NOT CHECKED EXPLICITLY) double *t: times at which the output is wanted (EQUALLY SPACED) int nargs: see above double *args: see above double rtol, double atol: relative and absolute tolerance levels desired Output: double *result: result (nt blocks of size 2dim) int *err: error: -10 if interrupted by CTRL-C (SIGINT) */ void bovy_rk4(void (*func)(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs), int dim, double * yo, int nt, double dt, double *t, int nargs, struct potentialArg * potentialArgs, double rtol, double atol, double *result, int * err){ //Declare and initialize double *yn= (double *) malloc ( dim * sizeof(double) ); double *yn1= (double *) malloc ( dim * sizeof(double) ); double *ynk= (double *) malloc ( dim * sizeof(double) ); double *a= (double *) malloc ( dim * sizeof(double) ); int ii, jj, kk; save_rk(dim,yo,result); result+= dim; *err= 0; for (ii=0; ii < dim; ii++) *(yn+ii)= *(yo+ii); for (ii=0; ii < dim; ii++) *(yn1+ii)= *(yo+ii); //Estimate necessary stepsize double init_dt= (*(t+1))-(*t); if ( dt == -9999.99 ) { dt= rk4_estimate_step(*func,dim,yo,init_dt,t,nargs,potentialArgs, rtol,atol); } long ndt= (long) (init_dt/dt); //Integrate the system double to= *t; // Handle KeyboardInterrupt gracefully #ifndef _WIN32 struct sigaction action; memset(&action, 0, sizeof(struct sigaction)); action.sa_handler= handle_sigint; sigaction(SIGINT,&action,NULL); #else if (SetConsoleCtrlHandler(CtrlHandler, TRUE)) {} #endif for (ii=0; ii < (nt-1); ii++){ if ( interrupted ) { *err= -10; interrupted= 0; // need to reset, bc library and vars stay in memory #ifdef USING_COVERAGE __gcov_flush(); #endif // LCOV_EXCL_START break; // LCOV_EXCL_STOP } for (jj=0; jj < (ndt-1); jj++) { bovy_rk4_onestep(func,dim,yn,yn1,to,dt,nargs,potentialArgs,ynk,a); to+= dt; //reset yn for (kk=0; kk < dim; kk++) *(yn+kk)= *(yn1+kk); } bovy_rk4_onestep(func,dim,yn,yn1,to,dt,nargs,potentialArgs,ynk,a); to+= dt; //save save_rk(dim,yn1,result); result+= dim; //reset yn for (kk=0; kk < dim; kk++) *(yn+kk)= *(yn1+kk); } // Back to default handler #ifndef _WIN32 action.sa_handler= SIG_DFL; sigaction(SIGINT,&action,NULL); #endif //Free allocated memory free(yn); free(yn1); free(ynk); free(a); //We're done } void bovy_rk4_onestep(void (*func)(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs), int dim, double * yn,double * yn1, double tn, double dt, int nargs, struct potentialArg * potentialArgs, double * ynk, double * a){ int ii; //calculate k1 func(tn,yn,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++) *(yn1+ii) += dt * *(a+ii) / 6.; for (ii=0; ii < dim; ii++) *(ynk+ii)= *(yn+ii) + dt * *(a+ii) / 2.; //calculate k2 func(tn+dt/2.,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++) *(yn1+ii) += dt * *(a+ii) / 3.; for (ii=0; ii < dim; ii++) *(ynk+ii)= *(yn+ii) + dt * *(a+ii) / 2.; //calculate k3 func(tn+dt/2.,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++) *(yn1+ii) += dt * *(a+ii) / 3.; for (ii=0; ii < dim; ii++) *(ynk+ii)= *(yn+ii) + dt * *(a+ii); //calculate k4 func(tn+dt,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++) *(yn1+ii) += dt * *(a+ii) / 6.; //yn1 is new value } /* RK6 integrator, same calling sequence as RK4 */ void bovy_rk6(void (*func)(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs), int dim, double * yo, int nt, double dt, double *t, int nargs, struct potentialArg * potentialArgs, double rtol, double atol, double *result, int * err){ //Declare and initialize double *yn= (double *) malloc ( dim * sizeof(double) ); double *yn1= (double *) malloc ( dim * sizeof(double) ); double *ynk= (double *) malloc ( dim * sizeof(double) ); double *a= (double *) malloc ( dim * sizeof(double) ); double *k1= (double *) malloc ( dim * sizeof(double) ); double *k2= (double *) malloc ( dim * sizeof(double) ); double *k3= (double *) malloc ( dim * sizeof(double) ); double *k4= (double *) malloc ( dim * sizeof(double) ); double *k5= (double *) malloc ( dim * sizeof(double) ); int ii, jj, kk; save_rk(dim,yo,result); result+= dim; *err= 0; for (ii=0; ii < dim; ii++) *(yn+ii)= *(yo+ii); for (ii=0; ii < dim; ii++) *(yn1+ii)= *(yo+ii); //Estimate necessary stepsize double init_dt= (*(t+1))-(*t); if ( dt == -9999.99 ) { dt= rk6_estimate_step(*func,dim,yo,init_dt,t,nargs,potentialArgs, rtol,atol); } long ndt= (long) (init_dt/dt); //Integrate the system double to= *t; // Handle KeyboardInterrupt gracefully #ifndef _WIN32 struct sigaction action; memset(&action, 0, sizeof(struct sigaction)); action.sa_handler= handle_sigint; sigaction(SIGINT,&action,NULL); #else if (SetConsoleCtrlHandler(CtrlHandler, TRUE)) {} #endif for (ii=0; ii < (nt-1); ii++){ if ( interrupted ) { *err= -10; interrupted= 0; // need to reset, bc library and vars stay in memory #ifdef USING_COVERAGE __gcov_flush(); #endif // LCOV_EXCL_START break; // LCOV_EXCL_STOP } for (jj=0; jj < (ndt-1); jj++) { bovy_rk6_onestep(func,dim,yn,yn1,to,dt,nargs,potentialArgs,ynk,a, k1,k2,k3,k4,k5); to+= dt; //reset yn for (kk=0; kk < dim; kk++) *(yn+kk)= *(yn1+kk); } bovy_rk6_onestep(func,dim,yn,yn1,to,dt,nargs,potentialArgs,ynk,a, k1,k2,k3,k4,k5); to+= dt; //save save_rk(dim,yn1,result); result+= dim; //reset yn for (kk=0; kk < dim; kk++) *(yn+kk)= *(yn1+kk); } // Back to default handler #ifndef _WIN32 action.sa_handler= SIG_DFL; sigaction(SIGINT,&action,NULL); #endif //Free allocated memory free(yn); free(yn1); free(ynk); free(a); free(k1); free(k2); free(k3); free(k4); free(k5); //We're done } /* RK6 SOLVER: needs 7 function evaluations per step x[i+1] = x[i] + (11*k_1 + 81*k_3 + 81*k_4 - 32*k_5 - 32*k_6 + 11*k_7)/120 k_1 = step * dxdt(start, x[i]) k_2 = step * dxdt(start + step/3, x[i] + k_1/3) k_3 = step * dxdt(start + 2*step/3, x[i] + 2 * k_2/3) k_4 = step * dxdt(start + step/3, x[i] + (k_1 + 4*k_2 - k_3)/12) k_5 = step * dxdt(start + step/2, x[i] + (-k_1 + 18*k_2 - 3*k_3 - 6*k_4)/16) k_6 = step * dxdt(start + step/2, x[i] + (9 * k_2 - 3*k_3 - 6*k_4 + 4*k_5)/8) k_7 = step * dxdt(start + step, x[i] + (9*k_1 - 36*k_2 + 63*k_3 + 72*k_4 -64*k_6)/44) */ void bovy_rk6_onestep(void (*func)(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs), int dim, double * yn,double * yn1, double tn, double dt, int nargs, struct potentialArg * potentialArgs, double * ynk, double * a, double * k1, double * k2, double * k3, double * k4, double * k5){ int ii; //calculate k1 func(tn,yn,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++) *(yn1+ii) += 11.* dt * *(a+ii) / 120.; for (ii=0; ii < dim; ii++) *(k1+ii)= dt * *(a+ii); for (ii=0; ii < dim; ii++) *(ynk+ii)= *(yn+ii) + *(k1+ii)/3.; //calculate k2 func(tn+dt/3.,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++) *(k2+ii)= dt * *(a+ii); for (ii=0; ii < dim; ii++) *(ynk+ii)= *(yn+ii) + 2. * *(k2+ii)/3.; //calculate k3 func(tn+2.*dt/3.,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++) *(yn1+ii) += 81. * dt * *(a+ii) / 120.; for (ii=0; ii < dim; ii++) *(k3+ii)= dt * *(a+ii); for (ii=0; ii < dim; ii++) *(ynk+ii)= *(yn+ii) + ( *(k1+ii) + 4. * *(k2+ii) - *(k3+ii))/12.; //calculate k4 func(tn+dt/3.,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++) *(yn1+ii) += 81.* dt * *(a+ii) / 120.; for (ii=0; ii < dim; ii++) *(k4+ii)= dt * *(a+ii); for (ii=0; ii < dim; ii++) *(ynk+ii)= *(yn+ii) + ( -*(k1+ii) + 18. * *(k2+ii) - 3. * *(k3+ii) -6.* *(k4+ii))/16.; //calculate k5 func(tn+dt/2.,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++) *(yn1+ii) -= 32.* dt * *(a+ii) / 120.; for (ii=0; ii < dim; ii++) *(k5+ii)= dt * *(a+ii); for (ii=0; ii < dim; ii++) *(ynk+ii)= *(yn+ii) + ( 9. * *(k2+ii) - 3. * *(k3+ii) -6.* *(k4+ii) + 4. * *(k5+ii))/8.; //calculate k6 func(tn+dt/2.,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++) *(yn1+ii) -= 32.* dt * *(a+ii) / 120.; for (ii=0; ii < dim; ii++) *(k5+ii)= dt * *(a+ii); //re-use k5 for k6 for (ii=0; ii < dim; ii++) *(ynk+ii)= *(yn+ii) + ( 9. * *(k1+ii) - 36. * *(k2+ii) +63.* *(k3+ii) + 72. * *(k4+ii) -64. * *(k5+ii))/44.; //calculate k7 func(tn+dt,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++) *(yn1+ii) += 11.* dt * *(a+ii) / 120.; //yn1 is new value } double rk4_estimate_step(void (*func)(double t, double *y, double *a,int nargs, struct potentialArg *), int dim, double *yo, double dt, double *t, int nargs,struct potentialArg * potentialArgs, double rtol,double atol){ //return dt; //scalars double err= 2.; double max_val; double to= *t; double init_dt= dt; double *yn= (double *) malloc ( dim * sizeof(double) ); double *y1= (double *) malloc ( dim * sizeof(double) ); double *y21= (double *) malloc ( dim * sizeof(double) ); double *y2= (double *) malloc ( dim * sizeof(double) ); double *ynk= (double *) malloc ( dim * sizeof(double) ); double *a= (double *) malloc ( dim * sizeof(double) ); double *scale= (double *) malloc ( dim * sizeof(double) ); int ii; //find maximum values max_val= fabs(*yo); for (ii=1; ii < dim; ii++) if ( fabs(*(yo+ii)) > max_val ) max_val= fabs(*(yo+ii)); //set up scale double c= fmax(atol, rtol * max_val); double s= log(exp(atol-c)+exp(rtol*max_val-c))+c; for (ii=0; ii < dim; ii++) *(scale+ii)= s; //find good dt //dt*= 2.; while ( err > 1. ){ //dt/= 2.; //copy initial condition for (ii=0; ii < dim; ii++) *(yn+ii)= *(yo+ii); for (ii=0; ii < dim; ii++) *(y1+ii)= *(yo+ii); for (ii=0; ii < dim; ii++) *(y21+ii)= *(yo+ii); //do one step with step dt, and one with step dt/2. //dt bovy_rk4_onestep(func,dim,yn,y1,to,dt,nargs,potentialArgs,ynk,a); //dt/2 bovy_rk4_onestep(func,dim,yn,y21,to,dt/2.,nargs,potentialArgs,ynk,a); for (ii=0; ii < dim; ii++) *(y2+ii)= *(y21+ii); bovy_rk4_onestep(func,dim,y21,y2,to+dt/2.,dt/2.,nargs,potentialArgs,ynk,a); //Norm err= 0.; for (ii=0; ii < dim; ii++) { err+= exp(2.*log(fabs(*(y1+ii)-*(y2+ii)))-2.* *(scale+ii)); } err= sqrt(err/dim); if ( ceil(pow(err,1./5.)) > 1. && init_dt / dt * ceil(pow(err,1./5.)) < _MAX_DT_REDUCE) dt/= ceil(pow(err,1./5.)); else break; } //free what we allocated free(yn); free(y1); free(y2); free(y21); free(ynk); free(a); free(scale); //return //printf("%f\n",dt); //fflush(stdout); return dt; } double rk6_estimate_step(void (*func)(double t, double *y, double *a,int nargs, struct potentialArg *), int dim, double *yo, double dt, double *t, int nargs,struct potentialArg * potentialArgs, double rtol,double atol){ //return dt; //scalars double err= 2.; double max_val; double to= *t; double init_dt= dt; double *yn= (double *) malloc ( dim * sizeof(double) ); double *y1= (double *) malloc ( dim * sizeof(double) ); double *y21= (double *) malloc ( dim * sizeof(double) ); double *y2= (double *) malloc ( dim * sizeof(double) ); double *ynk= (double *) malloc ( dim * sizeof(double) ); double *a= (double *) malloc ( dim * sizeof(double) ); double *k1= (double *) malloc ( dim * sizeof(double) ); double *k2= (double *) malloc ( dim * sizeof(double) ); double *k3= (double *) malloc ( dim * sizeof(double) ); double *k4= (double *) malloc ( dim * sizeof(double) ); double *k5= (double *) malloc ( dim * sizeof(double) ); double *scale= (double *) malloc ( dim * sizeof(double) ); int ii; //find maximum values max_val= fabs(*yo); for (ii=1; ii < dim; ii++) if ( fabs(*(yo+ii)) > max_val ) max_val= fabs(*(yo+ii)); //set up scale double c= fmax(atol, rtol * max_val); double s= log(exp(atol-c)+exp(rtol*max_val-c))+c; for (ii=0; ii < dim; ii++) *(scale+ii)= s; //find good dt //dt*= 2.; while ( err > 1. ){ //dt/= 2.; //copy initial condition for (ii=0; ii < dim; ii++) *(yn+ii)= *(yo+ii); for (ii=0; ii < dim; ii++) *(y1+ii)= *(yo+ii); for (ii=0; ii < dim; ii++) *(y21+ii)= *(yo+ii); //do one step with step dt, and one with step dt/2. //dt bovy_rk6_onestep(func,dim,yn,y1,to,dt,nargs,potentialArgs,ynk,a, k1,k2,k3,k4,k5); //dt/2 bovy_rk6_onestep(func,dim,yn,y21,to,dt/2.,nargs,potentialArgs,ynk,a, k1,k2,k3,k4,k5); for (ii=0; ii < dim; ii++) *(y2+ii)= *(y21+ii); bovy_rk6_onestep(func,dim,y21,y2,to+dt/2.,dt/2.,nargs,potentialArgs,ynk,a, k1,k2,k3,k4,k5); //Norm err= 0.; for (ii=0; ii < dim; ii++) { err+= exp(2.*log(fabs(*(y1+ii)-*(y2+ii)))-2.* *(scale+ii)); } err= sqrt(err/dim); if ( ceil(pow(err,1./7.)) > 1. && init_dt / dt * ceil(pow(err,1./7.)) < _MAX_DT_REDUCE) dt/= ceil(pow(err,1./7.)); else break; } //free what we allocated free(yn); free(y1); free(y2); free(y21); free(ynk); free(a); free(scale); free(k1); free(k2); free(k3); free(k4); free(k5); //return //printf("%f\n",dt); //fflush(stdout); return dt; } /* Runge-Kutta Dormand-Prince 5/4 integrator Usage: Provide the acceleration function func with calling sequence func (t,q,a,nargs,args) where double t: time double * q: current value (dimension: dim) double * a: will be set to the derivative by func int nargs: number of arguments the function takes double *args: arguments Other arguments are: int dim: dimension double *yo: initial value, dimension: dim int nt: number of times at which the output is wanted double dt_one: (optional) stepsize to use, must be an integer divisor of time difference between output steps (NOT CHECKED EXPLICITLY) double *t: times at which the output is wanted (EQUALLY SPACED) int nargs: see above double *args: see above double rtol, double atol: relative and absolute tolerance levels desired Output: double *result: result (nt blocks of size 2dim) int * err: if non-zero, something bad happened (1: maximum step reduction happened; -10: interrupted by CTRL-C (SIGINT) */ void bovy_dopr54(void (*func)(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs), int dim, double * yo, int nt, double dt_one, double *t, int nargs, struct potentialArg * potentialArgs, double rtol, double atol, double *result, int * err){ //Declare and initialize double *a= (double *) malloc ( dim * sizeof(double) ); double *a1= (double *) malloc ( dim * sizeof(double) ); double *k1= (double *) malloc ( dim * sizeof(double) ); double *k2= (double *) malloc ( dim * sizeof(double) ); double *k3= (double *) malloc ( dim * sizeof(double) ); double *k4= (double *) malloc ( dim * sizeof(double) ); double *k5= (double *) malloc ( dim * sizeof(double) ); double *k6= (double *) malloc ( dim * sizeof(double) ); double *yn= (double *) malloc ( dim * sizeof(double) ); double *yn1= (double *) malloc ( dim * sizeof(double) ); double *yerr= (double *) malloc ( dim * sizeof(double) ); double *ynk= (double *) malloc ( dim * sizeof(double) ); int ii; save_rk(dim,yo,result); result+= dim; *err= 0; for (ii=0; ii < dim; ii++) *(yn+ii)= *(yo+ii); double dt= (*(t+1))-(*t); if ( dt_one == -9999.99 ) { dt_one= rk4_estimate_step(*func,dim,yo,dt,t,nargs,potentialArgs, rtol,atol); } //Integrate the system double to= *t; //set up a1 func(to,yn,a1,nargs,potentialArgs); // Handle KeyboardInterrupt gracefully #ifndef _WIN32 struct sigaction action; memset(&action, 0, sizeof(struct sigaction)); action.sa_handler= handle_sigint; sigaction(SIGINT,&action,NULL); #else if (SetConsoleCtrlHandler(CtrlHandler, TRUE)) {} #endif for (ii=0; ii < (nt-1); ii++){ if ( interrupted ) { *err= -10; interrupted= 0; // need to reset, bc library and vars stay in memory #ifdef USING_COVERAGE __gcov_flush(); #endif // LCOV_EXCL_START break; // LCOV_EXCL_STOP } bovy_dopr54_onestep(func,dim,yn,dt,&to,&dt_one, nargs,potentialArgs,rtol,atol, a1,a,k1,k2,k3,k4,k5,k6,yn1,yerr,ynk,err); //save save_rk(dim,yn,result); result+= dim; } // Back to default handler #ifndef _WIN32 action.sa_handler= SIG_DFL; sigaction(SIGINT,&action,NULL); #endif // Free allocated memory free(a); free(a1); free(k1); free(k2); free(k3); free(k4); free(k5); free(k6); free(yn); free(yn1); free(yerr); free(ynk); } //one output step, consists of multiple steps potentially void bovy_dopr54_onestep(void (*func)(double t, double *y, double *a,int nargs, struct potentialArg *), int dim, double *yo, double dt, double *to,double * dt_one, int nargs,struct potentialArg * potentialArgs, double rtol,double atol, double * a1, double * a, double * k1, double * k2, double * k3, double * k4, double * k5, double * k6, double * yn1, double * yerr,double * ynk, int * err){ double init_dt_one= *dt_one; double init_to= *to; unsigned char accept; //printf("%f,%f\n",*to,init_to+dt); while ( ( dt >= 0. && *to < (init_to+dt)) || ( dt < 0. && *to > (init_to+dt)) ) { accept= 0; if ( init_dt_one/ *dt_one > _MAX_STEPREDUCE || *dt_one != *dt_one) { // check for NaN *dt_one= init_dt_one/_MAX_STEPREDUCE; accept= 1; if ( *err % 2 == 0) *err+= 1; } if ( dt >= 0. && *dt_one > (init_to+dt - *to) ) *dt_one= (init_to + dt - *to); if ( dt < 0. && *dt_one < (init_to+dt - *to) ) *dt_one = (init_to + dt - *to); *dt_one= bovy_dopr54_actualstep(func,dim,yo,*dt_one,to,nargs,potentialArgs, rtol,atol, a1,a,k1,k2,k3,k4,k5,k6,yn1,yerr,ynk, accept); } } double bovy_dopr54_actualstep(void (*func)(double t, double *y, double *a,int nargs, struct potentialArg *), int dim, double *yo, double dt, double *to, int nargs,struct potentialArg * potentialArgs, double rtol,double atol, double * a1, double * a, double * k1, double * k2, double * k3, double * k4, double * k5, double * k6, double * yn1, double * yerr,double * ynk, unsigned char accept){ //constant static const double c2= 0.2; static const double c3= 0.3; static const double c4= 0.8; static const double c5= 8./9.; static const double a21= 0.2; static const double a31= 3./40.; static const double a41= 44./45.; static const double a51= 19372./6561; static const double a61= 9017./3168.; static const double a71= 35./384.; static const double a32= 9./40.; static const double a42= -56./15.; static const double a52= -25360./2187.; static const double a62= -355./33.; static const double a43= 32./9.; static const double a53= 64448./6561.; static const double a63= 46732./5247.; static const double a73= 500./1113.; static const double a54= -212./729.; static const double a64= 49./176.; static const double a74= 125./192.; static const double a65= -5103./18656.; static const double a75= -2187./6784.; static const double a76= 11./84.; static const double b1= 35./384.; static const double b3= 500./1113.; static const double b4= 125./192.; static const double b5= -2187./6784.; static const double b6= 11./84.; //coefficients of the error estimate const double be1= b1-5179./57600.; const double be3= b3-7571./16695.; const double be4= b4-393./640.; const double be5= b5+92097./339200.; const double be6= b6-187./2100.; static const double be7= -1./40.; int ii; //setup yn1 for (ii=0; ii < dim; ii++) *(yn1+ii) = *(yo+ii); //calculate k1 for (ii=0; ii < dim; ii++) *(a+ii)= *(a1+ii); for (ii=0; ii < dim; ii++){ *(k1+ii)= dt * *(a+ii); *(yn1+ii) += b1* *(k1+ii); *(yerr+ii) = be1* *(k1+ii); *(ynk+ii)= *(yo+ii) + a21 * *(k1+ii); } //calculate k2 func(*to+c2*dt,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++){ *(k2+ii)= dt * *(a+ii); *(ynk+ii)= *(yo+ii) + a31 * *(k1+ii) + a32 * *(k2+ii); } //calculate k3 func(*to+c3*dt,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++){ *(k3+ii)= dt * *(a+ii); *(yn1+ii) += b3* *(k3+ii); *(yerr+ii) += be3* *(k3+ii); *(ynk+ii)= *(yo+ii) + a41 * *(k1+ii) + a42 * *(k2+ii) + a43 * *(k3+ii); } //calculate k4 func(*to+c4*dt,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++){ *(k4+ii)= dt * *(a+ii); *(yn1+ii) += b4* *(k4+ii); *(yerr+ii) += be4* *(k4+ii); *(ynk+ii)= *(yo+ii) + a51 * *(k1+ii) + a52 * *(k2+ii) + a53 * *(k3+ii) + a54 * *(k4+ii); } //calculate k5 func(*to+c5*dt,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++){ *(k5+ii)= dt * *(a+ii); *(yn1+ii) += b5* *(k5+ii); *(yerr+ii) += be5* *(k5+ii); *(ynk+ii)= *(yo+ii) + a61 * *(k1+ii) + a62 * *(k2+ii) + a63 * *(k3+ii) + a64 * *(k4+ii) + a65 * *(k5+ii); } //calculate k6 func(*to+dt,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++){ *(k6+ii)= dt * *(a+ii); *(yn1+ii) += b6* *(k6+ii); *(yerr+ii) += be6* *(k6+ii); *(ynk+ii)= *(yo+ii) + a71 * *(k1+ii) + a73 * *(k3+ii) //a72=0 + a74 * *(k4+ii) + a75 * *(k5+ii) + a76 * *(k6+ii); } //calculate k7 func(*to+dt,ynk,a,nargs,potentialArgs); for (ii=0; ii < dim; ii++) *(yerr+ii) += be7 * dt * *(a+ii); //yn1 is proposed new value //find maximum values double max_val= fabs(*yo); for (ii=1; ii < dim; ii++) if ( fabs(*(yo+ii)) > max_val ) max_val= fabs(*(yo+ii)); //set up scale double c= fmax(atol, rtol * max_val); double s= log(exp(atol-c)+exp(rtol*max_val-c))+c; //Norm double err= 0.; for (ii=0; ii < dim; ii++) err+= exp(2.*log(fabs(*(yerr+ii)))-2.* s); err= sqrt(err/dim); double corr= 0.85*pow(err,-.2); //Round to the nearest power of two double powertwo= round(log(corr)/log(2.)); if ( powertwo > _MAX_STEPCHANGE_POWERTWO ) powertwo= _MAX_STEPCHANGE_POWERTWO; else if ( powertwo < _MIN_STEPCHANGE_POWERTWO ) powertwo= _MIN_STEPCHANGE_POWERTWO; //printf("%f,%f\n",powertwo,err); //fflush(stdout); //accept or reject double dt_one; if ( ( powertwo >= 0. ) || accept ) {//accept, if the step is the smallest possible, always accept for (ii= 0; ii < dim; ii++) { *(a1+ii)= *(a+ii); *(yo+ii)= *(yn1+ii); } *to+= dt; //printf("%f,%f\n",*to,dt); } dt_one= dt*pow(2.,powertwo); return dt_one; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/bovy_rk.h0000644000175100001710000001005614327773303016164 0ustar00runnerdocker/* C implementations of Runge-Kutta integrators */ /* Copyright (c) 2011, Jo Bovy All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef __BOVY_RK_H__ #define __BOVY_RK_H__ #ifdef __cplusplus extern "C" { #endif /* include */ #include /* Function declarations */ void bovy_rk4(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *,int *); void bovy_rk4_onestep(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *,double *, double, double, int, struct potentialArg *, double *,double *); void bovy_rk6(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *,int *); void bovy_rk6_onestep(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *,double *, double, double, int, struct potentialArg *, double *,double *, double *, double *, double * , double *, double *); static inline void save_rk(int dim, double *yo, double *result){ int ii; for (ii=0; ii < dim; ii++) *result++= *yo++; } double rk4_estimate_step(void (*func)(double , double *, double *,int, struct potentialArg *), int, double *, double, double *, int,struct potentialArg *, double,double); double rk6_estimate_step(void (*func)(double , double *, double *,int, struct potentialArg *), int, double *, double, double *, int,struct potentialArg *, double,double); void bovy_dopr54(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *,int *); void bovy_dopr54_onestep(void (*func)(double, double *, double *,int, struct potentialArg *), int, double *, double, double *,double *, int,struct potentialArg *, double,double, double *, double *, double *, double *, double *, double *, double *, double *, double *, double *, double *,int *); double bovy_dopr54_actualstep(void (*func)(double, double *, double *,int, struct potentialArg *), int, double *, double, double *, int,struct potentialArg *, double,double, double *, double *, double *, double *, double *, double *, double *, double *, double *, double *, double *,unsigned char); #ifdef __cplusplus } #endif #endif /* bovy_rk.h */ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/bovy_symplecticode.c0000644000175100001710000007457714327773303020431 0ustar00runnerdocker/* C implementations of symplectic integrators */ /* Copyright (c) 2011, Jo Bovy All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #define _MAX_DT_REDUCE 10000. #include "signal.h" volatile sig_atomic_t interrupted= 0; // handle CTRL-C differently on UNIX systems and Windows #ifndef _WIN32 void handle_sigint(int signum) { interrupted= 1; } #else #include BOOL WINAPI CtrlHandler(DWORD fdwCtrlType) { switch (fdwCtrlType) { // Handle the CTRL-C signal. case CTRL_C_EVENT: interrupted= 1; // needed to avoid other control handlers like python from being called before us return TRUE; default: return FALSE; } } #endif static inline void leapfrog_leapq(int dim, double *q,double *p,double dt, double *qn){ int ii; for (ii=0; ii < dim; ii++) (*qn++)= (*q++) +dt * (*p++); } static inline void leapfrog_leapp(int dim, double *p,double dt,double *a, double *pn){ int ii; for (ii=0; ii< dim; ii++) (*pn++)= (*p++) + dt * (*a++); } static inline void save_qp(int dim, double *qo, double *po, double *result){ int ii; for (ii=0; ii < dim; ii++) *result++= *qo++; for (ii=0; ii < dim; ii++) *result++= *po++; } /* Leapfrog integrator Usage: Provide the acceleration function func with calling sequence func (t,q,a,nargs,args) where double t: time double * q: current position (dimension: dim) double * a: will be set to the derivative int nargs: number of arguments the function takes struct potentialArg * potentialArg structure pointer, see header file Other arguments are: int dim: dimension double *yo: initial value [qo,po], dimension: 2*dim int nt: number of times at which the output is wanted double dt: (optional) stepsize to use, must be an integer divisor of time difference between output steps (NOT CHECKED EXPLICITLY) double *t: times at which the output is wanted (EQUALLY SPACED) int nargs: see above double *args: see above double rtol, double atol: relative and absolute tolerance levels desired Output: double *result: result (nt blocks of size 2dim) int *err: error: -10 if interrupted by CTRL-C (SIGINT) */ void leapfrog(void (*func)(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs), int dim, double * yo, int nt, double dt, double *t, int nargs, struct potentialArg * potentialArgs, double rtol, double atol, double *result,int * err){ //Initialize double *qo= (double *) malloc ( dim * sizeof(double) ); double *po= (double *) malloc ( dim * sizeof(double) ); double *q12= (double *) malloc ( dim * sizeof(double) ); double *p12= (double *) malloc ( dim * sizeof(double) ); double *a= (double *) malloc ( dim * sizeof(double) ); int ii, jj, kk; for (ii=0; ii < dim; ii++) { *qo++= *(yo+ii); *po++= *(yo+dim+ii); } qo-= dim; po-= dim; save_qp(dim,qo,po,result); result+= 2 * dim; *err= 0; //Estimate necessary stepsize double init_dt= (*(t+1))-(*t); if ( dt == -9999.99 ) { dt= leapfrog_estimate_step(*func,dim,qo,po,init_dt,t,nargs,potentialArgs, rtol,atol); } long ndt= (long) (init_dt/dt); //Integrate the system double to= *t; // Handle KeyboardInterrupt gracefully #ifndef _WIN32 struct sigaction action; memset(&action, 0, sizeof(struct sigaction)); action.sa_handler= handle_sigint; sigaction(SIGINT,&action,NULL); #else if (SetConsoleCtrlHandler(CtrlHandler, TRUE)){} #endif for (ii=0; ii < (nt-1); ii++){ if ( interrupted ) { *err= -10; interrupted= 0; // need to reset, bc library and vars stay in memory #ifdef USING_COVERAGE __gcov_flush(); #endif // LCOV_EXCL_START break; // LCOV_EXCL_STOP } //drift half leapfrog_leapq(dim,qo,po,dt/2.,q12); //now drift full for a while for (jj=0; jj < (ndt-1); jj++){ //kick func(to+dt/2.,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,dt,a,p12); //drift leapfrog_leapq(dim,q12,p12,dt,qo); //reset to= to+dt; for (kk=0; kk < dim; kk++) { *(q12+kk)= *(qo+kk); *(po+kk)= *(p12+kk); } } //end with one last kick and drift //kick func(to+dt/2.,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,dt,a,po); //drift leapfrog_leapq(dim,q12,po,dt/2.,qo); to= to+dt; //save save_qp(dim,qo,po,result); result+= 2 * dim; } // Back to default handler #ifndef _WIN32 action.sa_handler= SIG_DFL; sigaction(SIGINT,&action,NULL); #endif //Free allocated memory free(qo); free(po); free(q12); free(a); //We're done } /* Fourth order symplectic integrator from Kinoshika et al. Usage: Provide the acceleration function func with calling sequence func (t,q,a,nargs,args) where double t: time double * q: current position (dimension: dim) double * a: will be set to the derivative int nargs: number of arguments the function takes struct potentialArg * potentialArg structure pointer, see header file Other arguments are: int dim: dimension double *yo: initial value [qo,po], dimension: 2*dim int nt: number of times at which the output is wanted double dt: (optional) stepsize to use, must be an integer divisor of time difference between output steps (NOT CHECKED EXPLICITLY) double *t: times at which the output is wanted (EQUALLY SPACED) int nargs: see above double *args: see above double rtol, double atol: relative and absolute tolerance levels desired Output: double *result: result (nt blocks of size 2dim) int *err: error: -10 if interrupted by CTRL-C (SIGINT) */ void symplec4(void (*func)(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs), int dim, double * yo, int nt, double dt, double *t, int nargs, struct potentialArg * potentialArgs, double rtol, double atol, double *result,int * err){ //coefficients double c1= 0.6756035959798289; double c4= c1; double c2= -0.1756035959798288; double c3= c2; double d1= 1.3512071919596578; double d3= d1; double d2= -1.7024143839193153; //d4=0 //Initialize double *qo= (double *) malloc ( dim * sizeof(double) ); double *po= (double *) malloc ( dim * sizeof(double) ); double *q12= (double *) malloc ( dim * sizeof(double) ); double *p12= (double *) malloc ( dim * sizeof(double) ); double *a= (double *) malloc ( dim * sizeof(double) ); int ii, jj, kk; for (ii=0; ii < dim; ii++) { *qo++= *(yo+ii); *po++= *(yo+dim+ii); } qo-= dim; po-= dim; save_qp(dim,qo,po,result); result+= 2 * dim; *err= 0; //Estimate necessary stepsize double init_dt= (*(t+1))-(*t); if ( dt == -9999.99 ) { dt= symplec4_estimate_step(*func,dim,qo,po,init_dt,t,nargs,potentialArgs, rtol,atol); } long ndt= (long) (init_dt/dt); //Integrate the system double to= *t; // Handle KeyboardInterrupt gracefully #ifndef _WIN32 struct sigaction action; memset(&action, 0, sizeof(struct sigaction)); action.sa_handler= handle_sigint; sigaction(SIGINT,&action,NULL); #else if (SetConsoleCtrlHandler(CtrlHandler, TRUE)) {} #endif for (ii=0; ii < (nt-1); ii++){ if ( interrupted ) { *err= -10; interrupted= 0; // need to reset, bc library and vars stay in memory #ifdef USING_COVERAGE __gcov_flush(); #endif // LCOV_EXCL_START break; // LCOV_EXCL_STOP } //drift for c1*dt leapfrog_leapq(dim,qo,po,c1*dt,q12); to+= c1*dt; //steps ignoring q4/p4 when output is not wanted for (jj=0; jj < (ndt-1); jj++){ //kick for d1*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d1*dt,a,p12); //drift for c2*dt leapfrog_leapq(dim,q12,p12,c2*dt,qo); //kick for d2*dt to+= c2*dt; func(to,qo,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d2*dt,a,po); //drift for c3*dt leapfrog_leapq(dim,qo,po,c3*dt,q12); to+= c3*dt; //kick for d3*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d3*dt,a,p12); //drift for (c4+c1)*dt leapfrog_leapq(dim,q12,p12,(c4+c1)*dt,qo); to+= (c4+c1)*dt; //reset for (kk=0; kk < dim; kk++) { *(q12+kk)= *(qo+kk); *(po+kk)= *(p12+kk); } } //steps not ignoring q4/p4 when output is wanted //kick for d1*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d1*dt,a,p12); //drift for c2*dt leapfrog_leapq(dim,q12,p12,c2*dt,qo); //kick for d2*dt to+= c2*dt; func(to,qo,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d2*dt,a,po); //drift for c3*dt leapfrog_leapq(dim,qo,po,c3*dt,q12); to+= c3*dt; //kick for d3*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d3*dt,a,p12); //drift for c4*dt leapfrog_leapq(dim,q12,p12,c4*dt,qo); to+= c4*dt; //p4=p3 for (kk=0; kk < dim; kk++) *(po+kk)= *(p12+kk); //save save_qp(dim,qo,po,result); result+= 2 * dim; } // Back to default handler #ifndef _WIN32 action.sa_handler= SIG_DFL; sigaction(SIGINT,&action,NULL); #endif //Free allocated memory free(qo); free(po); free(q12); free(a); //We're done } /* Sixth order symplectic integrator from Kinoshika et al., Yoshida (1990) Usage: Provide the acceleration function func with calling sequence func (t,q,a,nargs,args) where double t: time double * q: current position (dimension: dim) double * a: will be set to the derivative int nargs: number of arguments the function takes struct potentialArg * potentialArg structure pointer, see header file Other arguments are: int dim: dimension double *yo: initial value [qo,po], dimension: 2*dim int nt: number of times at which the output is wanted double dt: (optional) stepsize to use, must be an integer divisor of time difference between output steps (NOT CHECKED EXPLICITLY) double *t: times at which the output is wanted (EQUALLY SPACED) int nargs: see above double *args: see above double rtol, double atol: relative and absolute tolerance levels desired Output: double *result: result (nt blocks of size 2dim) int *err: error: -10 if interrupted by CTRL-C (SIGINT) */ void symplec6(void (*func)(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs), int dim, double * yo, int nt, double dt, double *t, int nargs, struct potentialArg * potentialArgs, double rtol, double atol, double *result,int * err){ //coefficients double c1= 0.392256805238780; double c8= c1; double c2= 0.510043411918458; double c7= c2; double c3= -0.471053385409758; double c6= c3; double c4= 0.687531682525198e-1; double c5= c4; double d1= 0.784513610477560; double d7= d1; double d2= 0.235573213359357; double d6= d2; double d3= -0.117767998417887e1; double d5= d3; double d4= 0.131518632068391e1; //d8=0 //Initialize double *qo= (double *) malloc ( dim * sizeof(double) ); double *po= (double *) malloc ( dim * sizeof(double) ); double *q12= (double *) malloc ( dim * sizeof(double) ); double *p12= (double *) malloc ( dim * sizeof(double) ); double *a= (double *) malloc ( dim * sizeof(double) ); int ii, jj, kk; for (ii=0; ii < dim; ii++) { *qo++= *(yo+ii); *po++= *(yo+dim+ii); } qo-= dim; po-= dim; save_qp(dim,qo,po,result); result+= 2 * dim; *err= 0; //Estimate necessary stepsize double init_dt= (*(t+1))-(*t); if ( dt == -9999.99 ) { dt= symplec6_estimate_step(*func,dim,qo,po,init_dt,t,nargs,potentialArgs, rtol,atol); } long ndt= (long) (init_dt/dt); //Integrate the system double to= *t; // Handle KeyboardInterrupt gracefully #ifndef _WIN32 struct sigaction action; memset(&action, 0, sizeof(struct sigaction)); action.sa_handler= handle_sigint; sigaction(SIGINT,&action,NULL); #else if (SetConsoleCtrlHandler(CtrlHandler, TRUE)) {} #endif for (ii=0; ii < (nt-1); ii++){ if ( interrupted ) { *err= -10; interrupted= 0; // need to reset, bc library and vars stay in memory #ifdef USING_COVERAGE __gcov_flush(); #endif // LCOV_EXCL_START break; // LCOV_EXCL_STOP } //drift for c1*dt leapfrog_leapq(dim,qo,po,c1*dt,q12); to+= c1*dt; //steps ignoring q8/p8 when output is not wanted for (jj=0; jj < (ndt-1); jj++){ //kick for d1*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d1*dt,a,p12); //drift for c2*dt leapfrog_leapq(dim,q12,p12,c2*dt,qo); to+= c2*dt; //kick for d2*dt func(to,qo,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d2*dt,a,po); //drift for c3*dt leapfrog_leapq(dim,qo,po,c3*dt,q12); to+= c3*dt; //kick for d3*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d3*dt,a,p12); //drift for c4*dt leapfrog_leapq(dim,q12,p12,c4*dt,qo); //kick for d4*dt to+= c4*dt; func(to,qo,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d4*dt,a,po); //drift for c5*dt leapfrog_leapq(dim,qo,po,c5*dt,q12); to+= c5*dt; //kick for d5*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d5*dt,a,p12); //drift for c6*dt leapfrog_leapq(dim,q12,p12,c6*dt,qo); //kick for d6*dt to+= c6*dt; func(to,qo,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d6*dt,a,po); //drift for c7*dt leapfrog_leapq(dim,qo,po,c7*dt,q12); to+= c7*dt; //kick for d7*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d7*dt,a,p12); //drift for (c8+c1)*dt leapfrog_leapq(dim,q12,p12,(c8+c1)*dt,qo); to+= (c8+c1)*dt; //reset for (kk=0; kk < dim; kk++) { *(q12+kk)= *(qo+kk); *(po+kk)= *(p12+kk); } } //steps not ignoring q8/p8 when output is wanted //kick for d1*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d1*dt,a,p12); //drift for c2*dt leapfrog_leapq(dim,q12,p12,c2*dt,qo); to+= c2*dt; //kick for d2*dt func(to,qo,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d2*dt,a,po); //drift for c3*dt leapfrog_leapq(dim,qo,po,c3*dt,q12); to+= c3*dt; //kick for d3*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d3*dt,a,p12); //drift for c4*dt leapfrog_leapq(dim,q12,p12,c4*dt,qo); to+= c4*dt; //kick for d4*dt func(to,qo,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d4*dt,a,po); //drift for c5*dt leapfrog_leapq(dim,qo,po,c5*dt,q12); to+= c5*dt; //kick for d5*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d5*dt,a,p12); //drift for c6*dt leapfrog_leapq(dim,q12,p12,c6*dt,qo); //kick for d6*dt to+= c6*dt; func(to,qo,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d6*dt,a,po); //drift for c7*dt leapfrog_leapq(dim,qo,po,c7*dt,q12); to+= c7*dt; //kick for d7*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d7*dt,a,p12); //drift for c8*dt leapfrog_leapq(dim,q12,p12,c8*dt,qo); to+= c8*dt; //p8=p7 for (kk=0; kk < dim; kk++) *(po+kk)= *(p12+kk); //save save_qp(dim,qo,po,result); result+= 2 * dim; } // Back to default handler #ifndef _WIN32 action.sa_handler= SIG_DFL; sigaction(SIGINT,&action,NULL); #endif //Free allocated memory free(qo); free(po); free(q12); free(a); //We're done } double leapfrog_estimate_step(void (*func)(double t, double *q, double *a,int nargs, struct potentialArg *), int dim, double *qo,double *po, double dt, double *t, int nargs,struct potentialArg * potentialArgs, double rtol,double atol){ //return dt; //scalars double err= 2.; double max_val_q, max_val_p; double to= *t; double init_dt= dt; //allocate and initialize double *q11= (double *) malloc ( dim * sizeof(double) ); double *q12= (double *) malloc ( dim * sizeof(double) ); double *p11= (double *) malloc ( dim * sizeof(double) ); double *p12= (double *) malloc ( dim * sizeof(double) ); double *qtmp= (double *) malloc ( dim * sizeof(double) ); double *ptmp= (double *) malloc ( dim * sizeof(double) ); double *a= (double *) malloc ( dim * sizeof(double) ); double *scale= (double *) malloc ( 2 * dim * sizeof(double) ); int ii; //find maximum values max_val_q= fabs(*qo); for (ii=1; ii < dim; ii++) if ( fabs(*(qo+ii)) > max_val_q ) max_val_q= fabs(*(qo+ii)); max_val_p= fabs(*po); for (ii=1; ii < dim; ii++) if ( fabs(*(po+ii)) > max_val_p ) max_val_p= fabs(*(po+ii)); //set up scale double c= fmax(atol, rtol * max_val_q); double s= log(exp(atol-c)+exp(rtol*max_val_q-c))+c; for (ii=0; ii < dim; ii++) *(scale+ii)= s; c= fmax(atol, rtol * max_val_p); s= log(exp(atol-c)+exp(rtol*max_val_p-c))+c; for (ii=0; ii < dim; ii++) *(scale+ii+dim)= s; //find good dt dt*= 2.; while ( err > 1. && init_dt / dt < _MAX_DT_REDUCE){ dt/= 2.; //do one leapfrog step with step dt, and one with step dt/2. //dt leapfrog_leapq(dim,qo,po,dt/2.,q12); func(to+dt/2.,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,dt,a,p11); leapfrog_leapq(dim,q12,p11,dt/2.,q11); //dt/2. leapfrog_leapq(dim,qo,po,dt/4.,q12); func(to+dt/4.,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,dt/2.,a,ptmp); leapfrog_leapq(dim,q12,ptmp,dt/2.,qtmp);//Take full step combining two half func(to+3.*dt/4.,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,dt/2.,a,p12); leapfrog_leapq(dim,qtmp,p12,dt/4.,q12);//Take full step combining two half //Norm err= 0.; for (ii=0; ii < dim; ii++) { err+= exp(2.*log(fabs(*(q11+ii)-*(q12+ii)))-2.* *(scale+ii)); err+= exp(2.*log(fabs(*(p11+ii)-*(p12+ii)))-2.* *(scale+ii+dim)); } err= sqrt(err/2./dim); } //free what we allocated free(q11); free(q12); free(p11); free(qtmp); free(ptmp); free(a); free(scale); //return //printf("%f\n",dt); //fflush(stdout); return dt; } double symplec4_estimate_step(void (*func)(double t, double *q, double *a,int nargs, struct potentialArg *), int dim, double *qo,double *po, double dt, double *t, int nargs,struct potentialArg * potentialArgs, double rtol,double atol){ //return dt; //coefficients double c1= 0.6756035959798289; double c4= c1; double c2= -0.1756035959798288; double c3= c2; double d1= 1.3512071919596578; double d3= d1; double d2= -1.7024143839193153; //d4=0 //scalars double err= 2.; double max_val_q, max_val_p; double to= *t; double init_dt= dt; //allocate and initialize double *q11= (double *) malloc ( dim * sizeof(double) ); double *q12= (double *) malloc ( dim * sizeof(double) ); double *p11= (double *) malloc ( dim * sizeof(double) ); double *p12= (double *) malloc ( dim * sizeof(double) ); double *qtmp= (double *) malloc ( dim * sizeof(double) ); double *ptmp= (double *) malloc ( dim * sizeof(double) ); double *a= (double *) malloc ( dim * sizeof(double) ); double *scale= (double *) malloc ( 2 * dim * sizeof(double) ); int ii; //find maximum values max_val_q= fabs(*qo); for (ii=1; ii < dim; ii++) if ( fabs(*(qo+ii)) > max_val_q ) max_val_q= fabs(*(qo+ii)); max_val_p= fabs(*po); for (ii=1; ii < dim; ii++) if ( fabs(*(po+ii)) > max_val_p ) max_val_p= fabs(*(po+ii)); //set up scale double c= fmax(atol, rtol * max_val_q); double s= log(exp(atol-c)+exp(rtol*max_val_q-c))+c; for (ii=0; ii < dim; ii++) *(scale+ii)= s; c= fmax(atol, rtol * max_val_p); s= log(exp(atol-c)+exp(rtol*max_val_p-c))+c; for (ii=0; ii < dim; ii++) *(scale+ii+dim)= s; //find good dt dt*= 2.; while ( err > 1. && init_dt / dt < _MAX_DT_REDUCE ){ dt/= 2.; //do one step with step dt, and one with step dt/2. /* dt */ //drift for c1*dt leapfrog_leapq(dim,qo,po,c1*dt,q12); to+= c1*dt; //kick for d1*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d1*dt,a,p12); //drift for c2*dt leapfrog_leapq(dim,q12,p12,c2*dt,qtmp); //kick for d2*dt to+= c2*dt; func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d2*dt,a,ptmp); //drift for c3*dt leapfrog_leapq(dim,qtmp,ptmp,c3*dt,q12); to+= c3*dt; //kick for d3*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,d3*dt,a,p11); //drift for c4*dt leapfrog_leapq(dim,q12,p11,c4*dt,q11); to+= c4*dt; //p4=p3 //reset to-= dt; /* dt/2 */ //drift for c1*dt/2 leapfrog_leapq(dim,qo,po,c1*dt/2.,q12); to+= c1*dt/2.; //kick for d1*dt/2 func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d1*dt/2.,a,p12); //drift for c2*dt/2 leapfrog_leapq(dim,q12,p12,c2*dt/2.,qtmp); //kick for d2*dt/2 to+= c2*dt/2.; func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d2*dt/2.,a,ptmp); //drift for c3*dt/2 leapfrog_leapq(dim,qtmp,ptmp,c3*dt/2.,q12); to+= c3*dt/2.; //kick for d3*dt/2 func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,d3*dt/2.,a,p12); //drift for (c4+c1)*dt/2, skipping q4/p4 leapfrog_leapq(dim,q12,p12,(c1+c4)*dt/2.,qtmp); to+= (c1+c4)*dt/2.; //kick for d1*dt/2 func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d1*dt/2.,a,ptmp); //drift for c2*dt/2 leapfrog_leapq(dim,qtmp,ptmp,c2*dt/2.,q12); //kick for d2*dt/2 to+= c2*dt/2.; func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,d2*dt/2.,a,p12); //drift for c3*dt/2 leapfrog_leapq(dim,q12,p12,c3*dt/2.,qtmp); to+= c3*dt/2.; //kick for d3*dt func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d3*dt/2.,a,ptmp); //drift for c4*dt/2. leapfrog_leapq(dim,qtmp,ptmp,c4*dt/2.,q12); to+= c4*dt/2.; //p4=p3 for (ii=0; ii < dim; ii++) *(p12+ii)= *(ptmp+ii); //Norm err= 0.; for (ii=0; ii < dim; ii++) { err+= exp(2.*log(fabs(*(q11+ii)-*(q12+ii)))-2.* *(scale+ii)); err+= exp(2.*log(fabs(*(p11+ii)-*(p12+ii)))-2.* *(scale+ii+dim)); } err= sqrt(err/2./dim); //reset to-= dt; } //free what we allocated free(q11); free(q12); free(p11); free(qtmp); free(ptmp); free(a); free(scale); //return //printf("%f\n",dt); //fflush(stdout); return dt; } double symplec6_estimate_step(void (*func)(double t, double *q, double *a,int nargs, struct potentialArg *), int dim, double *qo,double *po, double dt, double *t, int nargs,struct potentialArg * potentialArgs, double rtol,double atol){ //return dt; //coefficients double c1= 0.392256805238780; double c8= c1; double c2= 0.510043411918458; double c7= c2; double c3= -0.471053385409758; double c6= c3; double c4= 0.687531682525198e-1; double c5= c4; double d1= 0.784513610477560; double d7= d1; double d2= 0.235573213359357; double d6= d2; double d3= -0.117767998417887e1; double d5= d3; double d4= 0.131518632068391e1; //d8=0 //scalars double err= 2.; double max_val_q, max_val_p; double to= *t; double init_dt= dt; //allocate and initialize double *q11= (double *) malloc ( dim * sizeof(double) ); double *q12= (double *) malloc ( dim * sizeof(double) ); double *p11= (double *) malloc ( dim * sizeof(double) ); double *p12= (double *) malloc ( dim * sizeof(double) ); double *qtmp= (double *) malloc ( dim * sizeof(double) ); double *ptmp= (double *) malloc ( dim * sizeof(double) ); double *a= (double *) malloc ( dim * sizeof(double) ); double *scale= (double *) malloc ( 2 * dim * sizeof(double) ); int ii; //find maximum values max_val_q= fabs(*qo); for (ii=1; ii < dim; ii++) if ( fabs(*(qo+ii)) > max_val_q ) max_val_q= fabs(*(qo+ii)); max_val_p= fabs(*po); for (ii=1; ii < dim; ii++) if ( fabs(*(po+ii)) > max_val_p ) max_val_p= fabs(*(po+ii)); //set up scale double c= fmax(atol, rtol * max_val_q); double s= log(exp(atol-c)+exp(rtol*max_val_q-c))+c; for (ii=0; ii < dim; ii++) *(scale+ii)= s; c= fmax(atol, rtol * max_val_p); s= log(exp(atol-c)+exp(rtol*max_val_p-c))+c; for (ii=0; ii < dim; ii++) *(scale+ii+dim)= s; //find good dt dt*= 2.; while ( err > 1. && init_dt / dt < _MAX_DT_REDUCE ){ dt/= 2.; //do one step with step dt, and one with step dt/2. /* dt */ //drift for c1*dt leapfrog_leapq(dim,qo,po,c1*dt,q12); to+= c1*dt; //kick for d1*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d1*dt,a,p12); //drift for c2*dt leapfrog_leapq(dim,q12,p12,c2*dt,qtmp); to+= c2*dt; //kick for d2*dt func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d2*dt,a,ptmp); //drift for c3*dt leapfrog_leapq(dim,qtmp,ptmp,c3*dt,q12); to+= c3*dt; //kick for d3*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,d3*dt,a,p12); //drift for c4*dt leapfrog_leapq(dim,q12,p12,c4*dt,qtmp); to+= c4*dt; //kick for d4*dt func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d4*dt,a,ptmp); //drift for c5*dt leapfrog_leapq(dim,qtmp,ptmp,c5*dt,q12); to+= c5*dt; //kick for d5*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,d5*dt,a,p12); //drift for c6*dt leapfrog_leapq(dim,q12,p12,c6*dt,qtmp); to+= c6*dt; //kick for d6*dt func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d6*dt,a,ptmp); //drift for c7*dt leapfrog_leapq(dim,qtmp,ptmp,c7*dt,q12); to+= c7*dt; //kick for d7*dt func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,d7*dt,a,p11); //drift for c8*dt leapfrog_leapq(dim,q12,p11,c8*dt,q11); to+= c8*dt; //p8=p7 //reset to-= dt; /* dt/2 */ //drift for c1*dt/2 leapfrog_leapq(dim,qo,po,c1*dt/2.,q12); to+= c1*dt/2.; //kick for d1*dt/2 func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,po,d1*dt/2.,a,p12); //drift for c2*dt/2 leapfrog_leapq(dim,q12,p12,c2*dt/2.,qtmp); to+= c2*dt/2.; //kick for d2*dt/2 func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d2*dt/2.,a,ptmp); //drift for c3*dt/2 leapfrog_leapq(dim,qtmp,ptmp,c3*dt/2.,q12); to+= c3*dt/2.; //kick for d3*dt/2 func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,d3*dt/2.,a,p12); //drift for c4*dt/2 leapfrog_leapq(dim,q12,p12,c4*dt/2.,qtmp); to+= c4*dt/2.; //kick for d4*dt/2 func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d4*dt/2.,a,ptmp); //drift for c5*dt/2 leapfrog_leapq(dim,qtmp,ptmp,c5*dt/2.,q12); to+= c5*dt/2.; //kick for d5*dt/2 func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,d5*dt/2.,a,p12); //drift for c6*dt/2 leapfrog_leapq(dim,q12,p12,c6*dt/2.,qtmp); to+= c6*dt/2.; //kick for d6*dt func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d6*dt/2.,a,ptmp); //drift for c7*dt/2. leapfrog_leapq(dim,qtmp,ptmp,c7*dt/2.,q12); to+= c7*dt/2.; //kick for d7*dt/2 func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,d7*dt/2.,a,p12); //drift for (c1+c8)*dt/2 leapfrog_leapq(dim,q12,p12,(c1+c8)*dt/2.,qtmp); to+= (c1+c8)*dt/2.; //kick for d1*dt/2 func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d1*dt/2.,a,ptmp); //drift for c2*dt/2 leapfrog_leapq(dim,qtmp,ptmp,c2*dt/2.,q12); to+= c2*dt/2.; //kick for d2*dt/2 func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,d2*dt/2.,a,p12); //drift for c3*dt/2 leapfrog_leapq(dim,q12,p12,c3*dt/2.,qtmp); to+= c3*dt/2.; //kick for d3*dt/2 func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d3*dt/2.,a,ptmp); //drift for c4*dt/2 leapfrog_leapq(dim,qtmp,ptmp,c4*dt/2.,q12); to+= c4*dt/2.; //kick for d4*dt/2 func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,d4*dt/2.,a,p12); //drift for c5*dt/2 leapfrog_leapq(dim,q12,p12,c5*dt/2.,qtmp); to+= c5*dt/2.; //kick for d5*dt/2 func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d5*dt/2.,a,ptmp); //drift for c6*dt/2 leapfrog_leapq(dim,qtmp,ptmp,c6*dt/2.,q12); to+= c6*dt/2.; //kick for d6*dt/2 func(to,q12,a,nargs,potentialArgs); leapfrog_leapp(dim,ptmp,d6*dt/2.,a,p12); //drift for c7*dt/2 leapfrog_leapq(dim,q12,p12,c7*dt/2.,qtmp); to+= c7*dt/2.; //kick for d7*dt func(to,qtmp,a,nargs,potentialArgs); leapfrog_leapp(dim,p12,d7*dt/2.,a,ptmp); //drift for c8*dt/2. leapfrog_leapq(dim,qtmp,ptmp,c8*dt/2.,q12); to+= c8*dt/2.; //p8=p7 for (ii=0; ii < dim; ii++) *(p12+ii)= *(ptmp+ii); //Norm err= 0.; for (ii=0; ii < dim; ii++) { err+= exp(2.*log(fabs(*(q11+ii)-*(q12+ii)))-2.* *(scale+ii)); err+= exp(2.*log(fabs(*(p11+ii)-*(p12+ii)))-2.* *(scale+ii+dim)); } err= sqrt(err/2./dim); //reset to-= dt; } //free what we allocated free(q11); free(q12); free(p11); free(qtmp); free(ptmp); free(a); free(scale); //return //printf("%f\n",dt); //fflush(stdout); return dt; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/bovy_symplecticode.h0000644000175100001710000000634114327773303020416 0ustar00runnerdocker/* C implementations of symplectic integrators */ /* Copyright (c) 2011, Jo Bovy All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef __BOVY_SYMPLECTICODE_H__ #define __BOVY_SYMPLECTICODE_H__ #ifdef __cplusplus extern "C" { #endif #include "signal.h" #include /* Global variables */ extern volatile sig_atomic_t interrupted; /* Function declarations */ #ifndef _WIN32 void handle_sigint(int); #else #include "windows.h" BOOL WINAPI CtrlHandler(DWORD fdwCtrlType); #endif void leapfrog(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *,int *); double leapfrog_estimate_step(void (*func)(double , double *, double *,int, struct potentialArg *), int, double *,double *, double, double *, int,struct potentialArg *, double,double); void symplec4(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *,int *); double symplec4_estimate_step(void (*func)(double , double *, double *,int, struct potentialArg *), int, double *,double *, double, double *, int,struct potentialArg *, double,double); void symplec6(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *,int *); double symplec6_estimate_step(void (*func)(double , double *, double *,int, struct potentialArg *), int, double *,double *, double, double *, int,struct potentialArg *, double,double); #ifdef __cplusplus } #endif #endif /* bovy_symplecticode.h */ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/config.py0000644000175100001710000000712614327773303016163 0ustar00runnerdockerimport configparser import copy import os import os.path # The default configuration default_configuration= {'normalization': {'ro':'8.', 'vo':'220.'}, 'astropy': {'astropy-units':'False', 'astropy-coords':'True'}, 'plot': {'seaborn-bovy-defaults':'False'}, 'warnings': {'verbose':'False'}, 'version-check': {'do-check':'True', 'check-non-interactive':'True', 'check-non-interactive-every':'1', 'last-non-interactive-check':'2000-01-01'}} default_filename= os.path.join(os.path.expanduser('~'),'.galpyrc') def check_config(configuration): # Check that the configuration is a valid galpy configuration for sec_key in default_configuration.keys(): if not configuration.has_section(sec_key): return False for key in default_configuration[sec_key]: if not configuration.has_option(sec_key,key): return False return True def write_config(filename,configuration=None): # Writes default if configuration is None writeconfig= configparser.ConfigParser() # Write different sections for sec_key in default_configuration.keys(): writeconfig.add_section(sec_key) for key in default_configuration[sec_key]: if configuration is None \ or not configuration.has_section(sec_key) \ or not configuration.has_option(sec_key,key): writeconfig.set(sec_key,key, default_configuration[sec_key][key]) else: writeconfig.set(sec_key,key,configuration.get(sec_key,key)) with open(filename,'w') as configfile: writeconfig.write(configfile) return None # Read the configuration file __config__= configparser.ConfigParser() cfilename= __config__.read('.galpyrc') if not cfilename: cfilename= __config__.read(default_filename) if not cfilename: write_config(default_filename) cfilename= __config__.read(default_filename) if not check_config(__config__): write_config(cfilename[-1],__config__) cfilename= __config__.read(cfilename[-1]) # Store a version of the config in case we need to re-write parts of it, # but don't want to apply changes that we don't want to re-write configfilename= cfilename[-1] __orig__config__= copy.deepcopy(__config__) # Set configuration variables on the fly def set_ro(ro): """ NAME: set_ro PURPOSE: set the global configuration value of ro (distance scale) INPUT: ro - scale in kpc or astropy Quantity OUTPUT: (none) HISTORY: 2016-01-05 - Written - Bovy (UofT) """ from ..util._optional_deps import _APY_LOADED if _APY_LOADED: from astropy import units if _APY_LOADED and isinstance(ro,units.Quantity): ro= ro.to(units.kpc).value __config__.set('normalization','ro',str(ro)) def set_vo(vo): """ NAME: set_vo PURPOSE: set the global configuration value of vo (velocity scale) INPUT: vo - scale in km/s or astropy Quantity OUTPUT: (none) HISTORY: 2016-01-05 - Written - Bovy (UofT) """ from ..util._optional_deps import _APY_LOADED if _APY_LOADED: from astropy import units if _APY_LOADED and isinstance(vo,units.Quantity): vo= vo.to(units.km/units.s).value __config__.set('normalization','vo',str(vo)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/conversion.py0000644000175100001710000011023514327773303017077 0ustar00runnerdocker############################################################################### # # conversion: utilities to convert from galpy 'natural units' to physical # units # ############################################################################### import copy import math as m import warnings from functools import wraps from ..util._optional_deps import _APY_LOADED, _APY_UNITS from ..util.config import __config__ if not _APY_LOADED: _G= 4.302*10.**-3. #pc / Msolar (km/s)^2 _kmsInPcMyr= 1.0227121655399913 _PCIN10p18CM= 3.08567758 #10^18 cm _CIN10p5KMS= 2.99792458 #10^5 km/s _MSOLAR10p30KG= 1.9891 #10^30 kg _EVIN10m19J= 1.60217657 #10^-19 J else: from astropy import constants, units _G= constants.G.to(units.pc/units.Msun*units.km**2/units.s**2).value _kmsInPcMyr= (units.km/units.s).to(units.pc/units.Myr) _PCIN10p18CM= units.pc.to(units.cm)/10.**18. #10^18 cm _CIN10p5KMS= constants.c.to(units.km/units.s).value/10.**5. #10^5 km/s _MSOLAR10p30KG= units.Msun.to(units.kg)/10.**30. #10^30 kg _EVIN10m19J= units.eV.to(units.J)*10.**19. #10^-19 J _MyrIn1013Sec= 3.65242198*0.24*3.6 #use tropical year, like for pms _TWOPI= 2.*m.pi def dens_in_criticaldens(vo,ro,H=70.): """ NAME: dens_in_criticaldens PURPOSE: convert density to units of the critical density INPUT: vo - velocity unit in km/s ro - length unit in kpc H= (default: 70) Hubble constant in km/s/Mpc OUTPUT: conversion from units where vo=1. at ro=1. to units of the critical density HISTORY: 2014-01-28 - Written - Bovy (IAS) """ return vo**2./ro**2.*10.**6./H**2.*8.*m.pi/3. def dens_in_meanmatterdens(vo,ro,H=70.,Om=0.3): """ NAME: dens_in_meanmatterdens PURPOSE: convert density to units of the mean matter density INPUT: vo - velocity unit in km/s ro - length unit in kpc H= (default: 70) Hubble constant in km/s/Mpc Om= (default: 0.3) Omega matter OUTPUT: conversion from units where vo=1. at ro=1. to units of the mean matter density HISTORY: 2014-01-28 - Written - Bovy (IAS) """ return dens_in_criticaldens(vo,ro,H=H)/Om def dens_in_gevcc(vo,ro): """ NAME: dens_in_gevcc PURPOSE: convert density to GeV / cm^3 INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. to GeV/cm^3 HISTORY: 2014-06-16 - Written - Bovy (IAS) """ return vo**2./ro**2./_G*_MSOLAR10p30KG*_CIN10p5KMS**2./_EVIN10m19J/ _PCIN10p18CM**3.*10.**-4. def dens_in_msolpc3(vo,ro): """ NAME: dens_in_msolpc3 PURPOSE: convert density to Msolar / pc^3 INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. to Msolar/pc^3 HISTORY: 2013-09-01 - Written - Bovy (IAS) """ return vo**2./ro**2./_G*10.**-6. def force_in_2piGmsolpc2(vo,ro): """ NAME: force_in_2piGmsolpc2 PURPOSE: convert a force or acceleration to 2piG x Msolar / pc^2 INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. HISTORY: 2013-09-01 - Written - Bovy (IAS) """ return vo**2./ro/_G*10.**-3./_TWOPI def force_in_pcMyr2(vo,ro): """ NAME: force_in_pcMyr2 PURPOSE: convert a force or acceleration to pc/Myr^2 INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. HISTORY: 2013-09-01 - Written - Bovy (IAS) """ return vo**2./ro*_kmsInPcMyr**2.*10.**-3. def force_in_kmsMyr(vo,ro): """ NAME: force_in_kmsMyr PURPOSE: convert a force or acceleration to km/s/Myr INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. HISTORY: 2013-09-01 - Written - Bovy (IAS) """ return vo**2./ro*_kmsInPcMyr*10.**-3. def force_in_10m13kms2(vo,ro): """ NAME: force_in_10m13kms2 PURPOSE: convert a force or acceleration to 10^(-13) km/s^2 INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. HISTORY: 2014-01-22 - Written - Bovy (IAS) """ return vo**2./ro*_kmsInPcMyr*10.**-3./_MyrIn1013Sec def freq_in_Gyr(vo,ro): """ NAME: freq_in_Gyr PURPOSE: convert a frequency to 1/Gyr INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. HISTORY: 2013-09-01 - Written - Bovy (IAS) """ return vo/ro*_kmsInPcMyr def freq_in_kmskpc(vo,ro): """ NAME: freq_in_kmskpc PURPOSE: convert a frequency to km/s/kpc INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. HISTORY: 2013-09-01 - Written - Bovy (IAS) """ return vo/ro def surfdens_in_msolpc2(vo,ro): """ NAME: surfdens_in_msolpc2 PURPOSE: convert a surface density to Msolar / pc^2 INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. HISTORY: 2013-09-01 - Written - Bovy (IAS) """ return vo**2./ro/_G*10.**-3. def mass_in_msol(vo,ro): """ NAME: mass_in_msol PURPOSE: convert a mass to Msolar INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. HISTORY: 2013-09-01 - Written - Bovy (IAS) """ return vo**2.*ro/_G*10.**3. def mass_in_1010msol(vo,ro): """ NAME: mass_in_1010msol PURPOSE: convert a mass to 10^10 x Msolar INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. HISTORY: 2013-09-01 - Written - Bovy (IAS) """ return vo**2.*ro/_G*10.**-7. def time_in_Gyr(vo,ro): """ NAME: time_in_Gyr PURPOSE: convert a time to Gyr INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. HISTORY: 2013-09-01 - Written - Bovy (IAS) """ return ro/vo/_kmsInPcMyr def velocity_in_kpcGyr(vo,ro): """ NAME: velocity_in_kpcGyr PURPOSE: convert a velocity to kpc/Gyr INPUT: vo - velocity unit in km/s ro - length unit in kpc OUTPUT: conversion from units where vo=1. at ro=1. HISTORY: 2014-12-19 - Written - Bovy (IAS) """ return vo*_kmsInPcMyr def get_physical(obj,include_set=False): """ NAME: get_physical PURPOSE: return the velocity and length units for converting between physical and internal units as a dictionary for any galpy object, so they can easily be fed to galpy routines INPUT: obj - a galpy object or list of such objects (e.g., a Potential, list of Potentials, Orbit, actionAngle instance, DF instance) include_set= (False) if True, also include roSet and voSet, flags of whether the unit is explicitly set in the object OUTPUT: Dictionary {'ro':length unit in kpc,'vo':velocity unit in km/s}; note that this routine will *always* return these conversion units, even if the obj you provide does not have units turned on HISTORY: 2019-08-03 - Written - Bovy (UofT) """ # Try flattening the object in case it's a nested list of Potentials from ..potential import Force from ..potential import flatten as flatten_pot from ..potential import linearPotential, planarPotential try: new_obj= flatten_pot(obj) except: # pragma: no cover pass # hope for the best! else: # only apply flattening for potentials if isinstance(new_obj,(Force,planarPotential,linearPotential)) \ or (isinstance(new_obj,list) and len(new_obj) > 0 \ and isinstance(new_obj[0],(Force,planarPotential,linearPotential))): obj= new_obj if isinstance(obj,list): out_obj= obj[0] else: out_obj= obj out= {'ro':out_obj._ro,'vo':out_obj._vo} if include_set: out.update({'roSet':out_obj._roSet,'voSet':out_obj._voSet}) return out def physical_compatible(obj,other_obj): """ NAME: physical_compatible PURPOSE: test whether the velocity and length units for converting between physical and internal units are compatible for two galpy objects INPUT: obj - a galpy object or list of such objects (e.g., a Potential, list of Potentials, Orbit, actionAngle instance, DF instance) other_obj - another galpy object or list of such objects (e.g., a Potential, list of Potentials, Orbit, actionAngle instance, DF instance) OUTPUT: True if the units are compatible, False if not (compatible means that the units are the same when they are set for both objects) HISTORY: 2020-04-22 - Written - Bovy (UofT) """ if obj is None or other_obj is None: # if one is None, just state compat return True phys= get_physical(obj,include_set=True) other_phys= get_physical(other_obj,include_set=True) out= True if phys['roSet'] and other_phys['roSet']: out= out and m.fabs((phys['ro']-other_phys['ro'])/phys['ro']) < 1e-8 if phys['voSet'] and other_phys['voSet']: out= out and m.fabs((phys['vo']-other_phys['vo'])/phys['vo']) < 1e-8 return out # Parsers of different inputs with units def parse_length(x,ro=None,vo=None): return x.to(units.kpc).value/ro \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_length_kpc(x): return x.to(units.kpc).value \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_velocity(x,ro=None,vo=None): return x.to(units.km/units.s).value/vo \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_velocity_kms(x): return x.to(units.km/units.s).value \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_angle(x): return x.to(units.rad).value \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_time(x,ro=None,vo=None): return x.to(units.Gyr).value/time_in_Gyr(vo,ro) \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_mass(x,ro=None,vo=None): try: return x.to(units.pc*units.km**2/units.s**2).value\ /mass_in_msol(vo,ro)/_G \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x except units.UnitConversionError: pass return x.to(1e10*units.Msun).value/mass_in_1010msol(vo,ro) \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_energy(x,ro=None,vo=None): return x.to(units.km**2/units.s**2).value/vo**2. \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_angmom(x,ro=None,vo=None): return x.to(units.kpc*units.km/units.s).value/ro/vo \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_frequency(x,ro=None,vo=None): return x.to(units.km/units.s/units.kpc).value/\ freq_in_kmskpc(vo,ro) \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_force(x,ro=None,vo=None): try: return x.to(units.pc/units.Myr**2).value\ /force_in_pcMyr2(vo,ro) \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x except units.UnitConversionError: pass return x.to(units.Msun/units.pc**2).value\ /force_in_2piGmsolpc2(vo,ro) \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_dens(x,ro=None,vo=None): try: return x.to(units.Msun/units.pc**3).value\ /dens_in_msolpc3(vo,ro) \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x except units.UnitConversionError: pass # Try Gxdens return x.to(units.km**2/units.s**2/units.pc**2).value\ /dens_in_msolpc3(vo,ro)/_G \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_surfdens(x,ro=None,vo=None): try: return x.to(units.Msun/units.pc**2).value\ /surfdens_in_msolpc2(vo,ro) \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x except units.UnitConversionError: pass # Try Gxsurfdens return x.to(units.km**2/units.s**2/units.pc).value\ /surfdens_in_msolpc2(vo,ro)/_G \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x def parse_numdens(x,ro=None,vo=None): return x.to(1/units.kpc**3).value*ro**3 \ if _APY_LOADED and isinstance(x,units.Quantity) \ else x #Decorator to apply these transformations # NOTE: names with underscores in them signify return values that *always* have # units, which is depended on in the Orbit returns (see issue #326) _roNecessary= {'time': True, 'position': True, 'position_kpc': True, 'velocity': False, 'velocity2': False, 'velocity2surfacendensity': False, 'velocity_kms': False, 'energy': False, 'density': True, 'numberdensity': True, 'force': True, 'velocity2surfacedensity': True, 'surfacedensity': True, 'numbersurfacedensity': True, 'surfacedensitydistance': True, 'mass': True, 'action': True, 'frequency':True, 'frequency-kmskpc':True, 'forcederivative':True, 'angle':True, 'angle_deg':True, 'proper-motion_masyr':True, 'phasespacedensity':True, 'phasespacedensity2d':True, 'phasespacedensityvelocity':True, 'phasespacedensityvelocity2':True, 'dimensionless':False} _voNecessary= copy.copy(_roNecessary) _voNecessary['position']= False _voNecessary['position_kpc']= False _voNecessary['angle']= False _voNecessary['angle_deg']= False _voNecessary['velocity']= True _voNecessary['velocity2']= True _voNecessary['velocity_kms']= True _voNecessary['energy']= True def physical_conversion(quantity,pop=False): """Decorator to convert to physical coordinates: quantity = [position,velocity,time]""" def wrapper(method): @wraps(method) def wrapped(*args,**kwargs): use_physical_explicitly_set= \ kwargs.get('use_physical',None) is not None use_physical= kwargs.get('use_physical',True) and \ not kwargs.get('log',False) # Parse whether ro or vo should be considered to be set, because # the return value will have units anyway # (like in Orbit methods that return numbers with units, like ra) roSet= '_' in quantity # _ in quantity name means always units voSet= '_' in quantity # _ in quantity name means always units use_physical= use_physical or '_' in quantity # _ in quantity name means always units ro= kwargs.get('ro',None) if ro is None and \ (roSet or (hasattr(args[0],'_roSet') and args[0]._roSet)): ro= args[0]._ro if ro is None and isinstance(args[0],list) \ and hasattr(args[0][0],'_roSet') and args[0][0]._roSet: # For lists of Potentials ro= args[0][0]._ro if _APY_LOADED and isinstance(ro,units.Quantity): ro= ro.to(units.kpc).value vo= kwargs.get('vo',None) if vo is None and \ (voSet or (hasattr(args[0],'_voSet') and args[0]._voSet)): vo= args[0]._vo if vo is None and isinstance(args[0],list) \ and hasattr(args[0][0],'_voSet') and args[0][0]._voSet: # For lists of Potentials vo= args[0][0]._vo if _APY_LOADED and isinstance(vo,units.Quantity): vo= vo.to(units.km/units.s).value # Override Quantity output? _apy_units= kwargs.get('quantity',_APY_UNITS) #Remove ro, vo, use_physical, and quantity kwargs if necessary if pop and 'use_physical' in kwargs: kwargs.pop('use_physical') if pop and 'quantity' in kwargs: kwargs.pop('quantity') if pop and 'ro' in kwargs: kwargs.pop('ro') if pop and 'vo' in kwargs: kwargs.pop('vo') if use_physical and \ not (_voNecessary[quantity.lower()] and vo is None) and \ not (_roNecessary[quantity.lower()] and ro is None): from ..orbit import Orbit if quantity.lower() == 'time': fac= time_in_Gyr(vo,ro) if _apy_units: u= units.Gyr elif quantity.lower() == 'position': fac= ro if _apy_units: u= units.kpc elif quantity.lower() == 'position_kpc': # already in kpc fac= 1. if _apy_units: u= units.kpc elif quantity.lower() == 'velocity': fac= vo if _apy_units: u= units.km/units.s elif quantity.lower() == 'velocity2': fac= vo**2. if _apy_units: u= (units.km/units.s)**2 elif quantity.lower() == 'velocity_kms': # already in km/s fac= 1. if _apy_units: u= units.km/units.s elif quantity.lower() == 'frequency': if kwargs.get('kmskpc',False) and not _apy_units: fac= freq_in_kmskpc(vo,ro) else: fac= freq_in_Gyr(vo,ro) if _apy_units: u= units.Gyr**-1. elif quantity.lower() == 'frequency-kmskpc': fac= freq_in_kmskpc(vo,ro) if _apy_units: u= units.km/units.s/units.kpc elif quantity.lower() == 'action': fac= ro*vo if _apy_units: u= units.kpc*units.km/units.s elif quantity.lower() == 'energy': fac= vo**2. if _apy_units: u= units.km**2./units.s**2. elif quantity.lower() == 'angle': # in rad fac= 1. if _apy_units: u= units.rad elif quantity.lower() == 'angle_deg': # already in deg fac= 1. if _apy_units: u= units.deg elif quantity.lower() == 'proper-motion_masyr': # already in mas/yr fac= 1. if _apy_units: u= units.mas/units.yr elif quantity.lower() == 'force': fac= force_in_kmsMyr(vo,ro) if _apy_units: u= units.km/units.s/units.Myr elif quantity.lower() == 'density': fac= dens_in_msolpc3(vo,ro) if _apy_units: u= units.Msun/units.pc**3 elif quantity.lower() == 'numberdensity': fac= 1/ro**3. if _apy_units: u= 1/units.kpc**3 elif quantity.lower() == 'velocity2surfacedensity': fac= surfdens_in_msolpc2(vo,ro)*vo**2 if _apy_units: u= units.Msun/units.pc**2*(units.km/units.s)**2 elif quantity.lower() == 'surfacedensity': fac= surfdens_in_msolpc2(vo,ro) if _apy_units: u= units.Msun/units.pc**2 elif quantity.lower() == 'numbersurfacedensity': fac= 1./ro**2. if _apy_units: u= 1/units.kpc**2 elif quantity.lower() == 'surfacedensitydistance': fac= surfdens_in_msolpc2(vo,ro)*ro*1000. if _apy_units: u= units.Msun/units.pc elif quantity.lower() == 'mass': fac= mass_in_msol(vo,ro) if _apy_units: u= units.Msun elif quantity.lower() == 'forcederivative': fac= freq_in_Gyr(vo,ro)**2. if _apy_units: u= units.Gyr**-2. elif quantity.lower() == 'phasespacedensity': fac= 1./vo**3./ro**3. if _apy_units: u= 1/(units.km/units.s)**3/units.kpc**3 elif quantity.lower() == 'phasespacedensity2d': fac= 1./vo**2./ro**2. if _apy_units: u= 1/(units.km/units.s)**2/units.kpc**2 elif quantity.lower() == 'phasespacedensityvelocity': fac= 1./vo**2./ro**3. if _apy_units: u= 1/(units.km/units.s)**2/units.kpc**3 elif quantity.lower() == 'phasespacedensityvelocity2': fac= 1./vo/ro**3. if _apy_units: u= 1/(units.km/units.s)/units.kpc**3 elif quantity.lower() == 'dimensionless': fac= 1. if _apy_units: u= units.dimensionless_unscaled out= method(*args,**kwargs) if out is None: return out if _apy_units: return units.Quantity(out*fac,unit=u) else: return out*fac else: if use_physical and use_physical_explicitly_set: warnings.warn("Returning output(s) in internal units even though use_physical=True, because ro and/or vo not set") return method(*args,**kwargs) return wrapped return wrapper def potential_physical_input(method): """Decorator to convert inputs to Potential functions from physical to internal coordinates""" @wraps(method) def wrapper(*args,**kwargs): from ..potential import flatten as flatten_potential Pot= flatten_potential(args[0]) ro= kwargs.get('ro',None) if ro is None and hasattr(Pot,'_ro'): ro= Pot._ro if ro is None and isinstance(Pot,list) \ and hasattr(Pot[0],'_ro'): # For lists of Potentials ro= Pot[0]._ro if _APY_LOADED and isinstance(ro,units.Quantity): ro= ro.to(units.kpc).value if 't' in kwargs or 'M' in kwargs: vo= kwargs.get('vo',None) if vo is None and hasattr(Pot,'_vo'): vo= Pot._vo if vo is None and isinstance(Pot,list) \ and hasattr(Pot[0],'_vo'): # For lists of Potentials vo= Pot[0]._vo if _APY_LOADED and isinstance(vo,units.Quantity): vo= vo.to(units.km/units.s).value # Loop through args newargs= (Pot,) for ii in range(1,len(args)): if _APY_LOADED and isinstance(args[ii],units.Quantity): newargs= newargs+(args[ii].to(units.kpc).value/ro,) else: newargs= newargs+(args[ii],) args= newargs # phi and t kwargs, also do R, z, and x in case these are given as kwargs if 'phi' in kwargs and _APY_LOADED \ and isinstance(kwargs['phi'],units.Quantity): kwargs['phi']= kwargs['phi'].to(units.rad).value if 't' in kwargs and _APY_LOADED \ and isinstance(kwargs['t'],units.Quantity): kwargs['t']= kwargs['t'].to(units.Gyr).value\ /time_in_Gyr(vo,ro) if 'R' in kwargs and _APY_LOADED \ and isinstance(kwargs['R'],units.Quantity): kwargs['R']= kwargs['R'].to(units.kpc).value/ro if 'z' in kwargs and _APY_LOADED \ and isinstance(kwargs['z'],units.Quantity): kwargs['z']= kwargs['z'].to(units.kpc).value/ro if 'x' in kwargs and _APY_LOADED \ and isinstance(kwargs['x'],units.Quantity): kwargs['x']= kwargs['x'].to(units.kpc).value/ro # v kwarg for dissipative forces if 'v' in kwargs and _APY_LOADED \ and isinstance(kwargs['v'],units.Quantity): kwargs['v']= kwargs['v'].to(units.km/units.s).value/vo # Mass kwarg for rtide if 'M' in kwargs and _APY_LOADED \ and isinstance(kwargs['M'],units.Quantity): try: kwargs['M']= kwargs['M'].to(units.Msun).value\ /mass_in_msol(vo,ro) except units.UnitConversionError: kwargs['M']= kwargs['M'].to(units.pc*units.km**2/units.s**2)\ .value/mass_in_msol(vo,ro)/_G # kwargs that come up in quasiisothermaldf # z done above if 'dz' in kwargs and _APY_LOADED \ and isinstance(kwargs['dz'],units.Quantity): kwargs['dz']= kwargs['dz'].to(units.kpc).value/ro if 'dR' in kwargs and _APY_LOADED \ and isinstance(kwargs['dR'],units.Quantity): kwargs['dR']= kwargs['dR'].to(units.kpc).value/ro if 'zmax' in kwargs and _APY_LOADED \ and isinstance(kwargs['zmax'],units.Quantity): kwargs['zmax']= kwargs['zmax'].to(units.kpc).value/ro return method(*args,**kwargs) return wrapper def physical_conversion_actionAngle(quantity,pop=False): """Decorator to convert to physical coordinates for the actionAngle methods: quantity= call, actionsFreqs, or actionsFreqsAngles (or EccZmaxRperiRap for actionAngleStaeckel)""" def wrapper(method): @wraps(method) def wrapped(*args,**kwargs): use_physical= kwargs.get('use_physical',True) ro= kwargs.get('ro',None) if ro is None and hasattr(args[0],'_roSet') and args[0]._roSet: ro= args[0]._ro if _APY_LOADED and isinstance(ro,units.Quantity): ro= ro.to(units.kpc).value vo= kwargs.get('vo',None) if vo is None and hasattr(args[0],'_voSet') and args[0]._voSet: vo= args[0]._vo if _APY_LOADED and isinstance(vo,units.Quantity): vo= vo.to(units.km/units.s).value #Remove ro and vo kwargs if necessary if pop and 'use_physical' in kwargs: kwargs.pop('use_physical') if pop and 'ro' in kwargs: kwargs.pop('ro') if pop and 'vo' in kwargs: kwargs.pop('vo') if use_physical and not vo is None and not ro is None: out= method(*args,**kwargs) if 'call' in quantity or 'actions' in quantity: if 'actions' in quantity and len(out) < 4: # 1D system fac= [ro*vo] if _APY_UNITS: u= [units.kpc*units.km/units.s] else: fac= [ro*vo,ro*vo,ro*vo] if _APY_UNITS: u= [units.kpc*units.km/units.s, units.kpc*units.km/units.s, units.kpc*units.km/units.s] if 'Freqs' in quantity: FreqsFac= freq_in_Gyr(vo,ro) if len(out) < 4: # 1D system fac.append(FreqsFac) if _APY_UNITS: Freqsu= units.Gyr**-1. u.append(Freqsu) else: fac.extend([FreqsFac,FreqsFac,FreqsFac]) if _APY_UNITS: Freqsu= units.Gyr**-1. u.extend([Freqsu,Freqsu,Freqsu]) if 'Angles' in quantity: if len(out) < 4: # 1D system fac.append(1.) if _APY_UNITS: Freqsu= units.Gyr**-1. u.append(units.rad) else: fac.extend([1.,1.,1.]) if _APY_UNITS: Freqsu= units.Gyr**-1. u.extend([units.rad,units.rad,units.rad]) if 'EccZmaxRperiRap' in quantity: fac= [1.,ro,ro,ro] if _APY_UNITS: u= [1., units.kpc, units.kpc, units.kpc] if _APY_UNITS: newOut= () try: for ii in range(len(out)): newOut= newOut+(units.Quantity(out[ii]*fac[ii], unit=u[ii]),) except TypeError: # happens if out = scalar newOut= units.Quantity(out*fac[0],unit=u[0]) else: newOut= () try: for ii in range(len(out)): newOut= newOut+(out[ii]*fac[ii],) except TypeError: # happens if out = scalar newOut= out*fac[0] return newOut else: return method(*args,**kwargs) return wrapped return wrapper def actionAngle_physical_input(method): """Decorator to convert inputs to actionAngle functions from physical to internal coordinates""" @wraps(method) def wrapper(*args,**kwargs): if len(args) < 3: # orbit input return method(*args,**kwargs) ro= kwargs.get('ro',None) if ro is None and hasattr(args[0],'_ro'): ro= args[0]._ro if _APY_LOADED and isinstance(ro,units.Quantity): ro= ro.to(units.kpc).value vo= kwargs.get('vo',None) if vo is None and hasattr(args[0],'_vo'): vo= args[0]._vo if _APY_LOADED and isinstance(vo,units.Quantity): vo= vo.to(units.km/units.s).value # Loop through args newargs= () for ii in range(len(args)): if _APY_LOADED and isinstance(args[ii],units.Quantity): try: targ= args[ii].to(units.kpc).value/ro except units.UnitConversionError: try: targ= args[ii].to(units.km/units.s).value/vo except units.UnitConversionError: try: targ= args[ii].to(units.rad).value except units.UnitConversionError: raise units.UnitConversionError("Input units not understood") newargs= newargs+(targ,) else: newargs= newargs+(args[ii],) args= newargs return method(*args,**kwargs) return wrapper def physical_conversion_actionAngleInverse(quantity,pop=False): """Decorator to convert to physical coordinates for the actionAngleInverse methods: quantity= call, xvFreqs, or Freqs""" def wrapper(method): @wraps(method) def wrapped(*args,**kwargs): use_physical= kwargs.get('use_physical',True) ro= kwargs.get('ro',None) if ro is None and hasattr(args[0],'_roSet') and args[0]._roSet: ro= args[0]._ro if _APY_LOADED and isinstance(ro,units.Quantity): ro= ro.to(units.kpc).value vo= kwargs.get('vo',None) if vo is None and hasattr(args[0],'_voSet') and args[0]._voSet: vo= args[0]._vo if _APY_LOADED and isinstance(vo,units.Quantity): vo= vo.to(units.km/units.s).value #Remove ro and vo kwargs if necessary if pop and 'use_physical' in kwargs: kwargs.pop('use_physical') if pop and 'ro' in kwargs: kwargs.pop('ro') if pop and 'vo' in kwargs: kwargs.pop('vo') if use_physical and not vo is None and not ro is None: fac= [] u= [] out= method(*args,**kwargs) if 'call' in quantity or 'xv' in quantity: if 'xv' in quantity and len(out) < 4: # 1D system fac.extend([ro,vo]) if _APY_UNITS: u.extend([units.kpc,units.km/units.s]) else: fac.extend([ro,vo,vo,ro,vo,1.]) if _APY_UNITS: u.extend([units.kpc,units.km/units.s, units.km/units.s,units.kpc, units.km/units.s, units.rad]) if 'Freqs' in quantity: FreqsFac= freq_in_Gyr(vo,ro) if isinstance(out,float): # 1D system fac.append(FreqsFac) if _APY_UNITS: Freqsu= units.Gyr**-1. u.append(Freqsu) else: fac.extend([FreqsFac,FreqsFac,FreqsFac]) if _APY_UNITS: Freqsu= units.Gyr**-1. u.extend([Freqsu,Freqsu,Freqsu]) if _APY_UNITS: newOut= () try: for ii in range(len(out)): newOut= newOut+(units.Quantity(out[ii]*fac[ii], unit=u[ii]),) except TypeError: # Happens when out == scalar newOut= units.Quantity(out*fac[0],unit=u[0]) else: newOut= () try: for ii in range(len(out)): newOut= newOut+(out[ii]*fac[ii],) except TypeError: # Happens when out == scalar newOut= out*fac[0] return newOut else: return method(*args,**kwargs) return wrapped return wrapper def actionAngleInverse_physical_input(method): """Decorator to convert inputs to actionAngleInverse functions from physical to internal coordinates""" @wraps(method) def wrapper(*args,**kwargs): ro= kwargs.get('ro',None) if ro is None and hasattr(args[0],'_ro'): ro= args[0]._ro if _APY_LOADED and isinstance(ro,units.Quantity): ro= ro.to(units.kpc).value vo= kwargs.get('vo',None) if vo is None and hasattr(args[0],'_vo'): vo= args[0]._vo if _APY_LOADED and isinstance(vo,units.Quantity): vo= vo.to(units.km/units.s).value # Loop through args newargs= () for ii in range(len(args)): if _APY_LOADED and isinstance(args[ii],units.Quantity): try: targ= args[ii].to(units.kpc*units.km/units.s).value/ro/vo except units.UnitConversionError: try: targ= args[ii].to(units.rad).value except units.UnitConversionError: raise units.UnitConversionError("Input units not understood") newargs= newargs+(targ,) else: newargs= newargs+(args[ii],) args= newargs return method(*args,**kwargs) return wrapper ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/coords.py0000644000175100001710000021375714327773303016220 0ustar00runnerdocker############################################################################### # # coords: module for coordinate transformations between the equatorial # and Galactic coordinate frame # # # Main included functions: # radec_to_lb # lb_to_radec # radec_to_custom # custom_to_radec # lbd_to_XYZ # XYZ_to_lbd # rectgal_to_sphergal # sphergal_to_rectgal # vrpmllpmbb_to_vxvyvz # vxvyvz_to_vrpmllpmbb # pmrapmdec_to_pmllpmbb # pmllpmbb_to_pmrapmdec # pmrapmdec_to_custom # custom_to_pmrapmdec # cov_pmrapmdec_to_pmllpmbb # cov_dvrpmllbb_to_vxyz # XYZ_to_galcenrect # XYZ_to_galcencyl # galcenrect_to_XYZ # galcencyl_to_XYZ # rect_to_cyl # cyl_to_rect # rect_to_cyl_vec # cyl_to_rect_vec # vxvyvz_to_galcenrect # vxvyvz_to_galcencyl # galcenrect_to_vxvyvz # galcencyl_to_vxvyvz # dl_to_rphi_2d # rphi_to_dl_2d # Rz_to_coshucosv # Rz_to_uv # uv_to_Rz # Rz_to_lambdanu # Rz_to_lambdanu_jac # Rz_to_lambdanu_hess # lambdanu_to_Rz # ############################################################################## ############################################################################# #Copyright (c) 2010 - 2020, Jo Bovy #All rights reserved. # #Redistribution and use in source and binary forms, with or without #modification, are permitted provided that the following conditions are met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # #THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS #"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT #LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR #A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT #HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, #INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, #BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS #OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED #AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT #LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY #WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE #POSSIBILITY OF SUCH DAMAGE. ############################################################################# from functools import wraps import numpy from ..util import _rotate_to_arbitrary_vector from ..util._optional_deps import _APY_LOADED from ..util.config import __config__ _APY_COORDS= __config__.getboolean('astropy','astropy-coords') _APY_COORDS*= _APY_LOADED _DEGTORAD= numpy.pi/180. if _APY_LOADED: from astropy import units _K= (1.*units.mas/units.yr).to(units.km/units.s/units.kpc, equivalencies=units.dimensionless_angles())\ .value else: _K=4.74047 # numpy 1.14 einsum bug causes astropy conversions to fail in py2.7 -> turn off if _APY_COORDS: import astropy.coordinates as apycoords ra, dec= numpy.array([192.25*_DEGTORAD]), numpy.array([27.4*_DEGTORAD]) c= apycoords.SkyCoord(ra*units.rad,dec*units.rad, equinox='B1950',frame='fk4') # This conversion fails bc of einsum bug try: c= c.transform_to(apycoords.Galactic) except TypeError: # pragma: no cover _APY_COORDS= False def scalarDecorator(func): """Decorator to return scalar outputs as a set""" @wraps(func) def scalar_wrapper(*args,**kwargs): if numpy.array(args[0]).shape == (): scalarOut= True newargs= () for ii in range(len(args)): newargs= newargs+(numpy.array([args[ii]]),) args= newargs else: scalarOut= False result= func(*args,**kwargs) if scalarOut: out= () for ii in range(result.shape[1]): out= out+(result[0,ii],) return out else: return result return scalar_wrapper def degreeDecorator(inDegrees,outDegrees): """ NAME: degreeDecorator PURPOSE: Decorator to transform angles from and to degrees if necessary INPUT: inDegrees - list specifying indices of angle arguments (e.g., [0,1]) outDegrees - list, same as inDegrees, but for function return HISTORY: ____-__-__ - Written - Bovy 2019-03-02 - speedup - Nathaniel Starkman (UofT) """ # (modified) old degree decorator def wrapper(func): @wraps(func) def wrapped(*args, **kwargs): isdeg = kwargs.get('degree', False) if isdeg: args = [arg * numpy.pi / 180 if i in inDegrees else arg for i, arg in enumerate(args)] out = func(*args, **kwargs) if isdeg: for i in outDegrees: out[:, i] *= 180. / numpy.pi return out return wrapped return wrapper @scalarDecorator @degreeDecorator([0,1],[0,1]) def radec_to_lb(ra,dec,degree=False,epoch=2000.0): """ NAME: radec_to_lb PURPOSE: transform from equatorial coordinates to Galactic coordinates INPUT: ra - right ascension dec - declination degree - (Bool) if True, ra and dec are given in degree and l and b will be as well epoch - epoch of ra,dec (right now only 2000.0 and 1950.0 are supported when not using astropy's transformations internally; when internally using astropy's coordinate transformations, epoch can be None for ICRS, 'JXXXX' for FK5, and 'BXXXX' for FK4) OUTPUT: l,b For vector inputs [:,2] HISTORY: 2009-11-12 - Written - Bovy (NYU) 2014-06-14 - Re-written w/ numpy functions for speed and w/ decorators for beauty - Bovy (IAS) 2016-05-13 - Added support for using astropy's coordinate transformations and for non-standard epochs - Bovy (UofT) """ if _APY_COORDS: epoch, frame= _parse_epoch_frame_apy(epoch) c= apycoords.SkyCoord(ra*units.rad,dec*units.rad, equinox=epoch,frame=frame) c= c.transform_to(apycoords.Galactic) return numpy.array([c.l.to(units.rad).value,c.b.to(units.rad).value]).T #First calculate the transformation matrix T theta,dec_ngp,ra_ngp= get_epoch_angles(epoch) T= numpy.dot(numpy.array([[numpy.cos(theta),numpy.sin(theta),0.],[numpy.sin(theta),-numpy.cos(theta),0.],[0.,0.,1.]]),numpy.dot(numpy.array([[-numpy.sin(dec_ngp),0.,numpy.cos(dec_ngp)],[0.,1.,0.],[numpy.cos(dec_ngp),0.,numpy.sin(dec_ngp)]]),numpy.array([[numpy.cos(ra_ngp),numpy.sin(ra_ngp),0.],[-numpy.sin(ra_ngp),numpy.cos(ra_ngp),0.],[0.,0.,1.]]))) #Whether to use degrees and scalar input is handled by decorators XYZ= numpy.array([numpy.cos(dec)*numpy.cos(ra), numpy.cos(dec)*numpy.sin(ra), numpy.sin(dec)]) galXYZ= numpy.dot(T,XYZ) galXYZ[2][galXYZ[2] > 1.]= 1. galXYZ[2][galXYZ[2] < -1.]= -1. b= numpy.arcsin(galXYZ[2]) l= numpy.arctan2(galXYZ[1]/numpy.cos(b),galXYZ[0]/numpy.cos(b)) l[l<0.]+= 2.*numpy.pi out= numpy.array([l,b]) return out.T @scalarDecorator @degreeDecorator([0,1],[0,1]) def lb_to_radec(l,b,degree=False,epoch=2000.0): """ NAME: lb_to_radec PURPOSE: transform from Galactic coordinates to equatorial coordinates INPUT: l - Galactic longitude b - Galactic latitude degree - (Bool) if True, l and b are given in degree and ra and dec will be as well epoch - epoch of ra,dec (right now only 2000.0 and 1950.0 are supported when not using astropy's transformations internally; when internally using astropy's coordinate transformations, epoch can be None for ICRS, 'JXXXX' for FK5, and 'BXXXX' for FK4) OUTPUT: ra,dec For vector inputs [:,2] HISTORY: 2010-04-07 - Written - Bovy (NYU) 2014-06-14 - Re-written w/ numpy functions for speed and w/ decorators for beauty - Bovy (IAS) 2016-05-13 - Added support for using astropy's coordinate transformations and for non-standard epochs - Bovy (UofT) """ if _APY_COORDS: epoch, frame= _parse_epoch_frame_apy(epoch) c= apycoords.SkyCoord(l*units.rad,b*units.rad,frame='galactic') if not epoch is None and 'J' in epoch: c= c.transform_to(apycoords.FK5(equinox=epoch)) elif not epoch is None and 'B' in epoch: c= c.transform_to(apycoords.FK4(equinox=epoch)) else: c= c.transform_to(apycoords.ICRS) return numpy.array([c.ra.to(units.rad).value,c.dec.to(units.rad).value]).T #First calculate the transformation matrix T' theta,dec_ngp,ra_ngp= get_epoch_angles(epoch) T= numpy.dot(numpy.array([[numpy.cos(ra_ngp),-numpy.sin(ra_ngp),0.],[numpy.sin(ra_ngp),numpy.cos(ra_ngp),0.],[0.,0.,1.]]),numpy.dot(numpy.array([[-numpy.sin(dec_ngp),0.,numpy.cos(dec_ngp)],[0.,1.,0.],[numpy.cos(dec_ngp),0.,numpy.sin(dec_ngp)]]),numpy.array([[numpy.cos(theta),numpy.sin(theta),0.],[numpy.sin(theta),-numpy.cos(theta),0.],[0.,0.,1.]]))) #Whether to use degrees and scalar input is handled by decorators XYZ= numpy.array([numpy.cos(b)*numpy.cos(l), numpy.cos(b)*numpy.sin(l), numpy.sin(b)]) eqXYZ= numpy.dot(T,XYZ) dec= numpy.arcsin(eqXYZ[2]) ra= numpy.arctan2(eqXYZ[1],eqXYZ[0]) ra[ra<0.]+= 2.*numpy.pi return numpy.array([ra,dec]).T @scalarDecorator @degreeDecorator([0,1],[]) def lbd_to_XYZ(l,b,d,degree=False): """ NAME: lbd_to_XYZ PURPOSE: transform from spherical Galactic coordinates to rectangular Galactic coordinates (works with vector inputs) INPUT: l - Galactic longitude (rad) b - Galactic latitude (rad) d - distance (arbitrary units) degree - (bool) if True, l and b are in degrees OUTPUT: [X,Y,Z] in whatever units d was in For vector inputs [:,3] HISTORY: 2009-10-24- Written - Bovy (NYU) 2014-06-14 - Re-written w/ numpy functions for speed and w/ decorators for beauty - Bovy (IAS) """ #Whether to use degrees and scalar input is handled by decorators return numpy.array([d*numpy.cos(b)*numpy.cos(l), d*numpy.cos(b)*numpy.sin(l), d*numpy.sin(b)]).T def rectgal_to_sphergal(X,Y,Z,vx,vy,vz,degree=False): """ NAME: rectgal_to_sphergal PURPOSE: transform phase-space coordinates in rectangular Galactic coordinates to spherical Galactic coordinates (can take vector inputs) INPUT: X - component towards the Galactic Center (kpc) Y - component in the direction of Galactic rotation (kpc) Z - component towards the North Galactic Pole (kpc) vx - velocity towards the Galactic Center (km/s) vy - velocity in the direction of Galactic rotation (km/s) vz - velocity towards the North Galactic Pole (km/s) degree - (Bool) if True, return l and b in degrees OUTPUT: (l,b,d,vr,pmll x cos(b),pmbb) in (rad,rad,kpc,km/s,mas/yr,mas/yr) HISTORY: 2009-10-25 - Written - Bovy (NYU) """ lbd= XYZ_to_lbd(X,Y,Z,degree=degree) vrpmllpmbb= vxvyvz_to_vrpmllpmbb(vx,vy,vz,X,Y,Z,XYZ=True) if numpy.array(X).shape == (): return numpy.array([lbd[0],lbd[1],lbd[2],vrpmllpmbb[0],vrpmllpmbb[1],vrpmllpmbb[2]]) else: out=numpy.zeros((len(X),6)) out[:,0:3]= lbd out[:,3:6]= vrpmllpmbb return out def sphergal_to_rectgal(l,b,d,vr,pmll,pmbb,degree=False): """ NAME: sphergal_to_rectgal PURPOSE: transform phase-space coordinates in spherical Galactic coordinates to rectangular Galactic coordinates (can take vector inputs) INPUT: l - Galactic longitude (rad) b - Galactic latitude (rad) d - distance (kpc) vr - line-of-sight velocity (km/s) pmll - proper motion in the Galactic longitude direction (mu_l*cos(b) ) (mas/yr) pmbb - proper motion in the Galactic latitude (mas/yr) degree - (bool) if True, l and b are in degrees OUTPUT: (X,Y,Z,vx,vy,vz) in (kpc,kpc,kpc,km/s,km/s,km/s) HISTORY: 2009-10-25 - Written - Bovy (NYU) """ XYZ= lbd_to_XYZ(l,b,d,degree=degree) vxvyvz= vrpmllpmbb_to_vxvyvz(vr,pmll,pmbb,l,b,d,XYZ=False,degree=degree) if numpy.array(l).shape == (): return numpy.array([XYZ[0],XYZ[1],XYZ[2],vxvyvz[0],vxvyvz[1],vxvyvz[2]]) else: out=numpy.zeros((len(l),6)) out[:,0:3]= XYZ out[:,3:6]= vxvyvz return out @scalarDecorator @degreeDecorator([3,4],[]) def vrpmllpmbb_to_vxvyvz(vr,pmll,pmbb,l,b,d,XYZ=False,degree=False): """ NAME: vrpmllpmbb_to_vxvyvz PURPOSE: Transform velocities in the spherical Galactic coordinate frame to the rectangular Galactic coordinate frame (can take vector inputs) INPUT: vr - line-of-sight velocity (km/s) pmll - proper motion in the Galactic longitude (mu_l * cos(b))(mas/yr) pmbb - proper motion in the Galactic latitude (mas/yr) l - Galactic longitude b - Galactic latitude d - distance (kpc) XYZ - (bool) If True, then l,b,d is actually X,Y,Z (rectangular Galactic coordinates) degree - (bool) if True, l and b are in degrees OUTPUT: (vx,vy,vz) in (km/s,km/s,km/s) For vector inputs [:,3] HISTORY: 2009-10-24 - Written - Bovy (NYU) 2014-06-14 - Re-written w/ numpy functions for speed and w/ decorators for beauty - Bovy (IAS) """ #Whether to use degrees and scalar input is handled by decorators if XYZ: #undo the incorrect conversion that the decorator did if degree: l*= 180./numpy.pi b*= 180./numpy.pi lbd= XYZ_to_lbd(l,b,d,degree=False) l= lbd[:,0] b= lbd[:,1] d= lbd[:,2] R=numpy.zeros((3,3,len(l))) R[0,0]= numpy.cos(l)*numpy.cos(b) R[1,0]= -numpy.sin(l) R[2,0]= -numpy.cos(l)*numpy.sin(b) R[0,1]= numpy.sin(l)*numpy.cos(b) R[1,1]= numpy.cos(l) R[2,1]= -numpy.sin(l)*numpy.sin(b) R[0,2]= numpy.sin(b) R[2,2]= numpy.cos(b) invr= numpy.array([[vr,vr,vr], [d*pmll*_K,d*pmll*_K,d*pmll*_K], [d*pmbb*_K,d*pmbb*_K,d*pmbb*_K]]) return (R.T*invr.T).sum(-1) @scalarDecorator @degreeDecorator([3,4],[]) def vxvyvz_to_vrpmllpmbb(vx,vy,vz,l,b,d,XYZ=False,degree=False): """ NAME: vxvyvz_to_vrpmllpmbb PURPOSE: Transform velocities in the rectangular Galactic coordinate frame to the spherical Galactic coordinate frame (can take vector inputs) INPUT: vx - velocity towards the Galactic Center (km/s) vy - velocity in the direction of Galactic rotation (km/s) vz - velocity towards the North Galactic Pole (km/s) l - Galactic longitude b - Galactic latitude d - distance (kpc) XYZ - (bool) If True, then l,b,d is actually X,Y,Z (rectangular Galactic coordinates) degree - (bool) if True, l and b are in degrees OUTPUT: (vr,pmll x cos(b),pmbb) in (km/s,mas/yr,mas/yr); pmll = mu_l * cos(b) For vector inputs [:,3] HISTORY: 2009-10-24 - Written - Bovy (NYU) 2014-06-14 - Re-written w/ numpy functions for speed and w/ decorators for beauty - Bovy (IAS) """ #Whether to use degrees and scalar input is handled by decorators if XYZ: #undo the incorrect conversion that the decorator did if degree: l*= 180./numpy.pi b*= 180./numpy.pi lbd= XYZ_to_lbd(l,b,d,degree=False) l= lbd[:,0] b= lbd[:,1] d= lbd[:,2] R=numpy.zeros((3,3,len(l))) R[0,0]= numpy.cos(l)*numpy.cos(b) R[0,1]= -numpy.sin(l) R[0,2]= -numpy.cos(l)*numpy.sin(b) R[1,0]= numpy.sin(l)*numpy.cos(b) R[1,1]= numpy.cos(l) R[1,2]= -numpy.sin(l)*numpy.sin(b) R[2,0]= numpy.sin(b) R[2,2]= numpy.cos(b) invxyz= numpy.array([[vx,vx,vx], [vy,vy,vy], [vz,vz,vz]]) vrvlvb= (R.T*invxyz.T).sum(-1) vrvlvb[:,1]/= d*_K vrvlvb[:,2]/= d*_K return vrvlvb @scalarDecorator @degreeDecorator([],[0,1]) def XYZ_to_lbd(X,Y,Z,degree=False): """ NAME: XYZ_to_lbd PURPOSE: transform from rectangular Galactic coordinates to spherical Galactic coordinates (works with vector inputs) INPUT: X - component towards the Galactic Center (in kpc; though this obviously does not matter)) Y - component in the direction of Galactic rotation (in kpc) Z - component towards the North Galactic Pole (kpc) degree - (Bool) if True, return l and b in degrees OUTPUT: [l,b,d] in (rad or degree,rad or degree,kpc) For vector inputs [:,3] HISTORY: 2009-10-24 - Written - Bovy (NYU) 2014-06-14 - Re-written w/ numpy functions for speed and w/ decorators for beauty - Bovy (IAS) """ #Whether to use degrees and scalar input is handled by decorators d= numpy.sqrt(X**2.+Y**2.+Z**2.) b= numpy.arcsin(Z/d) l= numpy.arctan2(Y,X) l[l<0.]+= 2.*numpy.pi out= numpy.empty((len(d),3)) out[:,0]= l out[:,1]= b out[:,2]= d return out @scalarDecorator @degreeDecorator([2,3],[]) def pmrapmdec_to_pmllpmbb(pmra,pmdec,ra,dec,degree=False,epoch=2000.0): """ NAME: pmrapmdec_to_pmllpmbb PURPOSE: rotate proper motions in (ra,dec) into proper motions in (l,b) INPUT: pmra - proper motion in ra (multiplied with cos(dec)) [mas/yr] pmdec - proper motion in dec [mas/yr] ra - right ascension dec - declination degree - if True, ra and dec are given in degrees (default=False) epoch - epoch of ra,dec (right now only 2000.0 and 1950.0 are supported when not using astropy's transformations internally; when internally using astropy's coordinate transformations, epoch can be None for ICRS, 'JXXXX' for FK5, and 'BXXXX' for FK4) OUTPUT: (pmll x cos(b),pmbb) for vector inputs [:,2] HISTORY: 2010-04-07 - Written - Bovy (NYU) 2014-06-14 - Re-written w/ numpy functions for speed and w/ decorators for beauty - Bovy (IAS) """ theta,dec_ngp,ra_ngp= get_epoch_angles(epoch) #Whether to use degrees and scalar input is handled by decorators dec[dec == dec_ngp]+= 10.**-16 #deal w/ pole. sindec_ngp= numpy.sin(dec_ngp) cosdec_ngp= numpy.cos(dec_ngp) sindec= numpy.sin(dec) cosdec= numpy.cos(dec) sinrarangp= numpy.sin(ra-ra_ngp) cosrarangp= numpy.cos(ra-ra_ngp) #These were replaced by Poleski (2013)'s equivalent form that is better at the poles #cosphi= (sindec_ngp-sindec*sinb)/cosdec/cosb #sinphi= sinrarangp*cosdec_ngp/cosb cosphi= sindec_ngp*cosdec-cosdec_ngp*sindec*cosrarangp sinphi= sinrarangp*cosdec_ngp norm= numpy.sqrt(cosphi**2.+sinphi**2.) cosphi/= norm sinphi/= norm return (numpy.array([[cosphi,-sinphi],[sinphi,cosphi]]).T\ *numpy.array([[pmra,pmra],[pmdec,pmdec]]).T).sum(-1) @scalarDecorator @degreeDecorator([2,3],[]) def pmllpmbb_to_pmrapmdec(pmll,pmbb,l,b,degree=False,epoch=2000.0): """ NAME: pmllpmbb_to_pmrapmdec PURPOSE: rotate proper motions in (l,b) into proper motions in (ra,dec) INPUT: pmll - proper motion in l (multiplied with cos(b)) [mas/yr] pmbb - proper motion in b [mas/yr] l - Galactic longitude b - Galactic latitude degree - if True, l and b are given in degrees (default=False) epoch - epoch of ra,dec (right now only 2000.0 and 1950.0 are supported when not using astropy's transformations internally; when internally using astropy's coordinate transformations, epoch can be None for ICRS, 'JXXXX' for FK5, and 'BXXXX' for FK4) OUTPUT: (pmra x cos(dec),pmdec), for vector inputs [:,2] HISTORY: 2010-04-07 - Written - Bovy (NYU) 2014-06-14 - Re-written w/ numpy functions for speed and w/ decorators for beauty - Bovy (IAS) """ theta,dec_ngp,ra_ngp= get_epoch_angles(epoch) #Whether to use degrees and scalar input is handled by decorators radec = lb_to_radec(l,b,degree=False,epoch=epoch) ra= radec[:,0] dec= radec[:,1] dec[dec == dec_ngp]+= 10.**-16 #deal w/ pole. sindec_ngp= numpy.sin(dec_ngp) cosdec_ngp= numpy.cos(dec_ngp) sindec= numpy.sin(dec) cosdec= numpy.cos(dec) sinrarangp= numpy.sin(ra-ra_ngp) cosrarangp= numpy.cos(ra-ra_ngp) #These were replaced by Poleski (2013)'s equivalent form that is better at the poles #cosphi= (sindec_ngp-sindec*sinb)/cosdec/cosb #sinphi= sinrarangp*cosdec_ngp/cosb cosphi= sindec_ngp*cosdec-cosdec_ngp*sindec*cosrarangp sinphi= sinrarangp*cosdec_ngp norm= numpy.sqrt(cosphi**2.+sinphi**2.) cosphi/= norm sinphi/= norm return (numpy.array([[cosphi,sinphi],[-sinphi,cosphi]]).T\ *numpy.array([[pmll,pmll],[pmbb,pmbb]]).T).sum(-1) def cov_pmrapmdec_to_pmllpmbb(cov_pmradec,ra,dec,degree=False,epoch=2000.0): """ NAME: cov_pmrapmdec_to_pmllpmbb PURPOSE: propagate the proper motions errors through the rotation from (ra,dec) to (l,b) INPUT: covar_pmradec - uncertainty covariance matrix of the proper motion in ra (multiplied with cos(dec)) and dec [2,2] or [:,2,2] ra - right ascension dec - declination degree - if True, ra and dec are given in degrees (default=False) epoch - epoch of ra,dec (right now only 2000.0 and 1950.0 are supported when not using astropy's transformations internally; when internally using astropy's coordinate transformations, epoch can be None for ICRS, 'JXXXX' for FK5, and 'BXXXX' for FK4) OUTPUT: covar_pmllbb [2,2] or [:,2,2] [pmll here is pmll x cos(b)] HISTORY: 2010-04-12 - Written - Bovy (NYU) 2020-09-21 - Adapted for array input - Mackereth (UofT) """ scalar= not hasattr(ra, '__iter__') if scalar: cov_pmradec= cov_pmradec[numpy.newaxis,:,:] theta,dec_ngp,ra_ngp= get_epoch_angles(epoch) if degree: sindec_ngp= numpy.sin(dec_ngp) cosdec_ngp= numpy.cos(dec_ngp) sindec= numpy.sin(dec*_DEGTORAD) cosdec= numpy.cos(dec*_DEGTORAD) sinrarangp= numpy.sin(ra*_DEGTORAD-ra_ngp) cosrarangp= numpy.cos(ra*_DEGTORAD-ra_ngp) else: sindec_ngp= numpy.sin(dec_ngp) cosdec_ngp= numpy.cos(dec_ngp) sindec= numpy.sin(dec) cosdec= numpy.cos(dec) sinrarangp= numpy.sin(ra-ra_ngp) cosrarangp= numpy.cos(ra-ra_ngp) #These were replaced by Poleski (2013)'s equivalent form that is better at the poles #cosphi= (sindec_ngp-sindec*sinb)/cosdec/cosb #sinphi= sinrarangp*cosdec_ngp/cosb cosphi= sindec_ngp*cosdec-cosdec_ngp*sindec*cosrarangp sinphi= sinrarangp*cosdec_ngp norm= numpy.sqrt(cosphi**2.+sinphi**2.) cosphi/= norm sinphi/= norm P = numpy.zeros([len(cov_pmradec),2,2]) P[:,0,0] = cosphi P[:,0,1] = sinphi P[:,1,0] = -sinphi P[:,1,1] = cosphi out= numpy.einsum('aij,ajk->aik',P, numpy.einsum('aij,jka->aik',cov_pmradec,P.T)) if scalar: return out[0] else: return out def cov_dvrpmllbb_to_vxyz(d,e_d,e_vr,pmll,pmbb,cov_pmllbb,l,b, plx=False,degree=False): """ NAME: cov_dvrpmllbb_to_vxyz PURPOSE: propagate distance, radial velocity, and proper motion uncertainties to Galactic coordinates INPUT: d - distance [kpc, as/mas for plx] e_d - distance uncertainty [kpc, [as/mas] for plx] e_vr - low velocity uncertainty [km/s] pmll - proper motion in l (*cos(b)) [ [as/mas]/yr ] pmbb - proper motion in b [ [as/mas]/yr ] cov_pmllbb - uncertainty covariance for proper motion [pmll is pmll x cos(b)] l - Galactic longitude b - Galactic latitude KEYWORDS: plx - if True, d is a parallax, and e_d is a parallax uncertainty degree - if True, l and b are given in degree OUTPUT: cov(vx,vy,vz) [3,3] or [:,3,3] HISTORY: 2010-04-12 - Written - Bovy (NYU) 2020-09-21 - Adapted for array input - Mackereth (UofT) """ if plx: d= 1./d e_d*= d**2. if degree: l*= _DEGTORAD b*= _DEGTORAD scalar= not hasattr(d,'__iter__') if scalar: cov_pmllbb= cov_pmllbb[numpy.newaxis,:,:] ndata= len(cov_pmllbb) M = numpy.zeros((ndata,2,3)) M[:,0,0] = pmll M[:,1,0] = pmbb M[:,0,1] = d M[:,1,2] = d M= _K*M cov_dpmllbb= numpy.zeros((ndata,3,3)) cov_dpmllbb[:,0,0]= e_d**2. cov_dpmllbb[:,1:3,1:3]= cov_pmllbb cov_vlvb= numpy.einsum('aij,ajk->aik',M, numpy.einsum('aij,jka->aik',cov_dpmllbb,M.T)) if scalar: cov_vlvb= cov_vlvb[0] cov_vrvlvb= numpy.zeros((ndata,3,3)) cov_vrvlvb[:,0,0]= e_vr**2. cov_vrvlvb[:,1:3,1:3]= cov_vlvb R = numpy.zeros((ndata,3,3)) R[:,0,0] = numpy.cos(l)*numpy.cos(b) R[:,0,1] = numpy.sin(l)*numpy.cos(b) R[:,0,2] = numpy.sin(b) R[:,1,0] = -numpy.sin(l) R[:,1,1] = numpy.cos(l) R[:,2,0] = -numpy.cos(l)*numpy.sin(b) R[:,2,1] = -numpy.sin(l)*numpy.sin(b) R[:,2,2] = numpy.cos(b) out= numpy.einsum('ija,ajk->aik',R.T, numpy.einsum('aij,ajk->aik',cov_vrvlvb,R)) if scalar: return out[0] else: return out def cov_vxyz_to_galcencyl(cov_vxyz, phi, Xsun=1., Zsun=0.): """ NAME: cov_vxyz_to_galcencyl PURPOSE: propagate uncertainties in vxyz to galactocentric cylindrical coordinates INPUT: cov_vxyz - uncertainty covariance in U,V,W phi - angular position of star in galactocentric cylindrical coords OUTPUT: cov(vR,vT,vz) [3,3] HISTORY: 2018-03-22 - Written - Mackereth (LJMU) 2020-09-21- Moved to coords.py - Mackereth (UofT) """ cov_galcenrect = cov_vxyz_to_galcenrect(cov_vxyz, Xsun=Xsun, Zsun=Zsun) cov_galcencyl = cov_galcenrect_to_galcencyl(cov_galcenrect, phi) return cov_galcencyl def cov_vxyz_to_galcenrect(cov_vxyz,Xsun=1.,Zsun=0.): """ NAME: cov_vxyz_to_galcenrect PURPOSE: propagate uncertainties in vxyz to galactocentric rectangular coordinates INPUT: cov_vxyz - uncertainty covariance in U,V,W OUTPUT: cov(vx,vy,vz) [3,3] HISTORY: 2018-03-22 - Written - Mackereth (LJMU) 2020-09-21- Moved to coords.py - Mackereth (UofT) """ scalar= cov_vxyz.ndim < 3 if scalar: cov_vxyz= cov_vxyz[numpy.newaxis,:,:] dgc= numpy.sqrt(Xsun**2.+Zsun**2.) costheta, sintheta= Xsun/dgc, Zsun/dgc R = numpy.array([[costheta,0.,-sintheta], [0.,1.,0.], [sintheta,0.,costheta]]) R = numpy.ones([len(cov_vxyz),3,3])*R out= numpy.einsum('ija,ajk->aik',R.T, numpy.einsum('aij,ajk->aik',cov_vxyz,R)) if scalar: return out[0] else: return out def cov_galcenrect_to_galcencyl(cov_galcenrect, phi): """ NAME: cov_galcenrect_to_galcencyl PURPOSE: propagate uncertainties in galactocentric rectangular to galactocentric cylindrical coordinates INPUT: cov_galcenrect - uncertainty covariance in Galactocentric rectangular coords OUTPUT: cov(vR,vT,vz) [3,3] HISTORY: 2018-03-22 - Written - Mackereth (LJMU) 2020-09-21- Moved to coords.py - Mackereth (UofT) """ scalar= cov_galcenrect.ndim < 3 if scalar: cov_galcenrect= cov_galcenrect[numpy.newaxis,:,:] cosphi = numpy.cos(phi) sinphi = numpy.sin(phi) R = numpy.zeros([len(cov_galcenrect),3,3]) R[:,0,0] = cosphi R[:,0,1] = sinphi R[:,1,0] = -sinphi R[:,1,1] = cosphi R[:,2,2] = 1. out= numpy.einsum('aij,ajk->aik',R, numpy.einsum('aij,jka->aik',cov_galcenrect,R.T)) if scalar: return out[0] else: return out @scalarDecorator def XYZ_to_galcenrect(X,Y,Z,Xsun=1.,Zsun=0.,_extra_rot=True): """ NAME: XYZ_to_galcenrect PURPOSE: transform XYZ coordinates (wrt Sun) to rectangular Galactocentric coordinates INPUT: X - X Y - Y Z - Z Xsun - cylindrical distance to the GC Zsun - Sun's height above the midplane _extra_rot= (True) if True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition OUTPUT: (Xg, Yg, Zg) HISTORY: 2010-09-24 - Written - Bovy (NYU) 2016-05-12 - Edited to properly take into account the Sun's vertical position; dropped Ysun keyword - Bovy (UofT) 2018-04-18 - Tweaked to be consistent with astropy's Galactocentric frame - Bovy (UofT) """ if _extra_rot: X,Y,Z= numpy.dot(galcen_extra_rot,numpy.array([X,Y,Z])) dgc= numpy.sqrt(Xsun**2.+Zsun**2.) costheta, sintheta= Xsun/dgc, Zsun/dgc return numpy.dot(numpy.array([[costheta,0.,-sintheta], [0.,1.,0.], [sintheta,0.,costheta]]), numpy.array([-X+dgc,Y,numpy.sign(Xsun)*Z])).T @scalarDecorator def galcenrect_to_XYZ(X,Y,Z,Xsun=1.,Zsun=0.,_extra_rot=True): """ NAME: galcenrect_to_XYZ PURPOSE: transform rectangular Galactocentric to XYZ coordinates (wrt Sun) coordinates INPUT: X, Y, Z - Galactocentric rectangular coordinates Xsun - cylindrical distance to the GC (can be array of same length as X) Zsun - Sun's height above the midplane (can be array of same length as X) _extra_rot= (True) if True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition OUTPUT: (X, Y, Z) HISTORY: 2011-02-23 - Written - Bovy (NYU) 2016-05-12 - Edited to properly take into account the Sun's vertical position; dropped Ysun keyword - Bovy (UofT) 2017-10-24 - Allowed Xsun/Zsun to be arrays - Bovy (UofT) 2018-04-18 - Tweaked to be consistent with astropy's Galactocentric frame - Bovy (UofT) """ dgc= numpy.sqrt(Xsun**2.+Zsun**2.) costheta, sintheta= Xsun/dgc, Zsun/dgc if isinstance(Xsun,numpy.ndarray): zero= numpy.zeros(len(Xsun)) one= numpy.ones(len(Xsun)) Carr= numpy.rollaxis(numpy.array([[-costheta,zero,-sintheta], [zero,one,zero], [-numpy.sign(Xsun)*sintheta,zero, numpy.sign(Xsun)*costheta]]),2) out= ((Carr*numpy.array([[X,X,X],[Y,Y,Y],[Z,Z,Z]]).T).sum(-1) +numpy.array([dgc,zero,zero]).T) else: out= numpy.dot(numpy.array([[-costheta,0.,-sintheta], [0.,1.,0.], [-numpy.sign(Xsun)*sintheta,0., numpy.sign(Xsun)*costheta]]), numpy.array([X,Y,Z])).T+numpy.array([dgc,0.,0.]) if _extra_rot: return numpy.dot(galcen_extra_rot.T,out.T).T else: return out def rect_to_cyl(X,Y,Z): """ NAME: rect_to_cyl PURPOSE: convert from rectangular to cylindrical coordinates INPUT: X, Y, Z - rectangular coordinates OUTPUT: R,phi,z HISTORY: 2010-09-24 - Written - Bovy (NYU) 2019-06-21 - Changed such that phi in [-pi,pi] - Bovy (UofT) """ return (numpy.sqrt(X**2.+Y**2.),numpy.arctan2(Y,X),Z) def cyl_to_rect(R,phi,Z): """ NAME: cyl_to_rect PURPOSE: convert from cylindrical to rectangular coordinates INPUT: R, phi, Z - cylindrical coordinates OUTPUT: X,Y,Z HISTORY: 2011-02-23 - Written - Bovy (NYU) """ return (R*numpy.cos(phi),R*numpy.sin(phi),Z) def cyl_to_spher(R,Z, phi): """ NAME: cyl_to_spher PURPOSE: convert from cylindrical to spherical coordinates INPUT: R, Z, phi- cylindrical coordinates OUTPUT: R, theta, phi - spherical coordinates HISTORY: 2016-05-16 - Written - Aladdin """ theta = numpy.arctan2(R, Z) r = (R**2 + Z**2)**.5 return (r,theta, phi) def spher_to_cyl(r, theta, phi): """ NAME: spher_to_cyl PURPOSE: convert from spherical to cylindrical coordinates INPUT: r, theta, phi - spherical coordinates OUTPUT: R, z, phi - spherical coordinates HISTORY: 2016-05-20 - Written - Aladdin """ R = r*numpy.sin(theta) z = r*numpy.cos(theta) return (R,z, phi) @scalarDecorator def XYZ_to_galcencyl(X,Y,Z,Xsun=1.,Zsun=0.,_extra_rot=True): """ NAME: XYZ_to_galcencyl PURPOSE: transform XYZ coordinates (wrt Sun) to cylindrical Galactocentric coordinates INPUT: X - X Y - Y Z - Z Xsun - cylindrical distance to the GC Zsun - Sun's height above the midplane _extra_rot= (True) if True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition OUTPUT: R,phi,z HISTORY: 2010-09-24 - Written - Bovy (NYU) """ XYZ= numpy.atleast_2d(XYZ_to_galcenrect(X,Y,Z,Xsun=Xsun,Zsun=Zsun, _extra_rot=_extra_rot)) return numpy.array(rect_to_cyl(XYZ[:,0],XYZ[:,1],XYZ[:,2])).T @scalarDecorator def galcencyl_to_XYZ(R,phi,Z,Xsun=1.,Zsun=0.,_extra_rot=True): """ NAME: galcencyl_to_XYZ PURPOSE: transform cylindrical Galactocentric coordinates to XYZ coordinates (wrt Sun) INPUT: R, phi, Z - Galactocentric cylindrical coordinates Xsun - cylindrical distance to the GC (can be array of same length as R) Zsun - Sun's height above the midplane (can be array of same length as R) _extra_rot= (True) if True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition OUTPUT: X,Y,Z HISTORY: 2011-02-23 - Written - Bovy (NYU) 2017-10-24 - Allowed Xsun/Zsun to be arrays - Bovy (UofT) """ Xr,Yr,Zr= cyl_to_rect(R,phi,Z) return galcenrect_to_XYZ(Xr,Yr,Zr,Xsun=Xsun,Zsun=Zsun, _extra_rot=_extra_rot) @scalarDecorator def vxvyvz_to_galcenrect(vx,vy,vz,vsun=[0.,1.,0.],Xsun=1.,Zsun=0., _extra_rot=True): """ NAME: vxvyvz_to_galcenrect PURPOSE: transform velocities in XYZ coordinates (wrt Sun) to rectangular Galactocentric coordinates for velocities INPUT: vx - U vy - V vz - W vsun - velocity of the sun in the GC frame ndarray[3] Xsun - cylindrical distance to the GC Zsun - Sun's height above the midplane _extra_rot= (True) if True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition OUTPUT: [:,3]= vXg, vYg, vZg HISTORY: 2010-09-24 - Written - Bovy (NYU) 2016-05-12 - Edited to properly take into account the Sun's vertical position; dropped Ysun keyword - Bovy (UofT) 2018-04-18 - Tweaked to be consistent with astropy's Galactocentric frame - Bovy (UofT) """ if _extra_rot: vx,vy,vz= numpy.dot(galcen_extra_rot,numpy.array([vx,vy,vz])) dgc= numpy.sqrt(Xsun**2.+Zsun**2.) costheta, sintheta= Xsun/dgc, Zsun/dgc return numpy.dot(numpy.array([[costheta,0.,-sintheta], [0.,1.,0.], [sintheta,0.,costheta]]), numpy.array([-vx,vy,numpy.sign(Xsun)*vz])).T+numpy.array(vsun) @scalarDecorator def vxvyvz_to_galcencyl(vx,vy,vz,X,Y,Z,vsun=[0.,1.,0.],Xsun=1.,Zsun=0., galcen=False,_extra_rot=True): """ NAME: vxvyvz_to_galcencyl PURPOSE: transform velocities in XYZ coordinates (wrt Sun) to cylindrical Galactocentric coordinates for velocities INPUT: vx - U vy - V vz - W X - X in Galactocentric rectangular coordinates Y - Y in Galactocentric rectangular coordinates Z - Z in Galactocentric rectangular coordinates vsun - velocity of the sun in the GC frame ndarray[3] Xsun - cylindrical distance to the GC Zsun - Sun's height above the midplane galcen - if True, then X,Y,Z are in cylindrical Galactocentric coordinates rather than rectangular coordinates _extra_rot= (True) if True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition OUTPUT: vRg, vTg, vZg HISTORY: 2010-09-24 - Written - Bovy (NYU) """ vxyz= vxvyvz_to_galcenrect(vx,vy,vz,vsun=vsun,Xsun=Xsun,Zsun=Zsun, _extra_rot=_extra_rot) return numpy.array(\ rect_to_cyl_vec(vxyz[:,0],vxyz[:,1],vxyz[:,2],X,Y,Z,cyl=galcen)).T @scalarDecorator def galcenrect_to_vxvyvz(vXg,vYg,vZg,vsun=[0.,1.,0.],Xsun=1.,Zsun=0., _extra_rot=True): """ NAME: galcenrect_to_vxvyvz PURPOSE: transform rectangular Galactocentric coordinates to XYZ coordinates (wrt Sun) for velocities INPUT: vXg - Galactocentric x-velocity vYg - Galactocentric y-velocity vZg - Galactocentric z-velocity vsun - velocity of the sun in the GC frame ndarray[3] (can be array of same length as vXg; shape [3,N]) Xsun - cylindrical distance to the GC (can be array of same length as vXg) Zsun - Sun's height above the midplane (can be array of same length as vXg) _extra_rot= (True) if True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition OUTPUT: [:,3]= vx, vy, vz HISTORY: 2011-02-24 - Written - Bovy (NYU) 2016-05-12 - Edited to properly take into account the Sun's vertical position; dropped Ysun keyword - Bovy (UofT) 2017-10-24 - Allowed vsun/Xsun/Zsun to be arrays - Bovy (UofT) 2018-04-18 - Tweaked to be consistent with astropy's Galactocentric frame - Bovy (UofT) """ dgc= numpy.sqrt(Xsun**2.+Zsun**2.) costheta, sintheta= Xsun/dgc, Zsun/dgc if isinstance(Xsun,numpy.ndarray): zero= numpy.zeros(len(Xsun)) one= numpy.ones(len(Xsun)) Carr= numpy.rollaxis(numpy.array([[-costheta,zero,-sintheta], [zero,one,zero], [-numpy.sign(Xsun)*sintheta,zero, numpy.sign(Xsun)*costheta]]),2) out= ((Carr *numpy.array([[vXg-vsun[0],vXg-vsun[0],vXg-vsun[0]], [vYg-vsun[1],vYg-vsun[1],vYg-vsun[1]], [vZg-vsun[2],vZg-vsun[2],vZg-vsun[2]]]).T).sum(-1)) else: out= numpy.dot(numpy.array([[-costheta,0.,-sintheta], [0.,1.,0.], [-numpy.sign(Xsun)*sintheta,0., numpy.sign(Xsun)*costheta]]), numpy.array([vXg-vsun[0],vYg-vsun[1],vZg-vsun[2]])).T if _extra_rot: return numpy.dot(galcen_extra_rot.T,out.T).T else: return out @scalarDecorator def galcencyl_to_vxvyvz(vR,vT,vZ,phi,vsun=[0.,1.,0.],Xsun=1.,Zsun=0., _extra_rot=True): """ NAME: galcencyl_to_vxvyvz PURPOSE: transform cylindrical Galactocentric coordinates to XYZ (wrt Sun) coordinates for velocities INPUT: vR - Galactocentric radial velocity vT - Galactocentric rotational velocity vZ - Galactocentric vertical velocity phi - Galactocentric azimuth vsun - velocity of the sun in the GC frame ndarray[3] (can be array of same length as vRg; shape [3,N]) Xsun - cylindrical distance to the GC (can be array of same length as vRg) Zsun - Sun's height above the midplane (can be array of same length as vRg) _extra_rot= (True) if True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition OUTPUT: vx,vy,vz HISTORY: 2011-02-24 - Written - Bovy (NYU) 2017-10-24 - Allowed vsun/Xsun/Zsun to be arrays - Bovy (NYU) """ vXg, vYg, vZg= cyl_to_rect_vec(vR,vT,vZ,phi) return galcenrect_to_vxvyvz(vXg,vYg,vZg,vsun=vsun,Xsun=Xsun,Zsun=Zsun, _extra_rot=_extra_rot) def cyl_to_spher_vec(vR,vT,vz,R,z): """ NAME: cyl_to_spher_vec PURPOSE: transform vectors from cylindrical to spherical coordinates. vtheta is positive from pole towards equator. INPUT: vR - Galactocentric cylindrical radial velocity vT - Galactocentric cylindrical rotational velocity vz - Galactocentric cylindrical vertical velocity R - Galactocentric cylindrical radius z - Galactocentric cylindrical height OUTPUT: vr,vT,vtheta HISTORY: 2020-07-01 - Written - James Lane (UofT) """ r = numpy.sqrt(R**2.+z**2.) vr = (R*vR + z*vz)/r vtheta = (z*vR - R*vz)/r return (vr,vT,vtheta) def spher_to_cyl_vec(vr,vT,vtheta,theta): """ NAME: spher_to_cyl_vec PURPOSE: transform vectors from spherical polar to cylindrical coordinates. vtheta is positive from pole towards equator, theta is 0 at pole INPUT: vr - Galactocentric spherical radial velocity vT - Galactocentric spherical azimuthal velocity vtheta - Galactocentric spherical polar velocity theta - Galactocentric spherical polar angle OUTPUT: vR,vT,vz HISTORY: 2020-07-01 - Written - James Lane (UofT) """ vR = vr*numpy.sin(theta) + vtheta*numpy.cos(theta) vz = vr*numpy.cos(theta) - vtheta*numpy.sin(theta) return (vR,vT,vz) def rect_to_cyl_vec(vx,vy,vz,X,Y,Z,cyl=False): """ NAME: rect_to_cyl_vec PURPOSE: transform vectors from rectangular to cylindrical coordinates vectors INPUT: vx - vy - vz - X - X Y - Y Z - Z cyl - if True, X,Y,Z are already cylindrical OUTPUT: vR,vT,vz HISTORY: 2010-09-24 - Written - Bovy (NYU) """ if not cyl: R,phi,Z= rect_to_cyl(X,Y,Z) else: phi= Y vr=+vx*numpy.cos(phi)+vy*numpy.sin(phi) vt= -vx*numpy.sin(phi)+vy*numpy.cos(phi) return (vr,vt,vz) def cyl_to_rect_vec(vr,vt,vz,phi): """ NAME: cyl_to_rect_vec PURPOSE: transform vectors from cylindrical to rectangular coordinate vectors INPUT: vr - radial velocity vt - rotational velocity vz - vertical velocity phi - azimuth OUTPUT: vx,vy,vz HISTORY: 2011-02-24 - Written - Bovy (NYU) """ vx= vr*numpy.cos(phi)-vt*numpy.sin(phi) vy= vr*numpy.sin(phi)+vt*numpy.cos(phi) return (vx,vy,vz) def cyl_to_rect_jac(*args): """ NAME: cyl_to_rect_jac PURPOSE: calculate the Jacobian of the cylindrical to rectangular conversion INPUT: R, phi, Z- cylindrical coordinates vR, vT, vZ- cylindrical velocities if 6 inputs: R,vR,vT,z,vz,phi if 3: R, phi, Z OUTPUT: jacobian d(rect)/d(cyl) HISTORY: 2013-12-09 - Written - Bovy (IAS) """ out= numpy.zeros((6,6)) if len(args) == 3: R, phi, Z= args vR, vT, vZ= 0., 0., 0. outIndx= numpy.array([True,False,False,True,False,True],dtype='bool') elif len(args) == 6: R, vR, vT, Z, vZ, phi= args outIndx= numpy.ones(6,dtype='bool') cp= numpy.cos(phi) sp= numpy.sin(phi) out[0,0]= cp out[0,5]= -R*sp out[1,0]= sp out[1,5]= R*cp out[2,3]= 1. out[3,1]= cp out[3,2]= -sp out[3,5]= -vT*cp-vR*sp out[4,1]= sp out[4,2]= cp out[4,5]= -vT*sp+vR*cp out[5,4]= 1. if len(args) == 3: out= out[:3,outIndx] out[:,[1,2]]= out[:,[2,1]] return out def galcenrect_to_XYZ_jac(*args,**kwargs): """ NAME: galcenrect_to_XYZ_jac PURPOSE: calculate the Jacobian of the Galactocentric rectangular to Galactic coordinates INPUT: X,Y,Z- Galactocentric rectangular coordinates vX, vY, vZ- Galactocentric rectangular velocities if 6 inputs: X,Y,Z,vX,vY,vZ if 3: X,Y,Z Xsun - cylindrical distance to the GC Zsun - Sun's height above the midplane OUTPUT: jacobian d(galcen.)/d(Galactic) HISTORY: 2013-12-09 - Written - Bovy (IAS) """ Xsun= kwargs.get('Xsun',1.) dgc= numpy.sqrt(Xsun**2.+kwargs.get('Zsun',0.)**2.) costheta, sintheta= Xsun/dgc, kwargs.get('Zsun',0.)/dgc out= numpy.zeros((6,6)) out[0,0]= -costheta out[0,2]= -sintheta out[1,1]= 1. out[2,0]= -numpy.sign(Xsun)*sintheta out[2,2]= numpy.sign(Xsun)*costheta if len(args) == 3: return out[:3,:3] out[3,3]= -costheta out[3,5]= -sintheta out[4,4]= 1. out[5,3]= -numpy.sign(Xsun)*sintheta out[5,5]= numpy.sign(Xsun)*costheta return out def lbd_to_XYZ_jac(*args,**kwargs): """ NAME: lbd_to_XYZ_jac PURPOSE: calculate the Jacobian of the Galactic spherical coordinates to Galactic rectangular coordinates transformation INPUT: l,b,D- Galactic spherical coordinates vlos,pmll,pmbb- Galactic spherical velocities (some as proper motions) if 6 inputs: l,b,D,vlos,pmll x cos(b),pmbb if 3: l,b,D degree= (False) if True, l and b are in degrees OUTPUT: jacobian HISTORY: 2013-12-09 - Written - Bovy (IAS) """ out= numpy.zeros((6,6)) if len(args) == 3: l,b,D= args vlos, pmll, pmbb= 0., 0., 0. elif len(args) == 6: l,b,D,vlos,pmll,pmbb= args if kwargs.get('degree',False): l*= _DEGTORAD b*= _DEGTORAD cl= numpy.cos(l) sl= numpy.sin(l) cb= numpy.cos(b) sb= numpy.sin(b) out[0,0]= -D*cb*sl out[0,1]= -D*sb*cl out[0,2]= cb*cl out[1,0]= D*cb*cl out[1,1]= -D*sb*sl out[1,2]= cb*sl out[2,1]= D*cb out[2,2]= sb if len(args) == 3: if kwargs.get('degree',False): out[:,0]*= _DEGTORAD out[:,1]*= _DEGTORAD return out[:3,:3] out[3,0]= -sl*cb*vlos-cl*_K*D*pmll+sb*sl*_K*D*pmbb out[3,1]= -cl*sb*vlos-cb*cl*_K*D*pmbb out[3,2]= -sl*_K*pmll-sb*cl*_K*pmbb out[3,3]= cl*cb out[3,4]= -sl*_K*D out[3,5]= -cl*sb*_K*D out[4,0]= cl*cb*vlos-sl*_K*D*pmll-cl*sb*_K*D*pmbb out[4,1]= -sl*sb*vlos-sl*cb*_K*D*pmbb out[4,2]= cl*_K*pmll-sl*sb*_K*pmbb out[4,3]= sl*cb out[4,4]= cl*_K*D out[4,5]= -sl*sb*_K*D out[5,1]= cb*vlos-sb*_K*D*pmbb out[5,2]= cb*_K*pmbb out[5,3]= sb out[5,5]= cb*_K*D if kwargs.get('degree',False): out[:,0]*= _DEGTORAD out[:,1]*= _DEGTORAD return out def dl_to_rphi_2d(d,l,degree=False,ro=1.,phio=0.): """ NAME: dl_to_rphi_2d PURPOSE: convert Galactic longitude and distance to Galactocentric radius and azimuth INPUT: d - distance l - Galactic longitude [rad/deg if degree] KEYWORDS: degree= (False): l is in degrees rather than rad ro= (1) Galactocentric radius of the observer phio= (0) Galactocentric azimuth of the observer [rad/deg if degree] OUTPUT: (R,phi); phi in degree if degree HISTORY: 2012-01-04 - Written - Bovy (IAS) """ scalarOut, listOut= False, False if isinstance(d,(int,float)): d= numpy.array([d]) scalarOut= True elif isinstance(d,list): d= numpy.array(d) listOut= True if isinstance(l,(int,float)): l= numpy.array([l]) elif isinstance(l,list): l= numpy.array(l) if degree: l*= _DEGTORAD R= numpy.sqrt(ro**2.+d**2.-2.*d*ro*numpy.cos(l)) phi= numpy.arcsin(d/R*numpy.sin(l)) indx= (ro/numpy.cos(l) < d)*(numpy.cos(l) > 0.) phi[indx]= numpy.pi-numpy.arcsin(d[indx]/R[indx]*numpy.sin(l[indx])) if degree: phi/= _DEGTORAD phi+= phio if scalarOut: return (R[0],phi[0]) elif listOut: return (list(R),list(phi)) else: return (R,phi) def rphi_to_dl_2d(R,phi,degree=False,ro=1.,phio=0.): """ NAME: rphi_to_dl_2d PURPOSE: convert Galactocentric radius and azimuth to distance and Galactic longitude INPUT: R - Galactocentric radius phi - Galactocentric azimuth [rad/deg if degree] KEYWORDS: degree= (False): phi is in degrees rather than rad ro= (1) Galactocentric radius of the observer phio= (0) Galactocentric azimuth of the observer [rad/deg if degree] OUTPUT: (d,l); phi in degree if degree HISTORY: 2012-01-04 - Written - Bovy (IAS) """ scalarOut, listOut= False, False if isinstance(R,(int,float)): R= numpy.array([R]) scalarOut= True elif isinstance(R,list): R= numpy.array(R) listOut= True if isinstance(phi,(int,float)): phi= numpy.array([phi]) elif isinstance(phi,list): phi= numpy.array(phi) phi-= phio if degree: phi*= _DEGTORAD d= numpy.sqrt(R**2.+ro**2.-2.*R*ro*numpy.cos(phi)) l= numpy.arcsin(R/d*numpy.sin(phi)) indx= (ro/numpy.cos(phi) < R)*(numpy.cos(phi) > 0.) l[indx]= numpy.pi-numpy.arcsin(R[indx]/d[indx]*numpy.sin(phi[indx])) if degree: l/= _DEGTORAD if scalarOut: return (d[0],l[0]) elif listOut: return (list(d),list(l)) else: return (d,l) def Rz_to_coshucosv(R,z,delta=1.,oblate=False): """ NAME: Rz_to_coshucosv PURPOSE: calculate prolate confocal cosh(u) and cos(v) coordinates from R,z, and delta INPUT: R - radius z - height delta= focus oblate= (False) if True, compute oblate confocal coordinates instead of prolate OUTPUT: (cosh(u),cos(v)) HISTORY: 2012-11-27 - Written - Bovy (IAS) 2017-10-11 - Added oblate coordinates - Bovy (UofT) """ if oblate: d12= (R+delta)**2.+z**2. d22= (R-delta)**2.+z**2. else: d12= (z+delta)**2.+R**2. d22= (z-delta)**2.+R**2. coshu= 0.5/delta*(numpy.sqrt(d12)+numpy.sqrt(d22)) cosv= 0.5/delta*(numpy.sqrt(d12)-numpy.sqrt(d22)) if oblate: # cosv is currently really sinv cosv= numpy.sqrt(1.-cosv**2.) return (coshu,cosv) def Rz_to_uv(R,z,delta=1.,oblate=False): """ NAME: Rz_to_uv PURPOSE: calculate prolate or oblate confocal u and v coordinates from R,z, and delta INPUT: R - radius z - height delta= focus oblate= (False) if True, compute oblate confocal coordinates instead of prolate OUTPUT: (u,v) HISTORY: 2012-11-27 - Written - Bovy (IAS) 2017-10-11 - Added oblate coordinates - Bovy (UofT) """ coshu, cosv= Rz_to_coshucosv(R,z,delta,oblate=oblate) u= numpy.arccosh(coshu) v= numpy.arccos(cosv) return (u,v) def uv_to_Rz(u,v,delta=1.,oblate=False): """ NAME: uv_to_Rz PURPOSE: calculate R and z from prolate confocal u and v coordinates INPUT: u - confocal u v - confocal v delta= focus oblate= (False) if True, compute oblate confocal coordinates instead of prolate OUTPUT: (R,z) HISTORY: 2012-11-27 - Written - Bovy (IAS) 2017-10-11 - Added oblate coordinates - Bovy (UofT) """ if oblate: R= delta*numpy.cosh(u)*numpy.sin(v) z= delta*numpy.sinh(u)*numpy.cos(v) else: R= delta*numpy.sinh(u)*numpy.sin(v) z= delta*numpy.cosh(u)*numpy.cos(v) return (R,z) def vRvz_to_pupv(vR,vz,R,z,delta=1.,oblate=False,uv=False): """ NAME: vRvz_to_pupv PURPOSE: calculate momenta in prolate or oblate confocal u and v coordinates from cylindrical velocities vR,vz for a given focal length delta INPUT: vR - radial velocity in cylindrical coordinates vz - vertical velocity in cylindrical coordinates R - radius z - height delta= focus oblate= (False) if True, compute oblate confocal coordinates instead of prolate uv= (False) if True, the given R,z are actually u,v OUTPUT: (pu,pv) HISTORY: 2017-11-28 - Written - Bovy (UofT) """ if not uv: u,v= Rz_to_uv(R,z,delta,oblate=oblate) else: u,v= R,z if oblate: pu= delta*(vR*numpy.sinh(u)*numpy.sin(v)+vz*numpy.cosh(u)*numpy.cos(v)) pv= delta*(vR*numpy.cosh(u)*numpy.cos(v)-vz*numpy.sinh(u)*numpy.sin(v)) else: pu= delta*(vR*numpy.cosh(u)*numpy.sin(v)+vz*numpy.sinh(u)*numpy.cos(v)) pv= delta*(vR*numpy.sinh(u)*numpy.cos(v)-vz*numpy.cosh(u)*numpy.sin(v)) return (pu,pv) def pupv_to_vRvz(pu,pv,u,v,delta=1.,oblate=False): """ NAME: pupv_to_vRvz PURPOSE: calculate cylindrical vR and vz from momenta in prolate or oblate confocal u and v coordinates for a given focal length delta INPUT: pu - u momentum pv - v momentum u - u coordinate v - v coordinate delta= focus oblate= (False) if True, compute oblate confocal coordinates instead of prolate OUTPUT: (vR,vz) HISTORY: 2017-12-04 - Written - Bovy (UofT) """ if oblate: denom= delta*(numpy.sinh(u)**2.+numpy.cos(v)**2.) vR= (pu*numpy.sinh(u)*numpy.sin(v)+pv*numpy.cosh(u)*numpy.cos(v))/denom vz= (pu*numpy.cosh(u)*numpy.cos(v)-pv*numpy.sinh(u)*numpy.sin(v))/denom else: denom= delta*(numpy.sinh(u)**2.+numpy.sin(v)**2.) vR= (pu*numpy.cosh(u)*numpy.sin(v)+pv*numpy.sinh(u)*numpy.cos(v))/denom vz= (pu*numpy.sinh(u)*numpy.cos(v)-pv*numpy.cosh(u)*numpy.sin(v))/denom return (vR,vz) def Rz_to_lambdanu(R,z,ac=5.,Delta=1.): """ NAME: Rz_to_lambdanu PURPOSE: calculate the prolate spheroidal coordinates (lambda,nu) from galactocentric cylindrical coordinates (R,z) by solving eq. (2.2) in Dejonghe & de Zeeuw (1988a) for (lambda,nu): R^2 = (l+a) * (n+a) / (a-g) z^2 = (l+g) * (n+g) / (g-a) Delta^2 = g-a INPUT: R - Galactocentric cylindrical radius z - vertical height ac - axis ratio of the coordinate surfaces (a/c) = sqrt(-a) / sqrt(-g) (default: 5.) Delta - focal distance that defines the spheroidal coordinate system (default: 1.) Delta=sqrt(g-a) OUTPUT: (lambda,nu) HISTORY: 2015-02-13 - Written - Trick (MPIA) """ g = Delta**2 / (1.-ac**2) a = g - Delta**2 term = R**2 + z**2 - a - g discr = (R**2 + z**2 - Delta**2)**2 + (4. * Delta**2 * R**2) l = 0.5 * (term + numpy.sqrt(discr)) n = 0.5 * (term - numpy.sqrt(discr)) if isinstance(z,float) and z == 0.: l = R**2 - a n = -g elif isinstance(z,numpy.ndarray) and numpy.sum(z == 0.) > 0: if isinstance(R,float): l[z==0.] = R**2 - a if isinstance(R,numpy.ndarray): l[z==0.] = R[z==0.]**2 - a n[z==0.] = -g return (l,n) def Rz_to_lambdanu_jac(R,z,Delta=1.): """ NAME: Rz_to_lambdanu_jac PURPOSE: calculate the Jacobian of the cylindrical (R,z) to prolate spheroidal (lambda,nu) conversion INPUT: R - Galactocentric cylindrical radius z - vertical height Delta - focal distance that defines the spheroidal coordinate system (default: 1.) Delta=sqrt(g-a) OUTPUT: jacobian d((lambda,nu))/d((R,z)) HISTORY: 2015-02-13 - Written - Trick (MPIA) """ discr = (R**2 + z**2 - Delta**2)**2 + (4. * Delta**2 * R**2) dldR = R * (1. + (R**2 + z**2 + Delta**2) / numpy.sqrt(discr)) dndR = R * (1. - (R**2 + z**2 + Delta**2) / numpy.sqrt(discr)) dldz = z * (1. + (R**2 + z**2 - Delta**2) / numpy.sqrt(discr)) dndz = z * (1. - (R**2 + z**2 - Delta**2) / numpy.sqrt(discr)) dim = 1 if isinstance(R,numpy.ndarray): dim = len(R) elif isinstance(z,numpy.ndarray): dim = len(z) jac = numpy.zeros((2,2,dim)) jac[0,0,:] = dldR jac[0,1,:] = dldz jac[1,0,:] = dndR jac[1,1,:] = dndz if dim == 1: return jac[:,:,0] else: return jac def Rz_to_lambdanu_hess(R,z,Delta=1.): """ NAME: Rz_to_lambdanu_hess PURPOSE: calculate the Hessian of the cylindrical (R,z) to prolate spheroidal (lambda,nu) conversion INPUT: R - Galactocentric cylindrical radius z - vertical height Delta - focal distance that defines the spheroidal coordinate system (default: 1.) Delta=sqrt(g-a) OUTPUT: hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] HISTORY: 2015-02-13 - Written - Trick (MPIA) """ D = Delta R2 = R**2 z2 = z**2 D2 = D**2 discr = (R2 + z2 - D2)**2 + (4. * D2 * R2) d2ldR2 = 1. + (3.*R2+ z2+D2)/discr**0.5 - (2.*R2*(R2+z2+D2)**2)/discr**1.5 d2ndR2 = 1. - (3.*R2+ z2+D2)/discr**0.5 + (2.*R2*(R2+z2+D2)**2)/discr**1.5 d2ldz2 = 1. + ( R2+3.*z2-D2)/discr**0.5 - (2.*z2*(R2+z2-D2)**2)/discr**1.5 d2ndz2 = 1. - ( R2+3.*z2-D2)/discr**0.5 + (2.*z2*(R2+z2-D2)**2)/discr**1.5 d2ldRdz = 2.*R*z/discr**0.5 * ( 1. - ((R2+z2)**2-D**4)/discr) d2ndRdz = 2.*R*z/discr**0.5 * (-1. + ((R2+z2)**2-D**4)/discr) dim = 1 if isinstance(R,numpy.ndarray): dim = len(R) elif isinstance(z,numpy.ndarray): dim = len(z) hess = numpy.zeros((2,2,2,dim)) #Hessian for lambda: hess[0,0,0,:] = d2ldR2 hess[0,0,1,:] = d2ldRdz hess[0,1,0,:] = d2ldRdz hess[0,1,1,:] = d2ldz2 #Hessian for nu: hess[1,0,0,:] = d2ndR2 hess[1,0,1,:] = d2ndRdz hess[1,1,0,:] = d2ndRdz hess[1,1,1,:] = d2ndz2 if dim == 1: return hess[:,:,:,0] else: return hess def lambdanu_to_Rz(l,n,ac=5.,Delta=1.): """ NAME: lambdanu_to_Rz PURPOSE: calculate galactocentric cylindrical coordinates (R,z) from prolate spheroidal coordinates (lambda,nu), cf. eq. (2.2) in Dejonghe & de Zeeuw (1988a) INPUT: l - prolate spheroidal coordinate lambda n - prolate spheroidal coordinate nu ac - axis ratio of the coordinate surfaces (a/c) = sqrt(-a) / sqrt(-g) (default: 5.) Delta - focal distance that defines the spheroidal coordinate system (default: 1.) Delta=sqrt(g-a) OUTPUT: (R,z) HISTORY: 2015-02-13 - Written - Trick (MPIA) """ g = Delta**2 / (1.-ac**2) a = g - Delta**2 r2 = (l + a) * (n + a) / (a - g) z2 = (l + g) * (n + g) / (g - a) index = (r2 < 0.) * ((n+a) > 0.) * ((n+a) < 1e-10) if numpy.any(index): if isinstance(r2,numpy.ndarray): r2[index] = 0. else: r2 = 0. index = (z2 < 0.) * ((n+g) < 0.) * ((n+g) > -1e-10) if numpy.any(index): if isinstance(z2,numpy.ndarray): z2[index] = 0. else: z2 = 0. return (numpy.sqrt(r2),numpy.sqrt(z2)) @scalarDecorator @degreeDecorator([0,1],[0,1]) def radec_to_custom(ra,dec,T=None,degree=False): """ NAME: radec_to_custom PURPOSE: transform from equatorial coordinates to a custom set of sky coordinates INPUT: ra - right ascension dec - declination T= matrix defining the transformation: new_rect= T dot old_rect, where old_rect = [cos(dec)cos(ra),cos(dec)sin(ra),sin(dec)] and similar for new_rect degree - (Bool) if True, ra and dec are given in degree and l and b will be as well OUTPUT: custom longitude, custom latitude (with longitude -180 to 180) For vector inputs [:,2] HISTORY: 2009-11-12 - Written - Bovy (NYU) 2014-06-14 - Re-written w/ numpy functions for speed and w/ decorators for beauty - Bovy (IAS) 2019-03-02 - adjusted angle ranges - Nathaniel (UofT) """ if T is None: raise ValueError("Must set T= for radec_to_custom") #Whether to use degrees and scalar input is handled by decorators XYZ= numpy.array([numpy.cos(dec)*numpy.cos(ra), numpy.cos(dec)*numpy.sin(ra), numpy.sin(dec)]) galXYZ= numpy.dot(T,XYZ) b= numpy.arcsin(galXYZ[2]) # [-pi/2, pi/2] l= numpy.arctan2(galXYZ[1], galXYZ[0]) l[l<0] += 2 * numpy.pi # fix range to [0, 2 pi] out= numpy.array([l,b]) return out.T @scalarDecorator @degreeDecorator([2,3],[]) def pmrapmdec_to_custom(pmra,pmdec,ra,dec,T=None,degree=False): """ NAME: pmrapmdec_to_custom PURPOSE: rotate proper motions in (ra,dec) to proper motions in a custom set of sky coordinates (phi1,phi2) INPUT: pmra - proper motion in ra (multiplied with cos(dec)) [mas/yr] pmdec - proper motion in dec [mas/yr] ra - right ascension dec - declination T= matrix defining the transformation: new_rect= T dot old_rect, where old_rect = [cos(dec)cos(ra),cos(dec)sin(ra),sin(dec)] and similar for new_rect degree= (False) if True, ra and dec are given in degrees (default=False) OUTPUT: (pmphi1 x cos(phi2),pmph2) for vector inputs [:,2] HISTORY: 2016-10-24 - Written - Bovy (UofT/CCA) 2019-03-09 - uses custom_to_radec - Nathaniel Starkman (UofT) """ if T is None: raise ValueError("Must set T= for pmrapmdec_to_custom") # Need to figure out ra_ngp and dec_ngp for this custom set of sky coords ra_ngp, dec_ngp= custom_to_radec(0., numpy.pi/2, T=T) #Whether to use degrees and scalar input is handled by decorators dec[dec == dec_ngp]+= 10.**-16 #deal w/ pole. sindec_ngp= numpy.sin(dec_ngp) cosdec_ngp= numpy.cos(dec_ngp) sindec= numpy.sin(dec) cosdec= numpy.cos(dec) sinrarangp= numpy.sin(ra-ra_ngp) cosrarangp= numpy.cos(ra-ra_ngp) cosphi= sindec_ngp*cosdec-cosdec_ngp*sindec*cosrarangp sinphi= sinrarangp*cosdec_ngp norm= numpy.sqrt(cosphi**2.+sinphi**2.) cosphi/= norm sinphi/= norm return (numpy.array([[cosphi,-sinphi],[sinphi,cosphi]]).T\ *numpy.array([[pmra,pmra],[pmdec,pmdec]]).T).sum(-1) def custom_to_radec(phi1,phi2,T=None,degree=False): """ NAME: custom_to_radec PURPOSE: rotate a custom set of sky coordinates (phi1, phi2) to (ra, dec) given the rotation matrix T for (ra, dec) -> (phi1, phi2) INPUT: phi1 - custom sky coord phi2 - custom sky coord T - matrix defining the transformation (ra, dec) -> (phi1, phi2) degree - default: False. If True, phi1 and phi2 in degrees OUTPUT: (ra, dec) for vector inputs [:, 2] HISTORY: 2018-10-23 - Written - Nathaniel (UofT) """ if T is None: raise ValueError("Must set T= for custom_to_radec") return radec_to_custom(phi1, phi2, T=numpy.transpose(T), # T.T = inv(T) degree=degree) def custom_to_pmrapmdec(pmphi1,pmphi2,phi1,phi2,T=None,degree=False): """ NAME: custom_to_pmrapmdec PURPOSE: rotate proper motions in a custom set of sky coordinates (phi1,phi2) to ICRS (ra,dec) INPUT: pmphi1 - proper motion in custom (multiplied with cos(phi2)) [mas/yr] pmphi2 - proper motion in phi2 [mas/yr] phi1 - custom longitude phi2 - custom latitude T= matrix defining the transformation in cartesian coordinates: new_rect = T dot old_rect where old_rect = [cos(dec)cos(ra), cos(dec)sin(ra), sin(dec)] and similar for new_rect degree= (False) if True, phi1 and phi2 are given in degrees (default=False) OUTPUT: (pmra x cos(dec), dec) for vector inputs [:,2] HISTORY: 2019-03-02 - Written - Nathaniel Starkman (UofT) """ if T is None: raise ValueError("Must set T= for custom_to_pmrapmdec") return pmrapmdec_to_custom(pmphi1, pmphi2, phi1, phi2, T=numpy.transpose(T), # T.T = inv(T) degree=degree) def get_epoch_angles(epoch=2000.0): """ NAME: get_epoch_angles PURPOSE: get the angles relevant for the transformation from ra, dec to l,b for the given epoch INPUT: epoch - epoch of ra,dec (right now only 2000.0 and 1950.0 are supported when not using astropy's transformations internally; when internally using astropy's coordinate transformations, epoch can be None for ICRS, 'JXXXX' for FK5, and 'BXXXX' for FK4 [but for B1950 FK4 with no E aberration terms is assumed... really, there's no reason to use B1950 in 2018 when using galpy...)) OUTPUT: set of angles HISTORY: 2010-04-07 - Written - Bovy (NYU) 2016-05-13 - Added support for using astropy's coordinate transformations and for non-standard epochs - Bovy (UofT) 2018-04-18 - Edited J2000 angles to be fully consistent with astropy - BOvy (UofT) """ if epoch == 2000.0: # Following astropy's definition here theta= 122.9319185680026/180.*numpy.pi dec_ngp= 27.12825118085622/180.*numpy.pi ra_ngp= 192.8594812065348/180.*numpy.pi elif epoch == 1950.0: theta= 123./180.*numpy.pi dec_ngp= 27.4/180.*numpy.pi ra_ngp= 192.25/180.*numpy.pi elif epoch == None: # obtained below theta= theta_icrs dec_ngp= dec_ngp_icrs ra_ngp= ra_ngp_icrs elif _APY_LOADED: # Use astropy to get the angles epoch, frame= _parse_epoch_frame_apy(epoch) c= apycoords.SkyCoord(180.*units.deg,90.*units.deg, frame=frame,equinox=epoch) c= c.transform_to(apycoords.Galactic) theta= c.l.to(units.rad).value c= apycoords.SkyCoord(180.*units.deg,90.*units.deg, frame='galactic') if not epoch is None and 'J' in epoch: c= c.transform_to(apycoords.FK5(equinox=epoch)) elif not epoch is None and 'B' in epoch: c= c.transform_to(apycoords.FK4(equinox=epoch)) else: # pragma: no cover raise ValueError('epoch input not understood; should be None for ICRS, JXXXX, or BXXXX') dec_ngp= c.dec.to(units.rad).value ra_ngp= c.ra.to(units.rad).value else: raise OSError("Only epochs 1950 and 2000 are supported if you don't have astropy") return (theta,dec_ngp,ra_ngp) # Get ICRS angles once when astropy is installed if _APY_LOADED: c= apycoords.SkyCoord(180.*units.deg,90.*units.deg,frame='icrs') c= c.transform_to(apycoords.Galactic) theta_icrs= c.l.to(units.rad).value c= apycoords.SkyCoord(180.*units.deg,90.*units.deg, frame='galactic') c= c.transform_to(apycoords.ICRS) dec_ngp_icrs= c.dec.to(units.rad).value ra_ngp_icrs= c.ra.to(units.rad).value else: theta_icrs= 2.1455668515225916 dec_ngp_icrs= 0.4734773249532947 ra_ngp_icrs= 3.366032882941063 def _parse_epoch_frame_apy(epoch): if epoch == 2000.0 or epoch == '2000': epoch= 'J2000' elif epoch == 1950.0 or epoch == '1950': epoch= 'B1950' if not epoch is None and 'J' in epoch: frame= 'fk5' elif not epoch is None and 'B' in epoch: frame= 'fk4' else: frame= 'icrs' return (epoch,frame) # Matrix to rotate to the astropy Galactocentric frame: astropy's # Galactocentric frame is slightly off from the one that we get by simply # taking Galactic coordinates and transforming them: transformation from # Bovy (2011) maps NGP --> (0,0,1), astropy to (v. small, v. small, 1-v. small) # so we rotate Bovy (2011) such that we agree; for that we compute what NGP # goes to using astropy's transformations theta,dec_ngp,ra_ngp= get_epoch_angles(None) # None = ICRS, basis for astropy dec_gc,ra_gc= -28.936175/180.*numpy.pi,266.4051/180.*numpy.pi # from apy def. eta= 58.5986320306/180.*numpy.pi # astropy 'roll' angle gc_vec= numpy.array(\ [numpy.cos(theta)*(-numpy.sin(dec_ngp)*numpy.cos(dec_gc)*numpy.cos(ra_gc-ra_ngp) +numpy.cos(dec_ngp)*numpy.sin(dec_gc)) +numpy.sin(theta)*numpy.cos(dec_gc)*numpy.sin(ra_gc-ra_ngp), numpy.sin(theta)*(-numpy.sin(dec_ngp)*numpy.cos(dec_gc)*numpy.cos(ra_gc-ra_ngp) +numpy.cos(dec_ngp)*numpy.sin(dec_gc)) -numpy.cos(theta)*numpy.cos(dec_gc)*numpy.sin(ra_gc-ra_ngp), numpy.cos(dec_ngp)*numpy.cos(dec_gc)*numpy.cos(ra_gc-ra_ngp) +numpy.sin(dec_ngp)*numpy.sin(dec_gc)]) galcen_extra_rot1= _rotate_to_arbitrary_vector(numpy.atleast_2d(gc_vec), numpy.array([1.,0.,0.]), inv=False,_dontcutsmall=True)[0] ngp_vec= numpy.dot(galcen_extra_rot1,numpy.array(\ [-numpy.cos(dec_gc)*numpy.cos(dec_ngp)*numpy.cos(ra_ngp-ra_gc) -numpy.sin(dec_gc)*numpy.sin(dec_ngp), numpy.cos(eta)*numpy.cos(dec_ngp)*numpy.sin(ra_ngp-ra_gc) +numpy.sin(eta)*(-numpy.sin(dec_gc)*numpy.cos(dec_ngp)*numpy.cos(ra_ngp-ra_gc) +numpy.cos(dec_gc)*numpy.sin(dec_ngp)), -numpy.sin(eta)*numpy.cos(dec_ngp)*numpy.sin(ra_ngp-ra_gc) +numpy.cos(eta)*(-numpy.sin(dec_gc)*numpy.cos(dec_ngp)*numpy.cos(ra_ngp-ra_gc) +numpy.cos(dec_gc)*numpy.sin(dec_ngp))])) galcen_extra_rot2= _rotate_to_arbitrary_vector(numpy.atleast_2d(ngp_vec), numpy.array([0.,0.,1.]), inv=True,_dontcutsmall=True)[0] # Leave x axis alone, because in place by rot1 galcen_extra_rot2[0,0]= 1. galcen_extra_rot2[0,1:]= 0. galcen_extra_rot2[1:,0]= 0. galcen_extra_rot= numpy.dot(galcen_extra_rot2,galcen_extra_rot1) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667233486.721467 galpy-1.8.1/galpy/util/interp_2d/0000755000175100001710000000000014327773317016231 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/interp_2d/cubic_bspline_2d_coeffs.c0000644000175100001710000002127514327773303023112 0ustar00runnerdocker/***************************************************************************** * This code is based on work by Phillipe Thevenaz, which can be found * at http://bigwww.epfl.ch/thevenaz/interpolation/ ****************************************************************************/ #include "cubic_bspline_2d_coeffs.h" /*--------------------------------------------------------------------------*/ static void convert_to_interpolation_coefficients ( double c[], /* input samples --> output coefficients */ long data_length, /* number of samples or coefficients */ double z[], /* poles */ long nb_poles, /* number of poles */ double tolerance /* admissible relative error */ ); /*--------------------------------------------------------------------------*/ static void get_column ( double *data, /* input image array */ long width, /* width of the image */ long x, /* x coordinate of the selected line */ double line[], /* output linear array */ long height /* length of the line and height of the image */ ); /*--------------------------------------------------------------------------*/ static void get_row ( double *data, /* input image array */ long y, /* y coordinate of the selected line */ double line[], /* output linear array */ long width /* length of the line and width of the image */ ); /*--------------------------------------------------------------------------*/ static double initial_causal_coefficient ( double c[], /* coefficients */ long data_length, /* number of coefficients */ double z, /* actual pole */ double tolerance /* admissible relative error */ ); /*--------------------------------------------------------------------------*/ static double initial_anticausal_coefficient ( double c[], /* coefficients */ long data_length, /* number of samples or coefficients */ double z /* actual pole */ ); /*--------------------------------------------------------------------------*/ static void put_column ( double *data, /* output image array */ long width, /* width of the image */ long x, /* x coordinate of the selected line */ double line[], /* input linear array */ long height /* length of the line and height of the image */ ); /*--------------------------------------------------------------------------*/ //void put_row now declared in header file /***************************************************************************** * Definition of static procedures ****************************************************************************/ /*--------------------------------------------------------------------------*/ static void convert_to_interpolation_coefficients ( double c[], /* input samples --> output coefficients */ long data_length, /* number of samples or coefficients */ double z[], /* poles */ long nb_poles, /* number of poles */ double tolerance /* admissible relative error */ ) { /* begin convert_to_interpolation_coefficients */ double lambda = 1.0; long n, k; /* special case required by mirror boundaries */ // LCOV_EXCL_START if (data_length == 1L) { return; } // LCOV_EXCL_STOP /* compute the overall gain */ for (k = 0L; k < nb_poles; k++) { lambda = lambda * (1.0 - z[k]) * (1.0 - 1.0 / z[k]); } /* apply the gain */ for (n = 0L; n < data_length; n++) { c[n] *= lambda; } /* loop over all poles */ for (k = 0L; k < nb_poles; k++) { /* causal initialization */ c[0] = initial_causal_coefficient(c, data_length, z[k], tolerance); /* causal recursion */ for (n = 1L; n < data_length; n++) { c[n] += z[k] * c[n - 1L]; } /* anticausal initialization */ c[data_length - 1L] = initial_anticausal_coefficient(c, data_length, z[k]); /* anticausal recursion */ for (n = data_length - 2L; 0 <= n; n--) { c[n] = z[k] * (c[n + 1L] - c[n]); } } } /* end convert_to_interpolation_coefficients */ /*--------------------------------------------------------------------------*/ static double initial_causal_coefficient ( double c[], /* coefficients */ long data_length, /* number of coefficients */ double z, /* actual pole */ double tolerance /* admissible relative error */ ) { /* begin initial_causal_coefficient */ double sum, zn, z2n, iz; long n, horizon; /* this initialization corresponds to mirror boundaries */ horizon = data_length; if (tolerance > 0.0) { horizon = (long)ceil(log(tolerance) / log(fabs(z))); } if (horizon < data_length) { /* accelerated loop */ zn = z; sum = c[0]; for (n = 1L; n < horizon; n++) { sum += zn * c[n]; zn *= z; } return(sum); } else { /* full loop */ // LCOV_EXCL_START zn = z; iz = 1.0 / z; z2n = pow(z, (double)(data_length - 1L)); sum = c[0] + z2n * c[data_length - 1L]; z2n *= z2n * iz; for (n = 1L; n <= data_length - 2L; n++) { sum += (zn + z2n) * c[n]; zn *= z; z2n *= iz; } return(sum / (1.0 - zn * zn)); // LCOV_EXCL_STOP } } /* end initial_causal_coefficient */ /*--------------------------------------------------------------------------*/ static void get_column ( double *data, /* input image array */ long width, /* width of the image */ long x, /* x coordinate of the selected line */ double line[], /* output linear array */ long height /* length of the line */ ) { /* begin get_column */ long y; data = data + (ptrdiff_t)x; for (y = 0L; y < height; y++) { line[y] = (double)*data; data += (ptrdiff_t)width; } } /* end get_column */ /*--------------------------------------------------------------------------*/ static void get_row ( double *data, /* input image array */ long y, /* y coordinate of the selected line */ double line[], /* output linear array */ long width /* length of the line */ ) { /* begin get_row */ long x; data = data + (ptrdiff_t)(y * width); for (x = 0L; x < width; x++) { line[x] = (double)*data++; } } /* end get_row */ /*--------------------------------------------------------------------------*/ static double initial_anticausal_coefficient ( double c[], /* coefficients */ long data_length, /* number of samples or coefficients */ double z /* actual pole */ ) { /* begin initial_anticausal_coefficient */ /* this initialization corresponds to mirror boundaries */ return((z / (z * z - 1.0)) * (z * c[data_length - 2L] + c[data_length - 1L])); } /* end initial_anticausal_coefficient */ /*--------------------------------------------------------------------------*/ static void put_column ( double *data, /* output image array */ long width, /* width of the image */ long x, /* x coordinate of the selected line */ double line[], /* input linear array */ long height /* length of the line and height of the image */ ) { /* begin put_column */ long y; data = data + (ptrdiff_t)x; for (y = 0L; y < height; y++) { *data = (double)line[y]; data += (ptrdiff_t)width; } } /* end put_column */ /*--------------------------------------------------------------------------*/ void put_row ( double *data, /* output image array */ long y, /* y coordinate of the selected line */ double line[], /* input linear array */ long width /* length of the line and width of the image */ ) { /* begin put_row */ long x; data = data + (ptrdiff_t)(y * width); for (x = 0L; x < width; x++) { *data++ = (double)line[x]; } } /* end put_row */ /***************************************************************************** * Definition of extern procedures ****************************************************************************/ /*--------------------------------------------------------------------------*/ extern int samples_to_coefficients ( double *data, /* in-place processing */ long width, /* width of the image */ long height /* height of the image */ ) { /* begin SamplesToCoefficients */ double *line; double pole[4]; long nb_poles; long x, y; nb_poles = 1L; pole[0] = sqrt(3.0) - 2.0; /* convert the image samples into interpolation coefficients */ /* in-place separable process, along x */ line = (double *)malloc((size_t)(width * (long)sizeof(double))); // LCOV_EXCL_START if (line == (double *)NULL) { printf("Row allocation failed\n"); return(1); } // LCOV_EXCL_STOP for (y = 0L; y < height; y++) { get_row(data, y, line, width); convert_to_interpolation_coefficients(line, width, pole, nb_poles, DBL_EPSILON); put_row(data, y, line, width); } free(line); /* in-place separable process, along y */ line = (double *)malloc((size_t)(height * (long)sizeof(double))); // LCOV_EXCL_START if (line == (double *)NULL) { printf("Column allocation failed\n"); return(1); } // LCOV_EXCL_STOP for (x = 0L; x < width; x++) { get_column(data, width, x, line, height); convert_to_interpolation_coefficients(line, height, pole, nb_poles, DBL_EPSILON); put_column(data, width, x, line, height); } free(line); return(0); } /* end SamplesToCoefficients */ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/interp_2d/cubic_bspline_2d_coeffs.h0000644000175100001710000000153314327773303023112 0ustar00runnerdocker/***************************************************************************** * This code is based on work by Phillipe Thevenaz, which can be found * at http://bigwww.epfl.ch/thevenaz/interpolation/ ****************************************************************************/ #ifdef __cplusplus extern "C" { #endif #include #include #include #include #include //Macros to export functions in DLL on different OS #if defined(_WIN32) #define EXPORT __declspec(dllexport) #elif defined(__GNUC__) #define EXPORT __attribute__((visibility("default"))) #else // Just do nothing? #define EXPORT #endif /*--------------------------------------------------------------------------*/ void put_row(double *,long,double *,long); EXPORT int samples_to_coefficients(double *,long,long); #ifdef __cplusplus } #endif ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/interp_2d/cubic_bspline_2d_interpol.c0000644000175100001710000002047314327773303023500 0ustar00runnerdocker/***************************************************************************** * This code is based on work by Phillipe Thevenaz, which can be found * at http://bigwww.epfl.ch/thevenaz/interpolation/ ****************************************************************************/ #include "cubic_bspline_2d_interpol.h" /*--------------------------------------------------------------------------*/ extern double cubic_bspline_2d_interpol ( double *coeffs, /* input B-spline array of coefficients */ long width, /* width of the image */ long height, /* height of the image */ double x, /* x coordinate where to interpolate */ double y /* y coordinate where to interpolate */ ) { /* begin InterpolatedValue */ int spline_degree = 3; //double *p; long x_index[4], y_index[4]; double x_weight[4], y_weight[4]; double interpolated; double w/*, w2, w4, t, t0, t1*/; long width2 = 2L * width - 2L, height2 = 2L * height - 2L; long i, j, k; /* compute the interpolation indexes: floor(x) + {-1,0,1,2}, floor(y) + {-1,0,1,2} */ i = (long)floor(x) - spline_degree / 2L; j = (long)floor(y) - spline_degree / 2L; for (k = 0L; k <= spline_degree; k++) { x_index[k] = i++; y_index[k] = j++; } /* compute the interpolation weights */ /* x */ w = x - (double)x_index[1]; x_weight[3] = (1.0 / 6.0) * w * w * w; x_weight[0] = (1.0 / 6.0) + (1.0 / 2.0) * w * (w - 1.0) - x_weight[3]; x_weight[2] = w + x_weight[0] - 2.0 * x_weight[3]; x_weight[1] = 1.0 - x_weight[0] - x_weight[2] - x_weight[3]; /* y */ w = y - (double)y_index[1]; y_weight[3] = (1.0 / 6.0) * w * w * w; y_weight[0] = (1.0 / 6.0) + (1.0 / 2.0) * w * (w - 1.0) - y_weight[3]; y_weight[2] = w + y_weight[0] - 2.0 * y_weight[3]; y_weight[1] = 1.0 - y_weight[0] - y_weight[2] - y_weight[3]; /* apply the mirror boundary conditions */ for (k = 0L; k <= spline_degree; k++) { x_index[k] = (width == 1L) ? (0L) : ((x_index[k] < 0L) ? (-x_index[k] - width2 * ((-x_index[k]) / width2)) : (x_index[k] - width2 * (x_index[k] / width2))); if (width <= x_index[k]) { x_index[k] = width2 - x_index[k]; } y_index[k] = (height == 1L) ? (0L) : ((y_index[k] < 0L) ?(-y_index[k] - height2 * ((-y_index[k]) / height2)) : (y_index[k] - height2 * (y_index[k] / height2))); if (height <= y_index[k]) { y_index[k] = height2 - y_index[k]; } } /* perform interpolation */ interpolated = 0.0; /*for (j = 0L; j <= spline_degree; j++) { p = coeffs + (ptrdiff_t)(y_index[j] * width); w = 0.0; for (i = 0L; i <= spline_degree; i++) { w += x_weight[i] * p[x_index[i]]; } interpolated += y_weight[j] * w; }*/ for(i=0L; i<=spline_degree; i++) { for(j=0L; j<=spline_degree; j++) { interpolated += coeffs[x_index[i]*height+y_index[j]] * x_weight[i] * y_weight[j]; } } return(interpolated); } // LCOV_EXCL_START /*--------------------------------------------------------------------------*/ extern double cubic_bspline_2d_interpol_dx ( double *coeffs, /* input B-spline array of coefficients */ long width, /* width of the image */ long height, /* height of the image */ double x, /* x coordinate where to interpolate */ double y /* y coordinate where to interpolate */ ) { /* begin InterpolatedValue */ int spline_degree = 3; //double *p; long x_index[3], y_index[4]; double x_weight[3], y_weight[4]; double interpolated; double w/*, w2, w4, t, t0, t1*/; long width2 = 2L * width - 2L, height2 = 2L * height - 2L; long i, j, k; /* compute the interpolation indexes */ /* here we have to calculate B^2(x-j+0.5) */ i = (long)floor(x+1) - (spline_degree-1) / 2L; j = (long)floor(y) - spline_degree / 2L; for (k = 0L; k <= spline_degree; k++) { if(k #include #include #include /*--------------------------------------------------------------------------*/ extern double cubic_bspline_2d_interpol ( double *coeffs, /* input B-spline array of coefficients */ long width, /* width of the image */ long height, /* height of the image */ double x, /* x coordinate where to interpolate */ double y /* y coordinate where to interpolate */ ); extern double cubic_bspline_2d_interpol_dx ( double *coeffs, /* input B-spline array of coefficients */ long width, /* width of the image */ long height, /* height of the image */ double x, /* x coordinate where to interpolate */ double y /* y coordinate where to interpolate */ ); extern double cubic_bspline_2d_interpol_dy ( double *coeffs, /* input B-spline array of coefficients */ long width, /* width of the image */ long height, /* height of the image */ double x, /* x coordinate where to interpolate */ double y /* y coordinate where to interpolate */ ); #ifdef __cplusplus } #endif ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/interp_2d/interp_2d.c0000644000175100001710000001141514327773303020260 0ustar00runnerdocker#include "interp_2d.h" interp_2d * interp_2d_alloc(int size1, int size2) { interp_2d * i2d = (interp_2d *)malloc(sizeof(interp_2d)); i2d->size1 = size1; i2d->size2 = size2; i2d->xa = (double *)malloc(size1*sizeof(double)); i2d->ya = (double *)malloc(size2*sizeof(double)); i2d->za = (double *)malloc(size1*size2*sizeof(double)); return i2d; } void interp_2d_free(interp_2d * i2d) { free(i2d->xa); free(i2d->ya); free(i2d->za); free(i2d); } void interp_2d_init(interp_2d * i2d, const double * xa, const double * ya, const double * za, int type) { i2d->type = type; memcpy(i2d->xa,xa,(i2d->size1)*sizeof(double)); memcpy(i2d->ya,ya,(i2d->size2)*sizeof(double)); memcpy(i2d->za,za,(i2d->size1)*(i2d->size2)*sizeof(double)); // LCOV_EXCL_START if(type==INTERP_2D_CUBIC_BSPLINE) { samples_to_coefficients(i2d->za,i2d->size1,i2d->size2); } // LCOV_EXCL_STOP } // LCOV_EXCL_START double interp_2d_eval_linear(interp_2d * i2d, double x, double y, gsl_interp_accel * accx, gsl_interp_accel * accy) { int size1 = i2d->size1; int size2 = i2d->size2; double * xa = i2d->xa; double * ya = i2d->ya; double * za = i2d->za; //x = (x > xa[size1-1]) ? xa[size1-1] : x; //y = (y > ya[size2-1]) ? ya[size2-1] : y; int ix = gsl_interp_accel_find(accx, xa, size1, x); int iy = gsl_interp_accel_find(accy, ya, size2, y); double z00 = za[ix*size2+iy]; double z01 = za[ix*size2+iy+1]; double z10 = za[(ix+1)*size2+iy]; double z11 = za[(ix+1)*size2+iy+1]; double denom = (xa[ix+1]-xa[ix])*(ya[iy+1]-ya[iy]); return z00*(xa[ix+1]-x)*(ya[iy+1]-y)/denom + z10*(x-xa[ix])*(ya[iy+1]-y)/denom + z01*(xa[ix+1]-x)*(y-ya[iy])/denom + z11*(x-xa[ix])*(y-ya[iy])/denom; } void interp_2d_eval_grad_linear(interp_2d * i2d, double x, double y, double * grad, gsl_interp_accel * accx, gsl_interp_accel * accy) { int size1 = i2d->size1; int size2 = i2d->size2; double * xa = i2d->xa; double * ya = i2d->ya; double * za = i2d->za; //x = (x > xa[size1-1]) ? xa[size1-1] : x; //y = (y > ya[size2-1]) ? ya[size2-1] : y; int ix = gsl_interp_accel_find(accx, xa, size1, x); int iy = gsl_interp_accel_find(accy, ya, size2, y); double z00 = za[ix*size2+iy]; double z01 = za[ix*size2+iy+1]; double z10 = za[(ix+1)*size2+iy]; double z11 = za[(ix+1)*size2+iy+1]; double denom = (xa[ix+1]-xa[ix])*(ya[iy+1]-ya[iy]); grad[0] = -1*z00*(ya[iy+1]-y)/denom + z10*(ya[iy+1]-y)/denom + -1*z01*(y-ya[iy])/denom + z11*(y-ya[iy])/denom; grad[1] = -1*z00*(xa[ix+1]-x)/denom - z10*(x-xa[ix])/denom + z01*(xa[ix+1]-x)/denom + z11*(x-xa[ix])/denom; return; } // LCOV_EXCL_STOP double interp_2d_eval_cubic_bspline(interp_2d * i2d, double x, double y, gsl_interp_accel * accx, gsl_interp_accel * accy) { int size1 = i2d->size1; int size2 = i2d->size2; double * xa = i2d->xa; double * ya = i2d->ya; double * za = i2d->za; x = (x > xa[size1-1]) ? xa[size1-1] : x; x = (x < xa[0]) ? xa[0] : x; y = (y > ya[size2-1]) ? ya[size2-1] : y; y = (y < ya[0]) ? ya[0] : y; int ix = gsl_interp_accel_find(accx, xa, size1, x); int iy = gsl_interp_accel_find(accy, ya, size2, y); double x_norm = ix + (x-xa[ix])/(xa[ix+1]-xa[ix]); double y_norm = iy + (y-ya[iy])/(ya[iy+1]-ya[iy]); return cubic_bspline_2d_interpol(za,size1,size2,x_norm,y_norm); } // LCOV_EXCL_START void interp_2d_eval_grad_cubic_bspline(interp_2d * i2d, double x, double y, double * grad, gsl_interp_accel * accx, gsl_interp_accel * accy) { int size1 = i2d->size1; int size2 = i2d->size2; double * xa = i2d->xa; double * ya = i2d->ya; double * za = i2d->za; int ix = gsl_interp_accel_find(accx, xa, size1, x); int iy = gsl_interp_accel_find(accy, ya, size2, y); double x_norm = ix + (x-xa[ix])/(xa[ix+1]-xa[ix]); double y_norm = iy + (y-ya[iy])/(ya[iy+1]-ya[iy]); grad[0] = cubic_bspline_2d_interpol_dx(za,size1,size2,x_norm,y_norm)/(xa[ix+1]-xa[ix]); grad[1] = cubic_bspline_2d_interpol_dy(za,size1,size2,x_norm,y_norm)/(ya[iy+1]-ya[iy]); return; } double interp_2d_eval(interp_2d * i2d, double x, double y, gsl_interp_accel * accx, gsl_interp_accel * accy) { return (i2d->type == INTERP_2D_CUBIC_BSPLINE ? interp_2d_eval_cubic_bspline(i2d,x,y,accx,accy) : interp_2d_eval_linear(i2d,x,y,accx,accy) ); } void interp_2d_eval_grad(interp_2d * i2d, double x, double y, double * grad, gsl_interp_accel * accx, gsl_interp_accel * accy) { if(i2d->type == INTERP_2D_CUBIC_BSPLINE) { interp_2d_eval_grad_cubic_bspline(i2d,x,y,grad,accx,accy); } else { interp_2d_eval_grad_linear(i2d,x,y,grad,accx,accy); } return; } // LCOV_EXCL_STOP ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/interp_2d/interp_2d.h0000644000175100001710000000207614327773303020270 0ustar00runnerdocker#ifndef __INTERP_2D_H__ #define __INTERP_2D_H__ #ifdef __cplusplus extern "C" { #endif #include //#include //#include //#include #include #include "cubic_bspline_2d_coeffs.h" #include "cubic_bspline_2d_interpol.h" enum { INTERP_2D_LINEAR=0, INTERP_2D_CUBIC_BSPLINE=1 }; typedef struct { int size1; int size2; double * xa; double * ya; double * za; int type; }interp_2d; interp_2d * interp_2d_alloc(int size1, int size2); void interp_2d_free(interp_2d * i2d); void interp_2d_init(interp_2d * i2d, const double * xa, const double * ya, const double * za, int type); double interp_2d_eval(interp_2d * i2d, double x, double y, gsl_interp_accel * accx, gsl_interp_accel * accy); void interp_2d_eval_grad(interp_2d * i2d, double x, double y, double * grad, gsl_interp_accel * accx, gsl_interp_accel * accy); double interp_2d_eval_cubic_bspline(interp_2d * i2d, double x, double y, gsl_interp_accel * accx,gsl_interp_accel * accy); #ifdef __cplusplus } #endif #endif ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/leung_dop853.c0000644000175100001710000005010514327773303016717 0ustar00runnerdocker/* C implementations of Dormand-Prince 8(5,3) */ /* Copyright (c) 2018, Henry Leung All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include "bovy_symplecticode.h" #include "leung_dop853.h" #define _MAX_DT_REDUCE 10000. #define uround 2.3e-16 /* time increment coefficients */ double c2 = 0.526001519587677318785587544488e-1; double c3 = 0.789002279381515978178381316732e-1; double c4 = 0.118350341907227396726757197510; double c5 = 0.281649658092772603273242802490; double c6 = 0.333333333333333333333333333333; double c7 = 0.25; double c8 = 0.307692307692307692307692307692; double c9 = 0.651282051282051282051282051282; double c10 = 0.6; double c11 = 0.857142857142857142857142857142; double c12 = 1; double c13 = 1; double c14 = 0.1; double c15 = 0.2; double c16 = 0.777777777777777777777777777778; /* slope calculation coefficients */ double a21 = 5.26001519587677318785587544488e-2; double a31 = 1.97250569845378994544595329183e-2; double a32 = 5.91751709536136983633785987549e-2; double a41 = 2.95875854768068491816892993775e-2; double a43 = 8.87627564304205475450678981324e-2; double a51 = 2.41365134159266685502369798665e-1; double a53 = -8.84549479328286085344864962717e-1; double a54 = 9.24834003261792003115737966543e-1; double a61 = 3.7037037037037037037037037037e-2; double a64 = 1.70828608729473871279604482173e-1; double a65 = 1.25467687566822425016691814123e-1; double a71 = 3.7109375e-2; double a74 = 1.70252211019544039314978060272e-1; double a75 = 6.02165389804559606850219397283e-2; double a76 = -1.7578125e-2; double a81 = 3.70920001185047927108779319836e-2; double a84 = 1.70383925712239993810214054705e-1; double a85 = 1.07262030446373284651809199168e-1; double a86 = -1.53194377486244017527936158236e-2; double a87 = 8.27378916381402288758473766002e-3; double a91 = 6.24110958716075717114429577812e-1; double a94 = -3.36089262944694129406857109825e0; double a95 = -8.68219346841726006818189891453e-1; double a96 = 2.75920996994467083049415600797e1; double a97 = 2.01540675504778934086186788979e1; double a98 = -4.34898841810699588477366255144e1; double a101 = 4.77662536438264365890433908527e-1; double a104 = -2.48811461997166764192642586468e0; double a105 = -5.90290826836842996371446475743e-1; double a106 = 2.12300514481811942347288949897e1; double a107 = 1.52792336328824235832596922938e1; double a108 = -3.32882109689848629194453265587e1; double a109 = -2.03312017085086261358222928593e-2; double a111 = -9.3714243008598732571704021658e-1; double a114 = 5.18637242884406370830023853209e0; double a115 = 1.09143734899672957818500254654e0; double a116 = -8.14978701074692612513997267357e0; double a117 = -1.85200656599969598641566180701e1; double a118 = 2.27394870993505042818970056734e1; double a119 = 2.49360555267965238987089396762e0; double a1110 = -3.0467644718982195003823669022e0; double a121 = 2.27331014751653820792359768449e0; double a124 = -1.05344954667372501984066689879e1; double a125 = -2.00087205822486249909675718444e0; double a126 = -1.79589318631187989172765950534e1; double a127 = 2.79488845294199600508499808837e1; double a128 = -2.85899827713502369474065508674e0; double a129 = -8.87285693353062954433549289258e0; double a1210 = 1.23605671757943030647266201528e1; double a1211 = 6.43392746015763530355970484046e-1; double a141 = 5.61675022830479523392909219681e-2; double a147 = 2.53500210216624811088794765333e-1; double a148 = -2.46239037470802489917441475441e-1; double a149 = -1.24191423263816360469010140626e-1; double a1410 = 1.5329179827876569731206322685e-1; double a1411 = 8.20105229563468988491666602057e-3; double a1412 = 7.56789766054569976138603589584e-3; double a1413 = -8.298e-3; double a151 = 3.18346481635021405060768473261e-2; double a156 = 2.83009096723667755288322961402e-2; double a157 = 5.35419883074385676223797384372e-2; double a158 = -5.49237485713909884646569340306e-2; double a1511 = -1.08347328697249322858509316994e-4; double a1512 = 3.82571090835658412954920192323e-4; double a1513 = -3.40465008687404560802977114492e-4; double a1514 = 1.41312443674632500278074618366e-1; double a161 = -4.28896301583791923408573538692e-1; double a166 = -4.69762141536116384314449447206e0; double a167 = 7.68342119606259904184240953878e0; double a168 = 4.06898981839711007970213554331e0; double a169 = 3.56727187455281109270669543021e-1; double a1613 = -1.39902416515901462129418009734e-3; double a1614 = 2.9475147891527723389556272149e0; double a1615 = -9.15095847217987001081870187138e0; /*Final assembly coefficients */ double b1 = 5.42937341165687622380535766363e-2; double b6 = 4.45031289275240888144113950566; double b7 = 1.89151789931450038304281599044; double b8 = -5.8012039600105847814672114227; double b9 = 3.1116436695781989440891606237e-1; double b10 = -1.52160949662516078556178806805e-1; double b11 = 2.01365400804030348374776537501e-1; double b12 = 4.47106157277725905176885569043e-2; double bhh1 = 0.244094488188976377952755905512; double bhh2 = 0.733846688281611857341361741547; double bhh3 = 0.220588235294117647058823529412e-1; /* Dense output coefficients */ double d41 = -0.84289382761090128651353491142e+1; double d46 = 0.56671495351937776962531783590; double d47 = -0.30689499459498916912797304727e+1; double d48 = 0.23846676565120698287728149680e+1; double d49 = 0.21170345824450282767155149946e+1; double d410 = -0.87139158377797299206789907490; double d411 = 0.22404374302607882758541771650e+1; double d412 = 0.63157877876946881815570249290; double d413 = -0.88990336451333310820698117400e-1; double d414 = 0.18148505520854727256656404962e+2; double d415 = -0.91946323924783554000451984436e+1; double d416 = -0.44360363875948939664310572000e+1; double d51 = 0.10427508642579134603413151009e+2; double d56 = 0.24228349177525818288430175319e+3; double d57 = 0.16520045171727028198505394887e+3; double d58 = -0.37454675472269020279518312152e+3;; double d59 = -0.22113666853125306036270938578e+2; double d510 = 0.77334326684722638389603898808e+1; double d511 = -0.30674084731089398182061213626e+2; double d512 = -0.93321305264302278729567221706e+1; double d513 = 0.15697238121770843886131091075e+2; double d514 = -0.31139403219565177677282850411e+2; double d515 = -0.93529243588444783865713862664e+1; double d516 = 0.35816841486394083752465898540e+2; double d61 = 0.19985053242002433820987653617e+2; double d66 = -0.38703730874935176555105901742e+3; double d67 = -0.18917813819516756882830838328e+3; double d68 = 0.52780815920542364900561016686e+3;; double d69 = -0.11573902539959630126141871134e+2; double d610 = 0.68812326946963000169666922661e+1; double d611 = -0.10006050966910838403183860980e+1; double d612 = 0.77771377980534432092869265740; double d613 = -0.27782057523535084065932004339e+1; double d614 = -0.60196695231264120758267380846e+2; double d615 = 0.84320405506677161018159903784e+2; double d616 = 0.11992291136182789328035130030e+2; double d71 = -0.25693933462703749003312586129e+2; double d76 = -0.15418974869023643374053993627e+3; double d77 = -0.23152937917604549567536039109e+3; double d78 = 0.35763911791061412378285349910e+3; double d79 = 0.93405324183624310003907691704e+2; double d710 = -0.37458323136451633156875139351e+2; double d711 = 0.10409964950896230045147246184e+3; double d712 = 0.29840293426660503123344363579e+2; double d713 = -0.43533456590011143754432175058e+2; double d714 = 0.96324553959188282948394950600e+2; double d715 = -0.39177261675615439165231486172e+2; double d716 = -0.14972683625798562581422125276e+3; /* Error calculation coefficients */ double er1 = 0.1312004499419488073250102996e-1; double er6 = -0.1225156446376204440720569753e+1; double er7 = -0.4957589496572501915214079952; double er8 = 0.1664377182454986536961530415e+1; double er9 = -0.3503288487499736816886487290; double er10 = 0.3341791187130174790297318841; double er11 = 0.8192320648511571246570742613e-1; double er12 = -0.2235530786388629525884427845e-1; /* Core of DOP8(5, 3) integration Usage: Provide the acceleration function func with calling sequence func (t,q,a,nargs,args) where double t: time double * q: current value (dimension: dim) double * a: will be set to the derivative by func int nargs: number of arguments the function takes double *args: arguments Other arguments are: int dim: dimension double *x0: initial value, dimension: dim int nt: number of times at which the output is wanted double dt_one: (optional) stepsize to use, must be an integer divisor of time difference between output steps (NOT CHECKED EXPLICITLY) double *t: times at which the output is wanted (EQUALLY SPACED) int nargs: see above double *args: see above double rtol, double atol: relative and absolute tolerance levels desired Output: double *result: result (nt blocks of size 2dim) int * err: if non-zero, something bad happened (1: maximum step reduction happened; -10: interrupted by CTRL-C (SIGINT) */ void dop853(void(*func)(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs), int dim, double * y0, int nt, double dt, double *t, int nargs, struct potentialArg * potentialArgs, double rtol, double atol, double *result, int *err_) { rtol = exp(rtol); atol = exp(atol); // Same initial parameters also used in my python version double safe = 0.9; double beta = 0.0; double facold = 1.0e-4; double expo1 = 1.0 / 8.0 - beta * 0.2; double facc1 = 1.0 / 0.333; double facc2 = 1.0 / 6.0; double hmax; double pos_neg; hmax = t[nt - 1] - t[0]; pos_neg = custom_sign(1.0, hmax); // a check to see integrate forward or backward // Declare and initialize of others double *yy1 = (double *)malloc(dim * sizeof(double)); double *yy_temp = (double *)malloc(dim * sizeof(double)); double *k1 = (double *)malloc(dim * sizeof(double)); double *k2 = (double *)malloc(dim * sizeof(double)); double *k3 = (double *)malloc(dim * sizeof(double)); double *k4 = (double *)malloc(dim * sizeof(double)); double *k5 = (double *)malloc(dim * sizeof(double)); double *k6 = (double *)malloc(dim * sizeof(double)); double *k7 = (double *)malloc(dim * sizeof(double)); double *k8 = (double *)malloc(dim * sizeof(double)); double *k9 = (double *)malloc(dim * sizeof(double)); double *k10 = (double *)malloc(dim * sizeof(double)); double *rcont1 = (double*)malloc(dim * sizeof(double)); double *rcont2 = (double*)malloc(dim * sizeof(double)); double *rcont3 = (double*)malloc(dim * sizeof(double)); double *rcont4 = (double*)malloc(dim * sizeof(double)); double *rcont5 = (double*)malloc(dim * sizeof(double)); double *rcont6 = (double*)malloc(dim * sizeof(double)); double *rcont7 = (double*)malloc(dim * sizeof(double)); double *rcont8 = (double*)malloc(dim * sizeof(double)); int i; double hnew, ydiff, bspl; double dnf, dny, sk, h, h1, der2, der12; double sqr, err, err2, erri, deno; double fac, fac11; double s, s1; save_dop853(dim, y0, result); // save first result which is the initials result += dim; // shift to next memory #ifndef _WIN32 struct sigaction action; memset(&action, 0, sizeof(struct sigaction)); action.sa_handler = handle_sigint; sigaction(SIGINT, &action, NULL); #else SetConsoleCtrlHandler(CtrlHandler, TRUE); #endif // calculate k1 func(t[0], y0, k1, nargs, potentialArgs); // start to estimate initial time step dnf = 0.0; dny = 0.0; for (i = 0; i < dim; i++) // this loop only be vectorized with /fp:fast { sk = atol + rtol * fabs(y0[i]); sqr = k1[i] / sk; dnf += sqr * sqr; sqr = y0[i] / sk; dny += sqr * sqr; } h = custom_sign(min(sqrt(dny / dnf) * 0.01, fabs(hmax)), pos_neg); for (i = 0; i < dim; i++) k3[i] = y0[i] + h * k1[i]; // perform an explicit Euler step func(t[0] + h, k3, k2, nargs, potentialArgs); der2 = 0.0; // estimate the second derivative of the solution for (i = 0; i < dim; i++) // this loop only be vectorized with /fp:fast { sk = atol + rtol * fabs(y0[i]); sqr = (k2[i] - k1[i]) / sk; der2 += sqr * sqr; } der2 = sqrt(der2) / h; der12 = max(fabs(der2), sqrt(dnf)); h1 = pow(0.01 / der12, 1.0 / 8.0); h = custom_sign(min(100.0 * fabs(h), min(fabs(h1), fabs(hmax))), pos_neg); // finished estimate initial time step int reject = 0; double t_current = (double) t[0]; // store current integration time internally(not the current time wanted by user!!) double t_old = (double) t[0]; double t_old_older = t_old; int finished_user_t_ii = 0; // times indices wanted by user // basic integration step while (finished_user_t_ii < nt - 1) // check if the current computed time indices less than total inices needed { if (interrupted) { *err_ = -10; interrupted = 0; // need to reset, bc library and vars stay in memory #ifdef USING_COVERAGE __gcov_flush(); #endif // LCOV_EXCL_START break; // LCOV_EXCL_STOP } h = pos_neg * max(fabs(h), 1e3 * uround); // keep time step not too small // the twelve stages for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * a21 * k1[i]; func(t_current + c2 * h, yy1, k2, nargs, potentialArgs); for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a31*k1[i] + a32 * k2[i]); func(t_current + c3 * h, yy1, k3, nargs, potentialArgs); for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a41*k1[i] + a43 * k3[i]); func(t_current + c4 * h, yy1, k4, nargs, potentialArgs); for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a51*k1[i] + a53 * k3[i] + a54 * k4[i]); func(t_current + c5 * h, yy1, k5, nargs, potentialArgs); for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a61*k1[i] + a64 * k4[i] + a65 * k5[i]); func(t_current + c6 * h, yy1, k6, nargs, potentialArgs); for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a71*k1[i] + a74 * k4[i] + a75 * k5[i] + a76 * k6[i]); func(t_current + c7 * h, yy1, k7, nargs, potentialArgs); for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a81*k1[i] + a84 * k4[i] + a85 * k5[i] + a86 * k6[i] + a87 * k7[i]); func(t_current + c8 * h, yy1, k8, nargs, potentialArgs); for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a91*k1[i] + a94 * k4[i] + a95 * k5[i] + a96 * k6[i] + a97 * k7[i] + a98 * k8[i]); func(t_current + c9 * h, yy1, k9, nargs, potentialArgs); for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a101*k1[i] + a104 * k4[i] + a105 * k5[i] + a106 * k6[i] + a107 * k7[i] + a108 * k8[i] + a109 * k9[i]); func(t_current + c10 * h, yy1, k10, nargs, potentialArgs); for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a111*k1[i] + a114 * k4[i] + a115 * k5[i] + a116 * k6[i] + a117 * k7[i] + a118 * k8[i] + a119 * k9[i] + a1110 * k10[i]); func(t_current + c11 * h, yy1, k2, nargs, potentialArgs); for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a121*k1[i] + a124 * k4[i] + a125 * k5[i] + a126 * k6[i] + a127 * k7[i] + a128 * k8[i] + a129 * k9[i] + a1210 * k10[i] + a1211 * k2[i]); t_old_older = t_old; t_old = t_current; t_current = t_current + h; func(t_current, yy1, k3, nargs, potentialArgs); for (i = 0; i < dim; i++) { k4[i] = b1 * k1[i] + b6 * k6[i] + b7 * k7[i] + b8 * k8[i] + b9 * k9[i] + b10 * k10[i] + b11 * k2[i] + b12 * k3[i]; k5[i] = y0[i] + h * k4[i]; } // error estimation err = (double) 0.0; err2 = (double) 0.0; for (i = 0; i < dim; i++) { sk = atol + rtol * max(fabs(y0[i]), fabs(k5[i])); erri = k4[i] - bhh1 * k1[i] - bhh2 * k9[i] - bhh3 * k3[i]; sqr = erri / sk; err2 += sqr * sqr; erri = er1 * k1[i] + er6 * k6[i] + er7 * k7[i] + er8 * k8[i] + er9 * k9[i] + er10 * k10[i] + er11 * k2[i] + er12 * k3[i]; sqr = erri / sk; err += sqr * sqr; } deno = err + 0.01 * err2; if (deno <= 0.0) deno = (double) 1.0; err = fabs(h) * err * sqrt(1.0 / (deno* (double)dim)); // computation of hnew fac11 = pow(err, expo1); // Lund-stabilization fac = fac11 / pow(facold, beta); // we require fac1 <= hnew/h <= fac2 fac = max(facc2, min(facc1, fac / safe)); hnew = h / fac; if (err <= 1.0) // step accepted { facold = max(err, 1.0e-4); func(t_current, k5, k4, nargs, potentialArgs); // final preparation for dense output for (i = 0; i < dim; i++) { rcont1[i] = y0[i]; ydiff = k5[i] - y0[i]; rcont2[i] = ydiff; bspl = h * k1[i] - ydiff; rcont3[i] = bspl; rcont4[i] = ydiff - h * k4[i] - bspl; rcont5[i] = d41 * k1[i] + d46 * k6[i] + d47 * k7[i] + d48 * k8[i] + d49 * k9[i] + d410 * k10[i] + d411 * k2[i] + d412 * k3[i]; rcont6[i] = d51 * k1[i] + d56 * k6[i] + d57 * k7[i] + d58 * k8[i] + d59 * k9[i] + d510 * k10[i] + d511 * k2[i] + d512 * k3[i]; rcont7[i] = d61 * k1[i] + d66 * k6[i] + d67 * k7[i] + d68 * k8[i] + d69 * k9[i] + d610 * k10[i] + d611 * k2[i] + d612 * k3[i]; rcont8[i] = d71 * k1[i] + d76 * k6[i] + d77 * k7[i] + d78 * k8[i] + d79 * k9[i] + d710 * k10[i] + d711 * k2[i] + d712 * k3[i]; } // the next three function evaluations for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a141*k1[i] + a147 * k7[i] + a148 * k8[i] + a149 * k9[i] + a1410 * k10[i] + a1411 * k2[i] + a1412 * k3[i] + a1413 * k4[i]); func(t_old + c14 * h, yy1, k10, nargs, potentialArgs); for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a151*k1[i] + a156 * k6[i] + a157 * k7[i] + a158 * k8[i] + a1511 * k2[i] + a1512 * k3[i] + a1513 * k4[i] + a1514 * k10[i]); func(t_old + c15 * h, yy1, k2, nargs, potentialArgs); for (i = 0; i < dim; i++) yy1[i] = y0[i] + h * (a161*k1[i] + a166 * k6[i] + a167 * k7[i] + a168 * k8[i] + a169 * k9[i] + a1613 * k4[i] + a1614 * k10[i] + a1615 * k2[i]); func(t_old + c16 * h, yy1, k3, nargs, potentialArgs); for (i = 0; i < dim; i++) { rcont5[i] = h * (rcont5[i] + d413 * k4[i] + d414 * k10[i] + d415 * k2[i] + d416 * k3[i]); rcont6[i] = h * (rcont6[i] + d513 * k4[i] + d514 * k10[i] + d515 * k2[i] + d516 * k3[i]); rcont7[i] = h * (rcont7[i] + d613 * k4[i] + d614 * k10[i] + d615 * k2[i] + d616 * k3[i]); rcont8[i] = h * (rcont8[i] + d713 * k4[i] + d714 * k10[i] + d715 * k2[i] + d716 * k3[i]); k1[i] = k4[i]; y0[i] = k5[i]; } // loop for dense output in this time slot while ((finished_user_t_ii < nt - 1) && (fabs(t[finished_user_t_ii + 1]) < fabs(t_current))) { s = (t[finished_user_t_ii + 1] - t_old) / h; s1 = 1.0 - s; for (i = 0; i < dim; i++) yy_temp[i] = rcont1[i] + s * (rcont2[i] + s1 * (rcont3[i] + s * (rcont4[i] + s1 * (rcont5[i] + s * (rcont6[i] + s1 * (rcont7[i] + s * rcont8[i])))))); save_dop853(dim, yy_temp, result); // save first result result += dim; finished_user_t_ii++; } hnew = (fabs(hnew) > fabs(hmax)) ? pos_neg * hmax : hnew; if (reject) hnew = pos_neg * min(fabs(hnew), fabs(h)); reject = 0; } else { // step rejected since error too big hnew = h / min(facc1, fac11 / safe); reject = 1; // reverse time increment since error rejected t_current = t_old; t_old = t_old_older; } h = hnew; // current h } //Free allocated memory free(k10); free(k9); free(k8); free(k7); free(k6); free(k5); free(k4); free(k3); free(k2); free(k1); free(yy1); free(yy_temp); free(rcont8); free(rcont7); free(rcont6); free(rcont5); free(rcont4); free(rcont3); free(rcont2); free(rcont1); //We're done } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/leung_dop853.h0000644000175100001710000000463214327773303016730 0ustar00runnerdocker/* C implementations of Dormand-Prince 8(5,3) */ /* Copyright (c) 2018, Henry Leung All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef __LEUNG_DOP853__ #define __LEUNG_DOP853__ #ifdef __cplusplus extern "C" { #endif #include "signal.h" #include /* Global variables */ extern volatile sig_atomic_t interrupted; #ifndef _WIN32 void handle_sigint(int); #else #include "windows.h" BOOL WINAPI CtrlHandler(DWORD fdwCtrlType); #endif #ifndef max #define max(a,b) (((a) > (b)) ? (a) : (b)) #endif #ifndef min #define min(a,b) (((a) < (b)) ? (a) : (b)) #endif /* Function declarations */ static inline void save_dop853( int dim, double *y0, double *result) { int ii; for (ii = 0; ii < dim; ii++) *result++ = *y0++; } static inline double custom_sign( double x, double y) { return (y > 0.0) ? fabs(x) : -fabs(x); } void dop853 ( void(*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *, int * ); #ifdef __cplusplus } #endif #endif ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/leung_dop853.py0000644000175100001710000004517514327773303017140 0ustar00runnerdocker############################################################################# # DOP853 integrator ############################################################################# ############################################################################# # Copyright (c) 2018, Henry Leung # All rights reserved. # This code is written with reference of the c-version dop853 here: http://www.unige.ch/~hairer/software.html # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS # OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY # WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ############################################################################# import numpy # time increment coefficients c2 = 0.526001519587677318785587544488e-1 c3 = 0.789002279381515978178381316732e-1 c4 = 0.118350341907227396726757197510 c5 = 0.281649658092772603273242802490 c6 = 0.333333333333333333333333333333 c7 = 0.25 c8 = 0.307692307692307692307692307692 c9 = 0.651282051282051282051282051282 c10 = 0.6 c11 = 0.857142857142857142857142857142 c12 = 1 c13 = 1 c14 = 0.1 c15 = 0.2 c16 = 0.777777777777777777777777777778 # slope calculation coefficients a21 = 5.26001519587677318785587544488e-2 a31 = 1.97250569845378994544595329183e-2 a32 = 5.91751709536136983633785987549e-2 a41 = 2.95875854768068491816892993775e-2 a43 = 8.87627564304205475450678981324e-2 a51 = 2.41365134159266685502369798665e-1 a53 = -8.84549479328286085344864962717e-1 a54 = 9.24834003261792003115737966543e-1 a61 = 3.7037037037037037037037037037e-2 a64 = 1.70828608729473871279604482173e-1 a65 = 1.25467687566822425016691814123e-1 a71 = 3.7109375e-2 a74 = 1.70252211019544039314978060272e-1 a75 = 6.02165389804559606850219397283e-2 a76 = -1.7578125e-2 a81 = 3.70920001185047927108779319836e-2 a84 = 1.70383925712239993810214054705e-1 a85 = 1.07262030446373284651809199168e-1 a86 = -1.53194377486244017527936158236e-2 a87 = 8.27378916381402288758473766002e-3 a91 = 6.24110958716075717114429577812e-1 a94 = -3.36089262944694129406857109825e0 a95 = -8.68219346841726006818189891453e-1 a96 = 2.75920996994467083049415600797e1 a97 = 2.01540675504778934086186788979e1 a98 = -4.34898841810699588477366255144e1 a101 = 4.77662536438264365890433908527e-1 a104 = -2.48811461997166764192642586468e0 a105 = -5.90290826836842996371446475743e-1 a106 = 2.12300514481811942347288949897e1 a107 = 1.52792336328824235832596922938e1 a108 = -3.32882109689848629194453265587e1 a109 = -2.03312017085086261358222928593e-2 a111 = -9.3714243008598732571704021658e-1 a114 = 5.18637242884406370830023853209e0 a115 = 1.09143734899672957818500254654e0 a116 = -8.14978701074692612513997267357e0 a117 = -1.85200656599969598641566180701e1 a118 = 2.27394870993505042818970056734e1 a119 = 2.49360555267965238987089396762e0 a1110 = -3.0467644718982195003823669022e0 a121 = 2.27331014751653820792359768449e0 a124 = -1.05344954667372501984066689879e1 a125 = -2.00087205822486249909675718444e0 a126 = -1.79589318631187989172765950534e1 a127 = 2.79488845294199600508499808837e1 a128 = -2.85899827713502369474065508674e0 a129 = -8.87285693353062954433549289258e0 a1210 = 1.23605671757943030647266201528e1 a1211 = 6.43392746015763530355970484046e-1 a141 = 5.61675022830479523392909219681e-2 a147 = 2.53500210216624811088794765333e-1 a148 = -2.46239037470802489917441475441e-1 a149 = -1.24191423263816360469010140626e-1 a1410 = 1.5329179827876569731206322685e-1 a1411 = 8.20105229563468988491666602057e-3 a1412 = 7.56789766054569976138603589584e-3 a1413 = -8.298e-3 a151 = 3.18346481635021405060768473261e-2 a156 = 2.83009096723667755288322961402e-2 a157 = 5.35419883074385676223797384372e-2 a158 = -5.49237485713909884646569340306e-2 a1511 = -1.08347328697249322858509316994e-4 a1512 = 3.82571090835658412954920192323e-4 a1513 = -3.40465008687404560802977114492e-4 a1514 = 1.41312443674632500278074618366e-1 a161 = -4.28896301583791923408573538692e-1 a166 = -4.69762141536116384314449447206e0 a167 = 7.68342119606259904184240953878e0 a168 = 4.06898981839711007970213554331e0 a169 = 3.56727187455281109270669543021e-1 a1613 = -1.39902416515901462129418009734e-3 a1614 = 2.9475147891527723389556272149e0 a1615 = -9.15095847217987001081870187138e0 # Final assembly coefficients b1 = 5.42937341165687622380535766363e-2 b6 = 4.45031289275240888144113950566 b7 = 1.89151789931450038304281599044 b8 = -5.8012039600105847814672114227 b9 = 3.1116436695781989440891606237e-1 b10 = -1.52160949662516078556178806805e-1 b11 = 2.01365400804030348374776537501e-1 b12 = 4.47106157277725905176885569043e-2 bhh1 = 0.244094488188976377952755905512 bhh2 = 0.733846688281611857341361741547 bhh3 = 0.220588235294117647058823529412e-1 # Dense output coefficients d41 = -0.84289382761090128651353491142e+1 d46 = 0.56671495351937776962531783590 d47 = -0.30689499459498916912797304727e+1 d48 = 0.23846676565120698287728149680e+1 d49 = 0.21170345824450282767155149946e+1 d410 = -0.87139158377797299206789907490 d411 = 0.22404374302607882758541771650e+1 d412 = 0.63157877876946881815570249290 d413 = -0.88990336451333310820698117400e-1 d414 = 0.18148505520854727256656404962e+2 d415 = -0.91946323924783554000451984436e+1 d416 = -0.44360363875948939664310572000e+1 d51 = 0.10427508642579134603413151009e+2 d56 = 0.24228349177525818288430175319e+3 d57 = 0.16520045171727028198505394887e+3 d58 = -0.37454675472269020279518312152e+3 d59 = -0.22113666853125306036270938578e+2 d510 = 0.77334326684722638389603898808e+1 d511 = -0.30674084731089398182061213626e+2 d512 = -0.93321305264302278729567221706e+1 d513 = 0.15697238121770843886131091075e+2 d514 = -0.31139403219565177677282850411e+2 d515 = -0.93529243588444783865713862664e+1 d516 = 0.35816841486394083752465898540e+2 d61 = 0.19985053242002433820987653617e+2 d66 = -0.38703730874935176555105901742e+3 d67 = -0.18917813819516756882830838328e+3 d68 = 0.52780815920542364900561016686e+3 d69 = -0.11573902539959630126141871134e+2 d610 = 0.68812326946963000169666922661e+1 d611 = -0.10006050966910838403183860980e+1 d612 = 0.77771377980534432092869265740 d613 = -0.27782057523535084065932004339e+1 d614 = -0.60196695231264120758267380846e+2 d615 = 0.84320405506677161018159903784e+2 d616 = 0.11992291136182789328035130030e+2 d71 = -0.25693933462703749003312586129e+2 d76 = -0.15418974869023643374053993627e+3 d77 = -0.23152937917604549567536039109e+3 d78 = 0.35763911791061412378285349910e+3 d79 = 0.93405324183624310003907691704e+2 d710 = -0.37458323136451633156875139351e+2 d711 = 0.10409964950896230045147246184e+3 d712 = 0.29840293426660503123344363579e+2 d713 = -0.43533456590011143754432175058e+2 d714 = 0.96324553959188282948394950600e+2 d715 = -0.39177261675615439165231486172e+2 d716 = -0.14972683625798562581422125276e+3 # Error calculation coefficients er1 = 0.1312004499419488073250102996e-1 er6 = -0.1225156446376204440720569753e+1 er7 = -0.4957589496572501915214079952 er8 = 0.1664377182454986536961530415e+1 er9 = -0.3503288487499736816886487290 er10 = 0.3341791187130174790297318841 er11 = 0.8192320648511571246570742613e-1 er12 = -0.2235530786388629525884427845e-1 # machine limit related info from numpy unsigned_int_max = numpy.iinfo(numpy.int32).max uround = numpy.finfo(numpy.float).eps def custom_sign(a, b): return numpy.fabs(a) if b > 0.0 else -numpy.fabs(a) def hinit(func, x, t, pos_neg, f0, iord, hmax, rtol, atol, args): """ Estimate initial step size """ sk = atol + rtol * numpy.fabs(x) dnf = numpy.sum(numpy.square(f0 / sk), axis=0) dny = numpy.sum(numpy.square(x / sk), axis=0) h = numpy.sqrt(dny / dnf) * 0.01 h = numpy.min([h, numpy.fabs(hmax)]) h = custom_sign(h, pos_neg) # perform an explicit Euler step xx1 = x + h * f0 f1 = numpy.array(func(xx1, t[0] + h, *args)) # estimate the second derivative of the solution der2 = numpy.sum(numpy.square((f1 - f0) / sk), axis=0) der2 = numpy.sqrt(der2) / h # step size is computed such that h ** iord * max_d(norm(f0), norm(der2)) = 0.01 der12 = numpy.max([numpy.fabs(der2), numpy.sqrt(dnf)]) h1 = numpy.power(0.01 / der12, 1.0 / iord) h = numpy.min([100.0 * numpy.fabs(h), numpy.min([numpy.fabs(h1), numpy.fabs(hmax)])]) return custom_sign(h, pos_neg), f0, f1, xx1 def dense_output(t_current, t_old, h_current, rcont): """ Dense output function, basically extrapolatin """ # initialization s = (t_current - t_old) / h_current s1 = 1.0 - s return rcont[0] + s * (rcont[1] + s1 * ( rcont[2] + s * (rcont[3] + s1 * (rcont[4] + s * (rcont[5] + s1 * (rcont[6] + s * rcont[7])))))) def dopri853core(n, func, x, t, hmax, h, rtol, atol, nmax, safe, beta, fac1, fac2, pos_neg, args): """ Core of DOP8(5, 3) integration """ # array to store the result result = numpy.zeros((len(t), n)) # initial preparations facold = 1.0e-4 expo1 = 1.0 / 8.0 - beta * 0.2 facc1 = 1.0 / fac1 facc2 = 1.0 / fac2 k1 = numpy.array(func(x, t[0], *args)) hmax = numpy.fabs(hmax) iord = 8 if h == 0.0: # estimate initial time step h, k1, k2, k3 = hinit(func, x, t, pos_neg, k1, iord, hmax, rtol, atol, args) reject = 0 t_current = t[0] # store current integration time internally (not the current time wanted by user!!) t_old = t[0] finished_user_t_ii = 0 # times indices wanted by user result[0, :] = x # basic integration step while finished_user_t_ii < len(t) - 1: # check if the current computed time indices less than total inices needed # keep time step not too small h = pos_neg * numpy.max([numpy.fabs(h), 1e3 * uround]) # the twelve stages xx1 = x + h * a21 * k1 k2 = numpy.array(func(xx1, t_current + c2 * h, *args)) xx1 = x + h * (a31 * k1 + a32 * k2) k3 = numpy.array(func(xx1, t_current + c3 * h, *args)) xx1 = x + h * (a41 * k1 + a43 * k3) k4 = numpy.array(func(xx1, t_current + c4 * h, *args)) xx1 = x + h * (a51 * k1 + a53 * k3 + a54 * k4) k5 = numpy.array(func(xx1, t_current + c5 * h, *args)) xx1 = x + h * (a61 * k1 + a64 * k4 + a65 * k5) k6 = numpy.array(func(xx1, t_current + c6 * h, *args)) xx1 = x + h * (a71 * k1 + a74 * k4 + a75 * k5 + a76 * k6) k7 = numpy.array(func(xx1, t_current + c7 * h, *args)) xx1 = x + h * (a81 * k1 + a84 * k4 + a85 * k5 + a86 * k6 + a87 * k7) k8 = numpy.array(func(xx1, t_current + c8 * h, *args)) xx1 = x + h * (a91 * k1 + a94 * k4 + a95 * k5 + a96 * k6 + a97 * k7 + a98 * k8) k9 = numpy.array(func(xx1, t_current + c9 * h, *args)) xx1 = x + h * (a101 * k1 + a104 * k4 + a105 * k5 + a106 * k6 + a107 * k7 + a108 * k8 + a109 * k9) k10 = numpy.array(func(xx1, t_current + c10 * h, *args)) xx1 = x + h * (a111 * k1 + a114 * k4 + a115 * k5 + a116 * k6 + a117 * k7 + a118 * k8 + a119 * k9 + a1110 * k10) k2 = numpy.array(func(xx1, t_current + c11 * h, *args)) xx1 = x + h * ( a121 * k1 + a124 * k4 + a125 * k5 + a126 * k6 + a127 * k7 + a128 * k8 + a129 * k9 + a1210 * k10 + a1211 * k2) t_old_older = numpy.copy(t_old) t_old = numpy.copy(t_current) t_current += h k3 = numpy.array(func(xx1, t_current, *args)) k4 = b1 * k1 + b6 * k6 + b7 * k7 + b8 * k8 + b9 * k9 + b10 * k10 + b11 * k2 + b12 * k3 k5 = x + h * k4 # error estimation sk = atol + rtol * numpy.max([numpy.fabs(x), numpy.fabs(k5)], axis=0) erri = k4 - bhh1 * k1 - bhh2 * k9 - bhh3 * k3 err2 = numpy.sum(numpy.square(erri / sk), axis=0) erri = er1 * k1 + er6 * k6 + er7 * k7 + er8 * k8 + er9 * k9 + er10 * k10 + er11 * k2 + er12 * k3 err = numpy.sum(numpy.square(erri / sk), axis=0) deno = err + 0.01 * err2 deno = 1.0 if deno <= 0.0 else deno err = numpy.fabs(h) * err * numpy.sqrt(1.0 / (deno * n)) # computation of hnew fac11 = numpy.power(err, expo1) # Lund-stabilization fac = fac11 / pow(facold, beta) # we require fac1 <= hnew / h <= fac2 fac = numpy.max([facc2, numpy.min([facc1, fac / safe])]) hnew = h / fac if err <= 1.0: # step accepted facold = numpy.max([err, 1.0e-4]) k4 = numpy.array(func(k5, t_current, *args)) # final preparation for dense output rcont1 = numpy.copy(x) xdiff = k5 - x rcont2 = xdiff bspl = h * k1 - xdiff rcont3 = numpy.copy(bspl) rcont4 = xdiff - h * k4 - bspl rcont5 = d41 * k1 + d46 * k6 + d47 * k7 + d48 * k8 + d49 * k9 + d410 * k10 + d411 * k2 + d412 * k3 rcont6 = d51 * k1 + d56 * k6 + d57 * k7 + d58 * k8 + d59 * k9 + d510 * k10 + d511 * k2 + d512 * k3 rcont7 = d61 * k1 + d66 * k6 + d67 * k7 + d68 * k8 + d69 * k9 + d610 * k10 + d611 * k2 + d612 * k3 rcont8 = d71 * k1 + d76 * k6 + d77 * k7 + d78 * k8 + d79 * k9 + d710 * k10 + d711 * k2 + d712 * k3 # the next three function evaluations xx1 = x + h * ( a141 * k1 + a147 * k7 + a148 * k8 + a149 * k9 + a1410 * k10 + a1411 * k2 + a1412 * k3 + a1413 * k4) k10 = numpy.array(func(xx1, t_old + c14 * h, *args)) xx1 = x + h * ( a151 * k1 + a156 * k6 + a157 * k7 + a158 * k8 + a1511 * k2 + a1512 * k3 + a1513 * k4 + a1514 * k10) k2 = numpy.array(func(xx1, t_old + c15 * h, *args)) xx1 = x + h * ( a161 * k1 + a166 * k6 + a167 * k7 + a168 * k8 + a169 * k9 + a1613 * k4 + a1614 * k10 + a1615 * k2) k3 = numpy.array(func(xx1, t_old + c16 * h, *args)) # final preparation rcont5 = h * (rcont5 + d413 * k4 + d414 * k10 + d415 * k2 + d416 * k3) rcont6 = h * (rcont6 + d513 * k4 + d514 * k10 + d515 * k2 + d516 * k3) rcont7 = h * (rcont7 + d613 * k4 + d614 * k10 + d615 * k2 + d616 * k3) rcont8 = h * (rcont8 + d713 * k4 + d714 * k10 + d715 * k2 + d716 * k3) k1 = numpy.copy(k4) x = numpy.copy(k5) # loop for dense output in this time slot while (finished_user_t_ii < len(t) - 1) and (pos_neg * t[finished_user_t_ii + 1] < pos_neg * t_current): result[finished_user_t_ii + 1, :] = dense_output(t[finished_user_t_ii + 1], t_old, h, [rcont1, rcont2, rcont3, rcont4, rcont5, rcont6, rcont7, rcont8]) finished_user_t_ii += 1 if numpy.fabs(hnew) > hmax: hnew = pos_neg * hmax if reject: hnew = pos_neg * numpy.min([numpy.fabs(hnew), numpy.fabs(h)]) reject = 0 else: # step rejected since error too big hnew = h / numpy.min([facc1, fac11 / safe]) reject = 1 # reverse time increment since error rejected t_current = numpy.copy(t_old) t_old = numpy.copy(t_old_older) h = numpy.copy(hnew) # current h return result def dop853(func=None, x=None, t=None, hmax=0., h=0., rtol=1e-12, atol=1e-12, nmax=int(1e8), safe=0.9, beta=0., fac1=0.333, fac2=6., args=() ): """ To computes the numerical solution of a system of first order ordinary differential equations y'=f(x,y). It uses an explicit Runge-Kutta method of order 8(5,3) due to Dormand & Prince with step size control and dense output. Reference: E. Hairer, S.P. Norsett and G. Wanner, Solving ordinary differential equations I, non-stiff problems, 2nd edition, Springer Series in Computational Mathematics, Springer-Verlag (1993). :param func: function of the differential equation, usually take func([position, velocity], time) and return velocity, acceleration :type func: callable :param x: initial x, usually is [position, velocity] :type x: Union[float, ndarray] :param t: set of times at which one wants the result :type t: ndarray :param hmax: Maximal step size, default 0. which will be internally as t[-1]-t[0] :type hmax: float :param h: Initial step size, default 0. which will be computed by the function hinit() :type h: float :param rtol: Relative error tolerances, default 1e-8 :type rtol: Union[float, ndarray] :param atol: Absolute error tolerances, default 1e-8 :type atol: Union[float, ndarray] :param nmax: Maximum number of steps, default 1e8 :type nmax: int :param safe: Safety factor in the step size prediction, default 0.9 :type atol: float :param beta: | The "beta" for stabilized step size control (see section IV.2 of the reference). Larger values for beta | (e.g.<= 0.1) make the step size control more stable. Default beta=0. :type beta: float :param fac1, fac2: Parameters for step size selection; the new step size is chosen subject to the restriction fac1 <= hnew/hold <= fac2 :type fac1, fac2: float :return: integrated result :rtype: ndarray :History: 2018-Nov-23 - Written - Henry Leung (University of Toronto) """ # initialization n = len(x) # maximal step size, default a big one if hmax == 0.0: hmax = t[-1] - t[0] # see if integrate forward or integrate backward pos_neg = custom_sign(1., t[-1] - t[0]) result = dopri853core(n, func, x, t, hmax, h, rtol, atol, nmax, safe, beta, fac1, fac2, pos_neg, args) return result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/multi.py0000644000175100001710000001677414327773303016061 0ustar00runnerdocker#Brian Refsdal's parallel_map, from astropython.org #Not sure what license this is released under, but until I know better: # #Copyright (c) 2010, Brian Refsdal #All rights reserved. # #Redistribution and use in source and binary forms, with or without #modification, are permitted provided that the following conditions are #met: # #1. Redistributions of source code must retain the above copyright #notice, this list of conditions and the following disclaimer. # #2. Redistributions in binary form must reproduce the above copyright #notice, this list of conditions and the following disclaimer in the #documentation and/or other materials provided with the distribution. # #3. The name of the author may not be used to endorse or promote #products derived from this software without specific prior written #permission. # #THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS #"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT #LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR #A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT #HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, #SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT #LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, #DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY #THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT #(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE #OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import platform import numpy _multi=False _ncpus=1 try: # May raise ImportError import multiprocessing _multi=True # May raise NotImplementedError _ncpus = multiprocessing.cpu_count() except: pass try: import tqdm _TQDM_LOADED= True except ImportError: #pragma: no cover _TQDM_LOADED= False __all__ = ('parallel_map',) def worker(f, ii, chunk, out_q, err_q, lock, progressbar, iter_elapsed, tot_iter, pbar_proc): """ A worker function that maps an input function over a slice of the input iterable. :param f : callable function that accepts argument from iterable :param ii : process ID :param chunk: slice of input iterable :param out_q: thread-safe output queue :param err_q: thread-safe queue to populate on exception :param lock : thread-safe lock to protect a resource ( useful in extending parallel_map() ) :param progressbar: if True, display a progress bar :param iter_elapsed: shared-memory Value to track progress :param tot_iter: total number of iterations (for progressbar) :param pbar_proc: process to use to display the progressbar """ vals = [] progressbar*= _TQDM_LOADED if progressbar and ii == pbar_proc: pbar= tqdm.tqdm(total=tot_iter,leave=False) # iterate over slice for val in chunk: try: result = f(val) except Exception as e: err_q.put(e) return # Update progress bar; only update in first process, accumulate in others if progressbar: if ii == pbar_proc: pbar.update(iter_elapsed.value+1) iter_elapsed.value= 0 else: iter_elapsed.value+= 1 vals.append(result) if progressbar and ii == pbar_proc: pbar.close() # output the result and task ID to output queue out_q.put( (ii, vals) ) def run_tasks(procs, err_q, out_q, num): """ A function that executes populated processes and processes the resultant array. Checks error queue for any exceptions. :param procs: list of Process objects :param out_q: thread-safe output queue :param err_q: thread-safe queue to populate on exception :param num : length of resultant array """ # function to terminate processes that are still running. die = (lambda vals : [val.terminate() for val in vals if val.exitcode is None]) try: for proc in procs: proc.start() for proc in procs: proc.join() except Exception as e: # kill all slave processes on ctrl-C try: die(procs) finally: raise e if not err_q.empty(): # kill all on any exception from any one slave try: die(procs) finally: raise err_q.get() # Processes finish in arbitrary order. Process IDs double # as index in the resultant array. results=[None]*num; while not out_q.empty(): idx, result = out_q.get() results[idx] = result # Remove extra dimension added by array_split return list(numpy.concatenate(results)) def parallel_map(function, sequence, numcores=None, progressbar=False): """ A parallelized version of the native Python map function that utilizes the Python multiprocessing module to divide and conquer sequence. parallel_map does not yet support multiple argument sequences. :param function: callable function that accepts argument from iterable :param sequence: iterable sequence :param numcores: number of cores to use :param progressbar: if True, display a progressbar using tqdm """ if not callable(function): raise TypeError("input function '%s' is not callable" % repr(function)) if not numpy.iterable(sequence): raise TypeError("input '%s' is not iterable" % repr(sequence)) size = len(sequence) if not _multi or size == 1: return map(function, sequence) if numcores is None: numcores = _ncpus if platform.system() == 'Windows': # JB: don't think this works on Win return list(map(function,sequence)) # Use fork-based parallelism (because spawn fails with pickling issues, #457) ctx= multiprocessing.get_context('fork') # Returns a started SyncManager object which can be used for sharing # objects between processes. The returned manager object corresponds # to a spawned child process and has methods which will create shared # objects and return corresponding proxies. manager = ctx.Manager() # Create FIFO queue and lock shared objects and return proxies to them. # The managers handles a server process that manages shared objects that # each slave process has access to. Bottom line -- thread-safe. out_q = manager.Queue() err_q = manager.Queue() lock = manager.Lock() # if sequence is less than numcores, only use len sequence number of # processes if size < numcores: numcores = size # group sequence into numcores-worth of chunks sequence = numpy.array_split(sequence, numcores) # For progressbar: shared-memory variable to track iterations elapsed # in non-displaying processes iter_elapsed= multiprocessing.Value('i',0) procs = [ctx.Process(target=worker, args=(function, ii, chunk, out_q, err_q, lock, progressbar,iter_elapsed,size,numcores-1)) for ii, chunk in enumerate(sequence)] return run_tasks(procs, err_q, out_q, numcores) if __name__ == "__main__": """ Unit test of parallel_map() Create an arbitrary length list of references to a single matrix containing random floats and compute the eigenvals in serial and parallel. Compare the results and timings. """ import time numtasks = 5 #size = (1024,1024) size = (512,512) vals = numpy.random.rand(*size) f = numpy.linalg.eigvals iterable = [vals]*numtasks print ('Running numpy.linalg.eigvals %iX on matrix size [%i,%i]' % (numtasks,size[0],size[1])) tt = time.time() presult = parallel_map(f, iterable) print('parallel map in %g secs' % (time.time()-tt)) tt = time.time() result = map(f, iterable) print('serial map in %g secs' % (time.time()-tt)) assert (numpy.asarray(result) == numpy.asarray(presult)).all() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/plot.py0000644000175100001710000011504414327773303015673 0ustar00runnerdocker############################################################################## # # plot.py: general wrappers for matplotlib plotting # # 'public' methods: # end_print # dens2d # hist # plot # start_print # scatterplot (like hogg_scatterplot) # text # # this module also defines a custom matplotlib # projection in which the polar azimuth increases # clockwise (as in, the Galaxy viewed from the NGP) # ############################################################################# ############################################################################# #Copyright (c) 2010 - 2020, Jo Bovy #All rights reserved. # #Redistribution and use in source and binary forms, with or without #modification, are permitted provided that the following conditions are met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # #THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS #"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT #LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR #A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT #HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, #INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, #BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS #OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED #AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT #LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY #WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE #POSSIBILITY OF SUCH DAMAGE. ############################################################################# import re import matplotlib import matplotlib.cm as cm import matplotlib.pyplot as pyplot import matplotlib.ticker as ticker import numpy from matplotlib import rc from matplotlib.projections import PolarAxes, register_projection from matplotlib.ticker import NullFormatter from matplotlib.transforms import Affine2D, Bbox, IdentityTransform from mpl_toolkits.mplot3d import \ Axes3D # Necessary for 3D plotting (projection = '3d') from packaging.version import parse as parse_version from scipy import interpolate, ndimage, special _MPL_VERSION= parse_version(matplotlib.__version__) from ..util.config import __config__ if __config__.getboolean('plot','seaborn-bovy-defaults'): try: import seaborn as sns except: pass else: sns.set_style('ticks', {'xtick.direction': 'in', 'ytick.direction': 'in', 'axes.labelsize': 18.0, 'axes.titlesize': 18.0, 'figure.figsize': numpy.array([ 6.64, 4. ]), 'grid.linewidth': 2.0, 'legend.fontsize': 18.0, 'lines.linewidth': 2.0, 'lines.markeredgewidth': 0.0, 'lines.markersize': 14.0, 'patch.linewidth': 0.6, 'xtick.labelsize': 16.0, 'xtick.major.pad': 14.0, 'xtick.major.width': 2.0, 'xtick.minor.width': 1.0, 'ytick.labelsize': 16.0, 'ytick.major.pad': 14.0, 'ytick.major.width': 2.0,}) _DEFAULTNCNTR= 10 def end_print(filename,**kwargs): """ NAME: end_print PURPOSE: saves the current figure(s) to filename INPUT: filename - filename for plot (with extension) OPTIONAL INPUTS: format - file-format OUTPUT: (none) HISTORY: 2009-12-23 - Written - Bovy (NYU) """ if 'format' in kwargs: pyplot.savefig(filename,**kwargs) else: pyplot.savefig(filename,format=re.split(r'\.',filename)[-1],**kwargs) pyplot.close() def hist(x,xlabel=None,ylabel=None,overplot=False,**kwargs): """ NAME: hist PURPOSE: wrapper around matplotlib's hist function INPUT: x - array to histogram xlabel - (raw string!) x-axis label, LaTeX math mode, no $s needed ylabel - (raw string!) y-axis label, LaTeX math mode, no $s needed yrange - set the y-axis range +all pyplot.hist keywords OUTPUT: (from the matplotlib docs: http://matplotlib.sourceforge.net/api/pyplot_api.html#matplotlib.pyplot.hist) The return value is a tuple (n, bins, patches) or ([n0, n1, ...], bins, [patches0, patches1,...]) if the input contains multiple data HISTORY: 2009-12-23 - Written - Bovy (NYU) """ if not overplot: pyplot.figure() if 'xrange' in kwargs: xlimits= kwargs.pop('xrange') if not 'range' in kwargs: kwargs['range']= xlimits xrangeSet= True else: xrangeSet= False if 'yrange' in kwargs: ylimits= kwargs.pop('yrange') yrangeSet= True else: yrangeSet= False out= pyplot.hist(x,**kwargs) if overplot: return out _add_axislabels(xlabel,ylabel) if not 'range' in kwargs and not xrangeSet: if isinstance(x,list): xlimits=(numpy.array(x).min(),numpy.array(x).max()) else: pyplot.xlim(x.min(),x.max()) elif xrangeSet: pyplot.xlim(xlimits) else: pyplot.xlim(kwargs['range']) if yrangeSet: pyplot.ylim(ylimits) _add_ticks() return out def plot(*args,**kwargs): """ NAME: plot PURPOSE: wrapper around matplotlib's plot function INPUT: see http://matplotlib.sourceforge.net/api/pyplot_api.html#matplotlib.pyplot.plot xlabel - (raw string!) x-axis label, LaTeX math mode, no $s needed ylabel - (raw string!) y-axis label, LaTeX math mode, no $s needed xrange yrange scatter= if True, use pyplot.scatter and its options etc. colorbar= if True, and scatter==True, add colorbar crange - range for colorbar of scatter==True clabel= label for colorbar overplot=True does not start a new figure and does not change the ranges and labels gcf=True does not start a new figure (does change the ranges and labels) onedhists - if True, make one-d histograms on the sides onedhistcolor, onedhistfc, onedhistec onedhistxnormed, onedhistynormed - normed keyword for one-d histograms onedhistxweights, onedhistyweights - weights keyword for one-d histograms bins= number of bins for onedhists semilogx=, semilogy=, loglog= if True, plot logs OUTPUT: plot to output device, returns what pyplot.plot returns, or 3 Axes instances if onedhists=True HISTORY: 2009-12-28 - Written - Bovy (NYU) """ overplot= kwargs.pop('overplot',False) gcf= kwargs.pop('gcf',False) onedhists= kwargs.pop('onedhists',False) scatter= kwargs.pop('scatter',False) loglog= kwargs.pop('loglog',False) semilogx= kwargs.pop('semilogx',False) semilogy= kwargs.pop('semilogy',False) colorbar= kwargs.pop('colorbar',False) onedhisttype= kwargs.pop('onedhisttype','step') onedhistcolor= kwargs.pop('onedhistcolor','k') onedhistfc= kwargs.pop('onedhistfc','w') onedhistec= kwargs.pop('onedhistec','k') onedhistxnormed= kwargs.pop('onedhistxnormed',True) onedhistynormed= kwargs.pop('onedhistynormed',True) onedhistxweights= kwargs.pop('onedhistxweights',None) onedhistyweights= kwargs.pop('onedhistyweights',None) if 'bins' in kwargs: bins= kwargs['bins'] kwargs.pop('bins') elif onedhists: if isinstance(args[0],numpy.ndarray): bins= round(0.3*numpy.sqrt(args[0].shape[0])) elif isinstance(args[0],list): bins= round(0.3*numpy.sqrt(len(args[0]))) else: bins= 30 if onedhists: if overplot or gcf: fig= pyplot.gcf() else: fig= pyplot.figure() nullfmt = NullFormatter() # no labels # definitions for the axes left, width = 0.1, 0.65 bottom, height = 0.1, 0.65 bottom_h = left_h = left+width rect_scatter = [left, bottom, width, height] rect_histx = [left, bottom_h, width, 0.2] rect_histy = [left_h, bottom, 0.2, height] axScatter = pyplot.axes(rect_scatter) axHistx = pyplot.axes(rect_histx) axHisty = pyplot.axes(rect_histy) # no labels axHistx.xaxis.set_major_formatter(nullfmt) axHistx.yaxis.set_major_formatter(nullfmt) axHisty.xaxis.set_major_formatter(nullfmt) axHisty.yaxis.set_major_formatter(nullfmt) fig.sca(axScatter) elif not overplot and not gcf: pyplot.figure() ax=pyplot.gca() ax.set_autoscale_on(False) xlabel= kwargs.pop('xlabel',None) ylabel= kwargs.pop('ylabel',None) clabel= kwargs.pop('clabel',None) xlimits= kwargs.pop('xrange',None) if xlimits is None: if isinstance(args[0],list): xlimits=(numpy.array(args[0]).min(),numpy.array(args[0]).max()) else: xlimits=(args[0].min(),args[0].max()) ylimits= kwargs.pop('yrange',None) if ylimits is None: if isinstance(args[1],list): ylimits=(numpy.array(args[1]).min(),numpy.array(args[1]).max()) else: ylimits=(args[1].min(),args[1].max()) climits= kwargs.pop('crange',None) if climits is None and scatter: if 'c' in kwargs and isinstance(kwargs['c'],list): climits=(numpy.array(kwargs['c']).min(),numpy.array(kwargs['c']).max()) elif 'c' in kwargs: climits=(kwargs['c'].min(),kwargs['c'].max()) else: climits= None if scatter: out= pyplot.scatter(*args,**kwargs) elif loglog: out= pyplot.loglog(*args,**kwargs) elif semilogx: out= pyplot.semilogx(*args,**kwargs) elif semilogy: out= pyplot.semilogy(*args,**kwargs) else: out= pyplot.plot(*args,**kwargs) if overplot: pass else: if semilogy: ax= pyplot.gca() ax.set_yscale('log') elif semilogx: ax= pyplot.gca() ax.set_xscale('log') elif loglog: ax= pyplot.gca() ax.set_xscale('log') ax.set_yscale('log') pyplot.xlim(*xlimits) pyplot.ylim(*ylimits) _add_axislabels(xlabel,ylabel) if not semilogy and not semilogx and not loglog: _add_ticks() elif semilogy: _add_ticks(xticks=True,yticks=False) elif semilogx: _add_ticks(yticks=True,xticks=False) #Add colorbar if colorbar: cbar= pyplot.colorbar(out,fraction=0.15) if _MPL_VERSION < parse_version('3.1'): # pragma: no cover # https://matplotlib.org/3.1.0/api/api_changes.html#colorbarbase-inheritance cbar.set_clim(*climits) else: cbar.mappable.set_clim(*climits) if not clabel is None: cbar.set_label(clabel) #Add onedhists if not onedhists: return out histx, edges, patches= axHistx.hist(args[0], bins=bins, normed=onedhistxnormed, weights=onedhistxweights, histtype=onedhisttype, range=sorted(xlimits), color=onedhistcolor,fc=onedhistfc, ec=onedhistec) histy, edges, patches= axHisty.hist(args[1], bins=bins, orientation='horizontal', weights=onedhistyweights, normed=onedhistynormed, histtype=onedhisttype, range=sorted(ylimits), color=onedhistcolor,fc=onedhistfc, ec=onedhistec) axHistx.set_xlim( axScatter.get_xlim() ) axHisty.set_ylim( axScatter.get_ylim() ) axHistx.set_ylim( 0, 1.2*numpy.amax(histx)) axHisty.set_xlim( 0, 1.2*numpy.amax(histy)) return (axScatter,axHistx,axHisty) def plot3d(*args,**kwargs): """ NAME: plot3d PURPOSE: plot in 3d much as in 2d INPUT: see http://matplotlib.sourceforge.net/api/pyplot_api.html#matplotlib.pyplot.plot xlabel - (raw string!) x-axis label, LaTeX math mode, no $s needed ylabel - (raw string!) y-axis label, LaTeX math mode, no $s needed xrange yrange overplot=True does not start a new figure OUTPUT: HISTORY: 2011-01-08 - Written - Bovy (NYU) """ overplot= kwargs.pop('overplot',False) if not overplot: pyplot.figure() ax= pyplot.gcf().add_subplot(projection='3d') ax.set_autoscale_on(False) xlabel= kwargs.pop('xlabel',None) ylabel= kwargs.pop('ylabel',None) zlabel= kwargs.pop('zlabel',None) if 'xrange' in kwargs: xlimits= kwargs.pop('xrange') else: if isinstance(args[0],list): xlimits=(numpy.array(args[0]).min(),numpy.array(args[0]).max()) else: xlimits=(args[0].min(),args[0].max()) if 'yrange' in kwargs: ylimits= kwargs.pop('yrange') else: if isinstance(args[1],list): ylimits=(numpy.array(args[1]).min(),numpy.array(args[1]).max()) else: ylimits=(args[1].min(),args[1].max()) if 'zrange' in kwargs: zlimits= kwargs.pop('zrange') else: if isinstance(args[2],list): zlimits=(numpy.array(args[2]).min(),numpy.array(args[2]).max()) else: zlimits=(args[1].min(),args[2].max()) out= pyplot.plot(*args,**kwargs) if overplot: pass else: if xlabel != None: if xlabel[0] != '$': thisxlabel=r'$'+xlabel+'$' else: thisxlabel=xlabel ax.set_xlabel(thisxlabel) if ylabel != None: if ylabel[0] != '$': thisylabel=r'$'+ylabel+'$' else: thisylabel=ylabel ax.set_ylabel(thisylabel) if zlabel != None: if zlabel[0] != '$': thiszlabel=r'$'+zlabel+'$' else: thiszlabel=zlabel ax.set_zlabel(thiszlabel) ax.set_xlim3d(*xlimits) ax.set_ylim3d(*ylimits) ax.set_zlim3d(*zlimits) return out def dens2d(X,**kwargs): """ NAME: dens2d PURPOSE: plot a 2d density with optional contours INPUT: first argument is the density matplotlib.pyplot.imshow keywords (see http://matplotlib.sourceforge.net/api/axes_api.html#matplotlib.axes.Axes.imshow) xlabel - (raw string!) x-axis label, LaTeX math mode, no $s needed ylabel - (raw string!) y-axis label, LaTeX math mode, no $s needed xrange yrange noaxes - don't plot any axes overplot - if True, overplot colorbar - if True, add colorbar shrink= colorbar argument: shrink the colorbar by the factor (optional) conditional - normalize each column separately (for probability densities, i.e., cntrmass=True) gcf=True does not start a new figure (does change the ranges and labels) Contours: justcontours - if True, only draw contours contours - if True, draw contours (10 by default) levels - contour-levels cntrmass - if True, the density is a probability and the levels are probability masses contained within the contour cntrcolors - colors for contours (single color or array) cntrlabel - label the contours cntrlw, cntrls - linewidths and linestyles for contour cntrlabelsize, cntrlabelcolors,cntrinline - contour arguments cntrSmooth - use ndimage.gaussian_filter to smooth before contouring onedhists - if True, make one-d histograms on the sides onedhistcolor - histogram color retAxes= return all Axes instances retCont= return the contour instance OUTPUT: plot to output device, Axes instances depending on input HISTORY: 2010-03-09 - Written - Bovy (NYU) """ overplot= kwargs.pop('overplot',False) gcf= kwargs.pop('gcf',False) if not overplot and not gcf: pyplot.figure() xlabel= kwargs.pop('xlabel',None) ylabel= kwargs.pop('ylabel',None) zlabel= kwargs.pop('zlabel',None) if 'extent' in kwargs: extent= kwargs.pop('extent') else: xlimits= kwargs.pop('xrange',[0,X.shape[1]]) ylimits= kwargs.pop('yrange',[0,X.shape[0]]) extent= xlimits+ylimits if not 'aspect' in kwargs: kwargs['aspect']= (xlimits[1]-xlimits[0])/float(ylimits[1]-ylimits[0]) noaxes= kwargs.pop('noaxes',False) justcontours= kwargs.pop('justcontours',False) if ('contours' in kwargs and kwargs['contours']) or \ 'levels' in kwargs or justcontours or \ ('cntrmass' in kwargs and kwargs['cntrmass']): contours= True else: contours= False kwargs.pop('contours',None) if 'levels' in kwargs: levels= kwargs['levels'] kwargs.pop('levels') elif contours: if 'cntrmass' in kwargs and kwargs['cntrmass']: levels= numpy.linspace(0.,1.,_DEFAULTNCNTR) elif True in numpy.isnan(numpy.array(X)): levels= numpy.linspace(numpy.nanmin(X),numpy.nanmax(X),_DEFAULTNCNTR) else: levels= numpy.linspace(numpy.amin(X),numpy.amax(X),_DEFAULTNCNTR) cntrmass= kwargs.pop('cntrmass',False) conditional= kwargs.pop('conditional',False) cntrcolors= kwargs.pop('cntrcolors','k') cntrlabel= kwargs.pop('cntrlabel',False) cntrlw= kwargs.pop('cntrlw',None) cntrls= kwargs.pop('cntrls',None) cntrSmooth= kwargs.pop('cntrSmooth',None) cntrlabelsize= kwargs.pop('cntrlabelsize',None) cntrlabelcolors= kwargs.pop('cntrlabelcolors',None) cntrinline= kwargs.pop('cntrinline',None) retCumImage= kwargs.pop('retCumImage',False) cb= kwargs.pop('colorbar',False) shrink= kwargs.pop('shrink',None) onedhists= kwargs.pop('onedhists',False) onedhistcolor= kwargs.pop('onedhistcolor','k') retAxes= kwargs.pop('retAxes',False) retCont= kwargs.pop('retCont',False) if onedhists: if overplot or gcf: fig= pyplot.gcf() else: fig= pyplot.figure() nullfmt = NullFormatter() # no labels # definitions for the axes left, width = 0.1, 0.65 bottom, height = 0.1, 0.65 bottom_h = left_h = left+width rect_scatter = [left, bottom, width, height] rect_histx = [left, bottom_h, width, 0.2] rect_histy = [left_h, bottom, 0.2, height] axScatter = pyplot.axes(rect_scatter) axHistx = pyplot.axes(rect_histx) axHisty = pyplot.axes(rect_histy) # no labels axHistx.xaxis.set_major_formatter(nullfmt) axHistx.yaxis.set_major_formatter(nullfmt) axHisty.xaxis.set_major_formatter(nullfmt) axHisty.yaxis.set_major_formatter(nullfmt) fig.sca(axScatter) ax=pyplot.gca() ax.set_autoscale_on(False) if conditional: plotthis= X/numpy.tile(numpy.sum(X,axis=0),(X.shape[1],1)) else: plotthis= X if not justcontours: out= pyplot.imshow(plotthis,extent=extent,**kwargs) if not overplot: pyplot.axis(extent) _add_axislabels(xlabel,ylabel) _add_ticks() #Add colorbar if cb and not justcontours: if shrink is None: shrink= numpy.amin([float(kwargs.pop('aspect',1.))*0.87,1.]) CB1= pyplot.colorbar(out,shrink=shrink) if not zlabel is None: if zlabel[0] != '$': thiszlabel=r'$'+zlabel+'$' else: thiszlabel=zlabel CB1.set_label(thiszlabel) if contours or retCumImage: aspect= kwargs.get('aspect',None) origin= kwargs.get('origin',None) if cntrmass: #Sum from the top down! plotthis[numpy.isnan(plotthis)]= 0. sortindx= numpy.argsort(plotthis.flatten())[::-1] cumul= numpy.cumsum(numpy.sort(plotthis.flatten())[::-1])/numpy.sum(plotthis.flatten()) cntrThis= numpy.zeros(numpy.prod(plotthis.shape)) cntrThis[sortindx]= cumul cntrThis= numpy.reshape(cntrThis,plotthis.shape) else: cntrThis= plotthis if contours: if not cntrSmooth is None: cntrThis= ndimage.gaussian_filter(cntrThis,cntrSmooth, mode='nearest') cont= pyplot.contour(cntrThis,levels,colors=cntrcolors, linewidths=cntrlw,extent=extent,aspect=aspect, linestyles=cntrls,origin=origin) if cntrlabel: pyplot.clabel(cont,fontsize=cntrlabelsize, colors=cntrlabelcolors, inline=cntrinline) if noaxes: ax.set_axis_off() #Add onedhists if not onedhists: if retCumImage: return cntrThis elif retAxes: return pyplot.gca() elif retCont: return cont elif justcontours: return cntrThis else: return out histx= numpy.nansum(X.T,axis=1)*numpy.fabs(ylimits[1]-ylimits[0])/X.shape[1] #nansum bc nan is *no dens value* histy= numpy.nansum(X.T,axis=0)*numpy.fabs(xlimits[1]-xlimits[0])/X.shape[0] histx[numpy.isnan(histx)]= 0. histy[numpy.isnan(histy)]= 0. dx= (extent[1]-extent[0])/float(len(histx)) axHistx.plot(numpy.linspace(extent[0]+dx,extent[1]-dx,len(histx)),histx, drawstyle='steps-mid',color=onedhistcolor) dy= (extent[3]-extent[2])/float(len(histy)) axHisty.plot(histy,numpy.linspace(extent[2]+dy,extent[3]-dy,len(histy)), drawstyle='steps-mid',color=onedhistcolor) axHistx.set_xlim( axScatter.get_xlim() ) axHisty.set_ylim( axScatter.get_ylim() ) axHistx.set_ylim( 0, 1.2*numpy.amax(histx)) axHisty.set_xlim( 0, 1.2*numpy.amax(histy)) if retCumImage: return cntrThis elif retAxes: return (axScatter,axHistx,axHisty) elif justcontours: return cntrThis else: return out def start_print(fig_width=5,fig_height=5,axes_labelsize=16, text_fontsize=11,legend_fontsize=12, xtick_labelsize=10,ytick_labelsize=10, xtick_minor_size=2,ytick_minor_size=2, xtick_major_size=4,ytick_major_size=4): """ NAME: start_print PURPOSE: setup a figure for plotting INPUT: fig_width - width in inches fig_height - height in inches axes_labelsize - size of the axis-labels text_fontsize - font-size of the text (if any) legend_fontsize - font-size of the legend (if any) xtick_labelsize - size of the x-axis labels ytick_labelsize - size of the y-axis labels xtick_minor_size - size of the minor x-ticks ytick_minor_size - size of the minor y-ticks OUTPUT: (none) HISTORY: 2009-12-23 - Written - Bovy (NYU) """ fig_size = [fig_width,fig_height] params = {'axes.labelsize': axes_labelsize, 'font.size': text_fontsize, 'legend.fontsize': legend_fontsize, 'xtick.labelsize':xtick_labelsize, 'ytick.labelsize':ytick_labelsize, 'text.usetex': True, 'figure.figsize': fig_size, 'xtick.major.size' : xtick_major_size, 'ytick.major.size' : ytick_major_size, 'xtick.minor.size' : xtick_minor_size, 'ytick.minor.size' : ytick_minor_size, 'legend.numpoints':1, 'xtick.top': True, 'xtick.direction': 'in', 'ytick.right': True, 'ytick.direction': 'in'} pyplot.rcParams.update(params) rc('text.latex', preamble=r'\usepackage{amsmath}'+'\n' +r'\usepackage{amssymb}') def text(*args,**kwargs): """ NAME: text PURPOSE: thin wrapper around matplotlib's text and annotate use keywords: 'bottom_left=True' 'bottom_right=True' 'top_left=True' 'top_right=True' 'title=True' to place the text in one of the corners or use it as the title INPUT: see matplotlib's text (http://matplotlib.sourceforge.net/api/pyplot_api.html#matplotlib.pyplot.text) OUTPUT: prints text on the current figure HISTORY: 2010-01-26 - Written - Bovy (NYU) """ if kwargs.pop('title',False): pyplot.annotate(args[0],(0.5,1.05),xycoords='axes fraction', horizontalalignment='center', verticalalignment='top',**kwargs) elif kwargs.pop('bottom_left',False): pyplot.annotate(args[0],(0.05,0.05),xycoords='axes fraction',**kwargs) elif kwargs.pop('bottom_right',False): pyplot.annotate(args[0],(0.95,0.05),xycoords='axes fraction', horizontalalignment='right',**kwargs) elif kwargs.pop('top_right',False): pyplot.annotate(args[0],(0.95,0.95),xycoords='axes fraction', horizontalalignment='right', verticalalignment='top',**kwargs) elif kwargs.pop('top_left',False): pyplot.annotate(args[0],(0.05,0.95),xycoords='axes fraction', verticalalignment='top',**kwargs) else: pyplot.text(*args,**kwargs) def scatterplot(x,y,*args,**kwargs): """ NAME: scatterplot PURPOSE: make a 'smart' scatterplot that is a density plot in high-density regions and a regular scatterplot for outliers INPUT: x, y xlabel - (raw string!) x-axis label, LaTeX math mode, no $s needed ylabel - (raw string!) y-axis label, LaTeX math mode, no $s needed xrange yrange bins - number of bins to use in each dimension weights - data-weights aspect - aspect ratio conditional - normalize each column separately (for probability densities, i.e., cntrmass=True) gcf=True does not start a new figure (does change the ranges and labels) contours - if False, don't plot contours justcontours - if True, only draw contours, no density cntrcolors - color of contours (can be array as for dens2d) cntrlw, cntrls - linewidths and linestyles for contour cntrSmooth - use ndimage.gaussian_filter to smooth before contouring levels - contour-levels; data points outside of the last level will be individually shown (so, e.g., if this list is descending, contours and data points will be overplotted) onedhists - if True, make one-d histograms on the sides onedhistx - if True, make one-d histograms on the side of the x distribution onedhisty - if True, make one-d histograms on the side of the y distribution onedhistcolor, onedhistfc, onedhistec onedhistxnormed, onedhistynormed - normed keyword for one-d histograms onedhistxweights, onedhistyweights - weights keyword for one-d histograms cmap= cmap for density plot hist= and edges= - you can supply the histogram of the data yourself, this can be useful if you want to censor the data, both need to be set and calculated using scipy.histogramdd with the given range retAxes= return all Axes instances OUTPUT: plot to output device, Axes instance(s) or not, depending on input HISTORY: 2010-04-15 - Written - Bovy (NYU) """ xlabel= kwargs.pop('xlabel',None) ylabel= kwargs.pop('ylabel',None) if 'xrange' in kwargs: xrange= kwargs.pop('xrange') else: if isinstance(x,list): xrange=[numpy.amin(x),numpy.amax(x)] else: xrange=[x.min(),x.max()] if 'yrange' in kwargs: yrange= kwargs.pop('yrange') else: if isinstance(y,list): yrange=[numpy.amin(y),numpy.amax(y)] else: yrange=[y.min(),y.max()] ndata= len(x) bins= kwargs.pop('bins',round(0.3*numpy.sqrt(ndata))) weights= kwargs.pop('weights',None) levels= kwargs.pop('levels',special.erf(numpy.arange(1,4)/numpy.sqrt(2.))) aspect= kwargs.pop('aspect',(xrange[1]-xrange[0])/(yrange[1]-yrange[0])) conditional= kwargs.pop('conditional',False) contours= kwargs.pop('contours',True) justcontours= kwargs.pop('justcontours',False) cntrcolors= kwargs.pop('cntrcolors','k') cntrlw= kwargs.pop('cntrlw',None) cntrls= kwargs.pop('cntrls',None) cntrSmooth= kwargs.pop('cntrSmooth',None) onedhists= kwargs.pop('onedhists',False) onedhistx= kwargs.pop('onedhistx',onedhists) onedhisty= kwargs.pop('onedhisty',onedhists) onedhisttype= kwargs.pop('onedhisttype','step') onedhistcolor= kwargs.pop('onedhistcolor','k') onedhistfc= kwargs.pop('onedhistfc','w') onedhistec= kwargs.pop('onedhistec','k') onedhistls= kwargs.pop('onedhistls','solid') onedhistlw= kwargs.pop('onedhistlw',None) onedhistsbins= kwargs.pop('onedhistsbins',round(0.3*numpy.sqrt(ndata))) overplot= kwargs.pop('overplot',False) gcf= kwargs.pop('gcf',False) cmap= kwargs.pop('cmap',cm.gist_yarg) onedhistxnormed= kwargs.pop('onedhistxnormed',True) onedhistynormed= kwargs.pop('onedhistynormed',True) onedhistxweights= kwargs.pop('onedhistxweights',weights) onedhistyweights= kwargs.pop('onedhistyweights',weights) retAxes= kwargs.pop('retAxes',False) if onedhists or onedhistx or onedhisty: if overplot or gcf: fig= pyplot.gcf() else: fig= pyplot.figure() nullfmt = NullFormatter() # no labels # definitions for the axes left, width = 0.1, 0.65 bottom, height = 0.1, 0.65 bottom_h = left_h = left+width rect_scatter = [left, bottom, width, height] rect_histx = [left, bottom_h, width, 0.2] rect_histy = [left_h, bottom, 0.2, height] axScatter = pyplot.axes(rect_scatter) if onedhistx: axHistx = pyplot.axes(rect_histx) # no labels axHistx.xaxis.set_major_formatter(nullfmt) axHistx.yaxis.set_major_formatter(nullfmt) if onedhisty: axHisty = pyplot.axes(rect_histy) # no labels axHisty.xaxis.set_major_formatter(nullfmt) axHisty.yaxis.set_major_formatter(nullfmt) fig.sca(axScatter) data= numpy.array([x,y]).T if 'hist' in kwargs and 'edges' in kwargs: hist=kwargs['hist'] kwargs.pop('hist') edges=kwargs['edges'] kwargs.pop('edges') else: hist, edges= numpy.histogramdd(data,bins=bins,range=[xrange,yrange], weights=weights) if contours: cumimage= dens2d(hist.T,contours=contours,levels=levels, cntrmass=contours,cntrSmooth=cntrSmooth, cntrcolors=cntrcolors,cmap=cmap,origin='lower', xrange=xrange,yrange=yrange,xlabel=xlabel, ylabel=ylabel,interpolation='nearest', retCumImage=True,aspect=aspect, conditional=conditional, cntrlw=cntrlw,cntrls=cntrls, justcontours=justcontours,zorder=5*justcontours, overplot=(gcf or onedhists or overplot or onedhistx or onedhisty)) else: cumimage= dens2d(hist.T,contours=contours, cntrcolors=cntrcolors, cmap=cmap,origin='lower', xrange=xrange,yrange=yrange,xlabel=xlabel, ylabel=ylabel,interpolation='nearest', conditional=conditional, retCumImage=True,aspect=aspect, cntrlw=cntrlw,cntrls=cntrls, overplot=(gcf or onedhists or overplot or onedhistx or onedhisty)) #Set axes and labels pyplot.axis(list(xrange)+list(yrange)) if not overplot: _add_axislabels(xlabel,ylabel) _add_ticks() binxs= [] xedge= edges[0] for ii in range(len(xedge)-1): binxs.append((xedge[ii]+xedge[ii+1])/2.) binxs= numpy.array(binxs) binys= [] yedge= edges[1] for ii in range(len(yedge)-1): binys.append((yedge[ii]+yedge[ii+1])/2.) binys= numpy.array(binys) cumInterp= interpolate.RectBivariateSpline(binxs,binys,cumimage.T, kx=1,ky=1) cums= [] for ii in range(len(x)): cums.append(cumInterp(x[ii],y[ii])[0,0]) cums= numpy.array(cums) plotx= x[cums > levels[-1]] ploty= y[cums > levels[-1]] if not len(plotx) == 0: if not weights == None: w8= weights[cums > levels[-1]] for ii in range(len(plotx)): plot(plotx[ii],ploty[ii],overplot=True, color='%.2f'%(1.-w8[ii]),*args,**kwargs) else: plot(plotx,ploty,overplot=True,zorder=1,*args,**kwargs) #Add onedhists if not (onedhists or onedhistx or onedhisty): if retAxes: return pyplot.gca() else: return None if onedhistx: histx, edges, patches= axHistx.hist(x,bins=onedhistsbins, normed=onedhistxnormed, weights=onedhistxweights, histtype=onedhisttype, range=sorted(xrange), color=onedhistcolor,fc=onedhistfc, ec=onedhistec,ls=onedhistls, lw=onedhistlw) if onedhisty: histy, edges, patches= axHisty.hist(y,bins=onedhistsbins, orientation='horizontal', weights=onedhistyweights, normed=onedhistynormed, histtype=onedhisttype, range=sorted(yrange), color=onedhistcolor,fc=onedhistfc, ec=onedhistec,ls=onedhistls, lw=onedhistlw) if onedhistx and not overplot: axHistx.set_xlim( axScatter.get_xlim() ) axHistx.set_ylim( 0, 1.2*numpy.amax(histx)) if onedhisty and not overplot: axHisty.set_ylim( axScatter.get_ylim() ) axHisty.set_xlim( 0, 1.2*numpy.amax(histy)) if not onedhistx: axHistx= None if not onedhisty: axHisty= None if retAxes: return (axScatter,axHistx,axHisty) else: return None def _add_axislabels(xlabel,ylabel): """ NAME: _add_axislabels PURPOSE: add axis labels to the current figure INPUT: xlabel - (raw string!) x-axis label, LaTeX math mode, no $s needed ylabel - (raw string!) y-axis label, LaTeX math mode, no $s needed OUTPUT: (none; works on the current axes) HISTORY: 2009-12-23 - Written - Bovy (NYU) """ if xlabel != None: if xlabel[0] != '$': thisxlabel=r'$'+xlabel+'$' else: thisxlabel=xlabel pyplot.xlabel(thisxlabel) if ylabel != None: if ylabel[0] != '$': thisylabel=r'$'+ylabel+'$' else: thisylabel=ylabel pyplot.ylabel(thisylabel) def _add_ticks(xticks=True,yticks=True): """ NAME: _add_ticks PURPOSE: add minor axis ticks to a plot INPUT: (none; works on the current axes) OUTPUT: (none; works on the current axes) HISTORY: 2009-12-23 - Written - Bovy (NYU) """ ax=pyplot.gca() if xticks: xstep= ax.xaxis.get_majorticklocs() xstep= xstep[1]-xstep[0] ax.xaxis.set_minor_locator(ticker.MultipleLocator(xstep/5.)) if yticks: ystep= ax.yaxis.get_majorticklocs() ystep= ystep[1]-ystep[0] ax.yaxis.set_minor_locator(ticker.MultipleLocator(ystep/5.)) class GalPolarAxes(PolarAxes): ''' A variant of PolarAxes where theta increases clockwise ''' name = 'galpolar' class GalPolarTransform(PolarAxes.PolarTransform): def transform(self, tr): xy = numpy.zeros(tr.shape, numpy.float_) t = tr[:, 0:1] r = tr[:, 1:2] x = xy[:, 0:1] y = xy[:, 1:2] x[:] = r * numpy.cos(t) y[:] = -r * numpy.sin(t) return xy transform_non_affine = transform def inverted(self): return GalPolarAxes.InvertedGalPolarTransform() class InvertedGalPolarTransform(PolarAxes.InvertedPolarTransform): def transform(self, xy): x = xy[:, 0:1] y = xy[:, 1:] r = numpy.sqrt(x*x + y*y) theta = numpy.arctan2(y, x) return numpy.concatenate((theta, r), 1) def inverted(self): return GalPolarAxes.GalPolarTransform() def _set_lim_and_transforms(self): PolarAxes._set_lim_and_transforms(self) self.transProjection = self.GalPolarTransform() self.transData = ( self.transScale + self.transProjection + (self.transProjectionAffine + self.transAxes)) self._xaxis_transform = ( self.transProjection + self.PolarAffine(IdentityTransform(), Bbox.unit()) + self.transAxes) self._xaxis_text1_transform = ( self._theta_label1_position + self._xaxis_transform) self._yaxis_transform = ( Affine2D().scale(numpy.pi * 2.0, 1.0) + self.transData) self._yaxis_text1_transform = ( self._r_label1_position + Affine2D().scale(1.0 / 360.0, 1.0) + self._yaxis_transform) register_projection(GalPolarAxes) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/quadpack.py0000644000175100001710000000124514327773303016503 0ustar00runnerdocker############################################################################### # galpy.util.quadpack: some variations on scipy's quadpack ############################################################################### from scipy.integrate import quad def _infunc(x,func,gfun,hfun,more_args,epsrel,epsabs): a = gfun(x) b = hfun(x) myargs = (x,) + more_args retval= quad(func,a,b,args=myargs,epsrel=epsrel,epsabs=epsabs) #print x, a, b, retval return retval[0] def dblquad(func, a, b, gfun, hfun, args=(), epsabs=1.49e-8, epsrel=1.49e-8): return quad(_infunc,a,b,(func,gfun,hfun,args,epsrel,epsabs), epsabs=epsabs,epsrel=epsrel) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/galpy/util/symplecticode.py0000644000175100001710000001071214327773303017555 0ustar00runnerdocker############################################################################# #Symplectic ODE integrators #Follows scipy.integrate.odeint inputs as much as possible ############################################################################# ############################################################################# #Copyright (c) 2011, Jo Bovy #All rights reserved. # #Redistribution and use in source and binary forms, with or without #modification, are permitted provided that the following conditions are met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # #THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS #"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT #LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR #A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT #HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, #INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, #BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS #OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED #AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT #LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY #WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE #POSSIBILITY OF SUCH DAMAGE. ############################################################################# import numpy _MAX_DT_REDUCE= 10000. def leapfrog(func,yo,t,args=(),rtol=1.49012e-12,atol=1.49012e-12): """ NAME: leapfrog PURPOSE: leapfrog integrate an ode INPUT: func - force function of (y,*args) yo - initial condition [q,p] t - set of times at which one wants the result rtol, atol OUTPUT: y : array, shape (len(y0), len(t)) Array containing the value of y for each desired time in t, \ with the initial value y0 in the first row. HISTORY: 2011-02-02 - Written - Bovy (NYU) """ #Initialize qo= yo[0:len(yo)//2] po= yo[len(yo)//2:len(yo)] out= numpy.zeros((len(t),len(yo))) out[0,:]= yo #Estimate necessary step size dt= t[1]-t[0] #assumes that the steps are equally spaced init_dt= dt dt= _leapfrog_estimate_step(func,qo,po,dt,t[0],args,rtol,atol) ndt= int(init_dt/dt) #Integrate to= t[0] for ii in range(1,len(t)): for jj in range(ndt): #loop over number of sub-intervals #This could be made faster by combining the drifts #drift q12= leapfrog_leapq(qo,po,dt/2.) #kick force= func(q12,*args,t=to+dt/2) po= leapfrog_leapp(po,dt,force) #drift qo= leapfrog_leapq(q12,po,dt/2.) #Get ready for next to+= dt out[ii,0:len(yo)//2]= qo out[ii,len(yo)//2:len(yo)]= po return out def leapfrog_leapq(q,p,dt): return q+dt*p def leapfrog_leapp(p,dt,force): return p+dt*force def _leapfrog_estimate_step(func,qo,po,dt,to,args,rtol,atol): init_dt= dt qmax= numpy.amax(numpy.fabs(qo))+numpy.zeros(len(qo)) pmax= numpy.amax(numpy.fabs(po))+numpy.zeros(len(po)) scale= atol+rtol*numpy.array([qmax,pmax]).flatten() err= 2. dt*= 2. while err > 1. and init_dt/dt < _MAX_DT_REDUCE: #Do one leapfrog step with step dt and one with dt/2. #dt q12= leapfrog_leapq(qo,po,dt/2.) force= func(q12,*args,t=to+dt/2) p11= leapfrog_leapp(po,dt,force) q11= leapfrog_leapq(q12,p11,dt/2.) #dt/2. q12= leapfrog_leapq(qo,po,dt/4.) force= func(q12,*args,t=to+dt/4) ptmp= leapfrog_leapp(po,dt/2.,force) qtmp= leapfrog_leapq(q12,ptmp,dt/2.)#Take full step combining two half force= func(qtmp,*args,t=to+3.*dt/4) p12= leapfrog_leapp(ptmp,dt/2.,force) q12= leapfrog_leapq(qtmp,p12,dt/4.) #Norm delta= numpy.array([numpy.fabs(q11-q12),numpy.fabs(p11-p12)]).flatten() err= numpy.sqrt(numpy.mean((delta/scale)**2.)) dt/= 2. return dt ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667233486.697467 galpy-1.8.1/galpy.egg-info/0000755000175100001710000000000014327773317015060 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233486.0 galpy-1.8.1/galpy.egg-info/PKG-INFO0000644000175100001710000001352414327773316016161 0ustar00runnerdockerMetadata-Version: 2.1 Name: galpy Version: 1.8.1 Summary: Galactic Dynamics in python Home-page: http://github.com/jobovy/galpy Author: Jo Bovy Author-email: bovy@astro.utoronto.ca License: New BSD Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: C Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Topic :: Scientific/Engineering :: Astronomy Classifier: Topic :: Scientific/Engineering :: Physics Description-Content-Type: text/markdown License-File: LICENSE License-File: AUTHORS.txt

Galactic Dynamics in python

[galpy](http://www.galpy.org) is a Python package for galactic dynamics. It supports orbit integration in a variety of potentials, evaluating and sampling various distribution functions, and the calculation of action-angle coordinates for all static potentials. `galpy` is an [astropy](http://www.astropy.org/) [affiliated package](http://www.astropy.org/affiliated/) and provides full support for astropy’s [Quantity](http://docs.astropy.org/en/stable/api/astropy.units.Quantity.html) framework for variables with units. AUTHOR ====== Jo Bovy - bovy at astro dot utoronto dot ca See [AUTHORS.txt](https://github.com/jobovy/galpy/blob/main/AUTHORS.txt) for a full list of contributors. If you find this code useful in your research, please let me know. **If you use galpy in a publication, please cite** [Bovy (2015)](http://adsabs.harvard.edu/abs/2015ApJS..216...29B) **and link to http://github.com/jobovy/galpy**. See [the acknowledgement documentation section](http://docs.galpy.org/en/latest/index.html#acknowledging-galpy) for a more detailed guide to citing parts of the code. Thanks! LOOKING FOR HELP? ================= The latest documentation can be found [here](http://docs.galpy.org/en/latest/). You can also join the [galpy slack community](https://galpy.slack.com/) for any questions related to `galpy`; join [here](https://join.slack.com/t/galpy/shared_invite/zt-p6upr4si-mX7u8MRdtm~3bW7o8NA_Ww). If you find *any* bug in the code, please report these using the [Issue Tracker](http://github.com/jobovy/galpy/issues) or by joining the [galpy slack community](https://galpy.slack.com/). If you are having issues with the installation of `galpy`, please first consult the [Installation FAQ](http://docs.galpy.org/en/latest/installation.html#installation-faq). PYTHON VERSIONS AND DEPENDENCIES ================================ `galpy` supports Python 3. Specifically, galpy supports Python 3.7, 3.8, 3.9, 3.10, and 3.11. GitHub Actions CI builds regularly check support for Python 3.10 (and of 3.7, 3.8, and 3.9 using a more limited, core set of tests) on Linux and Windows (and 3.10 on Mac OS); Appveyor builds regularly check support for Python 3.10 on Windows. Python 2.7 is no longer supported. This package requires [Numpy](https://numpy.org/), [Scipy](http://www.scipy.org/), and [Matplotlib](http://matplotlib.sourceforge.net/). Certain advanced features require the GNU Scientific Library ([GSL](http://www.gnu.org/software/gsl/)), with action calculations requiring version 1.14 or higher. Other optional dependencies include: * Support for providing inputs and getting outputs as Quantities with units is provided through [`astropy`](http://www.astropy.org/). * Querying SIMBAD for the coordinates of an object in the `Orbit.from_name` initialization method requires [`astroquery`](https://astroquery.readthedocs.io/en/latest/). * Displaying a progress bar for certain operations (e.g., orbit integration of multiple objects at once) requires [`tqdm`](https://github.com/tqdm/tqdm). * Plotting arbitrary functions of Orbit attributes requires [`numexpr`](https://github.com/pydata/numexpr). * Speeding up the evaluation of certain functions in the C code requires [`numba`](https://numba.pydata.org/). * Constant-anisotropy DFs in `galpy.df.constantbetadf` require [`JAX`](https://github.com/google/jax). * Use of `SnapshotRZPotential` and `InterpSnapshotRZPotential` requires [`pynbody`](https://github.com/pynbody/pynbody). Other parts of the code may require additional packages and you will be alerted by the code if they are not installed. CONTRIBUTING TO GALPY ===================== If you are interested in contributing to galpy\'s development, take a look at [this brief guide](https://github.com/jobovy/galpy/wiki/Guide-for-new-contributors) on the wiki. This will hopefully help you get started! Some further development notes can be found on the [wiki](http://github.com/jobovy/galpy/wiki/). This includes a list of small and larger extensions of galpy that would be useful [here](http://github.com/jobovy/galpy/wiki/Possible-galpy-extensions) as well as a longer-term roadmap [here](http://github.com/jobovy/galpy/wiki/Roadmap). Please let the main developer know if you need any help contributing! DISK DF CORRECTIONS =================== The dehnendf and shudf disk distribution functions can be corrected to follow the desired surface-mass density and radial-velocity-dispersion profiles more closely (see [1999AJ\....118.1201D](http://adsabs.harvard.edu/abs/1999AJ....118.1201D)). Calculating these corrections is expensive, and a large set of precalculated corrections can be found [here](http://github.com/downloads/jobovy/galpy/galpy-dfcorrections.tar.gz) \[tar.gz archive\]. Install these by downloading them and unpacking them into the galpy/df/data directory before running the setup.py installation. E.g.: curl -O https://github.s3.amazonaws.com/downloads/jobovy/galpy/galpy-dfcorrections.tar.gz tar xvzf galpy-dfcorrections.tar.gz -C ./galpy/df/data/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233486.0 galpy-1.8.1/galpy.egg-info/SOURCES.txt0000644000175100001710000002447614327773316016760 0ustar00runnerdockerAUTHORS.txt HISTORY.txt LICENSE MANIFEST.in README.dev README.md README.nemo gsl-config.bat pyproject.toml setup.py galpy/__init__.py galpy.egg-info/PKG-INFO galpy.egg-info/SOURCES.txt galpy.egg-info/dependency_links.txt galpy.egg-info/requires.txt galpy.egg-info/top_level.txt galpy/actionAngle/__init__.py galpy/actionAngle/actionAngle.py galpy/actionAngle/actionAngleAdiabatic.py galpy/actionAngle/actionAngleAdiabaticGrid.py galpy/actionAngle/actionAngleAdiabatic_c.py galpy/actionAngle/actionAngleAxi.py galpy/actionAngle/actionAngleHarmonic.py galpy/actionAngle/actionAngleHarmonicInverse.py galpy/actionAngle/actionAngleInverse.py galpy/actionAngle/actionAngleIsochrone.py galpy/actionAngle/actionAngleIsochroneApprox.py galpy/actionAngle/actionAngleIsochroneInverse.py galpy/actionAngle/actionAngleSpherical.py galpy/actionAngle/actionAngleStaeckel.py galpy/actionAngle/actionAngleStaeckelGrid.py galpy/actionAngle/actionAngleStaeckel_c.py galpy/actionAngle/actionAngleTorus.py galpy/actionAngle/actionAngleTorus_c.py galpy/actionAngle/actionAngleVertical.py galpy/actionAngle/actionAngleTorus_c_ext/galpyPot.h galpy/actionAngle/actionAngle_c_ext/actionAngle.h galpy/actionAngle/actionAngle_c_ext/actionAngleAdiabatic.c galpy/actionAngle/actionAngle_c_ext/actionAngleStaeckel.c galpy/df/__init__.py galpy/df/constantbetaHernquistdf.py galpy/df/constantbetadf.py galpy/df/df.py galpy/df/diskdf.py galpy/df/eddingtondf.py galpy/df/evolveddiskdf.py galpy/df/isotropicHernquistdf.py galpy/df/isotropicNFWdf.py galpy/df/isotropicPlummerdf.py galpy/df/jeans.py galpy/df/kingdf.py galpy/df/osipkovmerrittHernquistdf.py galpy/df/osipkovmerrittNFWdf.py galpy/df/osipkovmerrittdf.py galpy/df/quasiisothermaldf.py galpy/df/sphericaldf.py galpy/df/streamdf.py galpy/df/streamgapdf.py galpy/df/streamspraydf.py galpy/df/surfaceSigmaProfile.py galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.2500_0.7500_0.0125_0.0000_151_5.0000_20.sav galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.2500_0.7500_0.2000_0.0000_151_5.0000_20.sav galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_0.6667_0.0125_0.0000_151_5.0000_20.sav galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_0.6667_0.2000_0.0000_151_5.0000_20.sav galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.0125_0.0000_151_5.0000_20.sav galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.1000_0.0000_151_5.0000_20.sav galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_-0.1000_151_5.0000_20.sav galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_-0.2000_151_5.0000_20.sav galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_151_5.0000_20.sav galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.1000_151_5.0000_20.sav galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.2000_151_5.0000_20.sav galpy/df/data/dfcorrection_shudf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_151_5.0000_20.sav galpy/df/data/dfcorrection_shudf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_151_5.0000_40.sav galpy/orbit/Orbits.py galpy/orbit/__init__.py galpy/orbit/integrateFullOrbit.py galpy/orbit/integrateLinearOrbit.py galpy/orbit/integratePlanarOrbit.py galpy/orbit/named_objects.json galpy/orbit/orbit_c_ext/integrateFullOrbit.c galpy/orbit/orbit_c_ext/integrateFullOrbit.h galpy/orbit/orbit_c_ext/integrateLinearOrbit.c galpy/orbit/orbit_c_ext/integratePlanarOrbit.c galpy/potential/AdiabaticContractionWrapperPotential.py galpy/potential/AnyAxisymmetricRazorThinDiskPotential.py galpy/potential/AnySphericalPotential.py galpy/potential/BurkertPotential.py galpy/potential/Cautun20.py galpy/potential/ChandrasekharDynamicalFrictionForce.py galpy/potential/CorotatingRotationWrapperPotential.py galpy/potential/CosmphiDiskPotential.py galpy/potential/DehnenBarPotential.py galpy/potential/DehnenBinney98.py galpy/potential/DehnenSmoothWrapperPotential.py galpy/potential/DiskSCFPotential.py galpy/potential/DissipativeForce.py galpy/potential/DoubleExponentialDiskPotential.py galpy/potential/EllipsoidalPotential.py galpy/potential/EllipticalDiskPotential.py galpy/potential/FerrersPotential.py galpy/potential/FlattenedPowerPotential.py galpy/potential/Force.py galpy/potential/GaussianAmplitudeWrapperPotential.py galpy/potential/HenonHeilesPotential.py galpy/potential/HomogeneousSpherePotential.py galpy/potential/Irrgang13.py galpy/potential/IsochronePotential.py galpy/potential/IsothermalDiskPotential.py galpy/potential/KGPotential.py galpy/potential/KingPotential.py galpy/potential/KuzminDiskPotential.py galpy/potential/KuzminKutuzovStaeckelPotential.py galpy/potential/LogarithmicHaloPotential.py galpy/potential/MN3ExponentialDiskPotential.py galpy/potential/McMillan17.py galpy/potential/MiyamotoNagaiPotential.py galpy/potential/MovingObjectPotential.py galpy/potential/NonInertialFrameForce.py galpy/potential/NullPotential.py galpy/potential/NumericalPotentialDerivativesMixin.py galpy/potential/PerfectEllipsoidPotential.py galpy/potential/PlummerPotential.py galpy/potential/Potential.py galpy/potential/PowerSphericalPotential.py galpy/potential/PowerSphericalPotentialwCutoff.py galpy/potential/PowerTriaxialPotential.py galpy/potential/PseudoIsothermalPotential.py galpy/potential/RazorThinExponentialDiskPotential.py galpy/potential/RingPotential.py galpy/potential/RotateAndTiltWrapperPotential.py galpy/potential/SCFPotential.py galpy/potential/SnapshotRZPotential.py galpy/potential/SoftenedNeedleBarPotential.py galpy/potential/SolidBodyRotationWrapperPotential.py galpy/potential/SphericalPotential.py galpy/potential/SphericalShellPotential.py galpy/potential/SpiralArmsPotential.py galpy/potential/SteadyLogSpiralPotential.py galpy/potential/TimeDependentAmplitudeWrapperPotential.py galpy/potential/TransientLogSpiralPotential.py galpy/potential/TriaxialGaussianPotential.py galpy/potential/TwoPowerSphericalPotential.py galpy/potential/TwoPowerTriaxialPotential.py galpy/potential/WrapperPotential.py galpy/potential/__init__.py galpy/potential/amuse.py galpy/potential/interpRZPotential.py galpy/potential/interpSphericalPotential.py galpy/potential/linearPotential.py galpy/potential/mwpot_helpers.py galpy/potential/mwpotentials.py galpy/potential/planarPotential.py galpy/potential/plotEscapecurve.py galpy/potential/plotRotcurve.py galpy/potential/verticalPotential.py galpy/potential/interppotential_c_ext/interppotential_calc_potential.c galpy/potential/potential_c_ext/BurkertPotential.c galpy/potential/potential_c_ext/ChandrasekharDynamicalFrictionForce.c galpy/potential/potential_c_ext/CorotatingRotationWrapperPotential.c galpy/potential/potential_c_ext/CosmphiDiskPotential.c galpy/potential/potential_c_ext/DehnenBarPotential.c galpy/potential/potential_c_ext/DehnenCorePotential.c galpy/potential/potential_c_ext/DehnenPotential.c galpy/potential/potential_c_ext/DehnenSmoothWrapperPotential.c galpy/potential/potential_c_ext/DiskSCFPotential.c galpy/potential/potential_c_ext/DoubleExponentialDiskPotential.c galpy/potential/potential_c_ext/EllipsoidalPotential.c galpy/potential/potential_c_ext/EllipticalDiskPotential.c galpy/potential/potential_c_ext/FlattenedPowerPotential.c galpy/potential/potential_c_ext/GaussianAmplitudeWrapperPotential.c galpy/potential/potential_c_ext/HenonHeilesPotential.c galpy/potential/potential_c_ext/HernquistPotential.c galpy/potential/potential_c_ext/HomogeneousSpherePotential.c galpy/potential/potential_c_ext/IsochronePotential.c galpy/potential/potential_c_ext/IsothermalDiskPotential.c galpy/potential/potential_c_ext/JaffePotential.c galpy/potential/potential_c_ext/KGPotential.c galpy/potential/potential_c_ext/KuzminDiskPotential.c galpy/potential/potential_c_ext/KuzminKutuzovStaeckelPotential.c galpy/potential/potential_c_ext/LogarithmicHaloPotential.c galpy/potential/potential_c_ext/LopsidedDiskPotential.c galpy/potential/potential_c_ext/MiyamotoNagaiPotential.c galpy/potential/potential_c_ext/MovingObjectPotential.c galpy/potential/potential_c_ext/NFWPotential.c galpy/potential/potential_c_ext/NonInertialFrameForce.c galpy/potential/potential_c_ext/PerfectEllipsoidPotential.c galpy/potential/potential_c_ext/PlummerPotential.c galpy/potential/potential_c_ext/PowerSphericalPotential.c galpy/potential/potential_c_ext/PowerSphericalPotentialwCutoff.c galpy/potential/potential_c_ext/PowerTriaxialPotential.c galpy/potential/potential_c_ext/PseudoIsothermalPotential.c galpy/potential/potential_c_ext/RotateAndTiltWrapperPotential.c galpy/potential/potential_c_ext/SCFPotential.c galpy/potential/potential_c_ext/SoftenedNeedleBarPotential.c galpy/potential/potential_c_ext/SolidBodyRotationWrapperPotential.c galpy/potential/potential_c_ext/SphericalPotential.c galpy/potential/potential_c_ext/SpiralArmsPotential.c galpy/potential/potential_c_ext/SteadyLogSpiralPotential.c galpy/potential/potential_c_ext/TimeDependentAmplitudeWrapperPotential.c galpy/potential/potential_c_ext/TransientLogSpiralPotential.c galpy/potential/potential_c_ext/TriaxialGaussianPotential.c galpy/potential/potential_c_ext/TwoPowerTriaxialPotential.c galpy/potential/potential_c_ext/ZeroPotential.c galpy/potential/potential_c_ext/galpy_potentials.c galpy/potential/potential_c_ext/galpy_potentials.h galpy/potential/potential_c_ext/interpRZPotential.c galpy/potential/potential_c_ext/interpSphericalPotential.c galpy/potential/potential_c_ext/verticalPotential.c galpy/snapshot/GadgetSnapshot.py galpy/snapshot/Snapshot.py galpy/snapshot/__init__.py galpy/snapshot/directnbody.py galpy/snapshot/nemo_util.py galpy/snapshot/snapshotMovies.py galpy/util/__init__.py galpy/util/_load_extension_libs.py galpy/util/_optional_deps.py galpy/util/ars.py galpy/util/bovy_conversion.py galpy/util/bovy_coords.c galpy/util/bovy_coords.h galpy/util/bovy_coords.py galpy/util/bovy_plot.py galpy/util/bovy_rk.c galpy/util/bovy_rk.h galpy/util/bovy_symplecticode.c galpy/util/bovy_symplecticode.h galpy/util/config.py galpy/util/conversion.py galpy/util/coords.py galpy/util/leung_dop853.c galpy/util/leung_dop853.h galpy/util/leung_dop853.py galpy/util/multi.py galpy/util/plot.py galpy/util/quadpack.py galpy/util/symplecticode.py galpy/util/interp_2d/cubic_bspline_2d_coeffs.c galpy/util/interp_2d/cubic_bspline_2d_coeffs.h galpy/util/interp_2d/cubic_bspline_2d_interpol.c galpy/util/interp_2d/cubic_bspline_2d_interpol.h galpy/util/interp_2d/interp_2d.c galpy/util/interp_2d/interp_2d.h././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233486.0 galpy-1.8.1/galpy.egg-info/dependency_links.txt0000644000175100001710000000000114327773316021125 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233486.0 galpy-1.8.1/galpy.egg-info/requires.txt0000644000175100001710000000004614327773316017457 0ustar00runnerdockerpackaging numpy>=1.7 scipy matplotlib ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233486.0 galpy-1.8.1/galpy.egg-info/top_level.txt0000644000175100001710000000014014327773316017604 0ustar00runnerdockergalpy galpy/actionAngle galpy/df galpy/orbit galpy/potential galpy/snapshot galpy/util libgalpy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/gsl-config.bat0000644000175100001710000000073714327773303014774 0ustar00runnerdocker@echo off rem simple Windows version of gsl-config for conda installs set ROOT=%CONDA_PREFIX%\Library set XCFLAGS=-I"%ROOT%\include" set XLIBS1=-L"%ROOT%\lib" -lgsl -lgslcblas set XLIBS2=-L"%ROOT%\lib" -lgsl set XVERSION=2.3 set XPREFIX=%ROOT% for %%p in (%*) do ( if x%%p == x--cflags echo %XCFLAGS% if x%%p == x--libs echo %XLIBS1% if x%%p == x--libs-without-cblas echo %XLIBS2% if x%%p == x--version echo %XVERSION% if x%%p == x--prefix echo %XPREFIX% ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/pyproject.toml0000644000175100001710000000012114327773303015153 0ustar00runnerdocker[build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667233486.721467 galpy-1.8.1/setup.cfg0000644000175100001710000000004614327773317014073 0ustar00runnerdocker[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1667233475.0 galpy-1.8.1/setup.py0000644000175100001710000003323414327773303013764 0ustar00runnerdockerimport distutils.ccompiler import glob import os import os.path import platform import subprocess import sys import sysconfig from setuptools import Extension, setup from setuptools.command.build_ext import build_ext from setuptools.errors import PlatformError PY3= sys.version > '3' WIN32= platform.system() == 'Windows' no_compiler = False # Flag for cases where we are sure there is no compiler exists in user's system long_description= '' previous_line= '' with open('README.md') as dfile: for line in dfile: if not 'image' in line and not 'target' in line \ and not 'DETAILED' in line and not '**main**' in line \ and not '**development' in line \ and not 'DETAILED' in previous_line: long_description+= line previous_line= line # Parse options; current options # --no-openmp: compile without OpenMP support # --coverage: compile with gcov support # --compiler= set the compiler by hand # --single_ext: compile all of the C code into a single extension (just for testing, do not use this) galpy_c_libraries = ['m','gsl','gslcblas','gomp'] if WIN32: # On Windows it's unnecessary and erroneous to include m galpy_c_libraries.remove('m') # Windows does not need 'gomp' whether compiled with OpenMP or not galpy_c_libraries.remove('gomp') #Option to use Intel compilers try: compiler_option_pos = ['--compiler=' in opt for opt in sys.argv]\ .index(True) except ValueError: use_intel_compiler= False else: use_intel_compiler= 'intel' in sys.argv[compiler_option_pos].split('=')[1] if use_intel_compiler and not WIN32: import numpy.distutils.intelccompiler elif use_intel_compiler and WIN32: import __intelcompiler if use_intel_compiler: # OpenMP by default included for Intel, see #416 galpy_c_libraries.remove('gomp') #Option to forego OpenMP try: openmp_pos = sys.argv.index('--no-openmp') except ValueError: if "PYODIDE" in os.environ: extra_compile_args= ["-DNO_OMP"] galpy_c_libraries.remove('gomp') else: extra_compile_args = ["-fopenmp" if not WIN32 else "/openmp"] else: del sys.argv[openmp_pos] extra_compile_args= ["-DNO_OMP"] if not WIN32: # Because windows guarantee do not have 'gomp' in the list galpy_c_libraries.remove('gomp') #Option to track coverage try: coverage_pos = sys.argv.index('--coverage') except ValueError: extra_link_args= [] else: del sys.argv[coverage_pos] extra_compile_args.extend(["-O0","--coverage","-D USING_COVERAGE"]) extra_link_args= ["--coverage"] #Option to compile everything into a single extension try: single_ext_pos = sys.argv.index('--single_ext') except ValueError: single_ext= False else: del sys.argv[single_ext_pos] single_ext= True #Option to not compile any extension try: no_ext_pos = sys.argv.index('--no_ext') except ValueError: no_ext= False else: del sys.argv[no_ext_pos] no_ext= True #code to check the GSL version; list cmd w/ shell=True only works on Windows # (https://docs.python.org/3/library/subprocess.html#converting-argument-sequence) cmd= ['gsl-config','--version'] try: if sys.version_info < (2,7): #subprocess.check_output does not exist gsl_version= subprocess.Popen(cmd,shell=sys.platform.startswith('win'), stdout=subprocess.PIPE).communicate()[0] else: gsl_version= \ subprocess.check_output(cmd, shell=sys.platform.startswith('win')) except (OSError,subprocess.CalledProcessError): if "PYODIDE" in os.environ: gsl_version= ['2','7'] else: gsl_version= ['0','0'] else: if PY3: gsl_version= gsl_version.decode('utf-8') gsl_version= gsl_version.split('.') extra_compile_args.append("-D GSL_MAJOR_VERSION=%s" % (gsl_version[0])) #HACK for testing #gsl_version= ['0','0'] # MSVC: inline does not exist (not C99!); default = not necessarily actual, but will have to do for now... # Note for the futureL could now get the actual compiler in the BuildExt class # below if distutils.ccompiler.get_default_compiler().lower() == 'msvc': extra_compile_args.append("-Dinline=__inline") # only msvc compiler can be tested with initialize(), msvc is a default on windows # check for 'msvc' not WIN32, user can use other compiler like 'mingw32', in such case compiler exists for them try: test_compiler = distutils.ccompiler.new_compiler() test_compiler.initialize() # try to initialize a test compiler to see if compiler presented except PlatformError: # this error will be raised if no compiler in the system no_compiler = True # To properly export GSL symbols on Windows, need to defined GSL_DLL and WIN32 if WIN32: extra_compile_args.append("-DGSL_DLL") extra_compile_args.append("-DWIN32") #main C extension galpy_c_src= ['galpy/util/bovy_symplecticode.c', 'galpy/util/bovy_rk.c', 'galpy/util/leung_dop853.c','galpy/util/bovy_coords.c'] galpy_c_src.extend(glob.glob('galpy/potential/potential_c_ext/*.c')) galpy_c_src.extend(glob.glob('galpy/potential/interppotential_c_ext/*.c')) galpy_c_src.extend(glob.glob('galpy/util/interp_2d/*.c')) galpy_c_src.extend(glob.glob('galpy/orbit/orbit_c_ext/*.c')) galpy_c_src.extend(glob.glob('galpy/actionAngle/actionAngle_c_ext/*.c')) galpy_c_include_dirs= ['galpy/util', 'galpy/util/interp_2d', 'galpy/potential/potential_c_ext', 'galpy/potential/interppotential_c_ext', 'galpy/orbit/orbit_c_ext', 'galpy/actionAngle/actionAngle_c_ext'] #actionAngleTorus C extension (files here, so we can compile a single extension if so desidered) actionAngleTorus_c_src= \ glob.glob('galpy/actionAngle/actionAngleTorus_c_ext/*.cc') actionAngleTorus_c_src.extend(\ glob.glob('galpy/actionAngle/actionAngleTorus_c_ext/torus/src/*.cc')) actionAngleTorus_c_src.extend(\ ['galpy/actionAngle/actionAngleTorus_c_ext/torus/src/utils/CHB.cc', 'galpy/actionAngle/actionAngleTorus_c_ext/torus/src/utils/Err.cc', 'galpy/actionAngle/actionAngleTorus_c_ext/torus/src/utils/Compress.cc', 'galpy/actionAngle/actionAngleTorus_c_ext/torus/src/utils/Numerics.cc', 'galpy/actionAngle/actionAngleTorus_c_ext/torus/src/utils/PJMNum.cc']) actionAngleTorus_c_src.extend(\ glob.glob('galpy/potential/potential_c_ext/*.c')) actionAngleTorus_c_src.extend(\ glob.glob('galpy/orbit/orbit_c_ext/integrateFullOrbit.c')) actionAngleTorus_c_src.extend(glob.glob('galpy/util/interp_2d/*.c')) actionAngleTorus_c_src.extend(glob.glob('galpy/util/*.c')) actionAngleTorus_include_dirs= \ ['galpy/actionAngle/actionAngleTorus_c_ext', 'galpy/actionAngle/actionAngleTorus_c_ext/torus/src', 'galpy/actionAngle/actionAngleTorus_c_ext/torus/src/utils', 'galpy/actionAngle/actionAngle_c_ext', 'galpy/util/interp_2d', 'galpy/util', 'galpy/orbit/orbit_c_ext', 'galpy/potential/potential_c_ext'] if single_ext: #add the code and libraries for the actionAngleTorus extensions if os.path.exists('galpy/actionAngle/actionAngleTorus_c_ext/torus/src'): galpy_c_src.extend(actionAngleTorus_c_src) galpy_c_src= list(set(galpy_c_src)) galpy_c_include_dirs.extend(actionAngleTorus_include_dirs) galpy_c_include_dirs= list(set(galpy_c_include_dirs)) #Installation of this extension using the GSL may (silently) fail, if the GSL #is built for the wrong architecture, on Mac you can install the GSL correctly #using #brew install gsl --universal galpy_c= Extension('libgalpy', sources=galpy_c_src, libraries=galpy_c_libraries, include_dirs=galpy_c_include_dirs, extra_compile_args=extra_compile_args, extra_link_args=extra_link_args) ext_modules=[] if float(gsl_version[0]) >= 1. \ and (float(gsl_version[0]) >= 2. or float(gsl_version[1]) >= 14.): galpy_c_incl= True ext_modules.append(galpy_c) else: galpy_c_incl= False # Add the actionAngleTorus extension (src and include specified above) actionAngleTorus_c= Extension('libgalpy_actionAngleTorus', sources=actionAngleTorus_c_src, libraries=galpy_c_libraries, include_dirs=actionAngleTorus_include_dirs, extra_compile_args=extra_compile_args, extra_link_args=extra_link_args) if float(gsl_version[0]) >= 1. \ and (float(gsl_version[0]) >= 2. or float(gsl_version[1]) >= 14.) and \ os.path.exists('galpy/actionAngle/actionAngleTorus_c_ext/torus/src') \ and not single_ext: actionAngleTorus_c_incl= True ext_modules.append(actionAngleTorus_c) else: actionAngleTorus_c_incl= False # Test whether compiler allows for the -fopenmp flag and all other flags # to guard against compilation errors # (https://stackoverflow.com/a/54518348) def compiler_has_flag(compiler,flagname): """Test whether a given compiler supports a given option""" import tempfile from setuptools.errors import CompileError with tempfile.NamedTemporaryFile('w', suffix='.cpp') as f: f.write('int main (int argc, char **argv) { return 0; }') try: compiler.compile([f.name], extra_postargs=[flagname]) except CompileError: return False return True # Now need to subclass BuildExt to access the compiler used and check flags class BuildExt(build_ext): def build_extensions(self): ct= self.compiler.compiler_type if ct == 'unix': for ext in self.extensions: # only add flags which pass the flag_filter extra_compile_args= [] for flag in ext.extra_compile_args: if compiler_has_flag(self.compiler,flag): extra_compile_args.append(flag) ext.extra_compile_args= extra_compile_args build_ext.build_extensions(self) setup(cmdclass=dict(build_ext=BuildExt), # this to allow compiler check above name='galpy', version='1.8.1', description='Galactic Dynamics in python', author='Jo Bovy', author_email='bovy@astro.utoronto.ca', license='New BSD', long_description=long_description, long_description_content_type='text/markdown', url='http://github.com/jobovy/galpy', package_dir = {'galpy/': ''}, packages=['galpy','galpy/orbit','galpy/potential', 'galpy/df','galpy/util','galpy/snapshot', 'galpy/actionAngle'], package_data={'galpy/orbit':['named_objects.json'], 'galpy/df':['data/*.sav'], "": ["README.md","README.dev","LICENSE","AUTHORS.rst"]}, include_package_data=True, install_requires=['packaging','numpy>=1.7','scipy','matplotlib'], ext_modules=ext_modules if not no_compiler and not no_ext else None, classifiers=[ "Development Status :: 6 - Mature", "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: C", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Topic :: Scientific/Engineering :: Astronomy", "Topic :: Scientific/Engineering :: Physics"] ) def print_gsl_message(num_messages=1): if num_messages > 1: this_str= 'these installations' else: this_str= 'this installation' print('If you believe that %s should have worked, make sure\n(1) that the GSL include/ directory can be found by the compiler (you might have to edit CFLAGS for this: export CFLAGS="$CFLAGS -I/path/to/gsl/include/", or equivalent for C-type shells; replace /path/to/gsl/include/ with the actual path to the include directory),\n(2) that the GSL library can be found by the linker (you might have to edit LDFLAGS for this: export LDFLAGS="$LDFLAGS -L/path/to/gsl/lib/", or equivalent for C-type shells; replace /path/to/gsl/lib/ with the actual path to the lib directory),\n(3) and that `gsl-config --version` returns the correct version' % this_str) num_gsl_warn= 0 if not galpy_c_incl: num_gsl_warn+= 1 print('\033[91;1m'+'WARNING: galpy C library not installed because your GSL version < 1.14'+'\033[0m') if not actionAngleTorus_c_incl and not single_ext: num_gsl_warn+= 1 print('\033[91;1m'+'WARNING: galpy action-angle-torus C library not installed because your GSL version < 1.14 or because you did not first download the torus code as explained in the installation guide in the html documentation'+'\033[0m') if num_gsl_warn > 0: print_gsl_message(num_messages=num_gsl_warn) print('\033[1m'+'These warning messages about the C code do not mean that the python package was not installed successfully'+'\033[0m') print('\033[1m'+'Finished installing galpy'+'\033[0m') print('You can run the test suite using `pytest -v tests/` to check the installation (but note that the test suite currently takes about 50 minutes to run)') #if single_ext, symlink the other (non-compiled) extensions to libgalpy.so (use EXT_SUFFIX for python3 compatibility) if PY3: _ext_suffix= sysconfig.get_config_var('EXT_SUFFIX') else: _ext_suffix= '.so' if single_ext: if not os.path.exists('libgalpy_actionAngleTorus%s' % _ext_suffix) \ and os.path.exists('galpy/actionAngle/actionAngleTorus_c_ext/torus/src'): os.symlink('libgalpy%s' % _ext_suffix, 'libgalpy_actionAngleTorus%s' % _ext_suffix)