././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6868846 galpy-1.10.2/0000755000175100001660000000000014761352040012314 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/AUTHORS.txt0000644000175100001660000000406414761352023014207 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=1741018131.0 galpy-1.10.2/HISTORY.txt0000644000175100001660000011660414761352023014227 0ustar00runnerdockerv1.10.2 (2025-03-03) ==================== - Implemented the IAS15 integrator of Rein & Spiegel 2015. - Speed-up the C implementation of PowerSphericalPotentialwCutoff (#706). - Combine the drift calculations in the Python leapfrog integrator for a small speed-up (#690). - Clarify the use of non-equispaced time arrays in orbit integration (#711). - Fix the handling of unbound orbits in the Orbit action-angle interface (#712). - Move the checks for non-axisymmetric and dissipative potentials from internal to public potential/force evaluation functions (e.g., from _evaluate[Potentials, Rforces, phitorques, zforces] to evaluate[Potentials, Rforces, phitorques, zforces]) for performance improvements. - Add an isDissipative attribute to force classes. - Print warning when initializing an Orbit with a SkyCoord that does not have the Sun's positional and velocity parameters set (#715). v1.10.1 (2024-11-01) ==================== - Propagate general plotting keywords in Potential.plot/plotPotentials. Also allow plotting potentials on physical axes. - Added the generalized particle-spray model as galpy.df.basestreamspraydf with two subclasses: chen24spraydf and fadal15spraydf. Enabled integrating orbits of stream particles with the progenitor's potential. Deprecating the old particle-spray model galpy.df.streamspraydf. v1.10.0 (2024-07-07) ==================== - Increased support for using OpenMP with clang and, in particular, added OpenMP support in the released Mac wheels. - Removed explicit support for using Intel compilers. - Switch to using Ruff as the code formatter. v1.9.2 (2024-03-04) =================== - Added KuzminLikeWrapperPotential, a potential wrapper that allows one to make a Kuzmin-like or Miyamoto-Nagai-like potential out of any spherical or axisymmetric potential (evaluated in the plane, i.e., treated as a spherical potential). Kuzmin-like potentials are obtained by replacing the spherical radius r with \sqrt(R^2 + (a + |z|^2)), while Miyamoto-Nagai-like potentials are obtained by replacing the spherical radius with \sqrt(R^2 + (a + \sqrt(z^2 + b^2))^2). The standard KuzminDiskPotential and MiyamotoNagaiPotential are obtained by applying this procedure to a point-mass potential and the Kuzmin/Miyamoto-Nagai-like potentials generalize this to any spherical potential. v1.9.1 (2023-11-06) =================== - Allow vector inputs of solar parameters to Orbit initialization: ro, zo, vo, and solarmotion (#595). Useful when sampling over the uncertainty in the solar parameters. - Converted all docstrings to numpy-style format with the help of GitHub Copilot. v1.9.0 (2023-07-02) =================== - Add specialized integration method to determine surfaces of sections of orbits and add Orbit.SOS and Orbit.plotSOS methods to compute and plot the surface of section of an orbit. - Also added a brute-force method to determine surfaces of sections and plotting them for force fields for which the specialized method does not work (e.g., orbits in a non-inertial bar frame). Methods Orbit.bruteSOS and Orbit.plotBruteSOS have similar interfaces as Orbit.SOS and Orbit.plotSOS. - Added a method, dMdE, to calculate the differential energy distribution of spherical distribution functions. - Added general support for DissipativeForce instances in 2D. - Implemented NonInertialFrameForce in 2D. - Started using black for code formatting. - Allow potentials' density and surface density to be plotted on physical axes. - Don't fail when writing a (fixed) configuration file, instead just warn. - Removed the deprecated galpy.util.bovy_coords, galpy.util.bovy_plot, and galpy.util.bovy_conversion modules. These were replaced by galpy.util.coords, galpy.util.plot, and galpy.util.conversion in v1.7. - Removed the deprecated phiforce method from potentials and the accompanying evaluatephiforces and evaluateplanarphiforces potential functions. These were replaced by phitorque, evaluatephitorques, and evaluateplanarphitorques in v1.8.0. - Switched to using Python 3.11 as the default Python version in the test suite. v1.8.3 (2023-03-27) =================== - Fixed issue in Linux wheels where the OpenMP library wasn't properly linked to (also fixed the setup.py script for the underlying issue). v1.8.2 (2023-02-28) =================== - Added actionAngleVertical to the public API, an actionAngle class for 1D action-angle calculations. actionAngleVertical was used internally in galpy before, but now follows the usual method structure for actionAngle classes. - Added actionAngleVerticalInverse, an inverse actionAngle class that allows one to go from (x,v) -> (J,theta,Omega) for 1D potentials using a custom implementation of the torus machinery with point transformations. - Made it possible to use an interpSphericalPotential as the potential in spherical distribution functions (#554) - Made the potential input explicitly positional-only for all galpy.potential functions to avoid errors when specifying it as a keyword argument. - Rewrite actionAngleAdiabatic's Python implementation as a combination of actionAngleSpherical (for the 2D planar part) and actionAngleVertical (for the 1D vertical part). - Dropped Python 3.7 support (to allow required positional-only arguments using PEP 570). - Dropped Appveyor CI builds entirely, now only using GitHub Actions for CI. - Added Orbit.animate3d to display a 3D animation of an integrated orbit (#532) with an optional Milky-Way representation at the origin when plotting x,y,z. - Improved the performance of Orbit.animate performance by using webgl, some UI tweaks. Also fixed using Orbit.animate in jupyterlab and retrolab (latter #534). - Run tests with deprecation warnings of various types (DeprecationWarning, FutureWarning, AstropyDeprecationWarning) turned into errors to guard against coming deprecations (#538) v1.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 reused 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=1741018131.0 galpy-1.10.2/LICENSE0000644000175100001660000000270614761352023013327 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=1741018131.0 galpy-1.10.2/MANIFEST.in0000644000175100001660000000115014761352023014050 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/actionAngle/actionAngleTorus_c_ext/torus/src/*.h include galpy/actionAngle/actionAngleTorus_c_ext/torus/src/utils/*.h include galpy/actionAngle/actionAngleTorus_c_ext/torus/src/utils/Numerics.templates include galpy/orbit/orbit_c_ext/*.h include galpy/potential/potential_c_ext/*.h include galpy/util/*.h include galpy/util/interp_2d/*.h ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6868846 galpy-1.10.2/PKG-INFO0000644000175100001660000001322714761352040013416 0ustar00runnerdockerMetadata-Version: 2.2 Name: galpy Version: 1.10.2 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.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Environment :: WebAssembly :: Emscripten Classifier: Topic :: Scientific/Engineering :: Astronomy Classifier: Topic :: Scientific/Engineering :: Physics Requires-Python: >=3.8 Description-Content-Type: text/markdown License-File: LICENSE License-File: AUTHORS.txt Requires-Dist: packaging Requires-Dist: numpy>=1.7 Requires-Dist: scipy Requires-Dist: matplotlib Provides-Extra: docs Requires-Dist: sphinxext-opengraph; extra == "docs" Requires-Dist: sphinx-design; extra == "docs" Requires-Dist: markupsafe==2.0.1; extra == "docs" Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: description-content-type Dynamic: home-page Dynamic: license Dynamic: provides-extra Dynamic: requires-dist Dynamic: requires-python Dynamic: summary

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.8, 3.9, 3.10, 3.11, 3.12 and 3.13. GitHub Actions CI builds regularly check support for Python 3.13 (and of 3.8, 3.9, 3.10, 3.11, and 3.12 using a more limited, core set of tests) on Linux and Windows (and 3.13 on Mac OS). 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! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/README.dev0000644000175100001660000000262114761352023013753 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) Finally, add 'self.hasC= True' to the initialization of the potential in question (after the initialization of the super class) 8) It should work now! 9) 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. 10) 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). 11) If you add a 1D potential, do the steps above, but for integrateLinearOrbit.* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/README.md0000644000175100001660000001373414761352023013604 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) [![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](https://codecov.io/gh/jobovy/galpy/branch/main/graph/badge.svg?token=nIqjwFncfP)](https://codecov.io/gh/jobovy/galpy) [![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/version.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.8, 3.9, 3.10, 3.11, 3.12 and 3.13. GitHub Actions CI builds regularly check support for Python 3.13 (and of 3.8, 3.9, 3.10, 3.11, and 3.12 using a more limited, core set of tests) on Linux and Windows (and 3.13 on Mac OS). 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! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/README.nemo0000644000175100001660000000222514761352023014133 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! ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6358843 galpy-1.10.2/galpy/0000755000175100001660000000000014761352040013430 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/__init__.py0000644000175100001660000000667014761352023015553 0ustar00runnerdocker__version__ = "1.10.2" # 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__) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6408842 galpy-1.10.2/galpy/actionAngle/0000755000175100001660000000000014761352040015654 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/__init__.py0000644000175100001660000000336614761352023017776 0ustar00runnerdockerfrom . import ( actionAngle, actionAngleAdiabatic, actionAngleAdiabaticGrid, actionAngleHarmonic, actionAngleHarmonicInverse, actionAngleInverse, actionAngleIsochrone, actionAngleIsochroneApprox, actionAngleIsochroneInverse, actionAngleSpherical, actionAngleStaeckel, actionAngleStaeckelGrid, actionAngleTorus, actionAngleVertical, actionAngleVerticalInverse, ) # # Exceptions # UnboundError = actionAngle.UnboundError # # Functions # estimateDeltaStaeckel = actionAngleStaeckel.estimateDeltaStaeckel estimateBIsochrone = actionAngleIsochroneApprox.estimateBIsochrone dePeriod = actionAngleIsochroneApprox.dePeriod # # Classes # actionAngle = actionAngle.actionAngle actionAngleInverse = actionAngleInverse.actionAngleInverse 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 actionAngleVertical = actionAngleVertical.actionAngleVertical actionAngleVerticalInverse = actionAngleVerticalInverse.actionAngleVerticalInverse ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngle.py0000644000175100001660000002625514761352023020465 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): """ Initialize an actionAngle object. Parameters ---------- ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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): """ Turn off automatic returning of outputs in physical units. Notes ------ - 2017-06-05 - Written - Bovy (UofT) """ self._roSet = False self._voSet = False return None def turn_physical_on(self, ro=None, vo=None): """ Turn on automatic returning of outputs in physical units. Parameters ---------- ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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 ro = conversion.parse_length_kpc(ro) if not ro is None: self._ro = ro if not vo is False: self._voSet = True vo = conversion.parse_velocity_kms(vo) if not vo is None: self._vo = vo return None def _parse_eval_args(self, *args, **kwargs): if 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): """ Evaluate the actions (jr,lz,jz) Parameters ---------- *args : tuple 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 **kwargs : dict Any other keyword arguments are passed to the _evaluate method. Returns ------- tuple (jr,lz,jz) Notes ----- - 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): """ Evaluate the actions and frequencies (jr,lz,jz,Omegar,Omegaphi,Omegaz) Parameters ---------- *args : tuple 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 **kwargs : dict Any other keyword arguments are passed to the _actionsFreqs method. Returns ------- tuple (jr,lz,jz,Omegar,Omegaphi,Omegaz) Notes ----- - 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): """ Evaluate the actions, frequencies, and angles (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) Parameters ---------- *args : tuple 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 **kwargs : dict Additional keyword arguments to be passed to _actionsFreqsAngles method. Returns ------- tuple (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) Notes ----- - 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): """ Evaluate the eccentricity, maximum height above the plane, peri- and apocenter. Parameters ---------- *args : tuple 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 **kwargs : dict A dictionary of keyword arguments. Returns ------- tuple (eccentricity, maximum height above the plane, peri-, and apocenter) Notes ----- - 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=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleAdiabatic.py0000644000175100001660000002672514761352023022251 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleAdiabatic # # methods: # __call__: returns (jr,lz,jz) # _EccZmaxRperiRap: return (e,zmax,rperi,rap) # ############################################################################### import copy import warnings import numpy from ..potential import MWPotential, toPlanarPotential, toVerticalPotential from ..potential.Potential import _check_c, _dim 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 .actionAngleSpherical import actionAngleSpherical from .actionAngleVertical import actionAngleVertical class actionAngleAdiabatic(actionAngle): """Action-angle formalism for axisymmetric potentials using the adiabatic approximation""" def __init__(self, *args, **kwargs): """ Initialize an actionAngleAdiabatic object. Parameters ---------- pot : potential or list of potentials The potential or list of potentials. gamma : float, optional Replace Lz by Lz+gamma Jz in effective potential. Default is 1.0. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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 actionAngleAdiabatic") 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.0) # Setup actionAngleSpherical object for calculations in Python # (if they become necessary) if _dim(self._pot) == 3: thispot = toPlanarPotential(self._pot) else: thispot = self._pot self._gamma = 0.0 self._aAS = actionAngleSpherical(pot=thispot, _gamma=self._gamma) # Check the units self._check_consistent_units() return None def _evaluate(self, *args, **kwargs): """ Evaluate the actions (jr,lz,jz). Parameters ---------- *args : tuple 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: bool, optional True/False to override the object-wide setting for whether or not to use the C implementation _justjr, _justjz: bool, optional If True, only calculate the radial or vertical action (internal use) **kwargs : dict scipy.integrate.quadrature keywords Returns ------- (jr,lz,jz) Actions (jr,lz,jz). Notes ----- - 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[0] ojz[ii] = tjz[0] olz[ii] = tlz[0] return (ojr, olz, ojz) else: if kwargs.get("_justjr", False): kwargs.pop("_justjr") return ( self._aAS(R[0], vR[0], vT[0], 0.0, 0.0, _Jz=0.0)[0], numpy.nan, numpy.nan, ) # Set up the actionAngleVertical object if _dim(self._pot) == 3: thisverticalpot = toVerticalPotential(self._pot, R[0]) aAV = actionAngleVertical(pot=thisverticalpot) Jz = aAV(z[0], vz[0]) else: # 2D in-plane Jz = numpy.zeros(1) if kwargs.get("_justjz", False): kwargs.pop("_justjz") return ( numpy.atleast_1d(numpy.nan), numpy.atleast_1d(numpy.nan), Jz, ) else: axiJ = self._aAS(R[0], vR[0], vT[0], 0.0, 0.0, _Jz=Jz) return ( numpy.atleast_1d(axiJ[0]), numpy.atleast_1d(axiJ[1]), numpy.atleast_1d(Jz), ) def _EccZmaxRperiRap(self, *args, **kwargs): """ Evaluate the eccentricity, maximum height above the plane, peri- and apocenter in the adiabatic approximation. Parameters ---------- *args : tuple 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: bool, optional True/False to override the object-wide setting for whether or not to use the C implementation Returns ------- (e,zmax,rperi,rap) Eccentricity, maximum height above the plane, peri- and apocenter. Notes ----- - 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.0 + zmax**2.0) 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[0] ozmax[ii] = tzmax[0] orperi[ii] = trperi[0] orap[ii] = trap[0] return (oecc, ozmax, orperi, orap) else: if _dim(self._pot) == 3: thisverticalpot = toVerticalPotential(self._pot, R[0]) aAV = actionAngleVertical(pot=thisverticalpot) zmax = aAV.calcxmax(z[0], vz[0], **kwargs) if self._gamma != 0.0: Jz = aAV(z[0], vz[0]) else: Jz = 0.0 else: zmax = 0.0 Jz = 0.0 _, _, rperi, Rap = self._aAS.EccZmaxRperiRap( R[0], vR[0], vT[0], 0.0, 0.0, _Jz=Jz ) rap = numpy.sqrt(Rap**2.0 + zmax**2.0) return ( numpy.atleast_1d((rap - rperi) / (rap + rperi)), numpy.atleast_1d(zmax), numpy.atleast_1d(rperi), numpy.atleast_1d(rap), ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleAdiabaticGrid.py0000644000175100001660000004664014761352023023055 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.0, gamma=1.0, Rmax=5.0, nR=16, nEz=16, nEr=31, nLz=31, numcores=1, **kwargs, ): """ Initialize an actionAngleAdiabaticGrid object Parameters ---------- pot : Potential or list of Potential instances The potential (instance) or list of potentials (instances) that make up the potential zmax : float Maximum height to which to calculate Ez gamma : float Replace Lz by Lz+gamma Jz in effective potential Rmax : float Maximum radius to which to calculate Er nR : int Number of radii to use in the grid nEz : int Number of Ez values to use in the grid nEr : int Number of Er values to use in the grid nLz : int Number of Lz values to use in the grid numcores : int Number of cores to use for multi-processing ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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 actionAngleAdiabaticGrid") 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.0, 1.0, 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.0 * 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.0, 1.0, # these two r dummies 0.0, numpy.sqrt(2.0 * 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.0, 1.0, # these two r dummies 0.0, numpy.sqrt(2.0 * y[jj] * self._EzZmaxs[ii]), _justjz=True, **kwargs, )[2][0] 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.0**-5.0), k=3 ) self._jz = jz self._jzInterp = interpolate.RectBivariateSpline( self._Rs, y, jz, kx=3, ky=3, s=0.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.0 / 2.0 / self._RL**2.0 ) self._ERRLmax = numpy.amax(self._ERRL) + 1.0 self._ERRLInterp = interpolate.InterpolatedUnivariateSpline( self._Lzs, numpy.log(-(self._ERRL - self._ERRLmax)), k=3 ) self._Ramax = 99.0 self._ERRa = ( _evaluatePotentials(self._pot, self._Ramax, 0.0) + self._Lzs**2.0 / 2.0 / self._Ramax**2.0 ) self._ERRamax = numpy.amax(self._ERRa) + 1.0 self._ERRaInterp = interpolate.InterpolatedUnivariateSpline( self._Lzs, numpy.log(-(self._ERRa - self._ERRamax)), k=3 ) y = numpy.linspace(0.0, 1.0, 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.0 * ( thisERRa + this * (thisERRL - thisERRa) - _evaluatePotentials( self._pot, thisRL, numpy.zeros((nEr - 1) * nLz) ) ) - thisLzs**2.0 / thisRL**2.0 ), 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.0 * ( thisERRa[x] + this[x] * (thisERRL[x] - thisERRa[x]) - _evaluatePotentials(self._pot, thisRL[x], 0.0) ) - thisLzs[x] ** 2.0 / thisRL[x] ** 2.0 ), thisLzs[x] / thisRL[x], 0.0, 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.0 * ( self._ERRa[ii] + y[jj] * (self._ERRL[ii] - self._ERRa[ii]) - _evaluatePotentials( self._pot, self._RL[ii], 0.0 ) ) - self._Lzs[ii] ** 2.0 / self._RL[ii] ** 2.0 ), self._Lzs[ii] / self._RL[ii], 0.0, 0.0, _justjr=True, **kwargs, )[0][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.0**-5.0), k=3 ) self._jrInterp = interpolate.RectBivariateSpline( self._Lzs, y, jr, kx=3, ky=3, s=0.0 ) # Check the units self._check_consistent_units() return None def _evaluate(self, *args, **kwargs): """ Evaluate the actions (jr,lz,jz). Parameters ---------- *args : tuple 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 **kwargs: dict, optional scipy.integrate.quadrature keywords (used when directly evaluating a point off the grid) Returns ------- tuple (jr,lz,jz) Notes ----- - 2012-07-27 - 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 # 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.0) Ez = Phi - Phio + vz**2.0 / 2.0 # 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.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.0**-5.0) 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.0 * 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.0, 1.0, # these two r dummies 0.0, numpy.sqrt(2.0 * Ez), _justjz=True, **kwargs, )[2] else: jz = ( self._jzInterp(R, Ez / thisEzZmax) * (numpy.exp(self._jzEzmaxInterp(R)) - 10.0**-5.0) )[0][0] # Radial action ERLz = numpy.fabs(R * vT) + self._gamma * jz ER = Phio + vR**2.0 / 2.0 + ERLz**2.0 / 2.0 / R**2.0 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.0) * ( ((ER - thisERRa) / (thisERRL - thisERRa) - 1.0) < 10.0**-2.0 ) ER[indx] = thisERRL[indx] indx = ((ER - thisERRa) / (thisERRL - thisERRa) < 0.0) * ( (ER - thisERRa) / (thisERRL - thisERRa) > -(10.0**-2.0) ) ER[indx] = thisERRa[indx] indx = ERLz < self._Lzmin indx += ERLz > self._Lzmax indx += (ER - thisERRa) / (thisERRL - thisERRa) > 1.0 indx += (ER - thisERRa) / (thisERRL - thisERRa) < 0.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.0**-5.0) if numpy.sum(indx) > 0: jr[indx] = self._aA( thisRL[indx], numpy.sqrt( 2.0 * (ER[indx] - _evaluatePotentials(self._pot, thisRL[indx], 0.0)) - ERLz[indx] ** 2.0 / thisRL[indx] ** 2.0 ), ERLz[indx] / thisRL[indx], numpy.zeros(len(thisRL)), numpy.zeros(len(thisRL)), _justjr=True, **kwargs, )[0] else: if (ER - thisERRa) / (thisERRL - thisERRa) > 1.0 and ( (ER - thisERRa) / (thisERRL - thisERRa) - 1.0 ) < 10.0**-2.0: ER = thisERRL elif (ER - thisERRa) / (thisERRL - thisERRa) < 0.0 and (ER - thisERRa) / ( thisERRL - thisERRa ) > -(10.0**-2.0): ER = thisERRa # Outside of grid? if ( ERLz < self._Lzmin or ERLz > self._Lzmax or (ER - thisERRa) / (thisERRL - thisERRa) > 1.0 or (ER - thisERRa) / (thisERRL - thisERRa) < 0.0 ): if _PRINTOUTSIDEGRID: # pragma: no cover print( "Outside of grid in ER/Lz", ERLz < self._Lzmin, ERLz > self._Lzmax, (ER - thisERRa) / (thisERRL - thisERRa) > 1.0, (ER - thisERRa) / (thisERRL - thisERRa) < 0.0, ER, thisERRL, thisERRa, (ER - thisERRa) / (thisERRL - thisERRa), ) jr = self._aA( thisRL[0], numpy.sqrt( 2.0 * (ER - _evaluatePotentials(self._pot, thisRL, 0.0)) - ERLz**2.0 / thisRL**2.0 )[0], (ERLz / thisRL)[0], 0.0, 0.0, _justjr=True, **kwargs, )[0] else: jr = ( self._jrInterp(ERLz, (ER - thisERRa) / (thisERRL - thisERRa)) * (numpy.exp(self._jrERRaInterp(ERLz)) - 10.0**-5.0) )[0][0] return (jr, R * vT, jz) def Jz(self, *args, **kwargs): """ Evaluate the action jz. Parameters ---------- *args : tuple 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 **kwargs: dict scipy.integrate.quadrature keywords Returns ------- float The action jz. Notes ----- - 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.0) Ez = Phi - Phio + self._eval_vz**2.0 / 2.0 # 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.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.0, 1.0, # these two r dummies 0.0, numpy.sqrt(2.0 * Ez), _justjz=True, **kwargs, )[2] else: jz = ( self._jzInterp(self._eval_R, Ez / thisEzZmax) * (numpy.exp(self._jzEzmaxInterp(self._eval_R)) - 10.0**-5.0) )[0][0] return jz ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleAdiabatic_c.py0000644000175100001660000001612714761352023022546 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): """ Use C to calculate actions using the adiabatic approximation Parameters ---------- pot : Potential or list of such instances Gravitational potential to compute actions in gamma : float as in Lz -> Lz+gamma * J_z R : numpy.ndarray R coordinate. vR : numpy.ndarray vR coordinate. vT : numpy.ndarray vT coordinate. z : numpy.ndarray z coordinate. vz : numpy.ndarray vz coordinate. Returns ------- tuple: (jr,jz,err) with radial, vertical action (numpy.ndarrays), and error (non-zero if error occurred) Notes ----- - 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): """ Calculate planar (Rperi,Rap) and the maximum height Zmax using the adiabatic approximation (rap = sqrt(Rap^2+Zmax^2)) Parameters ---------- pot : Potential or list of such instances Gravitational potential to compute actions in gamma : float as in Lz -> Lz+gamma * J_z R : numpy.ndarray R coordinate. vR : numpy.ndarray vR coordinate. vT : numpy.ndarray vT coordinate. z : numpy.ndarray z coordinate. vz : numpy.ndarray vz coordinate. Returns ------- tuple (Rperi,Rap,Zmax,err) Rperi,Rap,Zmax : numpy.ndarray, shape (len(R)) err - non-zero if error occurred Notes ----- - 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=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleHarmonic.py0000644000175100001660000001027414761352023022140 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): """ Initialize an actionAngleHarmonic object. Parameters ---------- omega : float or numpy.ndarray Frequencies (can be Quantity). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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): """ Evaluate the action for the harmonic oscillator Parameters ---------- 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) Returns ------- float or numpy.ndarray action Notes ----- - 2018-04-08 - Written - Bovy (UofT) """ if len(args) == 2: # x,vx x, vx = args return (vx**2.0 / self._omega + self._omega * x**2.0) / 2.0 else: # pragma: no cover raise ValueError("actionAngleHarmonic __call__ input not understood") def _actionsFreqs(self, *args, **kwargs): """ Evaluate the action and frequency for the harmonic oscillator Parameters ---------- 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) Returns ------- tuple (action,frequency) Notes ----- - 2018-04-08 - Written - Bovy (UofT) """ if len(args) == 2: # x,vx x, vx = args return ( (vx**2.0 / self._omega + self._omega * x**2.0) / 2.0, self._omega * numpy.ones_like(x), ) else: # pragma: no cover raise ValueError("actionAngleHarmonic __call__ input not understood") def _actionsFreqsAngles(self, *args, **kwargs): """ Evaluate the action, frequency, and angle for the harmonic oscillator Parameters ---------- 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) Returns ------- tuple (action,frequency,angle) Notes ----- - 2018-04-08 - Written - Bovy (UofT) """ if len(args) == 2: # x,vx x, vx = args return ( (vx**2.0 / self._omega + self._omega * x**2.0) / 2.0, 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=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleHarmonicInverse.py0000644000175100001660000000626514761352023023501 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): """ Initialize an actionAngleHarmonicInverse object. Parameters ---------- omega : float or Quantity Frequency. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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): """ Evaluate the phase-space coordinates (x,v) for a number of angles on a single torus Parameters ---------- j : float Action angle : numpy.ndarray Angle Returns ------- x_vx : list A list containing the phase-space coordinates [x,vx] Notes ----- - 2018-04-08 - Written - Bovy (UofT) """ return self._xvFreqs(j, angle, **kwargs)[:2] def _xvFreqs(self, j, angle, **kwargs): """ Evaluate the phase-space coordinates (x,v) for a number of angles on a single torus as well as the frequency Parameters ---------- j : float Action. angle : numpy.ndarray Angle. Returns ------- tuple Tuple containing: - x (numpy.ndarray): x-coordinate. - vx (numpy.ndarray): Velocity in x-direction. - Omega (float): Frequency. Notes ----- - 2018-04-08 - Written - Bovy (UofT) """ amp = numpy.sqrt(2.0 * 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): """ Return the frequency corresponding to a torus Parameters ---------- j : scalar action Returns ------- Omega : float frequency Notes ----- - 2018-04-08 - Written - Bovy (UofT) """ return self._omega ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleInverse.py0000644000175100001660000000723714761352023022020 0ustar00runnerdocker############################################################################### # actionAngleInverse.py: top-level class with common routines for inverse # actionAngle methods ############################################################################### from ..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): """ Evaluate the phase-space coordinates (x,v) for a number of angles on a single torus. Parameters ---------- jr : float Radial action. jphi : float Azimuthal action. jz : float Vertical action. angler : numpy.ndarray Radial angle. anglephi : numpy.ndarray Azimuthal angle. anglez : numpy.ndarray Vertical angle. Returns ------- numpy.ndarray [R,vR,vT,z,vz,phi] Notes ----- - 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): """ Evaluate the phase-space coordinates (x,v) for a number of angles on a single torus as well as the frequencies Parameters ---------- jr : float Radial action. jphi : float Azimuthal action. jz : float Vertical action. angler : numpy.ndarray Radial angle. anglephi : numpy.ndarray Azimuthal angle. anglez : numpy.ndarray Vertical angle. Returns ------- tuple A tuple containing the phase-space coordinates (R,vR,vT,z,vz,phi) and the frequencies (OmegaR,Omegaphi,Omegaz). Notes ----- - 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): """ Return the frequencies corresponding to a torus Parameters ---------- jr : float Radial action. jphi : float Azimuthal action. jz : float Vertical action. Returns ------- tuple A tuple of three floats representing the frequencies (OmegaR, Omegaphi, Omegaz). Notes ----- - 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=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleIsochrone.py0000644000175100001660000003451614761352023022336 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): """ Initialize an actionAngleIsochrone object. Parameters ---------- b : float or Quantity, optional Scale parameter of the isochrone parameter. ip : IsochronePotential, optional Instance of a IsochronePotential. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - Specify either b or ip - 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.0 + 1.0) self.amp = (self.b + rb) ** 2.0 * 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): """ Evaluate the actions (jr,lz,jz). Parameters ---------- *args : tuple 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 Returns ------- tuple (jr,lz,jz) Notes ----- - 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.0 / 2.0 + vT**2.0 / 2.0 + vz**2.0 / 2.0 L = numpy.sqrt(L2) # Actions Jphi = Lz Jz = L - numpy.fabs(Lz) Jr = self.amp / numpy.sqrt(-2.0 * E) - 0.5 * ( L + numpy.sqrt(L2 + 4.0 * self.amp * self.b) ) return (Jr, Jphi, Jz) def _actionsFreqs(self, *args, **kwargs): """ Evaluate the actions and frequencies (jr,lz,jz,Omegar,Omegaphi,Omegaz). Parameters ---------- *args : tuple 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 Returns ------- tuple (jr,lz,jz,Omegar,Omegaphi,Omegaz) Notes ----- - 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.0 / 2.0 + vT**2.0 / 2.0 + vz**2.0 / 2.0 L = numpy.sqrt(L2) # Actions Jphi = Lz Jz = L - numpy.fabs(Lz) Jr = self.amp / numpy.sqrt(-2.0 * E) - 0.5 * ( L + numpy.sqrt(L2 + 4.0 * self.amp * self.b) ) # Frequencies Omegar = (-2.0 * E) ** 1.5 / self.amp Omegaz = 0.5 * (1.0 + L / numpy.sqrt(L2 + 4.0 * self.amp * self.b)) * Omegar Omegaphi = copy.copy(Omegaz) indx = Lz < 0.0 Omegaphi[indx] *= -1.0 return (Jr, Jphi, Jz, Omegar, Omegaphi, Omegaz) def _actionsFreqsAngles(self, *args, **kwargs): """ Evaluate the actions, frequencies, and angles (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez). Parameters ---------- *args : tuple 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 Returns ------- tuple (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) Notes ----- - 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.0 / 2.0 + vT**2.0 / 2.0 + vz**2.0 / 2.0 L = numpy.sqrt(L2) # Actions Jphi = Lz Jz = L - numpy.fabs(Lz) Jr = self.amp / numpy.sqrt(-2.0 * E) - 0.5 * ( L + numpy.sqrt(L2 + 4.0 * self.amp * self.b) ) # Frequencies Omegar = (-2.0 * E) ** 1.5 / self.amp Omegaz = 0.5 * (1.0 + L / numpy.sqrt(L2 + 4.0 * self.amp * self.b)) * Omegar Omegaphi = copy.copy(Omegaz) indx = Lz < 0.0 Omegaphi[indx] *= -1.0 # Angles c = -self.amp / 2.0 / E - self.b e2 = 1.0 - L2 / self.amp / c * (1.0 + self.b / c) e = numpy.sqrt(e2) if self.b == 0.0: coseta = 1 / e * (1.0 - numpy.sqrt(R**2.0 + z**2.0) / c) else: s = 1.0 + numpy.sqrt(1.0 + (R**2.0 + z**2.0) / self.b**2.0) coseta = 1 / e * (1.0 - self.b / c * (s - 2.0)) pindx = coseta > 1.0 coseta[pindx] = 1.0 pindx = coseta < -1.0 coseta[pindx] = -1.0 eta = numpy.arccos(coseta) costheta = z / numpy.sqrt(R**2.0 + z**2.0) sintheta = R / numpy.sqrt(R**2.0 + z**2.0) vrindx = (vR * sintheta + vz * costheta) < 0.0 eta[vrindx] = 2.0 * numpy.pi - eta[vrindx] angler = eta - e * c / (c + self.b) * numpy.sin(eta) tan11 = numpy.arctan( numpy.sqrt((1.0 + e) / (1.0 - e)) * numpy.tan(0.5 * eta) ) tan12 = numpy.arctan( numpy.sqrt((1.0 + e + 2.0 * self.b / c) / (1.0 - e + 2.0 * self.b / c)) * numpy.tan(0.5 * eta) ) vzindx = (-vz * sintheta + vR * costheta) > 0.0 tan11[tan11 < 0.0] += numpy.pi tan12[tan12 < 0.0] += numpy.pi pindx = Lz / L > 1.0 Lz[pindx] = L[pindx] pindx = Lz / L < -1.0 Lz[pindx] = -L[pindx] sini = numpy.sqrt(L**2.0 - Lz**2.0) / L tani = numpy.sqrt(L**2.0 - Lz**2.0) / Lz sinpsi = costheta / sini pindx = (sinpsi > 1.0) * numpy.isfinite(sinpsi) sinpsi[pindx] = 1.0 pindx = (sinpsi < -1.0) * numpy.isfinite(sinpsi) sinpsi[pindx] = -1.0 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.0 * numpy.pi) anglez = ( psi + Omegaz / Omegar * angler - tan11 - 1.0 / numpy.sqrt(1.0 + 4 * self.amp * self.b / L2) * tan12 ) sinu = z / R / tani pindx = (sinu > 1.0) * numpy.isfinite(sinu) sinu[pindx] = 1.0 pindx = (sinu < -1.0) * numpy.isfinite(sinu) sinu[pindx] = -1.0 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.0 * numpy.pi) anglephi = anglephi % (2.0 * numpy.pi) anglez = anglez % (2.0 * numpy.pi) return (Jr, Jphi, Jz, Omegar, Omegaphi, Omegaz, angler, anglephi, anglez) def _EccZmaxRperiRap(self, *args, **kwargs): 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.0 / 2.0 + vT**2.0 / 2.0 + vz**2.0 / 2.0 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.0 / E me2 = L2 / self.amp / a e = numpy.sqrt(1.0 - me2) rperi = a * (1.0 - e) rap = a * (1.0 + e) else: smin = ( 0.5 * ( (2.0 * E - self.amp / self.b) + numpy.sqrt( (2.0 * E - self.amp / self.b) ** 2.0 + 2.0 * E * (4.0 * self.amp / self.b + L2 / self.b**2.0) ) ) / E ) smax = 2.0 - self.amp / E / self.b - smin rperi = smin * numpy.sqrt(1.0 - 2.0 / smin) * self.b rap = smax * numpy.sqrt(1.0 - 2.0 / smax) * self.b return ( (rap - rperi) / (rap + rperi), rap * numpy.sqrt(1.0 - Lz**2.0 / L2), rperi, rap, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleIsochroneApprox.py0000644000175100001660000011774214761352023023533 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.0 * 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): """ Initialize an actionAngleIsochroneApprox object. Parameters ---------- b : float or Quantity, optional Scale parameter of the isochrone parameter. ip : IsochronePotential, optional Instance of a IsochronePotential. aAI : actionAngleIsochrone, optional Instance of an actionAngleIsochrone. pot : Potential or list of Potentials, optional Potential to calculate action-angle variables for. tintJ : float, optional Time to integrate orbits for to estimate actions (can be Quantity). ntintJ : int, optional Number of time-integration points. integrate_method : str, optional Integration method to use. dt : float, optional orbit.integrate dt keyword (for fixed stepsize integration). maxn : int, optional Default value for all methods when using a grid in vec(n) up to this n (zero-based). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0)) self._tintJ = kwargs.get("tintJ", 100.0) 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.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): """ Evaluate the actions (jr,lz,jz). Parameters ---------- *args : tuple 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: bool, optional if True, return the cumulative average actions (to look at convergence). Returns ------- tuple (jr,lz,jz) Notes ----- - 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): """ Evaluate the actions (jr,lz,jz) and frequencies (Or,Op,Oz). Parameters ---------- *args : tuple 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: int, optional Use a grid in vec(n) up to this n (zero-based). Default is the object-wide default. ts : float, optional 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 : bool, optional if True and Orbits are given, the backward part of the orbit is integrated first and stored in the Orbit object Returns ------- tuple (jr,lz,jz,Or,Op,Oz) Notes ----- - 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): """ Evaluate the actions (jr,lz,jz), frequencies (Or,Op,Oz), and angles (angler,anglephi,anglez). Parameters ---------- *args : tuple 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: int, optional Use a grid in vec(n) up to this n (zero-based). Default is the object-wide default. ts : float, optional 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 : bool, optional if True and Orbits are given, the backward part of the orbit is integrated first and stored in the Orbit object Returns ------- tuple (jr,lz,jz,Or,Op,Oz,angler,anglephi,anglez) Notes ----- - 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 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.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.0 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): """ Plot the angles vs. each other, to check whether the isochrone approximation is good. Parameters ---------- *args : tuple 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 type: {'araz','araphi','azaphi','jr','lz','jz'}, optional type of plot to make deperiod: bool, optional if True, de-period the angles. downsample: bool, optional if True, downsample what's plotted to 400 points. **kwargs: dict, optional Any other keyword argument supported by plot.bovy_plot (e.g., xrange=[xmin,xmax]) Returns ------- plot Notes ----- - 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.0, scatter=True, edgecolor="none", xlabel=r"$t$", ylabel=r"$J^A_R / \langle J^A_R \rangle$", clabel=r"$\theta^A_R$", vmin=0.0, vmax=2.0 * numpy.pi, crange=[0.0, 2.0 * 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.0, scatter=True, edgecolor="none", xlabel=r"$t$", ylabel=r"$L^A_Z / \langle L^A_Z \rangle$", clabel=r"$\theta^A_\phi$", vmin=0.0, vmax=2.0 * numpy.pi, crange=[0.0, 2.0 * 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.0, scatter=True, edgecolor="none", xlabel=r"$t$", ylabel=r"$J^A_Z / \langle J^A_Z \rangle$", clabel=r"$\theta^A_Z$", vmin=0.0, vmax=2.0 * numpy.pi, crange=[0.0, 2.0 * 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.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.0 * numpy.pi + 0.5] yrange = [-0.5, 2.0 * numpy.pi + 0.5] vmin, vmax = 0.0, 2.0 * 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.0, 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.0, 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.0, 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.0**-7.0 # To avoid numpy warnings for vz = numpy.zeros((no, ntJ)) + 10.0**-7.0 # 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.0**-7.0 ) # To avoid numpy warnings for ovz = numpy.zeros((no, 2 * nt - 1)) + 10.0**-7.0 # 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): """ Estimate a good value for the scale of the isochrone potential by matching the slope of the rotation curve Parameters ---------- pot : Potential or list thereof Potential or list of potentials to estimate the scale of the isochrone potential for R : float or Quantity Galactocentric radius. z : float or Quantity Vertical height. phi : float or Quantity, optional Azimuth (optional). Returns ------- float or tuple If a single (R,z,[phi]) is given, a single value b is returned. If a list of (R,z,[phi]) is given, a tuple (bmin,bmedian,bmax) is returned. Notes ----- - 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.0 + 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) - 0.5 * r2 / (r2 + x**2.0)), 0.01, 100.0, ) 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.0 addto = numpy.cumsum(w.astype(int), axis=1) return arr + _TWOPI * addto ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleIsochroneInverse.py0000644000175100001660000002037414761352023023667 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): """ Initialize an actionAngleIsochroneInverse object. Parameters ---------- b : float or Quantity, optional Scale parameter of the isochrone parameter. ip : galpy.potential.IsochronePotential, optional Instance of a IsochronePotential. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - Either specify b or ip. - 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.0 + 1.0) self.amp = (self.b + rb) ** 2.0 * 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): """ Evaluate the phase-space coordinates (x,v) for a number of angles on a single torus. Parameters ---------- jr : float Radial action. jphi : float Azimuthal action. jz : float Vertical action. angler : numpy.ndarray Radial angle. anglephi : numpy.ndarray Azimuthal angle. anglez : numpy.ndarray Vertical angle. Returns ------- numpy.ndarray Phase-space coordinates [R,vR,vT,z,vz,phi]. Notes ----- - 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): """ Evaluate the phase-space coordinates (x,v) for a number of angles on a single torus as well as the frequencies. Parameters ---------- jr : float Radial action. jphi : float Azimuthal action. jz : float Vertical action. angler : numpy.ndarray Radial angle. anglephi : numpy.ndarray Azimuthal angle. anglez : numpy.ndarray Vertical angle. Returns ------- tuple A tuple containing the phase-space coordinates (R, vR, vT, z, vz, phi), and the frequencies (OmegaR, Omegaphi, Omegaz). Notes ----- - 2017-11-15 - Written - Bovy (UofT). """ L = jz + numpy.fabs(jphi) # total angular momentum L2 = L**2.0 sqrtfourbkL2 = numpy.sqrt(L2 + 4.0 * self.b * self.amp) H = -2.0 * self.amp**2.0 / (2.0 * jr + L + sqrtfourbkL2) ** 2.0 # Calculate the frequencies omegar = (-2.0 * H) ** 1.5 / self.amp omegaz = (1.0 + L / sqrtfourbkL2) / 2.0 * omegar # Start on getting the coordinates a = -self.amp / 2.0 / H - self.b ab = a + self.b e = numpy.sqrt(1.0 + L2 / (2.0 * H * a**2.0)) # Solve Kepler's-ish equation; ar must be between 0 and 2pi angler = (numpy.atleast_1d(angler) % (-2.0 * numpy.pi)) % (2.0 * 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.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.0, 2.0 * numpy.pi ) coseta = numpy.cos(eta) r = a * numpy.sqrt((1.0 - e * coseta) * (1.0 - e * coseta + 2.0 * self.b / a)) vr = numpy.sqrt(self.amp / ab) * a * e * numpy.sin(eta) / r taneta2 = numpy.tan(eta / 2.0) tan11 = numpy.arctan(numpy.sqrt((1.0 + e) / (1.0 - e)) * taneta2) tan12 = numpy.arctan( numpy.sqrt((a * (1.0 + e) + 2.0 * self.b) / (a * (1.0 - e) + 2.0 * self.b)) * taneta2 ) tan11[tan11 < 0.0] += numpy.pi tan12[tan12 < 0.0] += numpy.pi Lambdaeta = tan11 + L / sqrtfourbkL2 * tan12 psi = anglez - omegaz / omegar * angler + Lambdaeta lowerl = numpy.sqrt(1.0 - jphi**2.0 / L2) sintheta = numpy.sin(psi) * lowerl costheta = numpy.sqrt(1.0 - sintheta**2.0) 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.0] = numpy.pi - u[vtheta < 0.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.0 * numpy.pi) phi[phi < 0.0] += 2.0 * numpy.pi return (R, vR, jphi / R, z, vz, phi, omegar, numpy.sign(jphi) * omegaz, omegaz) def _Freqs(self, jr, jphi, jz, **kwargs): """ Return the frequencies corresponding to a torus Parameters ---------- jr : float Radial action jphi : float Azimuthal action jz : float Vertical action Returns ------- tuple A tuple of three floats representing the frequencies (OmegaR, Omegaphi, Omegaz) Notes ----- - 2017-11-15 - Written - Bovy (UofT). """ L = jz + numpy.fabs(jphi) # total angular momentum sqrtfourbkL2 = numpy.sqrt(L**2.0 + 4.0 * self.b * self.amp) H = -2.0 * self.amp**2.0 / (2.0 * jr + L + sqrtfourbkL2) ** 2.0 # Calculate the frequencies omegar = (-2.0 * H) ** 1.5 / self.amp omegaz = (1.0 + L / sqrtfourbkL2) / 2.0 * omegar return (omegar, numpy.sign(jphi) * omegaz, omegaz) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleSpherical.py0000644000175100001660000007333114761352023022315 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, optimize from ..potential import _dim, epifreq, omegac, vcirc from ..potential.planarPotential import _evaluateplanarPotentials from ..potential.Potential import _evaluatePotentials from ..potential.Potential import flatten as flatten_potential from ..util import quadpack from .actionAngle import UnboundError, actionAngle _EPS = 10.0**-15.0 class actionAngleSpherical(actionAngle): """Action-angle formalism for spherical potentials""" def __init__(self, *args, **kwargs): """ Initialize an actionAngleSpherical object. Parameters ---------- pot : Potential or list thereof A spherical potential. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). _gamma : float, optional Replace Lz by Lz+gamma Jz in effective potential when using this class as part of actionAngleAdiabatic (internal use). Notes ----- - 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, only potential # used in this class 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 # gamma for when we use this as part of the adiabatic approx. self._gamma = kwargs.get("_gamma", 0.0) # Check the units self._check_consistent_units() return None def _evaluate(self, *args, **kwargs): """ Evaluate the actions (jr,lz,jz). Parameters ---------- *args : tuple 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: bool, optional if True, use n=10 fixed_quad integration **kwargs: dict, optional scipy.integrate.quadrature or .fixed_quad keywords Returns ------- tuple (jr,lz,jz) Notes ----- - 2013-12-28 - Written - Bovy (IAS) """ fixed_quad = kwargs.pop("fixed_quad", False) extra_Jz = kwargs.pop("_Jz", None) 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: r = numpy.sqrt(R**2.0 + z**2.0) vr = (R * vR + z * vz) / r Lz = R * vT Lx = -z * vT Ly = z * vR - R * vz L2 = Lx * Lx + Ly * Ly + Lz * Lz E = ( _evaluateplanarPotentials(self._2dpot, r) + vR**2.0 / 2.0 + vT**2.0 / 2.0 + vz**2.0 / 2.0 ) L = numpy.sqrt(L2) vt = L / r if self._gamma != 0.0 and not extra_Jz is None: L += self._gamma * extra_Jz E += L**2.0 / 2.0 / r**2.0 - vt**2.0 / 2.0 # Actions Jphi = Lz Jz = L - numpy.fabs(Lz) # Jr requires some more work Jr = [] for ii in range(len(r)): rperi, rap = self._calc_rperi_rap(r[ii], vr[ii], vt[ii], E[ii], L[ii]) Jr.append(self._calc_jr(rperi, rap, E[ii], L[ii], fixed_quad, **kwargs)) return (numpy.array(Jr), Jphi, Jz) def _actionsFreqs(self, *args, **kwargs): """ Evaluate the actions and frequencies (jr,lz,jz,Omegar,Omegaphi,Omegaz). Parameters ---------- *args : tuple 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: bool, optional if True, use n=10 fixed_quad integration **kwargs: dict, optional scipy.integrate.quadrature or .fixed_quad keywords Returns ------- tuple (jr,lz,jz,Omegar,Omegaphi,Omegaz) Notes ----- - 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: r = numpy.sqrt(R**2.0 + z**2.0) vr = (R * vR + z * vz) / r Lz = R * vT Lx = -z * vT Ly = z * vR - R * vz L2 = Lx * Lx + Ly * Ly + Lz * Lz E = ( _evaluateplanarPotentials(self._2dpot, r) + vR**2.0 / 2.0 + vT**2.0 / 2.0 + vz**2.0 / 2.0 ) L = numpy.sqrt(L2) vt = L / r # Actions Jphi = Lz Jz = L - numpy.fabs(Lz) # Jr requires some more work Jr = [] Or = [] Op = [] for ii in range(len(r)): rperi, rap = self._calc_rperi_rap(r[ii], vr[ii], vt[ii], E[ii], L[ii]) Jr.append(self._calc_jr(rperi, rap, E[ii], L[ii], fixed_quad, **kwargs)) # Radial period if Jr[-1] < 10.0**-9.0: # Circular orbit Or.append(epifreq(self._2dpot, r[ii], use_physical=False)) Op.append(omegac(self._2dpot, r[ii], use_physical=False)) continue Rmean = ( numpy.exp((numpy.log(rperi) + numpy.log(rap)) / 2.0) if rperi > 0.0 else rap / 2.0 ) Or.append( self._calc_or(Rmean, rperi, rap, E[ii], L[ii], fixed_quad, **kwargs) ) Op.append( self._calc_op( Or[-1], Rmean, rperi, rap, E[ii], L[ii], fixed_quad, **kwargs ) ) Op = numpy.array(Op) Oz = copy.copy(Op) Op[vT < 0.0] *= -1.0 return (numpy.array(Jr), Jphi, Jz, numpy.array(Or), Op, Oz) def _actionsFreqsAngles(self, *args, **kwargs): """ Evaluate the actions, frequencies, and angles (jr,lz,jz,Omegar,Omegaphi,Omegaz,ar,aphi,az). Parameters ---------- *args : tuple 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: bool, optional if True, use n=10 fixed_quad integration **kwargs: dict, optional scipy.integrate.quadrature or .fixed_quad keywords Returns ------- tuple (jr,lz,jz,Omegar,Omegaphi,Omegaz,ar,aphi,az) Notes ----- - 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: r = numpy.sqrt(R**2.0 + z**2.0) vr = (R * vR + z * vz) / r vtheta = (z * vR - R * vz) / r Lz = R * vT Lx = -z * vT Ly = z * vR - R * vz L2 = Lx * Lx + Ly * Ly + Lz * Lz E = ( _evaluateplanarPotentials(self._2dpot, r) + vR**2.0 / 2.0 + vT**2.0 / 2.0 + vz**2.0 / 2.0 ) L = numpy.sqrt(L2) vt = L / r # Actions Jphi = Lz Jz = L - numpy.fabs(Lz) # Jr requires some more work Jr = [] Or = [] Op = [] ar = [] az = [] # Calculate the longitude of the ascending node asc = self._calc_long_asc(z, R, vtheta, phi, Lz, L) for ii in range(len(r)): rperi, rap = self._calc_rperi_rap(r[ii], vr[ii], vt[ii], E[ii], L[ii]) Jr.append(self._calc_jr(rperi, rap, E[ii], L[ii], fixed_quad, **kwargs)) # Radial period Rmean = ( numpy.exp((numpy.log(rperi) + numpy.log(rap)) / 2.0) if rperi > 0 else rap / 2.0 ) if Jr[-1] < 10.0**-9.0: # Circular orbit Or.append(epifreq(self._2dpot, r[ii], use_physical=False)) Op.append(omegac(self._2dpot, r[ii], use_physical=False)) else: Or.append( self._calc_or( Rmean, rperi, rap, E[ii], L[ii], fixed_quad, **kwargs ) ) Op.append( self._calc_op( Or[-1], Rmean, rperi, rap, E[ii], L[ii], fixed_quad, **kwargs, ) ) # Angles ar.append( self._calc_angler( Or[-1], r[ii], Rmean, rperi, rap, E[ii], L[ii], vr[ii], fixed_quad, **kwargs, ) ) az.append( self._calc_anglez( Or[-1], Op[-1], ar[-1], z[ii], r[ii], Rmean, rperi, rap, E[ii], L[ii], Lz[ii], vr[ii], vtheta[ii], phi[ii], fixed_quad, **kwargs, ) ) Op = numpy.array(Op) Oz = copy.copy(Op) Op[vT < 0.0] *= -1.0 ap = copy.copy(asc) ar = numpy.array(ar) az = numpy.array(az) ap[vT < 0.0] -= az[vT < 0.0] ap[vT >= 0.0] += az[vT >= 0.0] ar = ar % (2.0 * numpy.pi) ap = ap % (2.0 * numpy.pi) az = az % (2.0 * numpy.pi) return (numpy.array(Jr), Jphi, Jz, numpy.array(Or), Op, Oz, ar, ap, az) def _EccZmaxRperiRap(self, *args, **kwargs): """ Evaluate the eccentricity, maximum height above the plane, peri- and apocenter for a spherical potential. Parameters ---------- *args : tuple 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 Returns ------- tuple (e,zmax,rperi,rap) Notes ----- - 2017-12-22 - Written - Bovy (UofT) """ extra_Jz = kwargs.pop("_Jz", None) 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: r = numpy.sqrt(R**2.0 + z**2.0) vr = (R * vR + z * vz) / r Lz = R * vT Lx = -z * vT Ly = z * vR - R * vz L2 = Lx * Lx + Ly * Ly + Lz * Lz L = numpy.sqrt(L2) E = ( _evaluateplanarPotentials(self._2dpot, r) + vR**2.0 / 2.0 + vT**2.0 / 2.0 + vz**2.0 / 2.0 ) vt = L / r if self._gamma != 0.0 and not extra_Jz is None: L += self._gamma * extra_Jz E += L**2.0 / 2.0 / r**2.0 - vt**2.0 / 2.0 rperi, rap = [], [] for ii in range(len(r)): trperi, trap = self._calc_rperi_rap(r[ii], vr[ii], vt[ii], E[ii], L[ii]) rperi.append(trperi) rap.append(trap) rperi = numpy.array(rperi) rap = numpy.array(rap) return ( (rap - rperi) / (rap + rperi), rap * numpy.sqrt(1.0 - Lz**2.0 / L2), rperi, rap, ) def _calc_rperi_rap(self, r, vr, vt, E, L): if ( vr == 0.0 and numpy.fabs(vt - vcirc(self._2dpot, r, use_physical=False)) < _EPS ): # We are on a circular orbit rperi = r rap = r elif vr == 0.0 and vt > vcirc(self._2dpot, r, use_physical=False): # We are exactly at pericenter rperi = r if self._gamma != 0.0: startsign = _rapRperiAxiEq(r + 10.0**-8.0, E, L, self._2dpot) startsign /= numpy.fabs(startsign) else: startsign = 1.0 rend = _rapRperiAxiFindStart( r, E, L, self._2dpot, rap=True, startsign=startsign ) rap = optimize.brentq( _rapRperiAxiEq, rperi + 0.00001, rend, args=(E, L, self._2dpot) ) elif vr == 0.0 and vt < vcirc(self._2dpot, r, use_physical=False): # We are exactly at apocenter rap = r if self._gamma != 0.0: startsign = _rapRperiAxiEq(r - 10.0**-8.0, E, L, self._2dpot) startsign /= numpy.fabs(startsign) else: startsign = 1.0 rstart = _rapRperiAxiFindStart(r, E, L, self._2dpot, startsign=startsign) if rstart == 0.0: rperi = 0.0 else: rperi = optimize.brentq( _rapRperiAxiEq, rstart, rap - 0.000001, args=(E, L, self._2dpot) ) else: if self._gamma != 0.0: startsign = _rapRperiAxiEq(r, E, L, self._2dpot) startsign /= numpy.fabs(startsign) else: startsign = 1.0 rstart = _rapRperiAxiFindStart(r, E, L, self._2dpot, startsign=startsign) if rstart == 0.0: rperi = 0.0 else: try: rperi = optimize.brentq( _rapRperiAxiEq, rstart, r, (E, L, self._2dpot), maxiter=200 ) except RuntimeError: # pragma: no cover raise UnboundError("Orbit seems to be unbound") rend = _rapRperiAxiFindStart( r, E, L, self._2dpot, rap=True, startsign=startsign ) rap = optimize.brentq(_rapRperiAxiEq, r, rend, (E, L, self._2dpot)) return (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.0 if Rmean > rperi and not fixed_quad: Tr += numpy.array( quadpack.quadrature( _TrSphericalIntegrandSmall, 0.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.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( quadpack.quadrature( _TrSphericalIntegrandLarge, 0.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.0, numpy.sqrt(rap - Rmean), args=(E, L, self._2dpot, rap), n=10, **kwargs, )[0] Tr = 2.0 * Tr return 2.0 * numpy.pi / Tr def _calc_op(self, Or, Rmean, rperi, rap, E, L, fixed_quad, **kwargs): # Azimuthal period I = 0.0 if Rmean > rperi and not fixed_quad: I += numpy.array( quadpack.quadrature( _ISphericalIntegrandSmall, 0.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.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( quadpack.quadrature( _ISphericalIntegrandLarge, 0.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.0, numpy.sqrt(rap - Rmean), args=(E, L, self._2dpot, rap), n=10, **kwargs, )[0] I *= 2 * L return I * Or / 2.0 / numpy.pi def _calc_long_asc(self, z, R, vtheta, phi, Lz, L): i = numpy.arccos(Lz / L) sinu = z / R / numpy.tan(i) pindx = (sinu > 1.0) * (sinu < (1.0 + 10.0**-7.0)) sinu[pindx] = 1.0 pindx = (sinu < -1.0) * numpy.isfinite(sinu) sinu[pindx] = -1.0 u = numpy.arcsin(sinu) vzindx = vtheta > 0.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 * quadpack.quadrature( _TrSphericalIntegrandSmall, 0.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.0, numpy.sqrt(r - rperi), args=(E, L, self._2dpot, rperi), n=10, **kwargs, )[0] ) else: wr = 0.0 if vr < 0.0: wr = 2 * numpy.pi - wr else: if r < rap and not fixed_quad: wr = ( Or * quadpack.quadrature( _TrSphericalIntegrandLarge, 0.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.0, numpy.sqrt(rap - r), args=(E, L, self._2dpot, rap), n=10, **kwargs, )[0] ) else: wr = numpy.pi if vr < 0.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, vtheta, phi, fixed_quad, **kwargs, ): # First calculate psi i = numpy.arccos(Lz / L) sinpsi = z / r / numpy.sin(i) if numpy.isfinite(sinpsi): sinpsi = 1.0 if sinpsi > 1.0 else (-1.0 if sinpsi < -1.0 else sinpsi) psi = numpy.arcsin(sinpsi) if vtheta > 0.0: psi = numpy.pi - psi else: psi = phi psi = psi % (2.0 * numpy.pi) # Calculate dSr/dL dpsi = Op / Or * 2.0 * numpy.pi # this is the full I integral if r < Rmean: if not fixed_quad: wz = ( L * quadpack.quadrature( _ISphericalIntegrandSmall, 0.0, numpy.sqrt(r - rperi), args=(E, L, self._2dpot, rperi), **kwargs, )[0] ) elif fixed_quad: wz = ( L * integrate.fixed_quad( _ISphericalIntegrandSmall, 0.0, numpy.sqrt(r - rperi), args=(E, L, self._2dpot, rperi), n=10, **kwargs, )[0] ) if vr < 0.0: wz = dpsi - wz else: if not fixed_quad: wz = ( L * quadpack.quadrature( _ISphericalIntegrandLarge, 0.0, numpy.sqrt(rap - r), args=(E, L, self._2dpot, rap), **kwargs, )[0] ) elif fixed_quad: wz = ( L * integrate.fixed_quad( _ISphericalIntegrandLarge, 0.0, numpy.sqrt(rap - r), args=(E, L, self._2dpot, rap), n=10, **kwargs, )[0] ) if vr < 0.0: wz = dpsi / 2.0 + wz else: wz = dpsi / 2.0 - 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.0 * (E - _evaluateplanarPotentials(pot, r)) - L**2.0 / r**2.0) def _TrSphericalIntegrandSmall(t, E, L, pot, rperi): r = rperi + t**2.0 # part of the transformation return 2.0 * t / _JrSphericalIntegrand(r, E, L, pot) def _TrSphericalIntegrandLarge(t, E, L, pot, rap): r = rap - t**2.0 # part of the transformation return 2.0 * t / _JrSphericalIntegrand(r, E, L, pot) def _ISphericalIntegrandSmall(t, E, L, pot, rperi): r = rperi + t**2.0 # part of the transformation return 2.0 * t / _JrSphericalIntegrand(r, E, L, pot) / r**2.0 def _ISphericalIntegrandLarge(t, E, L, pot, rap): r = rap - t**2.0 # part of the transformation return 2.0 * t / _JrSphericalIntegrand(r, E, L, pot) / r**2.0 def _rapRperiAxiEq(R, E, L, pot): """The vr=0 equation that needs to be solved to find apo- and pericenter""" return E - _evaluateplanarPotentials(pot, R) - L**2.0 / 2.0 / R**2.0 def _rapRperiAxiFindStart(R, E, L, pot, rap=False, startsign=1.0): """ Find adequate start or end points to solve for rap and rperi Parameters ---------- R : float Galactocentric radius E : float energy L : float angular momentum pot : Potential object or list thereof Potential rap : bool, optional if True, find the rap end-point (default is False) startsign : float, optional set to -1 if the function is not positive (due to gamma in the modified adiabatic approximation) (default is 1.0) Returns ------- float rstart or rend Notes ----- - 2010-12-01 - Written - Bovy (NYU) """ if rap: rtry = 2.0 * R else: rtry = R / 2.0 while startsign * _rapRperiAxiEq(rtry, E, L, pot) > 0.0 and rtry > 0.000000001: if rap: if rtry > 100.0: # pragma: no cover raise UnboundError("Orbit seems to be unbound") rtry *= 2.0 else: rtry /= 2.0 if rtry < 0.000000001: return 0.0 return rtry ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleStaeckel.py0000644000175100001660000015367614761352023022151 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 ( PotentialError, _check_c, _evaluatePotentials, _evaluateRforces, _evaluatezforces, _isNonAxi, ) 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): """ Initialize an actionAngleStaeckel object. Parameters ---------- pot : potential or list of potentials (3D) The potential or list of potentials. delta : float or Quantity The focus. useu0 : bool, optional Use u0 to calculate dV (not recommended). Default is False. c : bool, optional If True, always use C for calculations. Default is False. order : int, optional Number of points to use in the Gauss-Legendre numerical integration of the relevant action, frequency, and angle integrals. Default is 10. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 2012-11-27 - Started - 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): """ Evaluate the actions (jr,lz,jz). Parameters ---------- *args : tuple 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: bool, optional 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: float, optional if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed). c: bool, optional True/False to override the object-wide setting for whether or not to use the C implementation. order: int, optional number of points to use in the Gauss-Legendre numerical integration of the relevant action integrals. fixed_quad: bool, optional if True, use Gaussian quadrature (scipy.integrate.fixed_quad instead of scipy.integrate.quad). **kwargs: dict, optional scipy.integrate.fixed_quad or .quad keywords when not using C Returns ------- tuple (jr,lz,jz) Notes ----- - 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.0 / 2.0 + vz[ii] ** 2.0 / 2.0 + vT[ii] ** 2.0 / 2.0 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[0] ojz[ii] = tjz[0] olz[ii] = tlz[0] 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[0] if hasattr(delta, "__len__") else 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): """ Evaluate the actions and frequencies (jr,lz,jz,Omegar,Omegaphi,Omegaz). Parameters ---------- *args : tuple 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: bool, optional 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: float, optional if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed). c: bool, optional True/False to override the object-wide setting for whether or not to use the C implementation. order: int, optional number of points to use in the Gauss-Legendre numerical integration of the relevant action integrals. fixed_quad: bool, optional if True, use Gaussian quadrature (scipy.integrate.fixed_quad instead of scipy.integrate.quad). **kwargs: dict, optional scipy.integrate.fixed_quad or .quad keywords when not using C Returns ------- tuple (jr,lz,jz,Omegar,Omegaphi,Omegaz) Notes ----- - 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.0 / 2.0 + vz[ii] ** 2.0 / 2.0 + vT[ii] ** 2.0 / 2.0 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.0**-3.0) + numpy.isnan(Omegaz) * ( jz < 10.0**-3.0 ) # 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): """ Evaluate the actions, frequencies, and angles (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez). Parameters ---------- *args : tuple 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: bool, optional 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: float, optional if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed). c: bool, optional True/False to override the object-wide setting for whether or not to use the C implementation. order: int, optional number of points to use in the Gauss-Legendre numerical integration of the relevant action integrals. fixed_quad: bool, optional if True, use Gaussian quadrature (scipy.integrate.fixed_quad instead of scipy.integrate.quad). **kwargs: dict, optional scipy.integrate.fixed_quad or .quad keywords when not using C Returns ------- tuple (jr,lz,jz,Omegar,Omegaphi,Omegaz,angler,anglephi,anglez) Notes ----- - 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.0 / 2.0 + vz[ii] ** 2.0 / 2.0 + vT[ii] ** 2.0 / 2.0 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.0**-3.0) + numpy.isnan(Omegaz) * ( jz < 10.0**-3.0 ) # 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): """ Evaluate the eccentricity, maximum height above the plane, peri- and apocenter in the Staeckel approximation. Parameters ---------- *args : tuple 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: bool, optional 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: float, optional if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed). c: bool, optional True/False to override the object-wide setting for whether or not to use the C implementation. Returns ------- tuple (e,zmax,rperi,rap) Notes ----- - 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.0, delta=delta)[0] rap_tmp, zmax = coords.uv_to_Rz(umax, vmin, delta=delta) rap = numpy.sqrt(rap_tmp**2.0 + zmax**2.0) e = (rap - rperi) / (rap + rperi) return (e, zmax, rperi, rap) def _uminumaxvmin(self, *args, **kwargs): """ Evaluate u_min, u_max, and v_min in the Staeckel approximation. Parameters ---------- *args : tuple 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: bool, optional 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: float, optional if object-wide option useu0 is set, u0 to use (if useu0 and useu0 is None, a good value will be computed). c: bool, optional True/False to override the object-wide setting for whether or not to use the C implementation. Returns ------- tuple (u_min, u_max, v_min) Notes ----- - 2017-12-12 - Written - Bovy (UofT) """ delta = numpy.atleast_1d(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.0 / 2.0 + vz[ii] ** 2.0 / 2.0 + vT[ii] ** 2.0 / 2.0 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) tkwargs["delta"] = delta[ii] if len(delta) > 1 else delta[0] tumin, tumax, tvmin = self._uminumaxvmin(*targs, **tkwargs) oumin[ii] = tumin[0] oumax[ii] = tumax[0] ovmin[ii] = tvmin[0] 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[0] ) 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): """ Initialize an actionAngleStaeckelSingle object Parameters ---------- *args : tuple 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 pot: Potential or list of Potentials Potential to use delta: float, optional focal length of confocal coordinate system Notes ----- - 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.0 - self._pux**2.0 / 2.0 / self._delta**2.0 - self._Lz**2.0 / 2.0 / self._delta**2.0 / self._sinhux**2.0 ) self._potupi2 = potentialStaeckel( self._ux, numpy.pi / 2.0, self._pot, self._delta ) dV = self._coshux**2.0 * self._potupi2 - ( self._sinhux**2.0 + self._sinvx**2.0 ) * potentialStaeckel(self._ux, self._vx, self._pot, self._delta) self._I3V = ( -self._E * self._sinvx**2.0 + self._pvx**2.0 / 2.0 / self._delta**2.0 + self._Lz**2.0 / 2.0 / self._delta**2.0 / self._sinvx**2.0 - dV ) self.calcUminUmax() self.calcVmin() return None def angleR(self, **kwargs): raise NotImplementedError( "'angleR' not yet implemented for Staeckel approximation" ) def TR(self, **kwargs): raise NotImplementedError("'TR' not implemented yet for Staeckel approximation") def Tphi(self, **kwargs): raise NotImplementedError( "'Tphi' not implemented yet for Staeckel approxximation" ) def I(self, **kwargs): raise NotImplementedError("'I' not implemented yet for Staeckel approxximation") def Jphi(self): # pragma: no cover return self._R * self._vT def JR(self, **kwargs): """ Calculate the radial action Parameters ---------- fixed_quad : bool, optional If True, use n=10 fixed_quad. Default is False. **kwargs scipy.integrate.quad keywords Returns ------- float J_R(R,vT,vT)/ro/vc + estimate of the error (nan for fixed_quad) Notes ----- - 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.0**-6: return numpy.array([0.0]) order = kwargs.pop("order", 10) if kwargs.pop("fixed_quad", False): # factor in next line bc integrand=/2delta^2 self._JR = ( 1.0 / numpy.pi * numpy.sqrt(2.0) * self._delta * integrate.fixed_quad( _JRStaeckelIntegrand, umin, umax, args=( self._E, self._Lz, self._I3U, self._delta, self._u0, self._sinhu0**2.0, self._vx, self._sinvx**2.0, self._potu0v0, self._pot, ), n=order, **kwargs, )[0] ) else: self._JR = ( 1.0 / numpy.pi * numpy.sqrt(2.0) * self._delta * integrate.quad( _JRStaeckelIntegrand, umin, umax, args=( self._E, self._Lz, self._I3U, self._delta, self._u0, self._sinhu0**2.0, self._vx, self._sinvx**2.0, self._potu0v0, self._pot, ), **kwargs, )[0] ) return self._JR def Jz(self, **kwargs): """ Calculate the vertical action Parameters ---------- fixed_quad : bool, optional If True, use n=10 fixed_quad. Default is False. **kwargs scipy.integrate.quad keywords Returns ------- float J_z(R,vT,vT)/ro/vc + estimate of the error Notes ----- - 2012-11-27 - Written - Bovy (IAS) """ if hasattr(self, "_JZ"): # pragma: no cover return self._JZ vmin = self.calcVmin() if (numpy.pi / 2.0 - vmin) < 10.0**-7: return numpy.array([0.0]) order = kwargs.pop("order", 10) if kwargs.pop("fixed_quad", False): # factor in next line bc integrand=/2delta^2 self._JZ = ( 2.0 / numpy.pi * numpy.sqrt(2.0) * self._delta * integrate.fixed_quad( _JzStaeckelIntegrand, vmin, numpy.pi / 2, args=( self._E, self._Lz, self._I3V, self._delta, self._ux, self._coshux**2.0, self._sinhux**2.0, self._potupi2, self._pot, ), n=order, **kwargs, )[0] ) else: # factor in next line bc integrand=/2delta^2 self._JZ = ( 2.0 / numpy.pi * numpy.sqrt(2.0) * self._delta * integrate.quad( _JzStaeckelIntegrand, vmin, numpy.pi / 2, args=( self._E, self._Lz, self._I3V, self._delta, self._ux, self._coshux**2.0, self._sinhux**2.0, self._potupi2, self._pot, ), **kwargs, )[0] ) return self._JZ def calcEL(self, **kwargs): """ Calculate the energy and angular momentum. Parameters ---------- **kwargs : dict scipy.integrate.quadrature keywords Returns ------- tuple A tuple containing the energy and angular momentum. Notes ----- - 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): """ Calculate the u 'apocenter' and 'pericenter' Returns ------- tuple (umin,umax) Notes ----- - 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.0, self._vx, self._sinvx**2.0, 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.0**-8.0 peps = _JRStaeckelIntegrandSquared( self._ux + eps, E, L, self._I3U, self._delta, self._u0, self._sinhu0**2.0, self._vx, self._sinvx**2.0, self._potu0v0, self._pot, ) meps = _JRStaeckelIntegrandSquared( self._ux - eps, E, L, self._I3U, self._delta, self._u0, self._sinhu0**2.0, self._vx, self._sinvx**2.0, self._potu0v0, self._pot, ) if peps < 0.0 and meps > 0.0: # we are at umax umax = self._ux rstart, prevr = _uminUmaxFindStart( self._ux, E, L, self._I3U, self._delta, self._u0, self._sinhu0**2.0, self._vx, self._sinvx**2.0, self._potu0v0, self._pot, ) if rstart == 0.0: umin = 0.0 else: try: umin = optimize.brentq( _JRStaeckelIntegrandSquared, numpy.atleast_1d(rstart)[0], numpy.atleast_1d(self._ux)[0] - eps, ( E, L, self._I3U, self._delta, self._u0, self._sinhu0**2.0, self._vx, self._sinvx**2.0, self._potu0v0, self._pot, ), maxiter=200, ) except RuntimeError: # pragma: no cover raise UnboundError("Orbit seems to be unbound") elif peps > 0.0 and meps < 0.0: # we are at umin umin = self._ux rend, prevr = _uminUmaxFindStart( self._ux, E, L, self._I3U, self._delta, self._u0, self._sinhu0**2.0, self._vx, self._sinvx**2.0, self._potu0v0, self._pot, umax=True, ) umax = optimize.brentq( _JRStaeckelIntegrandSquared, numpy.atleast_1d(self._ux)[0] + eps, numpy.atleast_1d(rend)[0], ( E, L, self._I3U, self._delta, self._u0, self._sinhu0**2.0, self._vx, self._sinvx**2.0, 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.0, self._vx, self._sinvx**2.0, self._potu0v0, self._pot, ) if rstart == 0.0: umin = 0.0 else: if numpy.fabs(prevr - self._ux) < 10.0**-2.0: rup = self._ux else: rup = prevr try: umin = optimize.brentq( _JRStaeckelIntegrandSquared, rstart, rup, ( E, L, self._I3U, self._delta, self._u0, self._sinhu0**2.0, self._vx, self._sinvx**2.0, 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.0, self._vx, self._sinvx**2.0, self._potu0v0, self._pot, umax=True, ) umax = optimize.brentq( _JRStaeckelIntegrandSquared, prevr, rend, ( E, L, self._I3U, self._delta, self._u0, self._sinhu0**2.0, self._vx, self._sinvx**2.0, self._potu0v0, self._pot, ), maxiter=200, ) self._uminumax = (umin, umax) return self._uminumax def calcVmin(self, **kwargs): """ Calculate the v 'pericenter' Returns ------- float v_min(R,vT,vT)/vc + estimate of the error Notes ----- - 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.0**-7.0: # We are at vmin or vmax eps = 10.0**-8.0 peps = _JzStaeckelIntegrandSquared( self._vx + eps, E, L, self._I3V, self._delta, self._ux, self._coshux**2.0, self._sinhux**2.0, self._potupi2, self._pot, ) meps = _JzStaeckelIntegrandSquared( self._vx - eps, E, L, self._I3V, self._delta, self._ux, self._coshux**2.0, self._sinhux**2.0, self._potupi2, self._pot, ) if peps < 0.0 and meps > 0.0: # pragma: no cover # we are at vmax, which cannot happen raise RuntimeError( "Orbit is at the vmax turning point in v, which mathematically cannot happen; something is very wrong!!" ) elif peps > 0.0 and meps < 0.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.0, self._sinhux**2.0, self._potupi2, self._pot, ) if rstart == 0.0: vmin = 0.0 else: try: vmin = optimize.brentq( _JzStaeckelIntegrandSquared, rstart, rstart / 0.9, ( E, L, self._I3V, self._delta, self._ux, self._coshux**2.0, self._sinhux**2.0, 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.0, ro=1.0): """ Calculate the energy and angular momentum. Parameters ---------- R : float Galactocentric radius (/ro). vR : float Radial part of the velocity (/vc). vT : float Azimuthal part of the velocity (/vc). z : float Vertical height (/ro). vz : float Vertical velocity (/vc). pot : Potential object galpy Potential object or list of such objects. vc : float, optional Circular velocity at ro (km/s). Default: 1.0. ro : float, optional Distance to the Galactic center (kpc). Default: 1.0. Returns ------- tuple Tuple containing energy and angular momentum. Notes ----- - 2012-11-30 - Written - Bovy (IAS) """ return ( _evaluatePotentials(pot, R, z) + vR**2.0 / 2.0 + vT**2.0 / 2.0 + vz**2.0 / 2.0, R * vT, ) def potentialStaeckel(u, v, pot, delta): """ Return the potential. Parameters ---------- u : float Confocal u. v : float Confocal v. pot : Potential object Potential. delta : float Focus. Returns ------- float Potential at (u, v). Notes ----- - 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 """ Return the radial force. Parameters ---------- u : float Confocal u. v : float Confocal v. pot : Potential object Potential. delta : float Focus. Returns ------- float Radial force. Notes ----- - 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 """ Return the vertical force. Parameters ---------- u : float Confocal u. v : float Confocal v. pot : Potential object Potential. delta : float Focus. Returns ------- Ffloat Vertical force. Notes ----- - 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.0 dU = (sinh2u + sin2v0) * potentialStaeckel(u, v0, pot, delta) - ( sinh2u0 + sin2v0 ) * potu0v0 return E * sinh2u - I3U - dU - Lz**2.0 / 2.0 / delta**2.0 / 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.0 dV = cosh2u0 * potu0pi2 - (sinh2u0 + sin2v) * potentialStaeckel(u0, v, pot, delta) return E * sin2v + I3V + dV - Lz**2.0 / 2.0 / delta**2.0 / sin2v def _uminUmaxFindStart( u, E, Lz, I3U, delta, u0, sinh2u0, v0, sin2v0, potu0v0, pot, umax=False ): """ Find adequate start or end points to solve for umin and umax Parameters ---------- u : float Current value of the coordinate to solve for (either umin or umax) E : float Energy Lz : float Angular momentum along z I3U : float Third isolating integral of motion delta : float Focus parameter of the confocal coordinate system u0 : float u coordinate of the center of the coordinate system sinh2u0 : float Hyperbolic sine of twice the u coordinate of the center of the coordinate system v0 : float v coordinate of the center of the coordinate system sin2v0 : float Sine of twice the v coordinate of the center of the coordinate system potu0v0 : float Potential at the center of the coordinate system pot : Potential object Instance of a galpy Potential object umax : bool, optional If True, solve for umax instead of umin (default is False) Returns ------- float Adequate start or end point to solve for umin or umax Notes ----- - 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.0 and utry > 0.000000001 ): prevu = utry if umax: if utry > 100.0: raise UnboundError("Orbit seems to be unbound") utry *= 1.1 else: utry *= 0.9 if utry < 0.000000001: return (0.0, prevu) return (utry, prevu) def _vminFindStart(v, E, Lz, I3V, delta, u0, cosh2u0, sinh2u0, potu0pi2, pot): """ Find adequate start point to solve for vmin Parameters ---------- v : float Velocity E : float Energy Lz : float Angular momentum along z-axis I3V : float Third isolating integral delta : float Staeckel delta parameter u0 : float Staeckel energy cosh2u0 : float Hyperbolic cosine squared of u0 sinh2u0 : float Hyperbolic sine squared of u0 potu0pi2 : float Potential at u0 times pi/2 pot : Potential object galpy Potential object Returns ------- float Adequate start point to solve for vmin Notes ----- - 2012-11-28 - Written - Bovy (IAS) """ vtry = 0.9 * v while ( _JzStaeckelIntegrandSquared( vtry, E, Lz, I3V, delta, u0, cosh2u0, sinh2u0, potu0pi2, pot ) >= 0.0 and vtry > 0.000000001 ): vtry *= 0.9 if vtry < 0.000000001: return 0.0 return vtry if vtry >= 0.000000001 else 0.0 @potential_physical_input @physical_conversion("position", pop=True) def estimateDeltaStaeckel(pot, R, z, no_median=False, delta0=1e-6): """ Estimate a good value for delta using eqn. (9) in Sanders (2012) Parameters ---------- pot : Potential instance or list thereof R : float or numpy.ndarray coordinates z : float or numpy.ndarray coordinates no_median : bool, optional if True, and input is array, return all calculated values of delta (useful for quickly estimating delta for many phase space points) delta0 : float, optional value to return when delta -(10.0**-10.0)) + pot_includes_scf) delta2[indx] = delta0**2.0 if not no_median: delta2 = numpy.median(delta2[True ^ numpy.isnan(delta2)]) else: delta2 = ( z**2.0 - R**2.0 # eqn. (9) has a sign error + ( 3.0 * R * _evaluatezforces(pot, R, z) - 3.0 * 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.0 and (delta2 > -(10.0**-10.0) or pot_includes_scf): delta2 = delta0**2.0 return numpy.sqrt(delta2) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleStaeckelGrid.py0000644000175100001660000010622314761352023022741 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.0, nE=25, npsi=25, nLz=30, numcores=1, interpecc=False, **kwargs, ): """ Initialize an actionAngleStaeckelGrid object Parameters ---------- pot : Potential or list of Potential instances The potential or list of potentials to use for the actionAngleStaeckelGrid object. delta : float or Quantity The focal length of the confocal coordinate system. Rmax : float The maximum R to consider (natural units). nE : int The number of grid points in energy. npsi : int The number of grid points in psi. nLz : int The number of grid points in Lz. numcores : int The number of cores to use for multi-processing. interpecc : bool If True, also interpolate the approximate eccentricity, zmax, rperi, and rapo. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 / 2.0 / self._RL**2.0 ) self._ERLmax = numpy.amax(self._ERL) + 1.0 self._ERLInterp = interpolate.InterpolatedUnivariateSpline( self._Lzs, numpy.log(-(self._ERL - self._ERLmax)), k=3 ) self._Ramax = 200.0 / 8.0 self._ERa = ( _evaluatePotentials(self._pot, self._Ramax, 0.0) + self._Lzs**2.0 / 2.0 / self._Ramax**2.0 ) # 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.0 self._ERaInterp = interpolate.InterpolatedUnivariateSpline( self._Lzs, numpy.log(-(self._ERa - self._ERamax)), k=3 ) y = numpy.linspace(0.0, 1.0, nE) self._nE = nE psis = numpy.linspace(0.0, 1.0, npsi) * numpy.pi / 2.0 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.0)] = numpy.nanmin(jrLzE[(jrLzE > 0.0)]) jzLzE[(jzLzE == 0.0)] = numpy.nanmin(jzLzE[(jzLzE > 0.0)]) if interpecc: zmaxLzE[(zmaxLzE == 0.0)] = numpy.nanmin(zmaxLzE[(zmaxLzE > 0.0)]) rperiLzE[(rperiLzE == 0.0)] = numpy.nanmin(rperiLzE[(rperiLzE > 0.0)]) rapLzE[(rapLzE == 0.0)] = numpy.nanmin(rapLzE[(rapLzE > 0.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.0)] = 1.0 jz[(jz > 1.0)] = 1.0 # Deal w/ NaN jr[numpy.isnan(jr)] = 0.0 jz[numpy.isnan(jz)] = 0.0 if interpecc: ecc[(ecc < 0.0)] = 0.0 ecc[(ecc > 1.0)] = 1.0 ecc[numpy.isnan(ecc)] = 0.0 ecc[numpy.isinf(ecc)] = 1.0 zmax[(zmax > 1.0)] = 1.0 zmax[numpy.isnan(zmax)] = 0.0 zmax[numpy.isinf(zmax)] = 1.0 rperi[(rperi > 1.0)] = 1.0 rperi[numpy.isnan(rperi)] = 0.0 rperi[numpy.isinf(rperi)] = 0.0 # typically orbits that can reach 0 rap[(rap > 1.0)] = 1.0 rap[numpy.isnan(rap)] = 0.0 rap[numpy.isinf(rap)] = 1.0 # 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.0**-5.0), k=3 ) self._jzLzInterp = interpolate.InterpolatedUnivariateSpline( self._Lzs, numpy.log(jzLzE + 10.0**-5.0), 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.0**-5.0), k=3 ) self._rperiLzInterp = interpolate.InterpolatedUnivariateSpline( self._Lzs, numpy.log(rperiLzE + 10.0**-5.0), k=3 ) self._rapLzInterp = interpolate.InterpolatedUnivariateSpline( self._Lzs, numpy.log(rapLzE + 10.0**-5.0), k=3 ) # Interpolate u0 self._logu0Interp = interpolate.RectBivariateSpline( self._Lzs, y, numpy.log(u0), kx=3, ky=3, s=0.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.0**-10.0), order=3 ) self._jzFiltered = ndimage.spline_filter( numpy.log(self._jz + 10.0**-10.0), order=3 ) if interpecc: self._eccFiltered = ndimage.spline_filter( numpy.log(self._ecc + 10.0**-10.0), order=3 ) self._zmaxFiltered = ndimage.spline_filter( numpy.log(self._zmax + 10.0**-10.0), order=3 ) self._rperiFiltered = ndimage.spline_filter( numpy.log(self._rperi + 10.0**-10.0), order=3 ) self._rapFiltered = ndimage.spline_filter( numpy.log(self._rap + 10.0**-10.0), order=3 ) # Check the units self._check_consistent_units() return None def _evaluate(self, *args, **kwargs): """ Evaluate the actions (jr,lz,jz) Parameters ---------- *args : tuple 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 **kwargs: dict, optional Keywords for actionAngleStaeckel.__call__ for off-the-grid evaluations Returns ------- tuple (jr,lz,jz) Notes ----- - 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.0 / 2.0 + vT**2.0 / 2.0 + vz**2.0 / 2.0 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.0) * ( ((E - thisERa) / (thisERL - thisERa) - 1.0) < 10.0**-2.0 ) E[indx] = thisERL[indx] indx = ((E - thisERa) / (thisERL - thisERa) < 0.0) * ( (E - thisERa) / (thisERL - thisERa) > -(10.0**-2.0) ) E[indx] = thisERa[indx] indx = Lz < self._Lzmin indx += Lz > self._Lzmax indx += (E - thisERa) / (thisERL - thisERa) > 1.0 indx += (E - thisERa) / (thisERL - thisERa) < 0.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.0 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.0 * thisEr / thisv2 / (1.0 + sinh2u0) # latter is cosh2u0 cos2psi[(cos2psi > 1.0) * (cos2psi < 1.0 + 10.0**-5.0)] = 1.0 indxCos2psi = cos2psi > 1.0 indxCos2psi += cos2psi < 0.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.0) ) 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.0) coords[2, :] = psi / numpy.pi * 2.0 * (self._npsi - 1.0) jr[indxc] = ( numpy.exp( ndimage.map_coordinates( self._jrFiltered, coords, order=3, prefilter=False ) ) - 10.0**-10.0 ) * (numpy.exp(self._jrLzInterp(Lz[indxc])) - 10.0**-5.0) # Switch to Ez-calculated psi sin2psi = ( 2.0 * thisEz[True ^ indxCos2psi] / thisv2[True ^ indxCos2psi] / (1.0 + sinh2u0[True ^ indxCos2psi]) ) # latter is cosh2u0 sin2psi[(sin2psi > 1.0) * (sin2psi < 1.0 + 10.0**-5.0)] = 1.0 indxSin2psi = sin2psi > 1.0 indxSin2psi += sin2psi < 0.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.0 * (self._npsi - 1.0) jz[indxc] = ( numpy.exp( ndimage.map_coordinates( self._jzFiltered, newcoords, order=3, prefilter=False ) ) - 10.0**-10.0 ) * (numpy.exp(self._jzLzInterp(Lz[indxc])) - 10.0**-5.0) 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] = 0.0 jz[jz < 0.0] = 0.0 return (jr, R * vT, jz) def Jz(self, *args, **kwargs): """ Evaluate the action jz Parameters ---------- *args : tuple 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 **kwargs: dict, optional Keywords for actionAngleStaeckel.__call__ for off-the-grid evaluations Returns ------- float or numpy.ndarray jz Notes ----- - 2012-07-30 - Written - Bovy (IAS@MPIA) """ return self(*args, **kwargs)[2] def JR(self, *args, **kwargs): """ Evaluate the action jr. Parameters ---------- *args : tuple 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 **kwargs: dict, optional Keywords for actionAngleStaeckel.__call__ for off-the-grid evaluations Returns ------- float The action jr. Notes ----- - 2012-07-30 - Written - Bovy (IAS@MPIA) """ return self(*args, **kwargs)[0] def _EccZmaxRperiRap(self, *args, **kwargs): """ Evaluate the eccentricity, maximum height above the plane, peri- and apocenter in the Staeckel approximation Parameters ---------- *args : tuple 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 Returns ------- tuple (e,zmax,rperi,rap) Notes ----- - 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.0 / 2.0 + vT**2.0 / 2.0 + vz**2.0 / 2.0 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.0) * ( ((E - thisERa) / (thisERL - thisERa) - 1.0) < 10.0**-2.0 ) E[indx] = thisERL[indx] indx = ((E - thisERa) / (thisERL - thisERa) < 0.0) * ( (E - thisERa) / (thisERL - thisERa) > -(10.0**-2.0) ) E[indx] = thisERa[indx] indx = Lz < self._Lzmin indx += Lz > self._Lzmax indx += (E - thisERa) / (thisERL - thisERa) > 1.0 indx += (E - thisERa) / (thisERL - thisERa) < 0.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.0 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.0 * thisEr / thisv2 / (1.0 + sinh2u0) # latter is cosh2u0 cos2psi[(cos2psi > 1.0) * (cos2psi < 1.0 + 10.0**-5.0)] = 1.0 indxCos2psi = cos2psi > 1.0 indxCos2psi += cos2psi < 0.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.0) ) 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.0) coords[2, :] = psi / numpy.pi * 2.0 * (self._npsi - 1.0) ecc[indxc] = ( numpy.exp( ndimage.map_coordinates( self._eccFiltered, coords, order=3, prefilter=False ) ) - 10.0**-10.0 ) rperi[indxc] = ( numpy.exp( ndimage.map_coordinates( self._rperiFiltered, coords, order=3, prefilter=False ) ) - 10.0**-10.0 ) * (numpy.exp(self._rperiLzInterp(Lz[indxc])) - 10.0**-5.0) # We do rap below with zmax # Switch to Ez-calculated psi sin2psi = ( 2.0 * thisEz[True ^ indxCos2psi] / thisv2[True ^ indxCos2psi] / (1.0 + sinh2u0[True ^ indxCos2psi]) ) # latter is cosh2u0 sin2psi[(sin2psi > 1.0) * (sin2psi < 1.0 + 10.0**-5.0)] = 1.0 indxSin2psi = sin2psi > 1.0 indxSin2psi += sin2psi < 0.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.0 * (self._npsi - 1.0) zmax[indxc] = ( numpy.exp( ndimage.map_coordinates( self._zmaxFiltered, newcoords, order=3, prefilter=False ) ) - 10.0**-10.0 ) * (numpy.exp(self._zmaxLzInterp(Lz[indxc])) - 10.0**-5.0) rap[indxc] = ( numpy.exp( ndimage.map_coordinates( self._rapFiltered, newcoords, order=3, prefilter=False ) ) - 10.0**-10.0 ) * (numpy.exp(self._rapLzInterp(Lz[indxc])) - 10.0**-5.0) 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] = 0.0 zmax[zmax < 0.0] = 0.0 rperi[rperi < 0.0] = 0.0 rap[rap < 0.0] = 0.0 return (ecc, zmax, rperi, rap) def vatu0(self, E, Lz, u0, R, retv2=False): """ Calculate the velocity at u0. Parameters ---------- E : float Energy. Lz : float Angular momentum. u0 : float u0. R : float Radius corresponding to u0, pi/2. retv2 : bool, optional If True, return v^2. Default is False. Returns ------- float or numpy.ndarray Velocity or velocity squared if retv2 is True. Notes ----- - 2012-11-29 - Written - Bovy (IAS). """ v2 = ( 2.0 * ( E - actionAngleStaeckel.potentialStaeckel( u0, numpy.pi / 2.0, self._pot, self._delta ) ) - Lz**2.0 / R**2.0 ) if retv2: return v2 v2[(v2 < 0.0) * (v2 > -(10.0**-7.0))] = 0.0 return numpy.sqrt(v2) def calcu0(self, E, Lz): """ Calculate the minimum of the u potential. Parameters ---------- E : float Energy. Lz : float Angular momentum. Returns ------- float Minimum of the u potential. Notes ----- - 2012-11-29 - Written - Bovy (IAS) """ logu0 = optimize.brent(_u0Eq, args=(self._delta, self._pot, E, Lz**2.0 / 2.0)) return numpy.exp(logu0) def Er(self, R, z, vR, vz, E, Lz, sinh2u0, u0): """ Calculate the 'radial energy' Parameters ---------- R : float Galactocentric cylindrical radius z : float vertical height vR : float Galactocentric radial velocity vz : float vertical velocity E : float energy Lz : float angular momentum sinh2u0 : float sinh^2 and u0 u0 : float sinh^2 and u0 Returns ------- float radial energy Notes ----- - 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.0 / 2.0 + Lz**2.0 / 2.0 / self._delta**2.0 * (1.0 / numpy.sinh(u) ** 2.0 - 1.0 / sinh2u0) - E * (numpy.sinh(u) ** 2.0 - sinh2u0) + (numpy.sinh(u) ** 2.0 + 1.0) * actionAngleStaeckel.potentialStaeckel( u, numpy.pi / 2.0, self._pot, self._delta ) - (sinh2u0 + 1.0) * actionAngleStaeckel.potentialStaeckel( u0, numpy.pi / 2.0, 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): """ Calculate the 'vertical energy' Parameters ---------- R : float Galactocentric cylindrical radius (can be Quantity) z : float height above the plane (can be Quantity) vR : float Galactocentric radial velocity (can be Quantity) vz : float Galactocentric vertical velocity (can be Quantity) E : float energy Lz : float angular momentum sinh2u0 : float sinh^2 and u0 u0 : float sinh^2 and u0 Returns ------- float vertical energy Notes ----- - 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.0 / 2.0 + Lz**2.0 / 2.0 / self._delta**2.0 * (1.0 / numpy.sin(v) ** 2.0 - 1.0) - E * (numpy.sin(v) ** 2.0 - 1.0) - (sinh2u0 + 1.0) * actionAngleStaeckel.potentialStaeckel( u0, numpy.pi / 2.0, self._pot, self._delta ) + (sinh2u0 + numpy.sin(v) ** 2.0) * 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.0 cosh2u = numpy.cosh(u) ** 2.0 dU = cosh2u * actionAngleStaeckel.potentialStaeckel(u, numpy.pi / 2.0, pot, delta) return -(E * sinh2u - dU - Lz22 / delta**2.0 / 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.0**-10.0) def _invEfunc(Ef, *args): """Inverse of Efunc""" # return Ef**2.+args[0] return numpy.exp(Ef) + args[0] - 10.0**-10.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleStaeckel_c.py0000644000175100001660000005343014761352023022436 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): """ Use C to calculate actions using the Staeckel approximation Parameters ---------- pot : Potential or list of such instances Potential delta : float Focal length of prolate spheroidal coordinates R : float Galactocentric radius vR : float Galactocentric radial velocity vT : float Galactocentric tangential velocity z : float Height vz : float Vertical velocity u0 : float, optional If set, u0 to use order : int, optional Order of Gauss-Legendre integration of the relevant integrals Returns ------- tuple (jr,jz,err) where: * jr,jz : array, shape (len(R)) * err - non-zero if error occurred Notes ----- - 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): """ Use C to calculate u0 in the Staeckel approximation Parameters ---------- E : numpy.ndarray Energy. Lz : numpy.ndarray Angular momentum. pot : Potential or list of such instances Potential or list of such instances. delta : numpy.ndarray Focal length of prolate spheroidal coordinates. Returns ------- tuple (u0,err) u0 : array, shape (len(E)) err - non-zero if error occurred Notes ----- - 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): """ Use C to calculate actions and frequencies using the Staeckel approximation Parameters ---------- pot : Potential or list of such instances Potential or list of such instances. delta : float Focal length of prolate spheroidal coordinates. R : float Galactocentric radius. vR : float Galactocentric radial velocity. vT : float Galactocentric tangential velocity. z : float Height. vz : float Vertical velocity. u0 : float, optional If set, u0 to use. order : int, optional Order of Gauss-Legendre integration of the relevant integrals. Returns ------- tuple (jr,jz,Omegar,Omegaphi,Omegaz,err) where: * jr,jz,Omegar,Omegaphi,Omegaz : array, shape (len(R)) * err - non-zero if error occurred Notes ----- - 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)) 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 ): """ Use C to calculate actions, frequencies, and angles using the Staeckel approximation Parameters ---------- pot : Potential or list of such instances Potential or list of such instances. delta : float Focal length of prolate spheroidal coordinates. R : float Galactocentric radius. vR : float Galactocentric radial velocity. vT : float Galactocentric tangential velocity. z : float Height. vz : float Vertical velocity. phi : float Azimuth. u0 : float, optional If set, u0 to use. order : int, optional Order of Gauss-Legendre integration of the relevant integrals. Returns ------- tuple (jr,jz,Omegar,Omegaphi,Omegaz,Angler,Anglephi,Anglez,err) where: * jr,jz,Omegar,Omegaphi,Omegaz,Angler,Anglephi,Anglez : array, shape (len(R)) * err - non-zero if error occurred Notes ----- - 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.0 * numpy.pi)) % ( 2.0 * numpy.pi ) Anglephi[Anglephi < 0.0] += 2.0 * 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): """ Use C to calculate umin, umax, and vmin using the Staeckel approximation Parameters ---------- pot : Potential or list of such instances Potential or list of such instances. delta : float Focal length of prolate spheroidal coordinates. R : float Galactocentric radius. vR : float Galactocentric radial velocity. vT : float Galactocentric tangential velocity. z : float Height. vz : float Vertical velocity. u0 : float, optional If set, u0 to use. Returns ------- tuple (umin,umax,vmin,err) where: * umin,umax,vmin : array, shape (len(R)) * err - non-zero if error occurred Notes ----- - 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=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleTorus.py0000644000175100001660000002673014761352023021520 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 occurred" class actionAngleTorus: """Action-angle formalism using the Torus machinery""" def __init__(self, *args, **kwargs): """ Initialize an actionAngleTorus object. Parameters ---------- pot : potential or list of potentials (3D) The potential or list of potentials (3D) to use. tol : float, optional Default tolerance to use when fitting tori (|dJ|/J). dJ : float, optional Default action difference when computing derivatives (Hessian or Jacobian). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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): """ Evaluate the phase-space coordinates (x,v) for a number of angles on a single torus. Parameters ---------- jr : float Radial action. jphi : float Azimuthal action. jz : float Vertical action. angler : numpy.ndarray Radial angle. anglephi : numpy.ndarray Azimuthal angle. anglez : numpy.ndarray Vertical angle. tol : float, optional Goal for |dJ|/|J| along the torus. Default is object-wide value. Returns ------- numpy.ndarray Array of shape (N, 6) containing [R, vR, vT, z, vz, phi]. Notes ----- - 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): """ Evaluate the phase-space coordinates (x,v) for a number of angles on a single torus as well as the frequencies. Parameters ---------- jr : float Radial action. jphi : float Azimuthal action. jz : float Vertical action. angler : numpy.ndarray Radial angle. anglephi : numpy.ndarray Azimuthal angle. anglez : arrnumpy.ndarrayay_like Vertical angle. tol : float, optional Goal for |dJ|/|J| along the torus. Default is object-wide value. Returns ------- tuple A tuple containing the following elements: - A numpy array of shape (N, 6) containing the phase-space coordinates (R, vR, vT, z, vz, phi) for a number of angles on a single torus. - OmegaR : float The radial frequency. - Omegaphi : float The azimuthal frequency. - Omegaz : float The vertical frequency. - AutoFit error message : int If AutoFit exited with non-zero return status, a warning message is issued. Notes ----- - 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): """ Return the frequencies corresponding to a torus Parameters ---------- jr : float Radial action jphi : float Azimuthal action jz : float Vertical action tol : float, optional Goal for |dJ|/|J| along the torus (default is object-wide value) Returns ------- tuple (OmegaR, Omegaphi, Omegaz) Notes ----- - 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): """ Return the Hessian d Omega / d J and frequencies Omega corresponding to a torus Parameters ---------- jr : float Radial action jphi : float Azimuthal action jz : float Vertical action tol : float, optional Goal for |dJ|/|J| along the torus. Default is object-wide value. dJ : float, optional Action difference when computing derivatives (Hessian or Jacobian). Default is object-wide value. nosym : bool, optional If True, don't explicitly symmetrize the Hessian (good to check errors). Default is False. Returns ------- tuple Tuple containing: - dO/dJ - Omegar - Omegaphi - Omegaz - Autofit error message Notes ----- - 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): """ 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 Parameters ---------- jr : float Radial action jphi : float Azimuthal action jz : float Vertical action angler : numpy.ndarray Radial angle anglephi : numpy.ndarray Azimuthal angle anglez : numpy.ndarray Vertical angle tol : float, optional Goal for |dJ|/|J| along the torus (default is object-wide value) dJ : float, optional Action difference when computing derivatives (Hessian or Jacobian) (default is object-wide value) nosym : bool, optional If True, don't explicitly symmetrize the Hessian (good to check errors) (default is False) Returns ------- tuple Tuple containing: - ([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) Notes ----- - 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=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleTorus_c.py0000644000175100001660000003513414761352023022020 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): """ Compute configuration (x,v) and frequencies of a set of angles on a single torus Parameters ---------- pot : Potential object or list thereof jr : float Radial action jphi : float Azimuthal action jz : float Vertical action angler : numpy.ndarray Radial angle anglephi : numpy.ndarray Azimuthal angle anglez : numpy.ndarray Vertical angle tol : float, optional Goal for |dJ|/|J| along the torus Returns ------- tuple (R,vR,vT,z,vz,phi,Omegar,Omegaphi,Omegaz,flag) Notes ----- - 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): """ Compute frequencies on a single torus Parameters ---------- pot : Potential object or list thereof jr : float Radial action jphi : float Azimuthal action jz : float Vertical action tol : float, optional Goal for |dJ|/|J| along the torus Returns ------- tuple (Omegar,Omegaphi,Omegaz,flag) Notes ----- - 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): """ Compute dO/dJ on a single torus Parameters ---------- pot : Potential object or list thereof jr : float Radial action jphi : float Azimuthal action jz : float Vertical action tol : float, optional Goal for |dJ|/|J| along the torus dJ : float, optional Action difference when computing derivatives (Hessian or Jacobian) Returns ------- tuple (dO/dJ,Omegar,Omegaphi,Omegaz,flag) Notes ----- - 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 ): """ Compute d(x,v)/d(J,theta) on a single torus, also compute dO/dJ and the frequencies Parameters ---------- pot : Potential object or list thereof jr : float Radial action jphi : float Azimuthal action jz : float Vertical action angler : numpy.ndarray Radial angle anglephi : numpy.ndarray Azimuthal angle anglez : numpy.ndarray Vertical angle tol : float, optional Goal for |dJ|/|J| along the torus dJ : float, optional Action difference when computing derivatives (Hessian or Jacobian) Returns ------- tuple (d[R,vR,vT,z,vz,phi]/d[J,theta],Omegar,Omegaphi,Omegaz,Autofit error message) Notes ----- - 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, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6408842 galpy-1.10.2/galpy/actionAngle/actionAngleTorus_c_ext/0000755000175100001660000000000014761352040022317 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleTorus_c_ext/galpyPot.h0000644000175100001660000000306614761352023024275 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=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleVertical.py0000644000175100001660000002763014761352023022155 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleVertical # # methods: # __call__: returns (j) # actionsFreqs: returns (j,omega) # actionsFreqsAngles: returns (j,omega,a) # calcxmax ############################################################################### import numpy from scipy import integrate, optimize from ..potential.linearPotential import evaluatelinearPotentials from .actionAngle import actionAngle class actionAngleVertical(actionAngle): """Action-angle formalism for one-dimensional potentials (or of the vertical potential in a galactic disk in the adiabatic approximation, hence the name)""" def __init__(self, *args, **kwargs): """ Initialize an actionAngleVertical object. Parameters ---------- pot : potential or list of 1D potentials (linearPotential or verticalPotential) Potential or list of 1D potentials. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 2012-06-01 - Written - Bovy (IAS) - 2018-05-19 - Conformed to the general actionAngle framework - Bovy (UofT) """ 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 actionAngleVertical") if not "pot" in kwargs: # pragma: no cover raise OSError("Must specify pot= for actionAngleVertical") self._pot = kwargs["pot"] return None def _evaluate(self, *args, **kwargs): """ Evaluate the action. Parameters ---------- *args : tuple 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) Returns ------- float or numpy.ndarray action Notes ----- - 2018-05-19 - Written based on re-write of existing code - Bovy (UofT) """ if len(args) == 2: # x,vx x, vx = args if isinstance(x, float): x = numpy.array([x]) vx = numpy.array([vx]) J = numpy.empty(len(x)) for ii in range(len(x)): E = vx[ii] ** 2.0 / 2.0 + evaluatelinearPotentials( self._pot, x[ii], use_physical=False ) xmax = self.calcxmax(x[ii], vx[ii], E) if xmax == -9999.99: J[ii] = 9999.99 else: J[ii] = ( 2.0 * integrate.quad( lambda xi: numpy.sqrt( 2.0 * ( E - evaluatelinearPotentials( self._pot, xi, use_physical=False ) ) ), 0.0, xmax, )[0] / numpy.pi ) return J else: # pragma: no cover raise ValueError("actionAngleVertical __call__ input not understood") def _actionsFreqs(self, *args, **kwargs): """ Evaluate the action and frequency. Parameters ---------- *args : tuple 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) Returns ------- tuple action,frequency Notes ----- - 2018-05-19 - Written based on re-write of existing code - Bovy (UofT) """ if len(args) == 2: # x,vx x, vx = args if isinstance(x, float): x = numpy.array([x]) vx = numpy.array([vx]) J = numpy.empty(len(x)) Omega = numpy.empty(len(x)) for ii in range(len(x)): E = vx[ii] ** 2.0 / 2.0 + evaluatelinearPotentials( self._pot, x[ii], use_physical=False ) xmax = self.calcxmax(x[ii], vx[ii], E) if xmax == -9999.99: J[ii] = 9999.99 Omega[ii] = 9999.99 else: J[ii] = ( 2.0 * integrate.quad( lambda xi: numpy.sqrt( 2.0 * ( E - evaluatelinearPotentials( self._pot, xi, use_physical=False ) ) ), 0.0, xmax, )[0] / numpy.pi ) # Transformed x = xmax-t^2 for singularity Omega[ii] = ( numpy.pi / 2.0 / integrate.quad( lambda t: 2.0 * t / numpy.sqrt( 2.0 * ( E - evaluatelinearPotentials( self._pot, xmax - t**2.0, use_physical=False ) ) ), 0, numpy.sqrt(xmax), )[0] ) return (J, Omega) else: # pragma: no cover raise ValueError("actionAngleVertical actionsFreqs input not understood") def _actionsFreqsAngles(self, *args, **kwargs): """ Evaluate the action, frequency, and angle. Parameters ---------- *args : tuple 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) Returns ------- tuple action,frequency,angle Notes ----- - 2018-05-19 - Written based on re-write of existing code - Bovy (UofT) """ if len(args) == 2: # x,vx x, vx = args if isinstance(x, float): x = numpy.array([x]) vx = numpy.array([vx]) J = numpy.empty(len(x)) Omega = numpy.empty(len(x)) angle = numpy.empty(len(x)) for ii in range(len(x)): E = vx[ii] ** 2.0 / 2.0 + evaluatelinearPotentials( self._pot, x[ii], use_physical=False ) xmax = self.calcxmax(x[ii], vx[ii], E) if xmax == -9999.99: J[ii] = 9999.99 Omega[ii] = 9999.99 angle[ii] = 9999.99 else: J[ii] = ( 2.0 * integrate.quad( lambda xi: numpy.sqrt( 2.0 * ( E - evaluatelinearPotentials( self._pot, xi, use_physical=False ) ) ), 0.0, xmax, )[0] / numpy.pi ) Omega[ii] = ( numpy.pi / 2.0 / integrate.quad( lambda t: 2.0 * t / numpy.sqrt( 2.0 * ( E - evaluatelinearPotentials( self._pot, xmax - t**2.0, use_physical=False ) ) ), 0, numpy.sqrt(xmax), )[0] ) angle[ii] = integrate.quad( lambda xi: 1.0 / numpy.sqrt( 2.0 * ( E - evaluatelinearPotentials( self._pot, xi, use_physical=False ) ) ), 0, numpy.fabs(x[ii]), )[0] angle *= Omega angle[(x >= 0.0) * (vx < 0.0)] = numpy.pi - angle[(x >= 0.0) * (vx < 0.0)] angle[(x < 0.0) * (vx <= 0.0)] = numpy.pi + angle[(x < 0.0) * (vx <= 0.0)] angle[(x < 0.0) * (vx > 0.0)] = ( 2.0 * numpy.pi - angle[(x < 0.0) * (vx > 0.0)] ) return (J, Omega, angle % (2.0 * numpy.pi)) else: # pragma: no cover raise ValueError( "actionAngleVertical actionsFreqsAngles input not understood" ) def calcxmax(self, x, vx, E=None): """ Calculate the maximum height Parameters ---------- x : float Position vx : float Velocity E : float, optional Energy (default is None) Returns ------- float Maximum height Notes ----- - 2012-06-01 - Written - Bovy (IAS) - 2018-05-19 - Re-written for new framework - Bovy (UofT) """ if E is None: E = E = vx**2.0 / 2.0 + evaluatelinearPotentials( self._pot, x, use_physical=False ) if vx == 0.0: # We are exactly at the maximum height xmax = numpy.fabs(x) else: xstart = x try: if x == 0.0: xend = 0.00001 else: xend = 2.0 * numpy.fabs(x) while ( E - evaluatelinearPotentials(self._pot, xend, use_physical=False) ) > 0.0: xend *= 2.0 if xend > 100.0: # pragma: no cover raise OverflowError except OverflowError: # pragma: no cover xmax = -9999.99 else: xmax = optimize.brentq( lambda xm: E - evaluatelinearPotentials(self._pot, xm, use_physical=False), xstart, xend, xtol=1e-14, ) while ( E - evaluatelinearPotentials(self._pot, xmax, use_physical=False) ) < 0: xmax -= 1e-14 return xmax ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngleVerticalInverse.py0000644000175100001660000015325414761352023023513 0ustar00runnerdocker############################################################################### # actionAngle: a Python module to calculate actions, angles, and frequencies # # class: actionAngleVerticalInverse # # Calculate (x,v) coordinates for a one-dimensional potential # given actions-angle coordinates # ############################################################################### import copy import warnings import numpy from matplotlib import cm, gridspec, pyplot from matplotlib.ticker import NullFormatter from numpy.polynomial import chebyshev, polynomial from scipy import interpolate, ndimage, optimize from ..potential import evaluatelinearForces, evaluatelinearPotentials from ..util import galpyWarning from ..util import plot as plot from .actionAngleHarmonic import actionAngleHarmonic from .actionAngleHarmonicInverse import actionAngleHarmonicInverse from .actionAngleInverse import actionAngleInverse from .actionAngleVertical import actionAngleVertical class actionAngleVerticalInverse(actionAngleInverse): """Inverse action-angle formalism for one dimensional systems""" def __init__( self, pot=None, Es=[0.1, 0.3], nta=128, setup_interp=False, use_pointtransform=False, pt_deg=7, pt_nxa=301, maxiter=100, angle_tol=1e-12, bisect=False, ): """ Initialize an actionAngleVerticalInverse object Parameters ---------- pot : Potential object or list of such objects a linearPotential/verticalPotential or list thereof Es : numpy.ndarray energies of the orbits to map the tori for, will be forcibly sorted (needs to be a dense grid when setting up the object for interpolation with setup_interp=True) nta : int number of auxiliary angles to sample the torus at when mapping the torus setup_interp : bool if True, setup interpolation grids that allow any torus within the E range to be accessed through interpolation use_pointtransform : bool if True, use a point transformation to improve the accuracy of the mapping pt_deg : int degree of the point transformation polynomial pt_nxa : int number of points to use in the point transformation maxiter : int maximum number of iterations of root-finding algorithms angle_tol : float tolerance for angle root-finding (f(x) is within tol of desired value) bisect : bool if True, use simple bisection for root-finding, otherwise first try Newton-Raphson (mainly useful for testing the bisection fallback) Notes ----- - 2018-04-11 - Started - Bovy (UofT) """ # actionAngleInverse.__init__(self,*args,**kwargs) if pot is None: # pragma: no cover raise OSError("Must specify pot= for actionAngleVerticalInverse") self._pot = pot self._aAV = actionAngleVertical(pot=self._pot) # Compute action, frequency, and xmax for each energy self._nE = len(Es) js = numpy.empty(self._nE) Omegas = numpy.empty(self._nE) xmaxs = numpy.empty(self._nE) self._Es = numpy.sort(numpy.array(Es)) for ii, E in enumerate(Es): if (E - evaluatelinearPotentials(self._pot, 0.0)) < 1e-14: # J=0, should be using vertical freq. from 2nd deriv. tJ, tO = self._aAV.actionsFreqs( 0.0, numpy.sqrt( 2.0 * (E + 1e-5 - evaluatelinearPotentials(self._pot, 0.0)) ), ) js[ii] = 0.0 Omegas[ii] = tO[0] xmaxs[ii] = 0.0 continue tJ, tO = self._aAV.actionsFreqs( 0.0, numpy.sqrt(2.0 * (E - evaluatelinearPotentials(self._pot, 0.0))) ) js[ii] = tJ[0] Omegas[ii] = tO[0] xmaxs[ii] = self._aAV.calcxmax( 0.0, numpy.sqrt(2.0 * (E - evaluatelinearPotentials(self._pot, 0.0))), E=E, ) self._js = js self._Omegas = Omegas self._xmaxs = xmaxs # Set harmonic-oscillator frequencies == frequencies self._OmegaHO = copy.copy(Omegas) # The following work properly for arrays of omega self._hoaa = actionAngleHarmonic(omega=self._OmegaHO) self._hoaainv = actionAngleHarmonicInverse(omega=self._OmegaHO) if use_pointtransform and pt_deg > 1: self._setup_pointtransform(pt_deg - (1 - pt_deg % 2), pt_nxa) # make odd else: # Setup identity point transformation self._pt_deg = 1 self._pt_nxa = pt_nxa self._pt_xmaxs = self._xmaxs self._pt_coeffs = numpy.zeros((self._nE, 2)) self._pt_coeffs[:, 1] = 1.0 self._pt_deriv_coeffs = numpy.ones((self._nE, 1)) self._pt_deriv2_coeffs = numpy.zeros((self._nE, 1)) # Now map all tori self._nta = nta self._thetaa = numpy.linspace(0.0, 2.0 * numpy.pi * (1.0 - 1.0 / nta), nta) self._maxiter = maxiter self._angle_tol = angle_tol self._bisect = bisect self._xgrid = self._create_xgrid() self._ja = _ja( self._xgrid, self._Egrid, self._pot, self._omegagrid, self._ptcoeffsgrid, self._ptderivcoeffsgrid, self._xmaxgrid, self._ptxmaxgrid, ) self._djadj = ( _djadj( self._xgrid, self._Egrid, self._pot, self._omegagrid, self._ptcoeffsgrid, self._ptderivcoeffsgrid, self._ptderiv2coeffsgrid, self._xmaxgrid, self._ptxmaxgrid, ) * numpy.atleast_2d(self._Omegas / self._OmegaHO).T ) # In case not 1! self._djadj[self._js < 1e-10] = 1.0 # J = 0 special case # Store mean(ja), this is only a better approx. of j w/ no PT! self._js_orig = copy.copy(self._js) self._js = numpy.nanmean(self._ja, axis=1) # Store better approximation to Omega self._Omegas_orig = copy.copy(self._Omegas) self._Omegas /= numpy.nanmean(self._djadj, axis=1) # Compute Fourier expansions self._nforSn = numpy.arange(self._ja.shape[1] // 2 + 1) self._nSn = ( numpy.real( numpy.fft.rfft( self._ja - numpy.atleast_2d(numpy.nanmean(self._ja, axis=1)).T, axis=1, ) )[:, 1:] / self._ja.shape[1] ) self._dSndJ = ( numpy.real( numpy.fft.rfft( self._djadj / numpy.atleast_2d(numpy.nanmean(self._djadj, axis=1)).T - 1.0, axis=1, ) )[:, 1:] / self._ja.shape[1] ) # Interpolation of small, noisy coeffs doesn't work, so set to zero if setup_interp: self._nSn[numpy.fabs(self._nSn) < 1e-16] = 0.0 self._dSndJ[numpy.fabs(self._dSndJ) < 1e-15] = 0.0 self._dSndJ /= numpy.atleast_2d(self._nforSn)[:, 1:] self._nforSn = self._nforSn[1:] self._js[self._Es < 1e-10] = 0.0 # Should use sqrt(2nd deriv. pot), but currently not implemented for 1D if self._nE > 1: self._OmegaHO[self._Es < 1e-10] = self._OmegaHO[1] self._Omegas[self._Es < 1e-10] = self._Omegas[1] self._nSn[self._js < 1e-10] = 0.0 self._dSndJ[self._js < 1e-10] = 0.0 # Setup interpolation if requested if setup_interp: self._interp = True self._setup_interp() else: self._interp = False return None def _setup_pointtransform(self, pt_deg, pt_nxa): # Setup a point transformation for each torus self._pt_deg = pt_deg self._pt_nxa = pt_nxa xamesh = numpy.linspace(-1.0, 1.0, pt_nxa) self._pt_coeffs = numpy.empty((self._nE, pt_deg + 1)) self._pt_deriv_coeffs = numpy.empty((self._nE, pt_deg)) self._pt_deriv2_coeffs = numpy.empty((self._nE, pt_deg - 1)) self._pt_xmaxs = numpy.sqrt(2.0 * self._js / self._OmegaHO) for ii in range(self._nE): if self._js[ii] < 1e-10: # Just use identity for small J self._pt_coeffs[ii] = 0.0 self._pt_coeffs[ii, 1] = 1.0 self._pt_deriv_coeffs[ii] = 1.0 self._pt_deriv2_coeffs[ii] = 0.0 self._pt_xmaxs[ii] = self._xmaxs[ii] + 1e-10 # avoid /0 coeffs = self._pt_coeffs[ii] # to start next fit continue Ea = self._js[ii] * self._OmegaHO[ii] # Function to optimize with least squares: p-p def opt_func(coeffs): # constraints: symmetric, maps [-1,1] --> [-1,1] ccoeffs = numpy.zeros(pt_deg + 1) ccoeffs[1] = 1.0 ccoeffs[3::2] = coeffs ccoeffs /= chebyshev.chebval(1, ccoeffs) pt = chebyshev.Chebyshev(ccoeffs) xmesh = pt(xamesh) * self._xmaxs[ii] # Compute v from (E,xmesh) v2mesh = 2.0 * ( self._Es[ii] - evaluatelinearPotentials(self._pot, xmesh) ) v2mesh[v2mesh < 0.0] = 0.0 vmesh = numpy.sqrt(v2mesh) # Compute v from va = 2(E-HO) and transform va2mesh = 2.0 * ( Ea - self._OmegaHO[ii] ** 2.0 * (xamesh * self._pt_xmaxs[ii]) ** 2.0 / 2.0 ) va2mesh[va2mesh < 0.0] = 0.0 vamesh = numpy.sqrt(va2mesh) piprime = pt.deriv()(xamesh) * self._xmaxs[ii] / self._pt_xmaxs[ii] vtildemesh = ( vamesh - numpy.sqrt(v2mesh) * (1.0 / piprime - piprime) ) / piprime return vmesh - vtildemesh if ii == 0: # Start from identity mapping start_coeffs = [0.0] start_coeffs.extend([0.0 for jj in range((pt_deg + 1) // 2 - 2)]) else: # Start from previous best fit start_coeffs = coeffs[3::2] / coeffs[1] coeffs = optimize.leastsq(opt_func, start_coeffs)[0] # Extract full Chebyshev parameters from constrained optimization ccoeffs = numpy.zeros(pt_deg + 1) ccoeffs[1] = 1.0 ccoeffs[3::2] = coeffs ccoeffs /= chebyshev.chebval(1, ccoeffs) # map exact [-1,1] --> [-1,1] coeffs = ccoeffs # Store point transformation as simple polynomial self._pt_coeffs[ii] = chebyshev.cheb2poly(coeffs) self._pt_deriv_coeffs[ii] = polynomial.polyder(self._pt_coeffs[ii], m=1) self._pt_deriv2_coeffs[ii] = polynomial.polyder(self._pt_coeffs[ii], m=2) return None def _create_xgrid(self): # Find x grid for regular grid in auxiliary angle (thetaa) # in practice only need to map 0 < thetaa < pi/2 to +x with +v bc symm # To efficiently start the search, we first compute thetaa for a dense # grid in x (at +v) xgrid = numpy.linspace(-1.0, 1.0, 2 * self._nta) xs = xgrid * numpy.atleast_2d(self._pt_xmaxs).T xta = _anglea( xs, numpy.tile(self._Es, (xs.shape[1], 1)).T, self._pot, numpy.tile(self._hoaa._omega, (xs.shape[1], 1)).T, numpy.rollaxis(numpy.tile(self._pt_coeffs, (xs.shape[1], 1, 1)), 1), numpy.rollaxis(numpy.tile(self._pt_deriv_coeffs, (xs.shape[1], 1, 1)), 1), numpy.tile(self._xmaxs, (xs.shape[1], 1)).T, numpy.tile(self._pt_xmaxs, (xs.shape[1], 1)).T, ) xta[numpy.isnan(xta)] = 0.0 # Zero energy orbit -> NaN # Now use Newton-Raphson to iterate to a regular grid cindx = numpy.nanargmin( numpy.fabs( (xta - numpy.rollaxis(numpy.atleast_3d(self._thetaa), 1) + numpy.pi) % (2.0 * numpy.pi) - numpy.pi ), axis=2, ) xgrid = xgrid[cindx].T * numpy.atleast_2d(self._pt_xmaxs).T Egrid = numpy.tile(self._Es, (self._nta, 1)).T omegagrid = numpy.tile(self._hoaa._omega, (self._nta, 1)).T xmaxgrid = numpy.tile(self._xmaxs, (self._nta, 1)).T ptxmaxgrid = numpy.tile(self._pt_xmaxs, (self._nta, 1)).T ptcoeffsgrid = numpy.rollaxis(numpy.tile(self._pt_coeffs, (self._nta, 1, 1)), 1) ptderivcoeffsgrid = numpy.rollaxis( numpy.tile(self._pt_deriv_coeffs, (self._nta, 1, 1)), 1 ) ptderiv2coeffsgrid = numpy.rollaxis( numpy.tile(self._pt_deriv2_coeffs, (self._nta, 1, 1)), 1 ) ta = _anglea( xgrid, Egrid, self._pot, omegagrid, ptcoeffsgrid, ptderivcoeffsgrid, xmaxgrid, ptxmaxgrid, ) mta = numpy.tile(self._thetaa, (len(self._Es), 1)) # Now iterate cntr = 0 unconv = numpy.ones(xgrid.shape, dtype="bool") # We'll fill in the -v part using the +v, also remove the endpoints unconv[:, self._nta // 4 : 3 * self._nta // 4 + 1] = False # Also don't bother with J=0 torus unconv[numpy.tile(self._js, (self._nta, 1)).T < 1e-10] = False dta = (ta[unconv] - mta[unconv] + numpy.pi) % (2.0 * numpy.pi) - numpy.pi unconv[unconv] = numpy.fabs(dta) > self._angle_tol # Don't allow too big steps maxdx = numpy.tile(self._pt_xmaxs / float(self._nta), (self._nta, 1)).T while not self._bisect: dtadx = _danglea( xgrid[unconv], Egrid[unconv], self._pot, omegagrid[unconv], ptcoeffsgrid[unconv], ptderivcoeffsgrid[unconv], ptderiv2coeffsgrid[unconv], xmaxgrid[unconv], ptxmaxgrid[unconv], ) dta = (ta[unconv] - mta[unconv] + numpy.pi) % (2.0 * numpy.pi) - numpy.pi dx = -dta / dtadx dx[numpy.fabs(dx) > maxdx[unconv]] = (numpy.sign(dx) * maxdx[unconv])[ numpy.fabs(dx) > maxdx[unconv] ] xgrid[unconv] += dx xgrid[unconv * (xgrid > ptxmaxgrid)] = ptxmaxgrid[ unconv * (xgrid > ptxmaxgrid) ] xgrid[unconv * (xgrid < -ptxmaxgrid)] = ptxmaxgrid[ unconv * (xgrid < -ptxmaxgrid) ] newta = _anglea( xgrid[unconv], Egrid[unconv], self._pot, omegagrid[unconv], ptcoeffsgrid[unconv], ptderivcoeffsgrid[unconv], xmaxgrid[unconv], ptxmaxgrid[unconv], ) ta[unconv] = newta unconv[unconv] = numpy.fabs(dta) > self._angle_tol cntr += 1 if numpy.sum(unconv) == 0: break if cntr > self._maxiter: warnings.warn( "Torus mapping with Newton-Raphson did not converge in {} iterations, falling back onto simple bisection (increase maxiter to try harder with Newton-Raphson)".format( self._maxiter ), galpyWarning, ) break if self._bisect or cntr > self._maxiter: # Reset cntr cntr = 0 # Start from nearest guess from below new_xgrid = numpy.linspace(-1.0, 1.0, 2 * self._nta) da = ( xta - numpy.rollaxis(numpy.atleast_3d(self._thetaa), 1) + numpy.pi ) % (2.0 * numpy.pi) - numpy.pi da[da >= 0.0] = -numpy.nanmax(numpy.fabs(da)) - 0.1 cindx = numpy.nanargmax(da, axis=2) tryx_min = (new_xgrid[cindx].T * numpy.atleast_2d(self._pt_xmaxs).T)[unconv] dx = ( 2.0 / (2.0 * self._nta - 1) * ptxmaxgrid ) # delta of initial x grid above while True: dx *= 0.5 xgrid[unconv] = tryx_min + dx[unconv] newta = ( _anglea( xgrid[unconv], Egrid[unconv], self._pot, omegagrid[unconv], ptcoeffsgrid[unconv], ptderivcoeffsgrid[unconv], xmaxgrid[unconv], ptxmaxgrid[unconv], ) + 2.0 * numpy.pi ) % (2.0 * numpy.pi) ta[unconv] = newta dta = (newta - mta[unconv] + numpy.pi) % (2.0 * numpy.pi) - numpy.pi tryx_min[newta < mta[unconv]] = xgrid[unconv][newta < mta[unconv]] unconv[unconv] = numpy.fabs(dta) > self._angle_tol tryx_min = tryx_min[numpy.fabs(dta) > self._angle_tol] cntr += 1 if numpy.sum(unconv) == 0: break if cntr > self._maxiter: warnings.warn( "Torus mapping with bisection did not converge in {} iterations".format( self._maxiter ) + " for energies:" + "".join(f" {k:g}" for k in sorted(set(Egrid[unconv]))), galpyWarning, ) break xgrid[:, self._nta // 4 + 1 : self._nta // 2 + 1] = xgrid[:, : self._nta // 4][ :, ::-1 ] xgrid[:, self._nta // 2 + 1 : 3 * self._nta // 4 + 1] = xgrid[ :, 3 * self._nta // 4 : ][:, ::-1] ta[:, self._nta // 4 + 1 : 3 * self._nta // 4] = _anglea( xgrid[:, self._nta // 4 + 1 : 3 * self._nta // 4], Egrid[:, self._nta // 4 + 1 : 3 * self._nta // 4], self._pot, omegagrid[:, self._nta // 4 + 1 : 3 * self._nta // 4], ptcoeffsgrid[:, self._nta // 4 + 1 : 3 * self._nta // 4], ptderivcoeffsgrid[:, self._nta // 4 + 1 : 3 * self._nta // 4], xmaxgrid[:, self._nta // 4 + 1 : 3 * self._nta // 4], ptxmaxgrid[:, self._nta // 4 + 1 : 3 * self._nta // 4], vsign=-1.0, ) self._dta = (ta - mta + numpy.pi) % (2.0 * numpy.pi) - numpy.pi self._mta = mta # Store these, they are useful (obv. arbitrary to return xgrid # and not just store it...) self._Egrid = Egrid self._omegagrid = omegagrid self._ptcoeffsgrid = ptcoeffsgrid self._ptderivcoeffsgrid = ptderivcoeffsgrid self._ptderiv2coeffsgrid = ptderiv2coeffsgrid self._ptxmaxgrid = ptxmaxgrid self._xmaxgrid = xmaxgrid return xgrid def plot_convergence( self, E, overplot=False, return_gridspec=False, shift_action=None ): if shift_action is None: shift_action = self._pt_deg > 1 # First find the torus for this energy indx = numpy.nanargmin(numpy.fabs(E - self._Es)) if numpy.fabs(E - self._Es[indx]) > 1e-10: raise ValueError( "Given energy not found; please specify an energy used in the initialization of the instance" ) if not overplot: gs = gridspec.GridSpec(2, 3, height_ratios=[4, 1]) else: gs = overplot # confusingly, we overload the meaning of overplot # mapping of thetaa --> x pyplot.subplot(gs[0]) plot.plot( self._thetaa, self._xgrid[indx], color="k", ls="--" if overplot else "-", ylabel=r"$x(\theta^A)$", gcf=True, overplot=overplot, ) if not overplot: pyplot.gca().xaxis.set_major_formatter(NullFormatter()) if not overplot: pyplot.subplot(gs[3]) negv = (self._thetaa > numpy.pi / 2.0) * ( self._thetaa < 3.0 * numpy.pi / 2.0 ) thetaa_out = numpy.empty_like(self._thetaa) one = numpy.ones(numpy.sum(True ^ negv)) thetaa_out[True ^ negv] = _anglea( self._xgrid[indx][True ^ negv], E, self._pot, self._OmegaHO[indx], self._pt_coeffs[indx], numpy.tile(self._pt_deriv_coeffs[indx], (numpy.sum(True ^ negv), 1)), self._xmaxs[indx] * one, self._pt_xmaxs[indx] * one, vsign=1.0, ) one = numpy.ones(numpy.sum(negv)) thetaa_out[negv] = _anglea( self._xgrid[indx][negv], E, self._pot, self._OmegaHO[indx], self._pt_coeffs[indx], numpy.tile(self._pt_deriv_coeffs[indx], (numpy.sum(negv), 1)), self._xmaxs[indx] * one, self._pt_xmaxs[indx] * one, vsign=-1.0, ) plot.plot( self._thetaa, ((thetaa_out - self._thetaa + numpy.pi) % (2.0 * numpy.pi)) - numpy.pi, color="k", gcf=True, xlabel=r"$\theta^A$", ylabel=r"$\theta^A[x(\theta^A)]-\theta^A$", ) # Recovery of the nSn from J^A(theta^A) behavior pyplot.subplot(gs[1]) plot.plot( self._thetaa, self._ja[indx], color="k", ls="--" if overplot else "-", ylabel=r"$J^A(\theta^A),J$", gcf=True, overplot=overplot, ) pyplot.axhline( self._js[indx] + shift_action * (self._js_orig[indx] - self._js[indx]), color="k", ls="--", ) if not overplot: pyplot.gca().xaxis.set_major_formatter(NullFormatter()) if not overplot: pyplot.subplot(gs[4]) plot.plot( self._thetaa, numpy.array( [ self._js[indx] + 2.0 * numpy.sum(self._nSn[indx] * numpy.cos(self._nforSn * x)) for x in self._thetaa ] ) / self._ja[indx] - 1.0, color="k", xlabel=r"$\theta^A$", ylabel=r"$\delta J^A/J^A$", gcf=True, ) # Recovery of the dSndJ from dJ^A/dJ(theta^A) behavior pyplot.subplot(gs[2]) plot.plot( self._thetaa, self._djadj[indx] / numpy.nanmean(self._djadj[indx]), color="k", ls="--" if overplot else "-", ylabel=r"$\mathrm{d}J^A/\mathrm{d}J(\theta^A)$", gcf=True, overplot=overplot, ) pyplot.axhline(1.0, color="k", ls="--") if not overplot: pyplot.gca().xaxis.set_major_formatter(NullFormatter()) if not overplot: pyplot.subplot(gs[5]) plot.plot( self._thetaa, numpy.array( [ 1.0 + 2.0 * numpy.sum( self._nforSn * self._dSndJ[indx] * numpy.cos(self._nforSn * x) ) for x in self._thetaa ] ) - self._djadj[indx] / numpy.nanmean(self._djadj[indx]), color="k", xlabel=r"$\theta^A$", ylabel=r"$\delta \mathrm{d}J^A/\mathrm{d}J(\theta^A)$", gcf=True, ) pyplot.tight_layout() if return_gridspec: return gs else: return None def plot_power(self, Es, symm=True, overplot=False, return_gridspec=False, ls="-"): Es = numpy.sort(numpy.atleast_1d(Es)) minn_for_cmap = 4 if len(Es) < minn_for_cmap: if not overplot: gs = gridspec.GridSpec(1, 2) else: gs = overplot # confusingly, we overload the meaning of overplot else: if not overplot: outer = gridspec.GridSpec(1, 2, width_ratios=[2.0, 0.05], wspace=0.05) gs = gridspec.GridSpecFromSubplotSpec( 1, 2, subplot_spec=outer[0], wspace=0.35 ) else: raise RuntimeError( f"plot_power with >= {minn_for_cmap} energies and overplot=True is not supported" ) for ii, E in enumerate(Es): # First find the torus for this energy indx = numpy.nanargmin(numpy.fabs(E - self._Es)) if numpy.fabs(E - self._Es[indx]) > 1e-10: raise ValueError( "Given energy not found; please specify an energy used in the initialization of the instance" ) # n S_n y = numpy.fabs(self._nSn[indx, symm :: symm + 1]) if len(Es) > 1 and E == Es[0]: y4minmax = numpy.fabs(self._nSn[:, symm :: symm + 1]) ymin = numpy.amax( [numpy.amin(y4minmax[numpy.isfinite(y4minmax)]), 1e-17] ) ymax = numpy.amax(y4minmax[numpy.isfinite(y4minmax)]) elif len(Es) == 1: ymin = numpy.amax([numpy.amin(y[numpy.isfinite(y)]), 1e-17]) ymax = numpy.amax(y[numpy.isfinite(y)]) if len(Es) < minn_for_cmap: label = rf"$E = {E:g}$" color = f"C{ii}" else: label = None color = cm.plasma((E - Es[0]) / (Es[-1] - Es[0])) pyplot.subplot(gs[0]) plot.plot( numpy.fabs(self._nforSn[symm :: symm + 1]), y, yrange=[ymin, ymax], ls=ls, gcf=True, semilogy=True, overplot=overplot, xrange=[0.0, self._nforSn[-1]], label=label, color=color, xlabel=r"$n$", ylabel=r"$|nS_n|$", ) # d S_n / d J y = numpy.fabs(self._dSndJ[indx, symm :: symm + 1]) if len(Es) > 1 and E == Es[0]: y4minmax = numpy.fabs(self._dSndJ[:, symm :: symm + 1]) ymin = numpy.amax( [numpy.amin(y4minmax[numpy.isfinite(y4minmax)]), 1e-17] ) ymax = numpy.amax(y4minmax[numpy.isfinite(y4minmax)]) elif len(Es) == 1: ymin = numpy.amax([numpy.amin(y[numpy.isfinite(y)]), 1e-17]) ymax = numpy.amax(y[numpy.isfinite(y)]) if len(Es) < minn_for_cmap: label = rf"$E = {E:g}$" color = f"C{ii}" else: label = None color = cm.plasma((E - Es[0]) / (Es[-1] - Es[0])) pyplot.subplot(gs[1]) plot.plot( numpy.fabs(self._nforSn[symm :: symm + 1]), y, yrange=[ymin, ymax], ls=ls, gcf=True, semilogy=True, overplot=overplot, xrange=[0.0, self._nforSn[-1]], label=label, color=color, xlabel=r"$n$", ylabel=r"$|\mathrm{d}S_n/\mathrm{d}J|$", ) if not overplot == gs: overplot = True if len(Es) < minn_for_cmap: if not overplot == gs: pyplot.subplot(gs[0]) pyplot.legend(fontsize=17.0, frameon=False) pyplot.subplot(gs[1]) pyplot.legend(fontsize=17.0, frameon=False) pyplot.tight_layout() else: pyplot.subplot(outer[1]) sm = pyplot.cm.ScalarMappable( cmap=cm.plasma, norm=pyplot.Normalize(vmin=Es[0], vmax=Es[-1]) ) sm._A = [] cbar = pyplot.colorbar( sm, cax=pyplot.gca(), use_gridspec=True, format=r"$%g$" ) cbar.set_label(r"$E$") outer.tight_layout(pyplot.gcf()) if return_gridspec: return gs else: return None def plot_orbit(self, E): ta = numpy.linspace(0.0, 2.0 * numpy.pi, 1001) if not self._interp: # First find the torus for this energy indx = numpy.nanargmin(numpy.fabs(E - self._Es)) if numpy.fabs(E - self._Es[indx]) > 1e-10: raise ValueError( "Given energy not found; please specify an energy used in the initialization of the instance" ) tJ = self._js[indx] else: tJ = self.J(E) x, v = self(tJ, ta) # First plot orbit in x,v pyplot.subplot(1, 2, 1) plot.plot( x, v, xlabel=r"$x$", ylabel=r"$v$", gcf=True, color="k", xrange=[1.1 * numpy.amin(x), 1.1 * numpy.amax(x)], yrange=[1.1 * numpy.amin(v), 1.1 * numpy.amax(v)], ) # Then plot energy pyplot.subplot(1, 2, 2) Eorbit = (v**2.0 / 2.0 + evaluatelinearPotentials(self._pot, x)) / E - 1.0 ymin, ymax = numpy.amin(Eorbit), numpy.amax(Eorbit) plot.plot( ta, Eorbit, xrange=[0.0, 2.0 * numpy.pi], yrange=[ymin - (ymax - ymin) * 3.0, ymax + (ymax - ymin) * 3.0], gcf=True, color="k", xlabel=r"$\theta$", ylabel=r"$E/E_{\mathrm{true}}-1$", ) pyplot.tight_layout() return None ################### FUNCTIONS FOR INTERPOLATION BETWEEN TORI############### def _setup_interp(self): self._Emin = self._Es[0] self._Emax = self._Es[-1] self._nnSn = self._nSn.shape[1] # won't be confusing... self._nSnNormalize = numpy.ones(self._nnSn) self._nSnFiltered = ndimage.spline_filter(self._nSn, order=3) self._dSndJFiltered = ndimage.spline_filter(self._dSndJ, order=3) self.J = interpolate.InterpolatedUnivariateSpline(self._Es, self._js, k=3) self.E = interpolate.InterpolatedUnivariateSpline(self._js, self._Es, k=3) self.OmegaHO = interpolate.InterpolatedUnivariateSpline( self._Es, self._OmegaHO, k=3 ) self.Omega = interpolate.InterpolatedUnivariateSpline( self._Es, self._Omegas, k=3 ) self.xmax = interpolate.InterpolatedUnivariateSpline(self._Es, self._xmaxs, k=3) self.ptxmax = interpolate.InterpolatedUnivariateSpline( self._Es, self._pt_xmaxs, k=3 ) self._nptcoeffs = self._pt_coeffs.shape[1] self._ptcoeffsFiltered = ndimage.spline_filter(self._pt_coeffs, order=3) self._ptderivcoeffsFiltered = ndimage.spline_filter( self._pt_deriv_coeffs, order=3 ) return None def _coords_for_map_coords(self, E): coords = numpy.empty((2, self._nnSn * len(E))) coords[0] = numpy.tile( (E - self._Emin) / (self._Emax - self._Emin) * (self._nE - 1.0), (self._nnSn, 1), ).T.flatten() coords[1] = numpy.tile(self._nforSn - 1, (len(E), 1)).flatten() return coords def nSn(self, E): if not self._interp: raise RuntimeError( "To evaluate nSn, interpolation must be activated at instantiation using setup_interp=True" ) evalE = numpy.atleast_1d(E) indxc = (evalE >= self._Emin) * (evalE <= self._Emax) coords = self._coords_for_map_coords(evalE[indxc]) out = numpy.empty((len(evalE), self._nnSn)) out[indxc] = numpy.reshape( ndimage.map_coordinates( self._nSnFiltered, coords, order=3, prefilter=False ), (numpy.sum(indxc), self._nnSn), ) out[True ^ indxc] = numpy.nan return out def dSndJ(self, E): if not self._interp: raise RuntimeError( "To evaluate dnSndJ, interpolation must be activated at instantiation using setup_interp=True" ) evalE = numpy.atleast_1d(E) indxc = (evalE >= self._Emin) * (evalE <= self._Emax) coords = self._coords_for_map_coords(evalE[indxc]) out = numpy.empty((len(evalE), self._nnSn)) out[indxc] = numpy.reshape( ndimage.map_coordinates( self._dSndJFiltered, coords, order=3, prefilter=False ), (numpy.sum(indxc), self._nnSn), ) out[True ^ indxc] = numpy.nan return out def _coords_for_map_coords_pt(self, E, deriv=False): coords = numpy.empty((2, (self._nptcoeffs - deriv) * len(E))) coords[0] = numpy.tile( (E - self._Emin) / (self._Emax - self._Emin) * (self._nE - 1.0), (self._nptcoeffs - deriv, 1), ).T.flatten() coords[1] = numpy.tile( numpy.arange(self._nptcoeffs - deriv), (len(E), 1) ).flatten() return coords def pt_coeffs(self, E): if not self._interp: raise RuntimeError( "To evaluate pt_coeffs, interpolation must be activated at instantiation using setup_interp=True" ) evalE = numpy.atleast_1d(E) indxc = (evalE >= self._Emin) * (evalE <= self._Emax) coords = self._coords_for_map_coords_pt(evalE[indxc], deriv=False) out = numpy.empty((len(evalE), self._nptcoeffs)) out[indxc] = numpy.reshape( ndimage.map_coordinates( self._ptcoeffsFiltered, coords, order=3, prefilter=False ), (numpy.sum(indxc), self._nptcoeffs), ) out[True ^ indxc] = numpy.nan return out def pt_deriv_coeffs(self, E): if not self._interp: raise RuntimeError( "To evaluate pt_deriv_coeffs, interpolation must be activated at instantiation using setup_interp=True" ) evalE = numpy.atleast_1d(E) indxc = (evalE >= self._Emin) * (evalE <= self._Emax) coords = self._coords_for_map_coords_pt(evalE[indxc], deriv=True) out = numpy.empty((len(evalE), self._nptcoeffs - 1)) out[indxc] = numpy.reshape( ndimage.map_coordinates( self._ptderivcoeffsFiltered, coords, order=3, prefilter=False ), (numpy.sum(indxc), self._nptcoeffs - 1), ) out[True ^ indxc] = numpy.nan return out def plot_interp(self, E, symm=True): truthaAV = actionAngleVerticalInverse( pot=self._pot, Es=[E], nta=self._nta, setup_interp=False, use_pointtransform=self._pt_deg > 1, pt_deg=self._pt_deg, pt_nxa=self._pt_nxa, ) # Check whether S_n is matched pyplot.subplot(2, 3, 1) y = numpy.fabs(self.nSn(E)[0, symm :: symm + 1]) ymin = numpy.amax([numpy.amin(y[numpy.isfinite(y)]), 1e-17]) ymax = numpy.amax(y[numpy.isfinite(y)]) plot.plot( numpy.fabs(self._nforSn[symm :: symm + 1]), y, yrange=[ymin, ymax], gcf=True, semilogy=True, xrange=[0.0, self._nforSn[-1]], label=r"$\mathrm{Interpolation}$", xlabel=r"$n$", ylabel=r"$|nS_n|$", ) plot.plot( self._nforSn[symm :: symm + 1], truthaAV._nSn[0, symm :: symm + 1], overplot=True, label=r"$\mathrm{Direct}$", ) pyplot.legend(fontsize=17.0, frameon=False) pyplot.subplot(2, 3, 4) y = ((self.nSn(E)[0] - truthaAV._nSn[0]) / truthaAV._nSn[0])[symm :: symm + 1] ymin = numpy.amin(y[numpy.isfinite(y)]) ymax = numpy.amax(y[numpy.isfinite(y)]) plot.plot( self._nforSn[symm :: symm + 1], y, yrange=[ymin, ymax], xrange=[0.0, self._nforSn[-1]], gcf=True, xlabel=r"$n$", ylabel=r"$S_{n,\mathrm{interp}}/S_{n,\mathrm{direct}}-1$", ) # Check whether d S_n / d J is matched pyplot.subplot(2, 3, 2) y = numpy.fabs(self.dSndJ(E)[0, symm :: symm + 1]) ymin = numpy.amax([numpy.amin(y[numpy.isfinite(y)]), 1e-18]) ymax = numpy.amax(y[numpy.isfinite(y)]) plot.plot( numpy.fabs(self._nforSn[symm :: symm + 1]), y, yrange=[ymin, ymax], xrange=[0.0, self._nforSn[-1]], gcf=True, semilogy=True, label=r"$\mathrm{Interpolation}$", xlabel=r"$n$", ylabel=r"$|\mathrm{d}S_n/\mathrm{d}J|$", ) plot.plot( self._nforSn[symm :: symm + 1], numpy.fabs(truthaAV._dSndJ[0, symm :: symm + 1]), overplot=True, label=r"$\mathrm{Direct}$", ) pyplot.legend(fontsize=17.0, frameon=False) pyplot.subplot(2, 3, 5) y = ((self.dSndJ(E)[0] - truthaAV._dSndJ[0]) / truthaAV._dSndJ[0])[ symm :: symm + 1 ] ymin = numpy.amin(y[numpy.isfinite(y)]) ymax = numpy.amax(y[numpy.isfinite(y)]) plot.plot( self._nforSn[symm :: symm + 1], y, yrange=[ymin, ymax], xrange=[0.0, self._nforSn[-1]], gcf=True, xlabel=r"$n$", ylabel=r"$(\mathrm{d}S_n/\mathrm{d}J)_{\mathrm{interp}}/(\mathrm{d}S_n/\mathrm{d}J)_{\mathrm{direct}}-1$", ) # Check energy along the torus pyplot.subplot(2, 3, 3) ta = numpy.linspace(0.0, 2.0 * numpy.pi, 1001) x, v = truthaAV(truthaAV._js, ta) Edirect = v**2.0 / 2.0 + evaluatelinearPotentials(self._pot, x) x, v = self(self.J(E), ta) Einterp = v**2.0 / 2.0 + evaluatelinearPotentials(self._pot, x) ymin, ymax = numpy.amin([Edirect, Einterp]), numpy.amax([Edirect, Einterp]) plot.plot( ta, Einterp, xrange=[0.0, 2.0 * numpy.pi], yrange=[ymin - (ymax - ymin) * 2.0, ymax + (ymax - ymin) * 2.0], gcf=True, label=r"$\mathrm{Interpolation}$", xlabel=r"$\theta$", ylabel=r"$E$", ) plot.plot(ta, Edirect, overplot=True, label=r"$\mathrm{Direct}$") pyplot.legend(fontsize=17.0, frameon=False) pyplot.subplot(2, 3, 6) plot.plot( ta, Einterp / Edirect - 1.0, xrange=[0.0, 2.0 * numpy.pi], gcf=True, label=r"$\mathrm{Interpolation}$", xlabel=r"$\theta$", ylabel=r"$E_{\mathrm{interp}}/E_{\mathrm{direct}}-1$", ) pyplot.tight_layout() return None def J(self, E): """ Return the action for the given energy. Parameters ---------- E : float Energy. Returns ------- float Action. Notes ----- - 2022-11-24 - Written - Bovy (UofT) """ indx = numpy.nanargmin(numpy.fabs(E - self._Es)) if numpy.fabs(E - self._Es[indx]) > 1e-10: raise ValueError( "Given energy not found; please specify an energy used in the initialization of the instance" ) return self._js[indx] def _evaluate(self, j, angle, **kwargs): """ Evaluate the phase-space coordinates (x,v) for a number of angles on a single torus Parameters ---------- j : float Action angle : numpy.ndarray Angle Returns ------- tuple Tuple containing the phase-space coordinates [x,vx] Notes ----- - 2018-04-08 - Written - Bovy (UofT) """ return self._xvFreqs(j, angle, **kwargs)[:2] def _xvFreqs(self, j, angle, **kwargs): """ Evaluate the phase-space coordinates (x,v) for a number of angles on a single torus as well as the frequency. Parameters ---------- j : float Action. angle : numpy.ndarray Angle. Returns ------- tuple (x,v,frequency) Notes ----- - 2018-04-15 - Written - Bovy (UofT) """ # Find torus if not self._interp: indx = numpy.nanargmin(numpy.fabs(j - self._js)) if numpy.fabs(j - self._js[indx]) > 1e-10: raise ValueError( "Given action/energy not found, to use interpolation, initialize with setup_interp=True" ) tnSn = self._nSn[indx] tdSndJ = self._dSndJ[indx] tOmegaHO = self._OmegaHO[indx] tOmega = self._Omegas[indx] txmax = self._xmaxs[indx] tptxmax = self._pt_xmaxs[indx] tptcoeffs = self._pt_coeffs[indx] tptderivcoeffs = self._pt_deriv_coeffs[indx] else: tE = self.E(j) tnSn = self.nSn(tE)[0] tdSndJ = self.dSndJ(tE)[0] tOmegaHO = self.OmegaHO(tE) tOmega = self.Omega(tE) txmax = self.xmax(tE) tptxmax = self.ptxmax(tE) tptcoeffs = self.pt_coeffs(tE)[0] tptderivcoeffs = self.pt_deriv_coeffs(tE)[0] # First we need to solve for a self._angle_tol # Don't allow too big steps maxda = 2.0 * numpy.pi / 101 while not self._bisect: danglea = 1.0 + 2.0 * numpy.sum( self._nforSn * tdSndJ * numpy.cos(self._nforSn * numpy.atleast_2d(anglea[unconv]).T), axis=1, ) dta = (ta[unconv] - angle[unconv] + numpy.pi) % (2.0 * numpy.pi) - numpy.pi da = -dta / danglea da[numpy.fabs(da) > maxda] = (numpy.sign(da) * maxda)[ numpy.fabs(da) > maxda ] anglea[unconv] += da unconv[unconv] = numpy.fabs(dta) > self._angle_tol newta = anglea[unconv] + 2.0 * numpy.sum( tdSndJ * numpy.sin(self._nforSn * numpy.atleast_2d(anglea[unconv]).T), axis=1, ) ta[unconv] = newta cntr += 1 if numpy.sum(unconv) == 0: break if cntr > self._maxiter: # pragma: no cover warnings.warn( "Angle mapping with Newton-Raphson did not converge in {} iterations, falling back onto simple bisection (increase maxiter to try harder with Newton-Raphson)".format( self._maxiter ), galpyWarning, ) break # Fallback onto simple bisection in case of non-convergence if self._bisect or cntr > self._maxiter: # Reset cntr cntr = 0 trya_min = numpy.zeros(numpy.sum(unconv)) da = 2.0 * numpy.pi while True: da *= 0.5 anglea[unconv] = trya_min + da newta = ( anglea[unconv] + 2.0 * numpy.sum( tdSndJ * numpy.sin(self._nforSn * numpy.atleast_2d(anglea[unconv]).T), axis=1, ) + 2.0 * numpy.pi ) % (2.0 * numpy.pi) dta = (newta - angle[unconv] + numpy.pi) % (2.0 * numpy.pi) - numpy.pi trya_min[newta < angle[unconv]] = anglea[unconv][newta < angle[unconv]] unconv[unconv] = numpy.fabs(dta) > self._angle_tol trya_min = trya_min[numpy.fabs(dta) > self._angle_tol] cntr += 1 if numpy.sum(unconv) == 0: break if cntr > self._maxiter: # pragma: no cover warnings.warn( "Angle mapping with bisection did not converge in {} iterations".format( self._maxiter ) + " for angles:" + "".join(f" {k:g}" for k in sorted(set(angle[unconv]))), galpyWarning, ) break # Then compute the auxiliary action ja = j + 2.0 * numpy.sum( tnSn * numpy.cos(self._nforSn * numpy.atleast_2d(anglea).T), axis=1 ) hoaainv = actionAngleHarmonicInverse(omega=tOmegaHO) xa, va = hoaainv(ja, anglea) x = txmax * polynomial.polyval((xa / tptxmax).T, tptcoeffs.T, tensor=False).T v = ( va / tptxmax * txmax * polynomial.polyval((xa / tptxmax).T, tptderivcoeffs.T, tensor=False).T ) return (x, v, tOmega) def _Freqs(self, j, **kwargs): """ Return the frequency corresponding to a torus Parameters ---------- j : float Action. Returns ------- float Frequency corresponding to a torus. Notes ----- - 2018-04-08 - Written - Bovy (UofT) """ # Find torus if not self._interp: indx = numpy.nanargmin(numpy.fabs(j - self._js)) if numpy.fabs(j - self._js[indx]) > 1e-10: raise ValueError( "Given action/energy not found, to use interpolation, initialize with setup_interp=True" ) tOmega = self._Omegas[indx] else: tE = self.E(j) tOmega = self.Omega(tE) return tOmega def _anglea(xa, E, pot, omega, ptcoeffs, ptderivcoeffs, xmax, ptxmax, vsign=1.0): """ Compute the auxiliary angle in the harmonic-oscillator for a grid in x and E Parameters ---------- xa : numpy.ndarray Position. E : float Energy. pot : Potential object The potential. omega : numpy.ndarray Harmonic-oscillator frequencies. ptcoeffs : numpy.ndarray Coefficients of the polynomial point transformation. ptderivcoeffs : numpy.ndarray Coefficients of the derivative of the polynomial point transformation. xmax : float Xmax of the true torus. ptxmax : float Xmax of the point-transformed torus. vsign : float, optional Sign of the velocity. Default is 1.0. Returns ------- numpy.ndarray Auxiliary angles. Notes ----- - 2018-04-13 - Written - Bovy (UofT) - 2018-11-19 - Added point transformation - Bovy (UofT) """ # Compute v x = xmax * polynomial.polyval((xa / ptxmax).T, ptcoeffs.T, tensor=False).T v2 = 2.0 * (E - evaluatelinearPotentials(pot, x)) v2[v2 < 0] = 0.0 v2[numpy.fabs(xa) == ptxmax] = 0.0 # just in case the pt mapping has small issues piprime = ( xmax / ptxmax * polynomial.polyval((xa / ptxmax).T, ptderivcoeffs.T, tensor=False).T ) # J=0 special case: piprime[(xmax == 0.0) * (ptxmax == xmax + 1e-10)] = polynomial.polyval( ( xa[(xmax == 0.0) * (ptxmax == xmax + 1e-10)] / ptxmax[(xmax == 0.0) * (ptxmax == xmax + 1e-10)] ).T, ptderivcoeffs[(xmax == 0.0) * (ptxmax == xmax + 1e-10)].T, tensor=False, ).T return numpy.arctan2(omega * xa, vsign * numpy.sqrt(v2) / piprime) def _danglea( xa, E, pot, omega, ptcoeffs, ptderivcoeffs, ptderiv2coeffs, xmax, ptxmax, vsign=1.0 ): """ Compute the derivative of the auxiliary angle in the harmonic-oscillator for a grid in x and E at constant E Parameters ---------- xa : numpy.ndarray Position. E : float Energy. pot : Potential object The potential. omega : numpy.ndarray Harmonic-oscillator frequencies. ptcoeffs : numpy.ndarray Coefficients of the polynomial point transformation. ptderivcoeffs : numpy.ndarray Coefficients of the derivative of the polynomial point transformation. ptderiv2coeffs : numpy.ndarray Coefficients of the second derivative of the polynomial point transformation. xmax : float Xmax of the true torus. ptxmax : float Xmax of the point-transformed torus. vsign : float, optional Sign of the velocity. Default is 1.0. Returns ------- numpy.ndarray d auxiliary angles / d x (2D array) Notes ----- - 2018-04-13 - Written - Bovy (UofT) - 2018-11-22 - Added point transformation - Bovy (UofT) """ # Compute v x = xmax * polynomial.polyval((xa / ptxmax).T, ptcoeffs.T, tensor=False).T v2 = 2.0 * (E - evaluatelinearPotentials(pot, x)) v2[v2 < 1e-15] = 1e-15 piprime = ( xmax / ptxmax * polynomial.polyval((xa / ptxmax).T, ptderivcoeffs.T, tensor=False).T ) anglea = numpy.arctan2(omega * xa * piprime, vsign * numpy.sqrt(v2)) piprime2 = ( xmax / ptxmax**2.0 * polynomial.polyval((xa / ptxmax).T, ptderiv2coeffs.T, tensor=False).T ) return ( omega * numpy.cos(anglea) ** 2.0 * v2**-1.5 * ( v2 * (piprime + xa * piprime2) - xa * evaluatelinearForces(pot, x) * piprime**2.0 ) ) def _ja(xa, E, pot, omega, ptcoeffs, ptderivcoeffs, xmax, ptxmax): """ Compute the auxiliary action in the harmonic-oscillator for a grid in x and E Parameters ---------- xa : numpy.ndarray position E : numpy.ndarray Energy pot : Potential object the potential omega : numpy.ndarray harmonic-oscillator frequencies ptcoeffs : numpy.ndarray coefficients of the polynomial point transformation ptderivcoeffs : numpy.ndarray coefficients of the derivative of the polynomial point transformation xmax : float xmax of the true torus ptxmax : float xmax of the point-transformed torus Returns ------- numpy.ndarray auxiliary actions Notes ----- - 2018-04-14 - Written - Bovy (UofT) - 2018-11-22 - Added point transformation - Bovy (UofT) """ x = xmax * polynomial.polyval((xa / ptxmax).T, ptcoeffs.T, tensor=False).T v2over2 = E - evaluatelinearPotentials(pot, x) v2over2[v2over2 < 0.0] = 0.0 piprime = ( xmax / ptxmax * polynomial.polyval((xa / ptxmax).T, ptderivcoeffs.T, tensor=False).T ) out = numpy.empty_like(xa) gIndx = True ^ ((xmax == 0.0) * (ptxmax == xmax + 1e-10)) out[gIndx] = ( v2over2[gIndx] / omega[gIndx] / piprime[gIndx] ** 2.0 + omega[gIndx] * xa[gIndx] ** 2.0 / 2.0 ) # J=0 special case out[True ^ gIndx] = 0.0 return out def _djadj(xa, E, pot, omega, ptcoeffs, ptderivcoeffs, ptderiv2coeffs, xmax, ptxmax): """ Compute the derivative of the auxiliary action in the harmonic-oscillator wrt the action for a grid in x and E Parameters ---------- xa : numpy.ndarray position E : float Energy pot : galpy.potential.Potential the potential omega : numpy.ndarray harmonic-oscillator frequencies ptcoeffs : numpy.ndarray coefficients of the polynomial point transformation ptderivcoeffs : numpy.ndarray coefficients of the derivative of the polynomial point transformation ptderiv2coeffs : numpy.ndarray coefficients of the second derivative of the polynomial point transformation xmax : float xmax of the true torus ptxmax : float xmax of the point-transformed torus Returns ------- numpy.ndarray d(auxiliary actions)/d(action) Notes ----- - 2018-04-14 - Written - Bovy (UofT) - 2018-11-23 - Added point transformation - Bovy (UofT) """ x = xmax * polynomial.polyval((xa / ptxmax).T, ptcoeffs.T, tensor=False).T v2 = 2.0 * (E - evaluatelinearPotentials(pot, x)) piprime = ( xmax / ptxmax * polynomial.polyval((xa / ptxmax).T, ptderivcoeffs.T, tensor=False).T ) piprime2 = ( xmax / ptxmax**2.0 * polynomial.polyval((xa / ptxmax).T, ptderiv2coeffs.T, tensor=False).T ) # J=0 special case: piprime[(xmax == 0.0) * (ptxmax == xmax + 1e-10)] = polynomial.polyval( ( x[(xmax == 0.0) * (ptxmax == xmax + 1e-10)] / ptxmax[(xmax == 0.0) * (ptxmax == xmax + 1e-10)] ).T, ptderivcoeffs[(xmax == 0.0) * (ptxmax == xmax + 1e-10)].T, tensor=False, ).T gIndx = True ^ ((xmax == 0.0) * (ptxmax == xmax + 1e-10)) dxAdE = numpy.empty_like(xa) dxAdE[gIndx] = ( xa[gIndx] * piprime[gIndx] ** 2.0 / ( v2[gIndx] * (1.0 + piprime2[gIndx] / piprime[gIndx] * xa[gIndx]) - xa[gIndx] * evaluatelinearForces(pot, x[gIndx]) * piprime[gIndx] ) ) dxAdE[(xmax == 0.0) * (ptxmax == xmax + 1e-10)] = 1.0 return ( 1.0 + ( evaluatelinearForces(pot, x) / piprime + omega**2.0 * xa - piprime**-3.0 * piprime2 * v2 ) * dxAdE ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6418843 galpy-1.10.2/galpy/actionAngle/actionAngle_c_ext/0000755000175100001660000000000014761352040021262 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngle_c_ext/actionAngle.h0000644000175100001660000000144614761352023023665 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=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngle_c_ext/actionAngleAdiabatic.c0000644000175100001660000004535714761352023025453 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=1741018131.0 galpy-1.10.2/galpy/actionAngle/actionAngle_c_ext/actionAngleStaeckel.c0000644000175100001660000021156014761352023025334 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=1741018143.6458843 galpy-1.10.2/galpy/df/0000755000175100001660000000000014761352040014021 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/__init__.py0000644000175100001660000000461614761352023016142 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 chen24spraydf = streamspraydf.chen24spraydf fardal15spraydf = streamspraydf.fardal15spraydf streamspraydf = streamspraydf.streamspraydf ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/constantbetaHernquistdf.py0000644000175100001660000001045314761352023021301 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): """ Initialize a Hernquist DF with constant anisotropy. Parameters ---------- pot : HernquistPotential Hernquist potential which determines the DF. beta : float Anisotropy parameter. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 self._GMa = self._psi0 * self._pot.a**2.0 # Final factor is mass to make the DF that of the mass density self._fEnorm = ( (2.0**self._beta / (2.0 * numpy.pi) ** 2.5) * scipy.special.gamma(5.0 - 2.0 * self._beta) / scipy.special.gamma(1.0 - self._beta) / scipy.special.gamma(3.5 - self._beta) / self._GMa ** (1.5 - self._beta) * self._psi0 * self._pot.a ) def fE(self, E): """ Calculate the energy portion of a Hernquist distribution function Parameters ---------- E : float, numpy.ndarray, or Quantity The energy. Returns ------- float or numpy.ndarray The value of the energy portion of the DF Notes ----- - 2020-07-22 - Written """ Etilde = -numpy.atleast_1d(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.0: # isotropic case sqrtEtilde = numpy.sqrt(Etilde) fE = ( self._psi0 * self._pot.a / numpy.sqrt(2.0) / (2 * numpy.pi) ** 3 / self._GMa**1.5 * sqrtEtilde / (1 - Etilde) ** 2.0 * ( (1.0 - 2.0 * Etilde) * (8.0 * Etilde**2.0 - 8.0 * Etilde - 3.0) + ( (3.0 * numpy.arcsin(sqrtEtilde)) / numpy.sqrt(Etilde * (1.0 - Etilde)) ) ) ) elif self._beta == 0.5: fE = (3.0 * Etilde**2.0) / (4.0 * numpy.pi**3.0 * self._pot.a) elif self._beta == -0.5: fE = ( (20.0 * Etilde**3.0 - 20.0 * Etilde**4.0 + 6.0 * Etilde**5.0) / (1.0 - Etilde) ** 4 ) / (4.0 * numpy.pi**3.0 * self._GMa * self._pot.a) else: fE = ( self._fEnorm * numpy.power(Etilde, 2.5 - self._beta) * scipy.special.hyp2f1( 5.0 - 2.0 * self._beta, 1.0 - 2.0 * self._beta, 3.5 - self._beta, Etilde, ) ) if len(Etilde_out) > 0: fE[Etilde_out] = 0.0 return fE.reshape(E.shape) 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=1741018131.0 galpy-1.10.2/galpy/df/constantbetadf.py0000644000175100001660000003665514761352023017412 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 import interpSphericalPotential from ..potential.Potential import _evaluatePotentials from ..util import conversion, quadpack 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 ): """ Initialize a spherical DF with constant anisotropy parameter. Parameters ---------- pot : Potential or list of Potential instances, optional Spherical potential which determines the DF. denspot : Potential or list of Potential instances, optional Potential instance or list thereof that represent the density of the tracers (assumed to be spherical; if None, set equal to pot). beta : float, optional Anisotropy parameter. Default is None. rmax : float or Quantity, optional Maximum radius to consider; DF is cut off at E = Phi(rmax). Default is None. scale : float or Quantity, optional Characteristic scale radius to aid sampling calculations. Not necessary, and will also be overridden by value from pot if available. Default is None. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). """ anisotropicsphericaldf.__init__( self, pot=pot, denspot=denspot, rmax=rmax, scale=scale, ro=ro, vo=vo ) self._beta = beta def _call_internal(self, *args): """ Evaluate the DF for a constant anisotropy Hernquist. Parameters ---------- E : float The energy. L : float The angular momentum. Returns ------- float The value of the DF. Notes ----- - 2020-07-22 - Written - Lane (UofT) """ E, L, _ = args return L ** (-2 * self._beta) * self.fE(E) def _dMdE(self, E): if not hasattr(self, "_rphi"): self._rphi = self._setup_rphi_interpolator() fE = self.fE(E) out = numpy.zeros_like(E) out[fE > 0.0] = ( (2.0 * numpy.pi) ** 2.5 * special.gamma(1.0 - self._beta) / 2.0 ** (self._beta - 1.0) / special.gamma(1.5 - self._beta) * fE[fE > 0.0] * numpy.array( [ integrate.quad( lambda r: r ** (2.0 - 2.0 * self._beta) * (tE - _evaluatePotentials(self._pot, r, 0.0)) ** (0.5 - self._beta), 0.0, self._rphi(tE), )[0] for ii, tE in enumerate(E) if fE[ii] > 0.0 ] ) ) return out 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.0, 1.0, 20001) coseta_cmf = ( cosetas * special.hyp2f1(0.5, self._beta, 1.5, cosetas**2.0) / numpy.sqrt(numpy.pi) / special.gamma(1.0 - 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.0 ) * v ** (2.0 - 2.0 * self._beta) else: return self.fE(_evaluatePotentials(self._pot, r, 0) + 0.5 * v**2.0) * v ** ( 2.0 - 2.0 * self._beta ) def _vmomentdensity(self, r, n, m): if m % 2 == 1 or n % 2 == 1: return 0.0 return ( 2.0 * numpy.pi * r ** (-2.0 * self._beta) * integrate.quad( lambda v: v ** (2.0 - 2.0 * self._beta + m + n) * self.fE(_evaluatePotentials(self._pot, r, 0) + 0.5 * v**2.0), 0.0, self._vmax_at_r(self._pot, r), )[0] * special.gamma(m / 2.0 - self._beta + 1.0) * special.gamma((n + 1) / 2.0) / special.gamma(0.5 * (m + n - 2.0 * self._beta + 3.0)) ) 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.0, twobeta=None, rmax=None, scale=None, ro=None, vo=None, ): """ Initialize a spherical DF with constant anisotropy parameter Parameters ---------- pot : Potential instance or list thereof, optional Potential instance or list thereof denspot : Potential instance or list thereof, optional Potential instance or list thereof that represent the density of the tracers (assumed to be spherical; if None, set equal to pot) beta : float, optional anisotropy parameter twobeta : float, optional twice the anisotropy parameter (useful for \beta = half-integer, which is a special case); has priority over beta rmax : float or Quantity, optional maximum radius to consider; DF is cut off at E = Phi(rmax) scale : float or Quantity, optional Characteristic scale radius to aid sampling calculations. Optional and will also be overridden by value from pot if available. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 else: twobeta = 2.0 * beta if ( isinstance(pot, interpSphericalPotential) and beta < -0.5 ): # pragma: no cover raise RuntimeError( "constantbetadf with beta < -0.5 is not supported for use with interpSphericalPotential." ) _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.0**self._beta / (2.0 * numpy.pi) ** 1.5 / special.gamma(1.0 - self._alpha) / special.gamma(1.0 - 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, 0) # Build interpolator r(pot) self._rphi = self._setup_rphi_interpolator() # 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.0 ** (guesspow * (1.0 - self._alpha)) startt = numpy.ones_like(Es) * guesst startval = numpy.zeros_like(Es) while numpy.any(startval == 0.0): guesspow += 1 guesst = 10.0 ** (guesspow * (1.0 - self._alpha)) indx = startval == 0.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.0 / 3.0 * (1.0 - self._alpha), k=3 ) def sample(self, R=None, z=None, phi=None, n=1, return_orbit=True, rmin=0.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.0 - 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): """ Calculate the energy portion of a constant-beta distribution function Parameters ---------- E : float, numpy.ndarray, or Quantity The energy. Returns ------- numpy.ndarray The value of the energy portion of the DF Notes ----- - 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.reshape(E.shape) / ( 2.0 * numpy.pi**1.5 * 2 ** (0.5 - self._beta) * special.gamma(1.0 - 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( [ quadpack.quadrature( lambda t: _fEintegrand_smallr( t, self._pot, tE, self._gradfunc, self._alpha, self._rphi(tE), ), 10.0 ** self._logstartt(tE), self._rphi(tE) ** (1.0 - self._alpha), )[0] for tE in Eint[indx] ] ) # Add constant part at the beginning out[indx] += 10.0 ** self._logstartt(Eint[indx]) * _fEintegrand_smallr( 10.0 ** 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( [ quadpack.quadrature( lambda t: _fEintegrand_larger( t, self._pot, tE, self._gradfunc, self._alpha ), 0.0, 0.5 / self._rphi(tE), )[0] for tE in Eint[indx] ] ) return -out.reshape(E.shape) * 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.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.0 / (1.0 - alpha) * t ** (alpha / (1.0 - alpha)) * _fEintegrand_raw( t ** (1.0 / (1.0 - 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.0 / t**2 * _fEintegrand_raw(1.0 / t, pot, E, dmp1nudrmp1, alpha) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6488843 galpy-1.10.2/galpy/df/data/0000755000175100001660000000000014761352040014732 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000022500000000000010214 xustar00127 path=galpy-1.10.2/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.2500_0.7500_0.0125_0.0000_151_5.0000_20.sav 22 mtime=1741018131.0 galpy-1.10.2/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.2500_0.7500_0.0125_0.0000_0000644000175100001660000000641214761352023031042 0ustar00runnerdocker ](](G?A8G?~e](G?qG?q2Pe](G?ZVG?:ge](G?LNG?+Ye](G?yG??z=e](G?|(G?\qCe](G?,G?|e](G?G?Gle](G?ܡ G?7I3e](G?`tG?YμTe](G?G?TWe](G?1zG?@e](G?/ʌG?.J{e](G?N*G?|rQce](G?A >G?be](G?g~G?q7Ȓe](G?]~G?8me](G? @ G?+e](G?hG?)+e](G?d;G?^Ye](G?y20G?kKe](G?VG?͒e](G?}^!G?ne](G? ocgG?!use](G?(MXG?e](G?ŸՋ=G?0e](G?gG?MG'5e](G??MG?e](G?YY"RG?ufe](G?G?6Te](G?gG?m;e](G?@[QG?ATLe](G?!gG?le](G? wMG?ge](G?nG?ޞ e](G?+"WG?e](G?>ߝG?CbWe](G?] G?/Ce](G?g>hG?yJe](G?G?jYe](G?iG?^"e](G?+3oG?Tm]*e](G?Bv|G?M0e](G?\6G?G8@e](G?w{yG?D7ze](G?hQ̟G?Bv(n}e](G?$G?B9Me](G?ت{G?CW{e](G?6G?Ee](G?N=G?I&e](G?:Z̴\G?MKle](G?\G?R]ke](G?`G?X e](G?5G?^l_e](G?fG?e6>!e](G?MG?lc\e](G?VmG?sKe](G?(G?{F,e](G?H9G?zMe](G?h&7G?zse](G?4G?e](G?$yG?;Te](G?D9G?Me](G?ݚG?4e](G?gG?ҝ[e](G?G?]|e](G?,.ҾCG?EBe](G?DetG?ʁAe](G?[XG?їBe](G?rR%G?Me](G?٪WG?pVUe](G?xAVG?Ԣe](G?2f^G?Ge](G?ULBG?cke](G?G?te](G?=G?M0Pne](G?ڷG?ݔe](G?hG? S{e](G? EG?9ʧe](G?G?yye](G?CG?:2e](G?[:G?ue](G?$w,G?"re](G?*_?G?&@sle](G?/G?*2߉e](G?4gG?-* Ie](G?9BpG?1+Xe](G?>\1G?4ge](G?BoG?8*{,"e](G?F@G?;;Fje](G?J`G?>$w3e](G?NO3SG?@憣xe](G?QLBG?Cse](G?U2G?E,e](G?X':G?HS=be](G?ZG{G?J=e](G?]G?L2e](G?`4nG?Ne](G?b,4G?Py=e](G?d >G?R;љe](G?f{`G?Se](G?hrG?UuCNe](G?jکYG?Ve](G?lG?XR 2e](G?nCqG?Ye](G?oG?Zەe](G?qD=G?\e](G?r]G?]*e](G?sʐG?^e](G?u G?_'ُe](G?AP]UG?hτe](G?wuG?iFe](G?呹G?iUe](G?0MrG?iƥxe](G?uN6G?jXte](G? G?j6Zfe](G?󒳏G?jhme](G?)G?jCe](G?_fG?jP e](G?G?jZe](G?wKG?ke](G?B$G?k5?e](G?XXG?kV/9e](G?-G?kve](G?UG?kiGe](G?w7G?k=Ge](G?vG?kce](G?G?kFe](G?Ŭ@G?k,5{e](G?֐G?l'e](G?G3G?lie](G?9G?l, Ye](G?@ G?l=Ce](G?&G?lKe=ee.././@PaxHeader0000000000000000000000000000022500000000000010214 xustar00127 path=galpy-1.10.2/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.2500_0.7500_0.2000_0.0000_151_5.0000_20.sav 22 mtime=1741018131.0 galpy-1.10.2/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.2500_0.7500_0.2000_0.0000_0000644000175100001660000000641214761352023031034 0ustar00runnerdocker ](](G?պQ8G?sLze](G?k,ġG?2'e](G?OХ!JG?Yge](G?ci7G?@we](G?c8G?&ae](G?8&rG?hNe](G?jsG?#cHe](G?蠃G?Cڪ~be](G?ֈh\G?]إ8Xe](G?܍XlvG?1{e](G?¬G?HA&|1e](G?gK bG?i1@e](G?Ϫr(#G?ve](G?o)1PG?SO;me](G?LG?fGe](G?*=XG?JKe](G?X-COG?h`@e](G?U;G?b)We](G?!Q`G?*Kje](G?eRG?e](G?4G?Ie](G?[*G?Z]8e](G? RG?ɓ*e](G?ZRrWG?awe](G?tDG?{9Ke](G?@DG?kD.e](G?"4OG?6ve](G?T"G?be](G?t횙yG?/'Se](G?+QBG?$~ge](G?=G?.̻eWe](G?$KG?7|Ve](G??$G??(]e](G?#BI=G?DJ%#le](G?8jkG?H5Ve](G?Jp G?LPe](G?[G?R@zAe](G?o(SG?\$Ee](G? VG?hee](G?G?w$e](G?DG?kkBe](G?G?Ze](G? G?R8he](G?0`G?10jQe](G?$[IG?T0Ĭe](G?8 l;"G?Ĉze](G?K+G?We](G?_wG?o,e](G?sY,IG?鐀ne](G?y'8G?-==e](G?e(G?Pfξe](G?G? ke](G?JpG? Wgze](G?2G?#ᘂܰe](G?݅7arG?.̢e](G?o&G?87p?e](G?PG?B/Ͽe](G? VEG?L;ee](G?*G?V&"ɚZe](G?#xsiG?_0e](G?/G?gk8e](G?;Al oG?p@7հe](G?FU`G?xH4e](G?PXWG?e](G?ZwG?Y{_e](G?dw hG?ec{e](G?mN%G? 3 e](G?G?h8e](G?B0G?[ѝe](G?wG?e](G?ﱱA}YOG?5Se](G?ﶞt.QG?e](G?D!rG?jrD e](G?¦-?G?+He](G?G?YvLee](G?ǥoDG?$8a{4e](G?L.G?7@e](G?λKG?߿;,e](G?CiG?Pf1e](G?OP'G?$Re](G?G?L0e](G?ڄOG?&?Qe](G?wFG?*sRG?נ[0e](G?㪋G?mG?~V`e](G?TG?3`De](G?%oG?G^le](G?aWCG?n e](G?YfG?~e](G?ZkG?@}e](G?LqiG?h;e](G?D%G?&e](G?gqG?ǡfe](G? )$G?!ae](G?@w0G?9#/e](G?sP|G?Nʳe](G? AG?b=6e](G?G?u.ue](G?!G?7Re](G?גG?|@e](G?[C3G?p;pue](G?oVQG?>/e](G?#ZKG?\ؓe](G?boG?)e](G?*lG?=Oe](G?"tG?҉pe](G?~TG?C%e](G?,[G?2e](G?F%G??se](G?NG? 38e](G?7G? Ze](G? G? i]e](G?IG? Tp&ge](G?0FG? #Ywe](G?=pG?!e](G?qUG?$ e](G?#(G?-e](G?NGG?Uae](G?İG?g3e](G?urCG? ie](G?ujG? e](G?7KG?Cݎe](G?qZG?}epe](G?&rMG?ve](G?<+QqG?HSgQe](G?HdG?[Q,e](G?\[G?e](G?l,BkG?2e](G?:"Z?G?MB$e](G?#G?rbe](G?@2G?&e](G?ڀ7G?Hve](G?NhQ%G?e](G?v2yPG?850e](G?S5ZG?݊me](G?HNG?m~e](G?(G?^Ð$e](G?)1YG?SUe](G?DG?JA4Pe](G?[ScG?Eǧe](G?#׊BG?A [e](G?G?@o82e](G?&G?Ae](G?1C4-G?C0qw0e](G?HG?FR ce](G?aYG?K7e](G?z^MG?Q[Wve](G?14G?XMPe](G?E]vG?_[e](G?ˆw G?gD e](G?*1G?ol(be](G?AQ6G?xOQe](G?S3G?;bie](G?*,e](G?cPG?GEe](G?fQFG?JTe](G?heG?Ldie](G?jp,G?O ie](G?l=G?Qde](G?n_5՚G?S e](G?p5G?T]e](G?q9,G?VO`e](G?s4܌G?X#nne](G?tzyG?Ye](G?uܞG?ZWe](G?v_G?\IHe](G?x SG?]|e](G?yj G?^ne](G?z Z\G?_;=e](G?z1LG?`D=Ce](G?{bhG?a4e](G?|EA%G?b`e](G?};9G?c(Ze](G?}G?cpe](G?~@G?d?Ee](G?UG?e2[e](G?8G?eȓւe](G?G?fS8(e](G?"G?fٗe](G?KBG?gLYe](G?[G?g'e](G?>fG?h#He](G? 9G?hte](G?\G?h8e](G?.Ok_G?LAe](G?hBOG?3e](G? v G?YNe](G?޲`G?>>~e](G?aG?B522e](G? ~YG?zLee](G?xb7G?D#׵e](G?G?¯Bne](G?gEdsgG?~ i e](G?TwW6G?@ʫe](G?*oޙ?G? -L..e](G?-BG2G?︣ e](G?j/vG?iĦ8ԫe](G?߈0aG?)~Q|e](G?G?^e](G?i2EG?ГC6Ee](G?IrG?j2e](G?2vNqG?<7e](G?TE6pG?Le](G?p AG?sHe](G?`9ZG?^'&e](G?|gG?I%~Qe](G?mG?5>.Te](G?2e](G?WRMG?xɛ]e](G?cgG?:kŬe](G?uKsb"G?@-e](G?ȽG?Yfe](G?N*G?{`e](G?}G?e](G?ۑmG?ͥke](G?ؠ$G?0@z e](G?:G?Nge](G?,G?Se](G?Q0G?,e](G?,…G?No!e](G?]*mG?2re](G?*53G?+p e](G?6̎G?7D&Xe](G?BneC;G?B8ywe](G?Mpb|G?MjAe](G?XܮG?We](G?bCT8`G?aDe](G?k@iG?k?Z+Je](G?u&G?tu2Ie](G?}yusG?}?flyie](G?0KuG?'*e](G?8ê G?p\e](G?BI"G?..@[e](G?8tӈ9G?IUe](G?(e](G?G?'e](G?#gG?!=e](G?ZƺG? De](G?bG?\e](G?;G?Ig:/e](G?8G?ӰҺge](G?%6AG?ӈT`e](G?p4ǃG?Qke](G? PG?d(4e](G?3G?VG?Ke](G?OJG?[lbe](G? G?8Ve](G? G?(e](G?8G?@e](G?tG?\z#e](G?@yxG?yje](G?ׇ)G?Te](G?%lG?Le](G?OG? O+e](G?ΈgG? ne](G?;Pe](G?K=G?@CI`e](G?N6nG?BEe](G?PcG?DZ$e](G?SJ:G?Fe](G?U3G?G]He](G?W8@G?I9\e](G?YĤ]G?J#.e](G?[oG?L0e](G?]0G?MGlYe](G?_~ßG?NLie](G?a?IG?P?ne](G?bG?Qse](G?dJdG?RTte](G?fG?Se](G?g6 G?Te](G?hHG?V {,Ye](G?jV;)G?WWe](G?kXG?Xe](G?l G?Yֶe](G?ntpG?Y4ۺe](G?o2`fG?Z/e](G?pGڄ[G?['e](G?qQ.5G?\j\Ve](G?rNG?].e](G?s@ G?]{"e](G?t(gG?^R#e](G?u:G?_JƐe](G?u"G?_gYe](G?vKG?`te](G?waG?a#"e](G?xV~G?ae](G?xǜG?b> e](G?ym°G?bIe](G?z qG?c?We](G?z G?c(He](G?{4٭SG?d+e](G?{[G?d|9e](G?|AqG?elKe](G?|.yG?eere](G?}5qG?eܝe](G?}G?f xe](G?~PG?fve](G?~xkG?fɻLe](G?~G?ga`e](G?7oG?gcee.././@PaxHeader0000000000000000000000000000022500000000000010214 xustar00127 path=galpy-1.10.2/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.1000_0.0000_151_5.0000_20.sav 22 mtime=1741018131.0 galpy-1.10.2/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.1000_0.0000_0000644000175100001660000000641214761352023031025 0ustar00runnerdocker ](](G?G?iuZe](G?xrVG?j{Ye](G?xq}G?'7F.e](G?DcG?rs{]e](G?- G?lLjXTQe](G?߾ʶG?)e](G? N%G?ivDe](G?$\G?@@G>~e](G?7G?te](G?DpYG?5 Pe](G?MssG?Qͅ8e](G?RG?h]pe](G?TŅ.}G?zTCWe](G?T&|G?a(/e](G?QSG?q9U7e](G?LeY'G?9%|e](G?mT>igG?x]e](G?DqG?ŹEOe](G?R~BG?ՑU$e](G?3/G?ʄjRe](G?ؽrB=G?,de](G?Pz1G?ee](G?GG? le](G?זּpG?\ e](G?﯐;G?ۘ{e](G?\G?\V Ae](G?.G?&ae](G?G?\e](G?c9LG?@e](G?8[\G?V'ce](G?nnG?Ne](G?ەG?A\i{e](G?8ߓG?ݰe](G?Ƅ![G?rpe](G?ȿG?}e](G?꧕HG?Wee](G?Jځ%G?1re](G?hHG?ȕ\e](G? C)G?ɟ_e](G?6G? j6e](G?wG?D>e](G?֙JG?vᒂe](G?UŜG?he](G?yqG?Âe](G?۠G?$ e](G?0DeG?.ne](G?ޱ2G?SA e](G?%-RG? ]e](G?G?e](G?AG?G,#e](G?1y=G?Ne](G?q\G?q e](G?@קG?BXe](G?G?m"Je](G?eYsG?:w$e](G?'G?m"e](G?zG?M4Wje](G?H]]G?z2 e](G?|G?,lc}pe](G?󈏕G?$}7e](G?G?}^&e](G?R4G?ke](G?YZG?ge](G?NqG?I*=e](G?ILG?Ȥe](G?R|UG?^(e](G?9G%G?Re](G?ްy҄G?_$e](G?|lG?פf"e](G?G?KErLe](G? G?/:Pe](G?.3n#G?E e](G?=|G?E@& e](G?0nG?vM^e](G?GG?Fe](G?,XG?ѹue](G?+ϲBG?e](G?2 G?%C|oe](G?WtɠG?MNe](G?%G?r}e](G?sAG?2e](G?isG?V1e](G?mnG?ےe](G? OG?@~~Ee](G?WmG?C%e](G?r _G?3սe](G?NG?OOe](G?%~8G?isse](G?cРsG?g}be](G?.WG?8e](G?׹C0%G?e](G? q{fG?Ɵe](G?@ռG?Le](G?q?G?1e](G?1 \G?8 Aee.././@PaxHeader0000000000000000000000000000022600000000000010215 xustar00128 path=galpy-1.10.2/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_-0.1000_151_5.0000_20.sav 22 mtime=1741018131.0 galpy-1.10.2/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_-0.10000000644000175100001660000000641214761352023030745 0ustar00runnerdocker ](](G?啠5G?6Ae](G?]dG?%e.e](G?ɼ>\G?ػȣe](G?D-gG?q|Ȭe](G?3f?G?+%We](G?:lwG?^e](G?ΐG?8ee](G?3.\ *G?6 @e](G?ٖvG?_+,e](G?^QG? r`Re](G?UvG?={e](G?Me/G??SRe](G?hG?yj9be](G?sj[G?MwMe](G?qqG?ܜDe](G?dսG?Bbqe](G?LnbwhG?bW~e](G?-L#G?tge]e](G? G?zEze](G?߅`EρG?vnk%e](G?G?ilBe](G?ݲ,G?U4ze](G?_[G?;l߫Ae](G?5ȿɭ>G?v;e](G? G?OP~e](G?ϝ|G?٦Qe](G? $[G?\ne](G?E3Z{aG?9; e](G?79[eG?l )=e](G?t$>!G?I;e](G?(QG?%9M7e](G?i xG?ke](G?>G?)7e](G? G?òoQe](G?"cG?ije](G?|ApG?'de](G?dSӏG?nte](G?D `G?Te](G?|& 7_G?<$ e](G?qp-G?&,{Hye](G?aU6G?=<8.e](G?R G?qe](G?G<`rvG?m#e](G?=G? !e](G?6BG?Dlj]e](G?1f;G?z2e](G?-:G?a U,e](G?+LqG?ISe](G?+tG?3ze](G?,; G?  e](G?.-w!G?gfe](G?16FG?RHe](G?5F2G?SIbe](G?:RNsG?Xqxe](G?@Q]{G? `e](G?G:p:G?cTe](G?OG?Z)oze](G?WoGG? e](G?`7IG?,Ǒe](G?jG?ٱe](G?ukB"YG?Ce](G?c@xG?<6|qe](G?틷&3?G?vse](G?QG:G? {]e](G?#AԑG?7Ee](G?#aHG?e](G?N7LG?nIJe](G?ǡ=G?{jy9e](G?ѲG? Ջe](G?G?Le](G?튛}dG?bGe](G?p$G?:Ve](G?in6G?Ť(e](G?hG?ʄ0e](G?!aJ$G?ϰֿVe](G?.I FG?&e](G?;$+G?1?e](G?GG?e](G?THe G?u(e](G?`xjG?%ܠe](G?lo[G?*e](G?yG?dDde](G?K(G?ݸw\e](G?ʤG?gZQe](G?jWG? XGe](G?f*pG?s e](G?-qG?-"e](G? WG? e](G?( G?'O(e](G?j~G?-Ke](G?ݽG?4F&*e](G?K?aG?:F.#e](G?{OG?@sWHoe](G?OrG?G>2D+e](G?Q?G?MfZe](G? l G?Sue](G?G?Ykae](G?k2G?_E"Jxe](G?'٭{G?eLNMe](G?0 pG?j *e](G?7G?p!TbCe](G??ÆG?u%Àe](G?G3jG?z/e](G?Nv1crG?e](G?U~lrG?ie](G?\MG?se](G?b7G?}z^|e](G?iD=CG?e](G?om@G?_*,e](G?uaWG?rme](G?{!hG?$ Me](G?"*G?<\@+>e](G?qG?3Ee](G?2ͰG? ګDe](G?-G?•.e](G?#4G?[A2e](G?spG?^1he](G? kNG?1yCe](G?Qe](G?zqG?+#{ e](G?CSG?2Q%e](G?DƋ)G?9]mWe](G?]G?@Ge](G?}T}G?G ңe](G?O\?iG?M0e](G?vJͦG?T(CSe](G?"]íG?Z| e](G?*oG?`2e](G?3G?fwSe](G?:G?lP@e](G?BMfG?rIDh#e](G?JLG?w re](G?QV G?}Cr*e](G?XR#G?'e](G?_lBG?̊e](G?e^AnG?o_e](G?kDbe](G?}Di+G?ue](G?{G?'e](G?W-QG?=8e](G?mxTG?1Ye](G?TEwG?{e](G? a\G?ﲲ e](G?>` G?@e](G?"#G?﹯C*e](G?*RG?u8mHe](G?55޳G?/ &,e](G?bb G?AIe](G?9՛G?7'7e](G?i -mG? @e](G?lL)G? e](G?(iG?rCe](G?T7G?re](G?^rrG?ke~e](G?I*G?èke](G?G?e](G?7>G?--Ne](G?Uٌ֎G?@\e](G?ԯ-G?> *e](G?')NG?'0؀e](G?hq?G?A?e](G?ԑ/OG?.Lse](G?֢G?mve](G?؝*gG? @X\Ge](G?ځjG? e2Ge](G?P{ߊG?>e](G? >SG?|be](G?߲eG?גape](G?FG?$+;Wee.././@PaxHeader0000000000000000000000000000022500000000000010214 xustar00127 path=galpy-1.10.2/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_151_5.0000_20.sav 22 mtime=1741018131.0 galpy-1.10.2/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_0000644000175100001660000000641214761352023031026 0ustar00runnerdocker ](](G?nD9G?>mo:e](G?yv}G?fD2e](G?omG?QOwe](G?nӁG??4/cZe](G?ZWhG?A&˲$e](G?ҳTG?CN$e](G?WG?7Pue](G?rP*`G? ,k5e](G?AZ^+G?ﲪ!s?Ie](G?. hG? ~e](G?g ɶ8G?3(.te](G?^¶G?8Ďe](G? vG?*4+e](G?8}G?l\e](G?e+G?򢻔>e](G?f5LG?ĨN oe](G?C2G? d@ e](G?1χOG?֐e](G?݃ֆG?ˇe](G?Dh`G?z\e](G?VG? Oge](G?k'G?| e](G?AWTNG?We](G?6bG?0e](G?L/H`G?zee](G?h,-G?Ba)e](G?dep~G?qe](G?*KEG?<2V7e](G?hV e](G?^:G?*,e](G?aVG?9<=ze](G?힢G?9we](G?؊G?Te](G?#_ G?7ij3e](G?@ "G?͉Ce](G?5mG?%mRe](G?;PG?^~e](G?b|G?ocT]e](G?Ϥ?RG?] `e](G?j<|G?L2e](G? JIUG?= _e](G?#7G?0kO1e](G?"=G?%e](G?G?+JAe](G?^hqG?GPRe](G?~xG? ok"e](G?¿*G?zwe](G?G?:e](G?kG?Z&e](G?CG?g%Ee](G?n rsG?P-ge](G?@G?{e](G?kG?zf,ޥe](G? waG?I]e](G?'G?nrCe](G?!U+G?k e](G?-GG?_e](G?8v' G?6?e](G?DK G?,e](G?Of2G?[!1e](G?[G? e](G?g8NG? 1),e](G?reG?e](G?~%ItrG?zɓ e](G?sVpG?O>e](G?G?BGe](G?hG?!qDe](G?asG?&Me](G?gYG?,5޾C?e](G?DWG?1 de](G?wG?6gb6e](G?ԹTxT4G?Nv\e](G?:A&G?ﵰke](G?GЗh3G?e](G?uڅG?@.!e](G?MwG?`CKe](G?2|٫G?eDE}e](G?.)gmG?P@)e](G?xxG?!@%e](G?ﱰ±G?3X}e](G?9:ZG?xzuTe](G?︞kGG?e](G??8G?o2dMYe](G?iG?pe](G?P:vG? Ve](G?yG?64'[e](G?ǣ0G?MA,e](G?Gr߇G?OY5e](G?OLG?= e](G?<G?j*e](G?ю&G?F:e](G?LJToWG?䖈use](G?ɽfG?:e](G?CBPwG?]\e](G?G?OÈe](G?۾ /G?]e](G?݄4G?$fse](G?7rG?x:Ue](G? G?e](G?cHG?Le](G?ވbG? e](G?H*G?=4 e](G?SG?N#lz.e](G?p>G?S#ce](G?$G?L%e](G?P iG?;%DIJe](G?n>`G?ne](G?vG?ʸ|e](G?ށ/G?Ȧ|7e](G?z}#}G?/eqe](G?frSG?Lqe](G?GAlҒG?ͭe](G?eҧG?8d'e](G?YU)G?REe](G?@G? de](G?dxG?ғw[e](G?|wcH5G?輻e](G?#G?%#e](G?RG?ﯫ%aDe](G?xۃG?p!e](G?ovOG?mKG)e](G?8D81G?﹩e](G?ړG?͊e](G?EG? Lke](G?p lG?Ne](G?裡AG?ŧ\;G?GZWde](G? ClG?Zm e](G?# nG?Y ^e](G?pшIG?E#_Ee](G?]|G?I`e](G?X~#G?e](G?ңG?e](G?ՠG?;ge](G?6ggG?}e](G?7 pG?Om]e](G?܊[G?ge](G?ܲ G?$Ϝe](G?r#G?xUPe](G?G?9e](G?JG?ae](G?>v&G?!'5e](G?.+9G?>\e](G?FvG?Pv*K*e](G?iG?V e](G?m(G?P&e](G?ފG??=|e](G?+fG?#(e](G? "G?ce](G?+)̧G?· he](G?){G?e](G?ɟG?S e](G?.G? Re](G?VG?4A|We](G?~G?[UtVe](G?vd-oG?ӧ#e](G?4fG?޽e](G?@cG?+}e](G?~G?e](G?6̹G?(Yie](G?6iG?Ie](G?eZ 7G? H1e](G?&G?E{Oe](G?vGG?ze](G?sG?燎ee.././@PaxHeader0000000000000000000000000000022500000000000010214 xustar00127 path=galpy-1.10.2/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.2000_151_5.0000_20.sav 22 mtime=1741018131.0 galpy-1.10.2/galpy/df/data/dfcorrection_dehnendf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.2000_0000644000175100001660000000641214761352023031030 0ustar00runnerdocker ](](G?˝5o'G?L֔d|e](G?bP4xG?>sKսe](G?叩6G?=e](G?:OG?MXe](G?M"GG?䫗+e](G??G?Ftq'e](G? G?1MgDe](G?P2OG?lle](G?+WG?^$|se](G?3OJ6G?0Q^Ze](G?HR{G?ee](G?4Lŝ^G?b'e](G?nWlOJG?_w.Le](G?{&G?w e](G?UGs/G?lϖ3Se](G?/A|7G?M>4e](G?E]giG?"^$e](G?N;G?!%e](G?nFG?󲶾!~e](G?eZrG?rMn6Ue](G?oh\G?/ae](G?FdG?ˁ0e](G?!GAG?~˹wG?H74e](G?U9IG?!G?s{ie](G?vG?v]Le](G?ҹ+G?zPI&e](G?#6G?}mbf e](G?+QG?wne](G?3اG?ҥde](G?;X<7G?[e](G?BolG?/ek_e](G?I<G?1we](G?Q fQG?ٓe](G?W߽\BG?e](G?^XG?0h/ne](G?dG?^Me](G?k9?`G?XYZe](G?qJ[&G?Ka?e](G?w*I-G?M)s"9e](G?|٦(G?אּ9Qe](G?Y^=G?h6e](G?XG?BāZe](G?.bG?pe](G?fG?﹋9je](G?UG?2"e](G?#’/G?ᅣNpe](G?VrG?c^|^Ue](G?сG?-5s{e](G?*%G?[Q8e](G?'lG?ʂ2e](G?ﯼG?/ghe](G?edzcG?υe](G?'7SG?ewe](G?I8dnG?6̯e](G?fAG?qle](G? G?ؘBQe](G?ÜG?ګwtLe](G?wG?ܬ/e](G?4WWG?ޙ%Ee](G?kG?us\e](G?UŢG??J.~e](G?мG?e](G? ^$G?OGe](G?<-G?5ze](G?VOԘAG?輐T+je](G?XQG?3 ue](G?D4\MyG?뛹_FCe](G?CG? $Ize](G? v\G?@FOe](G?6^G?}oe](G?G?m Oe](G?㤫nG?Ћ2e](G?nyqG?:/e](G?{Dz)G?Ee](G?r G? Je](G?PG?e](G?Ce6G?@e](G?g G?'~e](G?~tG?2êe](G? G?M e](G?܅G?e](G?uDZG?e](G?ZHG?z de](G?4QɵG?"{ie](G?)G? Te](G?'G?]Xҗe](G?󄻵#G? Ne](G?76.G?{1Fe](G?G?(e](G?~s%G?~*e](G?G?[1te](G?ۛG?5e](G?6͐G?k]e](G? OG?[2we](G?W@G?LŸe](G?dWG?;6r+e](G?넋|@3G?49$}e](G?jt}G?>1,v}e](G?nMhG?aByGe](G?G?뚠je](G?.g7G?e](G?@gG?6q,.e](G?쨠-G?:[e](G?G-G?s e](G?H'e](G?t(IG?Eqe](G?osC=G?">}ğ~e](G?&7G?,O'4e](G?/_FG?5 e](G?8n}G??Fe](G?AkBG?H,CdžKe](G?IjG?PZe](G?QjҰG?X:ee](G?Y{GhHG?`mDe](G?` G?h_] e](G?h` G?o Ve](G?n⡽klG?v"D$e](G?uv*G?}IFPe](G?{FfKG?ˤO$qe](G?;Z~G?Π=wSe](G?;n`G?yЊ]e](G?ύ8G?1I}e](G?hG?{˃e](G?:G?@A>e](G?A%Q/G?ۛxCe](G?Ղy2G? je](G?ר{jG?(&Ye](G?ٵG?aPe](G?۩DG?,vDe](G?݅* G?Ɂe](G?KOG?n>(e](G?gG?? ?e](G?◛G?,i)e](G?[G?[e](G?唗G?ve](G?G?,Ze](G?IG?zG?MhAe](G?G?$Ie](G?H^G?Bhe](G?{G?ъ?e](G?ł<G?1νee.././@PaxHeader0000000000000000000000000000022200000000000010211 xustar00124 path=galpy-1.10.2/galpy/df/data/dfcorrection_shudf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_151_5.0000_40.sav 22 mtime=1741018131.0 galpy-1.10.2/galpy/df/data/dfcorrection_shudf_expSurfaceSigmaProfile_0.3333_1.0000_0.2000_0.0000_1510000644000175100001660000000641214761352023030613 0ustar00runnerdocker ](](G?e\%~G?me](G? VG?O"~2e](G?~ԐgG?+s"e](G?:oJG?K Qe](G?M1q,G?[ ?,e](G?𗏒N9G?8>e](G?ye`NG?@e](G?OSG?ѳae](G?Z_G?SBe](G?үG?ȹ{gne](G?(]ΙG?6Ud̓e](G?~G?twe](G?p 8G?SQe](G?b yG?W1hre](G?A$G?R ibe](G?T>G?ꃬe](G? CG?Se](G?D1`G?3/e](G?*p@G?Z}6e](G?H7G? e](G?zv=G?]Ke](G?0*G?ړW e](G?=yG?q^e](G?G?t;e](G?6wG?Ue](G?G?tie](G?X}rNG? ʗe](G?ꧾb:G?Oe](G? rG?6|e](G?,>G?鹺Pe](G?F|EG?\9e](G?;~jG? /e](G?G?ͺ#e](G?ЁaRG?va% e](G?ꋮjvL3G?雭#[re](G?R.vG?DLs"e](G?3 G?+t#=e](G?1|h G?2Ig e](G?K0>^G?Ibe](G?wuD/G?hXbe](G?nunG?e](G?:p]G?8M*e](G? P.rG?_4fe](G?1,G?b_$e](G?Q8G?ꩅghe](G?qZKpG?㰫e](G?둘G?Ae](G?fåG?|*Se](G?뾸FG?9`ue](G?RG?KmŽe](G?tnG?Te](G?nXX+G?]׊e](G?KjLyG?pRTce](G?뾺hG?7e](G?]2G?fe](G?16oG?9e](G?t]mG?!Bhe](G?짮'zחG?C^j e](G?6*G?W? [e](G?hbRG?b.e](G?qtq7G?kG7e](G?윂 l G?{8qe](G?젒|_ G?엄Ie](G?|KG?9ze](G?G?,e](G?*-sG?Oɣ"e](G?cG?4=oe](G?QLmRG?L=ze](G?YWTG?[Ɇfe](G?۳G?b/+ e](G?<~G?hXe](G?wZ(G?vO2e](G?+ھG? y9e](G?9"TǪG?>uqe](G?BG? j)e](G?Ӈ,_$G? Ve](G?5[MG?Jbe](G?@aG?͙@e](G?7hrG?n}7He](G?)an_G?% e](G?(^îG?5)=|}e](G?e](G?IXdG?P2FeG?ˤO$=e](G?;vG?Π=e](G?;nG?yЊpe](G?ύlG?1I} e](G?h؋G?{%e](G?:G?@Abe](G?A%QG?ۛxAe](G?ՂyG? e](G?ר{TmG?(#e](G?ٵG?ae](G?۩:G?,vB)e](G?݅*(KG?Ʉ e](G?KCG?n>e](G?pG?? ?te](G?◛G?,ge](G?_aG?[e](G?唗G?ve](G?+G?,Ze](G?IG?ze](G?$G?e](G? G?b6e](G?TG?e](G?@G?MhA:e](G?G?$e](G?IG?Be](G?{cG?ъ!e](G?ł;qG?1δee.././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/df.py0000644000175100001660000000514114761352023014766 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): """ Initialize a DF object. Parameters ---------- ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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): """ Turn off automatic returning of outputs in physical units. Notes ----- - 2017-06-05 - Written - Bovy (UofT) """ self._roSet = False self._voSet = False return None def turn_physical_on(self, ro=None, vo=None): """ Turn on automatic returning of outputs in physical units. Parameters ---------- ro : float or Quantity, optional Reference distance (kpc). If False, don't turn it on. vo : float or Quantity, optional Reference velocity (km/s). If False, don't turn it on. Notes ----- - 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 ro = conversion.parse_length_kpc(ro) if not ro is None: self._ro = ro if not vo is False: self._voSet = True vo = conversion.parse_velocity_kms(vo) if not vo is None: self._vo = vo return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/diskdf.py0000644000175100001660000033277414761352023015660 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.0**-14.0 _NSIGMA = 4.0 _INTERPDEGREE = 3 _RMIN = 10.0**-10.0 _MAXD_REJECTLOS = 4.0 _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 from ..orbit import Orbit from ..potential import PowerSphericalPotential from ..util import conversion, quadpack, save_pickles from ..util.ars import ars from ..util.conversion import ( _APY_LOADED, _APY_UNITS, 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.0 class diskdf(df): """Class that represents a disk DF""" def __init__( self, dftype="dehnen", surfaceSigma=expSurfaceSigmaProfile, profileParams=(1.0 / 3.0, 1.0, 0.2), correct=False, beta=0.0, ro=None, vo=None, **kwargs, ): """ Initialize a DF Parameters ---------- dftype : str, optional 'dehnen' or 'corrected-dehnen', 'shu' or 'corrected-shu' surfaceSigma : instance or class name of the target surface density and sigma_R profile, optional (default: both exponential) profileParams : tuple, optional 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 : float, optional power-law index of the rotation curve correct : bool, optional correct the DF (i.e., DFcorrection kwargs are also given) ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). **kwargs : dict, optional DFcorrection kwargs (except for those already specified) Notes ----- - 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.0 / (1.0 + 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 self._psp = PowerSphericalPotential( normalize=1.0, alpha=2.0 - 2.0 * self._beta ).toPlanar() # Setup aA objects for frequency and rap,rperi calculation self._aA = actionAngleAdiabatic(pot=self._psp, gamma=0.0) return None @physical_conversion("phasespacedensity2d", pop=True) def __call__(self, *args, **kwargs): """ Evaluate the distribution function Parameters ---------- *args : tuple Either: 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,L - energy (/vo^2; or can be Quantity) and 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] marginalizeVperp : bool, optional marginalize over perpendicular velocity (only supported with 1a) for single orbits above) marginalizeVlos : bool, optional marginalize over line-of-sight velocity (only supported with 1a) for single orbits above) nsigma : float, optional number of sigma to integrate over when marginalizing **kwargs: dict, optional scipy.integrate.quad keywords Returns ------- float or numpy.ndarray value of DF Notes ----- - 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.0, 0.0], ro=1.0) * _DEGTORAD vlos = o.vlos(ro=1.0, vo=1.0, obs=[1.0, 0.0, 0.0, 0.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.0 / R**self._beta * ( 1.0 / self._gamma**2.0 - 1.0 - R * self._surfaceSigmaProfile.surfacemassDerivative(R, log=True) - R * self._surfaceSigmaProfile.sigma2Derivative(R, log=True) ) ) if numpy.fabs(va) > sigmaR1: va = 0.0 # To avoid craziness near the center if numpy.fabs(numpy.sin(alphalos)) < numpy.sqrt(1.0 / 2.0): 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.0 / 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.0, 0.0], ro=1.0) * _DEGTORAD vperp = o.vll(ro=1.0, vo=1.0, obs=[1.0, 0.0, 0.0, 0.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.0 + 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.0 / R**self._beta * ( 1.0 / self._gamma**2.0 - 1.0 - R * self._surfaceSigmaProfile.surfacemassDerivative(R, log=True) - R * self._surfaceSigmaProfile.sigma2Derivative(R, log=True) ) ) if numpy.fabs(va) > sigmaR1: va = 0.0 # To avoid craziness near the center if numpy.fabs(numpy.sin(alphaperp)) < numpy.sqrt(1.0 / 2.0): 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.0 / 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): """ Evaluate the target Sigma_R^2(R) Parameters ---------- R : float or Quantity Radius at which to evaluate. log : bool, optional If True, return the log (default: False). Returns ------- float Target Sigma_R^2(R). Notes ----- - 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): """ Evaluate the target surface mass at R. Parameters ---------- R : float or Quantity Radius at which to evaluate. log : bool, optional If True, return the log (default: False). Returns ------- float or Quantity Target surface mass at R. Notes ----- - 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): """ Evaluate the target surface mass along the line of sight given Galactic longitude and distance. Parameters ---------- d : float or Quantity Distance along the line of sight. l : float or Quantity Galactic longitude in degrees, unless deg=False. deg : bool, optional If False, l is in radians. Default is True. log : bool, optional If True, return the logarithm of the surface mass. Default is False. Returns ------- float or Quantity Surface mass times distance. Notes ----- - 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 ): """ Evaluate the surface mass along the line of sight (LOS) given Galactic longitude and distance. Parameters ---------- d : float or Quantity Distance along the line of sight. l : float or Quantity Galactic longitude (in deg, unless deg=False). nsigma : float, optional Number of sigma to integrate the velocities over. target : bool, optional If True, use target surfacemass (default). romberg : bool, optional If True, use a romberg integrator (default: False). deg : bool, optional If False, l is in radians. relative : bool, optional If True, return d. Returns ------- float Sigma(d,l) x d Notes ----- - 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): """ Sample a distance along the line of sight Parameters ---------- l : float or Quantity Galactic longitude. n : int, optional Number of distances to sample. maxd : float or Quantity, optional Maximum distance to consider (for the rejection sampling). target : bool, optional If True, sample from the 'target' surface mass density, rather than the actual surface mass density (default=True). Returns ------- list List of samples. Notes ----- - 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.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.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): """ Sample a radial and azimuthal velocity at R Parameters ---------- R : float or Quantity Galactocentric distance. n : int, optional Number of distances to sample. nsigma : float, optional Number of sigma to rejection-sample on. target : bool, optional If True, sample using the 'target' sigma_R rather than the actual sigma_R (default=True). Returns ------- list List of samples. Notes ----- - 2011-03-24 - Written - Bovy (NYU) """ # Determine where the max of the v-distribution is using asymmetric drift maxVR = 0.0 maxVT = optimize.brentq(_vtmaxEq, 0.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.0 + vtg**2.0) ): # 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, ): """ Sample along a given LOS Parameters ---------- los : float or Quantity Line of sight Galactic longitude. n : int, optional Number of distances to sample. deg : bool, optional If False, los is in radians. maxd : float or Quantity, optional Maximum distance to consider (for the rejection sampling). nsigma : float, optional Number of sigma to integrate the velocities over. targetSurfmass : bool, optional If True, use target surface mass (default=True). targetSigma2 : bool, optional If True, use target sigma_R^2 (default=True). Returns ------- list List of Orbits sampled. Notes ----- - target=False uses target distribution for derivatives (this is a detail) - 2011-03-24 - Written - 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): """ Estimate the asymmetric drift (vc-mean-vphi) from an approximation to the Jeans equation. Parameters ---------- R : float or Quantity Radius at which to calculate the asymmetric drift. Returns ------- float Asymmetric drift at R. Notes ----- - 2011-04-02 - Written - Bovy (NYU). """ sigmaR2 = self.targetSigma2(R, use_physical=False) return ( sigmaR2 / 2.0 / R**self._beta * ( 1.0 / self._gamma**2.0 - 1.0 - 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): """ Calculate the surface-mass at R by marginalizing over velocity Parameters ---------- R : float or Quantity Radius at which to calculate the surfacemass density. romberg : bool, optional If True, use a romberg integrator (default: False) nsigma : float, optional Number of sigma to integrate the velocities over relative : bool, optional If True, return the relative surface mass at R (default: False) Returns ------- float Surface mass at R Notes ----- - 2011-03-XX - 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.0 else: norm = numpy.exp(logSigmaR) # Use the asymmetric drift equation to estimate va va = ( sigmaR2 / 2.0 / R**self._beta * ( 1.0 / self._gamma**2.0 - 1.0 - R * self._surfaceSigmaProfile.surfacemassDerivative(R, log=True) - R * self._surfaceSigmaProfile.sigma2Derivative(R, log=True) ) ) if numpy.fabs(va) > sigmaR1: va = 0.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.0, lambda x: nsigma, [R, self, logSigmaR, logsigmaR2, sigmaR1, self._gamma], tol=10.0**-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.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): """ Calculate the product sigma_R^2 x surface-mass at R by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate the sigma_R^2 x surfacemass density. romberg : bool, optional If True, use a romberg integrator (default: False). nsigma : float, optional Number of sigma to integrate the velocities over. relative : bool, optional If True, return the relative density (default: False). Returns ------- float Sigma_R^2 x surface-mass at R. Notes ----- - 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.0 else: norm = numpy.exp(logSigmaR + logsigmaR2) # Use the asymmetric drift equation to estimate va va = ( sigmaR2 / 2.0 / R**self._beta * ( 1.0 / self._gamma**2.0 - 1.0 - R * self._surfaceSigmaProfile.surfacemassDerivative(R, log=True) - R * self._surfaceSigmaProfile.sigma2Derivative(R, log=True) ) ) if numpy.fabs(va) > sigmaR1: va = 0.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.0, lambda x: nsigma, [R, self, logSigmaR, logsigmaR2, sigmaR1, self._gamma], tol=10.0**-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.0, lambda x: nsigma, (R, self, logSigmaR, logsigmaR2, sigmaR1, self._gamma), epsrel=_EPSREL, )[0] / numpy.pi * norm ) def vmomentsurfacemass(self, *args, **kwargs): """ Calculate the an arbitrary moment of the velocity distribution at R times the surfacmass Parameters ---------- R: float or Quantity Galactocentric radius at which to calculate the moment. n: int vR^n in the moment m: int vT^m in the moment nsigma : int, optional number of sigma to integrate the velocities over romberg : bool, optional If True, use a romberg integrator (default: False) deriv : str, optional Calculates derivative of the moment wrt R or phi (default: None) Returns ------- float or Quantity at R (no support for units) Notes ----- - 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.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.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.0 else: norm = numpy.exp(logSigmaR + logsigmaR2 * (n + m) / 2.0) / self._gamma**m # Use the asymmetric drift equation to estimate va va = ( sigmaR2 / 2.0 / R**self._beta * ( 1.0 / self._gamma**2.0 - 1.0 - R * self._surfaceSigmaProfile.surfacemassDerivative(R, log=True) - R * self._surfaceSigmaProfile.sigma2Derivative(R, log=True) ) ) if numpy.fabs(va) > sigmaR1: va = 0.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.0**-8, ) / numpy.pi * norm / 2.0 ) 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.0 ) 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.0**-8, ) / numpy.pi * norm / 2.0 ) 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.0 ) @potential_physical_input @physical_conversion("frequency-kmskpc", pop=True) def oortA(self, R, romberg=False, nsigma=None, phi=0.0): """ Calculate the Oort function A. Parameters ---------- R : float or Quantity Radius at which to calculate A. phi : float, optional Azimuth (default: 0.0). nsigma : int, optional Number of sigma to integrate the velocities over. romberg : bool, optional If True, use a romberg integrator (default: False). Returns ------- float or Quantity Oort A at R. Notes ----- - 2011-04-19 - Written - Bovy (NYU) """ # 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.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.0 * 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.0): """ Calculate the Oort function B. Parameters ---------- R : float Radius at which to calculate B (can be Quantity). romberg : bool, optional If True, use a romberg integrator (default: False). nsigma : float, optional Number of sigma to integrate the velocities over. phi : float, optional Azimuth angle (in radians) at which to calculate B. Returns ------- float or Quantity Oort B at R. Notes ----- - 2011-04-19 - Written - Bovy (NYU). """ # 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.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.0 * 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.0): """ Calculate the Oort function C. Parameters ---------- R : float or Quantity Radius at which to calculate C (can be Quantity). nsigma : int, optional Number of sigma to integrate the velocities over. romberg : bool, optional If True, use a romberg integrator (default: False). phi : float, optional Azimuth (default: 0.0). Returns ------- float or Quantity Oort C at R. Notes ----- - 2011-04-19 - Written - Bovy (NYU) """ # - 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.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.0): """ Calculate the Oort function K. Parameters ---------- R : float Radius at which to calculate K (can be Quantity). phi : float, optional Azimuth angle (in radians) at which to calculate K. nsigma : int, optional Number of sigma to integrate the velocities over. romberg : bool, optional If True, use a romberg integrator (default: False). Returns ------- float or Quantity Oort K at R. Notes ----- - 2011-04-19 - Written - Bovy (NYU) """ # - 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.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.0): """ Calculate sigma_R^2 at R by marginalizing over velocity. Parameters ---------- R : float Radius at which to calculate sigma_R^2 density. romberg : bool, optional If True, use a romberg integrator (default: False). nsigma : int, optional Number of sigma to integrate the velocities over. phi : float, optional Azimuth angle at which to calculate sigma_R^2 density. Returns ------- float or Quantity Sigma_R^2 at R. Notes ----- - 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.0): """ Calculate sigma_T^2 at R by marginalizing over velocity Parameters ---------- R : float Radius at which to calculate sigma_T^2 (can be Quantity) romberg : bool, optional If True, use a romberg integrator (default: False) nsigma : int, optional Number of sigma to integrate the velocities over phi : float, optional Azimuth (default: 0.0) Returns ------- float or Quantity Sigma_T^2 at R Notes ----- - 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.0 / surfmass ) / surfmass @potential_physical_input @physical_conversion("velocity2", pop=True) def sigmaR2(self, R, romberg=False, nsigma=None, phi=0.0): """ Calculate sigma_R^2 at R by marginalizing over velocity. Parameters ---------- R : float Radius at which to calculate sigma_R^2. romberg : bool, optional If True, use a romberg integrator (default: False). nsigma : int, optional Number of sigma to integrate the velocities over. phi : float, optional Azimuth (default: 0.0). Returns ------- float or Quantity Sigma_R^2 at R. Notes ----- - 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.0): """ Calculate the mean tangential velocity at a given radius by marginalizing over velocity. Parameters ---------- R : float Radius at which to calculate the mean tangential velocity. romberg : bool, optional If True, use a Romberg integrator. Default is False. nsigma : float, optional Number of sigma to integrate the velocities over. phi : float, optional Azimuth angle at which to calculate the mean tangential velocity. Returns ------- float or Quantity The mean tangential velocity at the given radius. Notes ----- - 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.0): """ Calculate at R by marginalizing over velocity. Parameters ---------- R : float Radius at which to calculate . romberg : bool, optional If True, use a romberg integrator (default: False). nsigma : float, optional Number of sigma to integrate the velocities over. phi : float, optional Azimuth angle at which to calculate . Returns ------- float or Quantity at R. Notes ----- - 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.0): """ Calculate skew in vT at R by marginalizing over velocity Parameters ---------- R : float Radius at which to calculate romberg : bool, optional If True, use a romberg integrator (default: False) nsigma : float, optional Number of sigma to integrate the velocities over phi : float, optional Azimuth (default: 0.0) Returns ------- float Skew in vT Notes ----- - 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.0 return (vt3 - 3.0 * vt * vt2 + 2.0 * vt**3.0) * s2 ** (-1.5) @potential_physical_input def skewvR(self, R, romberg=False, nsigma=None, phi=0.0): """ Calculate skew in vR at R by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate . romberg : bool, optional If True, use a romberg integrator (default: False). nsigma : float, optional Number of sigma to integrate the velocities over. phi : float, optional Azimuth (in radians) at which to calculate the skew in vR. Returns ------- float Skew in vR. Notes ----- - 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.0 return (vr3 - 3.0 * vr * vr2 + 2.0 * vr**3.0) * s2 ** (-1.5) @potential_physical_input def kurtosisvT(self, R, romberg=False, nsigma=None, phi=0.0): """ Calculate excess kurtosis in vT at R by marginalizing over velocity Parameters ---------- R : float or Quantity Radius at which to calculate romberg : bool, optional If True, use a romberg integrator (default: False) nsigma : float, optional Number of sigma to integrate the velocities over phi : float, optional (default: 0.0) Returns ------- float kurtosisvT Notes ----- - 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.0 return (vt4 - 4.0 * vt * vt3 + 6.0 * vt**2.0 * vt2 - 3.0 * vt**4.0) * s2 ** ( -2.0 ) - 3.0 @potential_physical_input def kurtosisvR(self, R, romberg=False, nsigma=None, phi=0.0): """ Calculate excess kurtosis in vR at R by marginalizing over velocity Parameters ---------- R : float or Quantity Radius at which to calculate romberg : bool, optional If True, use a romberg integrator (default: False) nsigma : float, optional Number of sigma to integrate the velocities over phi : float or Quantity, optional Azimuth (default: 0.0) Returns ------- float KurtosisvR Notes ----- - 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.0 return (vr4 - 4.0 * vr * vr3 + 6.0 * vr**2.0 * vr2 - 3.0 * vr**4.0) * s2 ** ( -2.0 ) - 3.0 def _ELtowRRapRperi(self, E, L): """ Calculate the radial frequency based on energy and angular momentum and return the pericenter and apocenter radii. Parameters ---------- E : float Energy. L : float Angular momentum. Returns ------- tuple Tuple containing: - wR(E.L) : float Radial frequency. - rap : float Apocenter radius. - rperi : float Pericenter radius. Notes ----- - 2010-07-11 - Written - Bovy (NYU) """ if self._beta == 0.0: xE = numpy.exp(E - 0.5) else: # non-flat rotation curve xE = (2.0 * E / (1.0 + 1.0 / self._beta)) ** (1.0 / 2.0 / self._beta) _, _, rperi, rap = self._aA.EccZmaxRperiRap( xE, numpy.sqrt(2.0 * (E - self._psp(xE)) - L**2.0 / xE**2.0), L / xE, 0.0, 0.0, ) return ( self._aA._aAS.actionsFreqs(xE, 0.0, L / xE, 0.0, 0.0)[3][0], rap[0], rperi[0], ) def sample( self, n=1, rrange=None, returnROrbit=True, returnOrbit=False, nphi=1.0, los=None, losdeg=True, nsigma=None, maxd=None, target=True, ): """ Sample n*nphi points from this disk DF. Parameters ---------- n : int, optional Number of desired samples. Default is 1. rrange : list, optional If you only want samples in this rrange, set this keyword (only works when asking for an (RZ)Orbit). returnROrbit : bool, optional If True, return a planarROrbit instance: [R,vR,vT] (default). returnOrbit : bool, optional If True, return a planarOrbit instance (including phi). nphi : float, optional Number of azimuths to sample for each E,L. los : float, optional Line of sight sampling along this line of sight. losdeg : bool, optional If True, los is in degrees (default). nsigma : int, optional Number of sigma to rejection-sample on. maxd : float, optional Maximum distance to consider (for the rejection sampling). target : bool, optional If True, use target surface mass and sigma2 profiles (default). Returns ------- list 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 Notes ----- - 2010-07-10 - Started - Bovy (NYU) """ raise NotImplementedError("'sample' method for this disk df is not implemented") def _estimatemeanvR(self, R, phi=0.0, log=False): """ Quickly estimate the mean radial velocity at a given radius R. Parameters ---------- R : float Radius at which to evaluate (/ro). phi : float, optional Azimuth angle (not used). log : bool, optional If True, return the logarithm of the target Sigma_R^2(R). Returns ------- float The target Sigma_R^2(R). Notes ----- - 2010-03-28 - Written - Bovy (NYU) """ return 0.0 def _estimatemeanvT(self, R, phi=0.0, log=False): """ Quickly estimate the mean tangential velocity at a given radius. Parameters ---------- R : float Radius at which to evaluate (/ro). phi : float, optional Azimuth angle (not used). log : bool, optional If True, return the logarithm of the estimate. Returns ------- float The estimated mean tangential velocity. Notes ----- - 2010-03-28 - Written - Bovy (NYU) """ return R**self._beta - self.asymmetricdrift(R, use_physical=False) def _estimateSigmaR2(self, R, phi=0.0, log=False): """ Quickly estimate SigmaR2. Parameters ---------- R : float Radius at which to evaluate (/ro). phi : float, optional Azimuth (not used). log : bool, optional If True, return the log (default: False). Returns ------- float Target Sigma_R^2(R). Notes ----- - 2010-03-28 - Written - Bovy (NYU) """ return self.targetSigma2(R, log=log, use_physical=False) def _estimateSigmaT2(self, R, phi=0.0, log=False): """ Quickly estimate SigmaT2. Parameters ---------- R : float Radius at which to evaluate (/ro). phi : float, optional Azimuth (not used). log : bool, optional If True, return the log (default: False). Returns ------- float Target Sigma_R^2(R). Notes ----- - 2010-03-28 - Written - Bovy (NYU) """ if log: return self.targetSigma2(R, log=log, use_physical=False) - 2.0 * numpylog( self._gamma ) else: return self.targetSigma2(R, log=log, use_physical=False) / self._gamma**2.0 class dehnendf(diskdf): """Dehnen's 'new' df""" def __init__( self, surfaceSigma=expSurfaceSigmaProfile, profileParams=(1.0 / 3.0, 1.0, 0.2), correct=False, beta=0.0, **kwargs, ): """ Initialize a Dehnen 'new' DF. Parameters ---------- surfaceSigma : instance or class name of the target surface density and sigma_R profile, optional Default: both exponential. profileParams : tuple, optional 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 : float, optional Power-law index of the rotation curve. correct : bool, optional If True, correct the DF. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). **kwargs: dict, optional DFcorrection kwargs (except for those already specified). Notes ----- - 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.0, logsigmaR2=0.0): """ Evaluate the distribution function. Parameters ---------- E : float or Quantity Energy. L : float or Quantity Angular momentum. logSigmaR : float, optional Logarithm of the radial velocity dispersion. logsigmaR2 : float, optional Logarithm of the square of the radial velocity dispersion. Returns ------- float DF(E,L) Notes ----- - 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.0: xE = numpy.exp(E - 0.5) logOLLE = numpylog(L / xE - 1.0) else: # non-flat rotation curve xE = (2.0 * E / (1.0 + 1.0 / self._beta)) ** (1.0 / 2.0 / 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.0 / 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.0 / numpy.pi ) def sample( self, n=1, rrange=None, returnROrbit=True, returnOrbit=False, nphi=1.0, los=None, losdeg=True, nsigma=None, targetSurfmass=True, targetSigma2=True, maxd=None, **kwargs, ): """ Sample n*nphi points from this DF. Parameters ---------- n : int, optional Number of desired samples (specifying this rather than calling this routine n times is more efficient). Default is 1. rrange : list or tuple, optional If you only want samples in this rrange, set this keyword (only works when asking for an (RZ)Orbit). Default is None. returnROrbit : bool, optional If True, return a planarROrbit instance: [R,vR,vT] (default). Default is True. returnOrbit : bool, optional If True, return a planarOrbit instance (including phi). Default is False. nphi : float, optional Number of azimuths to sample for each E,L. Default is 1.0. los : float or Quantity, optional If set, sample along this line of sight (deg) (assumes that the Sun is located at R=1,phi=0). Default is None. losdeg : bool, optional If False, los is in radians (default=True). Default is True. nsigma : int, optional Number of sigma to rejection-sample on. Default is None. targetSurfmass : bool, optional If True, use target surface mass profile. Default is True. targetSigma2 : bool, optional If True, use target sigma2 profile. Default is True. maxd : float or Quantity, optional Maximum distance to consider (for the rejection sampling). Default is None. **kwargs : dict, optional Additional keyword arguments. Returns ------- out : list 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. Notes ----- - 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, 0.0], [True, False], [0.05, 2.0], _ars_hx, _ars_hpx, nsamples=n, hxparams=(self._surfaceSigmaProfile, self._corr), ) ) else: xE = numpy.array( ars( [0.0, 0.0], [True, False], [0.05, 2.0], _ars_hx, _ars_hpx, nsamples=n, hxparams=(self._surfaceSigmaProfile, None), ) ) # Calculate E if self._beta == 0.0: E = numpylog(xE) + 0.5 else: # non-flat rotation curve E = 0.5 * xE ** (2.0 * self._beta) * (1.0 + 1.0 / self._beta) # Then sample Lz LCE = xE ** (self._beta + 1.0) OR = xE ** (self._beta - 1.0) 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) out = [] for ii in range(int(n)): try: wR, rap, rperi = self._ELtowRRapRperi(E[ii], Lz[ii]) except ValueError: # pragma: no cover # Tests don't get here anymore, because of improvements # in the rperi/rap calculation, but leaving the try/except # in because it can do no harm continue TR = 2.0 * numpy.pi / wR tr = stats.uniform.rvs() * TR if tr > TR / 2.0: tr -= TR / 2.0 thisOrbit = Orbit([rperi, 0.0, Lz[ii] / rperi]) else: thisOrbit = Orbit([rap, 0.0, Lz[ii] / rap]) thisOrbit.integrate(numpy.array([0.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.0, ] ).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.0 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.0, ] ).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, ) ) # Trim to make sure output has the right size 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.0: E = vR**2.0 / 2.0 + vT**2.0 / 2.0 + numpylog(R) xE = numpy.exp(E - 0.5) OE = xE**-1.0 LCE = xE dRedR = xE / R else: # non-flat rotation curve E = ( vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / self._beta * R ** (2.0 * self._beta) ) xE = (2.0 * E / (1.0 + 1.0 / self._beta)) ** (1.0 / 2.0 / self._beta) OE = xE ** (self._beta - 1.0) LCE = xE ** (self._beta + 1.0) dRedR = xE / 2.0 / self._beta / E * R ** (2.0 * self._beta - 1.0) 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.0: E = vR**2.0 / 2.0 + vT**2.0 / 2.0 + numpylog(R) xE = numpy.exp(E - 0.5) OE = xE**-1.0 LCE = xE dRedvR = xE * vR else: # non-flat rotation curve E = ( vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / self._beta * R ** (2.0 * self._beta) ) xE = (2.0 * E / (1.0 + 1.0 / self._beta)) ** (1.0 / 2.0 / self._beta) OE = xE ** (self._beta - 1.0) LCE = xE ** (self._beta + 1.0) dRedvR = xE / 2.0 / 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.0: E = vR**2.0 / 2.0 + vT**2.0 / 2.0 + numpylog(R) xE = numpy.exp(E - 0.5) OE = xE**-1.0 LCE = xE dRedvT = xE * vT else: # non-flat rotation curve E = ( vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / self._beta * R ** (2.0 * self._beta) ) xE = (2.0 * E / (1.0 + 1.0 / self._beta)) ** (1.0 / 2.0 / self._beta) OE = xE ** (self._beta - 1.0) LCE = xE ** (self._beta + 1.0) dRedvT = xE / 2.0 / 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.0: E = vR**2.0 / 2.0 + vT**2.0 / 2.0 + numpylog(R) xE = numpy.exp(E - 0.5) OE = xE**-1.0 LCE = xE else: # non-flat rotation curve E = ( vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / self._beta * R ** (2.0 * self._beta) ) xE = (2.0 * E / (1.0 + 1.0 / self._beta)) ** (1.0 / 2.0 / self._beta) OE = xE ** (self._beta - 1.0) LCE = xE ** (self._beta + 1.0) L = R * vT sigma2xE = self._surfaceSigmaProfile.sigma2(xE, log=False) return ( self._surfaceSigmaProfile.surfacemassDerivative(xE, log=True) - (1.0 + OE * (L - LCE) / sigma2xE) * self._surfaceSigmaProfile.sigma2Derivative(xE, log=True) + (L - LCE) / sigma2xE * (self._beta - 1.0) * xE ** (self._beta - 2.0) - OE * (self._beta + 1.0) / 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.0: E = vR**2.0 / 2.0 + vT**2.0 / 2.0 + numpylog(R) xE = numpy.exp(E - 0.5) OE = xE**-1.0 else: # non-flat rotation curve E = ( vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / self._beta * R ** (2.0 * self._beta) ) xE = (2.0 * E / (1.0 + 1.0 / self._beta)) ** (1.0 / 2.0 / self._beta) OE = xE ** (self._beta - 1.0) sigma2xE = self._surfaceSigmaProfile.sigma2(xE, log=False) return OE / sigma2xE class shudf(diskdf): """Shu's df (1969)""" def __init__( self, surfaceSigma=expSurfaceSigmaProfile, profileParams=(1.0 / 3.0, 1.0, 0.2), correct=False, beta=0.0, **kwargs, ): """ Initialize a Shu DF. Parameters ---------- surfaceSigma : instance or class name of the target surface density and sigma_R profile, optional Default: both exponential. profileParams : tuple, optional 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 : float, optional Power-law index of the rotation curve. correct : bool, optional If True, correct the DF. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). **kwargs: dict, optional DFcorrection kwargs (except for those already specified). Notes ----- - 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.0, logsigmaR2=0.0): """ Evaluate the distribution function. Parameters ---------- E : float Energy (/vo^2). L : float Angular momentum (/ro/vo). logSigmaR : float, optional Logarithm of the radial velocity dispersion squared. logsigmaR2 : float, optional Logarithm of the radial velocity dispersion squared. Returns ------- float DF(E,L). Notes ----- - 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.0: xL = L logECLE = numpylog(-numpylog(xL) - 0.5 + E) else: # non-flat rotation curve xL = L ** (1.0 / (self._beta + 1.0)) logECLE = numpylog( -0.5 * (1.0 / self._beta + 1.0) * xL ** (2.0 * self._beta) + E ) if xL < 0.0: # We must remove counter-rotating mass return 0.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.0 / numpy.pi ) def sample( self, n=1, rrange=None, returnROrbit=True, returnOrbit=False, nphi=1.0, los=None, losdeg=True, nsigma=None, maxd=None, targetSurfmass=True, targetSigma2=True, **kwargs, ): """ Sample n*nphi points from this DF. Parameters ---------- n : int, optional Number of desired samples (specifying this rather than calling this routine n times is more efficient). Default is 1. rrange : list or tuple, optional If you only want samples in this rrange, set this keyword (only works when asking for an (RZ)Orbit). Default is None. returnROrbit : bool, optional If True, return a planarROrbit instance: [R,vR,vT] (default). Default is True. returnOrbit : bool, optional If True, return a planarOrbit instance (including phi). Default is False. nphi : float, optional Number of azimuths to sample for each E,L. Default is 1.0. los : float or Quantity, optional If set, sample along this line of sight (deg) (assumes that the Sun is located at R=1,phi=0). Default is None. losdeg : bool, optional If False, los is in radians (default=True). Default is True. nsigma : int, optional Number of sigma to rejection-sample on. Default is None. targetSurfmass : bool, optional If True, use target surface mass profile. Default is True. targetSigma2 : bool, optional If True, use target sigma2 profile. Default is True. maxd : float or Quantity, optional Maximum distance to consider (for the rejection sampling). Default is None. **kwargs : dict, optional Additional keyword arguments. Returns ------- out : list 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. Notes ----- - 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, 0.0], [True, False], [0.05, 2.0], _ars_hx, _ars_hpx, nsamples=n, hxparams=(self._surfaceSigmaProfile, self._corr), ) ) else: xL = numpy.array( ars( [0.0, 0.0], [True, False], [0.05, 2.0], _ars_hx, _ars_hpx, nsamples=n, hxparams=(self._surfaceSigmaProfile, None), ) ) # Calculate Lz Lz = xL ** (self._beta + 1.0) # Then sample E if self._beta == 0.0: ECL = numpylog(xL) + 0.5 else: ECL = 0.5 * (1.0 / self._beta + 1.0) * xL ** (2.0 * 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) out = [] for ii in range(n): try: wR, rap, rperi = self._ELtowRRapRperi(E[ii], Lz[ii]) except ValueError: # pragma: no cover continue TR = 2.0 * numpy.pi / wR tr = stats.uniform.rvs() * TR if tr > TR / 2.0: tr -= TR / 2.0 thisOrbit = Orbit([rperi, 0.0, Lz[ii] / rperi]) else: thisOrbit = Orbit([rap, 0.0, Lz[ii] / rap]) thisOrbit.integrate(numpy.array([0.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.0, ] ).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.0 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.0, ] ).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, ) ) # Trim to make sure output has the right size 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.0: xL = L dRldR = vT ECL = numpylog(xL) + 0.5 dECLEdR = 0.0 else: # non-flat rotation curve xL = L ** (1.0 / (self._beta + 1.0)) dRldR = L ** (1.0 / (self._beta + 1.0)) / R / (self._beta + 1.0) ECL = 0.5 * (1.0 / self._beta + 1.0) * xL ** (2.0 * self._beta) dECLdRl = (1.0 + self._beta) * xL ** (2.0 * self._beta - 1) dEdR = R ** (2.0 * self._beta - 1.0) dECLEdR = dECLdRl * dRldR - dEdR sigma2xL = self._surfaceSigmaProfile.sigma2(xL, log=False) return ( self._surfaceSigmaProfile.surfacemassDerivative(xL, log=True) - (1.0 + (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.0: xL = L else: # non-flat rotation curve xL = L ** (1.0 / (self._beta + 1.0)) 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.0: xL = L dRldvT = R ECL = numpylog(xL) + 0.5 dECLEdvT = 1.0 / vT - vT else: # non-flat rotation curve xL = L ** (1.0 / (self._beta + 1.0)) dRldvT = L ** (1.0 / (self._beta + 1.0)) / vT / (self._beta + 1.0) ECL = 0.5 * (1.0 / self._beta + 1.0) * xL ** (2.0 * self._beta) dECLdRl = (1.0 + self._beta) * xL ** (2.0 * self._beta - 1) dEdvT = vT dECLEdvT = dECLdRl * dRldvT - dEdvT sigma2xL = self._surfaceSigmaProfile.sigma2(xL, log=False) return ( self._surfaceSigmaProfile.surfacemassDerivative(xL, log=True) - (1.0 + (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.0 / 3.0, 1.0, 0.2), correct=False, beta=0.0, **kwargs, ): """ Initialize a Schwarzschild DF. Parameters ---------- surfaceSigma : instance or class name of the target surface density and sigma_R profile, optional (default: both exponential) profileParams : tuple, optional 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 : float, optional Power-law index of the rotation curve correct : bool, optional If True, correct the DF ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). **kwargs : dict, optional DFcorrection kwargs (except for those already specified) Notes ----- - 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.0 * 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.0 * df.eval(E, L, logSigmaR, logsigmaR2) * 2.0 * 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.0 * 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.0 * numpy.pi / df._gamma * df._dlnfdR(R, vR * sigmaR1, vT * sigmaR1 / gamma) ) # correct else: return 0.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 quadpack.romberg(twodfunc, gfun(x), hfun(x), args=thisargs, tol=tol) def bovy_dblquad(func, a, b, gfun, hfun, args=(), tol=1.48e-08): """ Compute a double integral using Romberg integration for the one-dimensional integrals and a specified tolerance. Parameters ---------- func : callable Function of two variables to integrate. a : float Lower limit of integration in the outer integral. b : float Upper limit of integration in the outer integral. gfun : callable Function of one variable that returns the lower limit of integration in the inner integral for a given value of the outer variable. hfun : callable Function of one variable that returns the upper limit of integration in the inner integral for a given value of the outer variable. args : tuple, optional Extra arguments to pass to the integrand function. tol : float, optional Desired absolute tolerance. Returns ------- float The value of the double integral. Notes ----- - 2010-03-11 - Written - Bovy (NYU) """ return quadpack.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): """ Initialize the corrections: set them, load them, or calculate and save them. Parameters ---------- corrections : numpy.ndarray, optional If set, these are the corrections and they should be used as such. npoints : int, optional Number of points from 0 to Rmax. rmax : float, optional Correct up to this radius (/ro) (default: 5). savedir : str, optional Save the corrections in this directory. surfaceSigmaProfile : object Target surfacemass and sigma_R^2 instance. beta : float, optional Power-law index of the rotation curve (when calculating). dftype : class, optional Classname of the DF. niter : int, optional Number of iterations to perform to calculate the corrections. interp_k : str, optional 'k' keyword to give to InterpolatedUnivariateSpline. Notes ----- - 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.0) 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.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.0 * self._rmax) self._surfaceInterpolate = interpolate.InterpolatedUnivariateSpline( interpRs, numpylog(numpy.append(self._corrections[:, 0], 1.0)), k=self._interp_k, ) self._sigma2Interpolate = interpolate.InterpolatedUnivariateSpline( interpRs, numpylog(numpy.append(self._corrections[:, 1], 1.0)), 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): """ Calculate the correction in Sigma and sigma2 at R. Parameters ---------- R : float Galactocentric radius (/ro). log : bool, optional If True, return the log of the correction. Returns ------- tuple (Sigma correction, sigma2 correction). Notes ----- - 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.0 * self._rmax) if numpy.sum(rmax_indx) > 0: out[:, rmax_indx] = 0.0 #'normal' R r_indx = (R >= _RMIN) * (R <= (2.0 * 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.0 * self._rmax): out = numpy.array([0.0, 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): """ Calculate the derivative of the log of the correction in Sigma and sigma2 at R. Parameters ---------- R : float Galactocentric radius(/ro) Returns ------- numpy.ndarray [d log(Sigma correction)/dR, d log(sigma2 correction)/dR] Notes ----- - 2010-03-10 - Written - Bovy (NYU) """ if R < _RMIN: out = numpy.array([self._surfaceDerivSmallR, self._sigma2DerivSmallR]) elif R > (2.0 * self._rmax): out = numpy.array([0.0, 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"): """ Calculate the energy and angular momentum. Parameters ---------- vR : float Radial velocity. vT : float Rotational velocity. R : float Galactocentric radius. beta : float Parameter that determines the shape of the rotation curve. dftype : str, optional Type of disk distribution function. Default is "dehnen". Returns ------- tuple Energy and angular momentum. Notes ----- - 2010-03-10 - Written - Bovy (NYU) """ if dftype == "schwarzschild": # Compute E in the epicycle approximation gamma = numpy.sqrt(2.0 / (1.0 + beta)) L = R * vT if beta == 0.0: xL = L else: # non-flat rotation curve xL = L ** (1.0 / (beta + 1.0)) return ( 0.5 * vR**2.0 + 0.5 * gamma**2.0 * (vT - R**beta) ** 2.0 + xL ** (2.0 * beta) / 2.0 + axipotential(xL, beta=beta), L, ) else: return (axipotential(R, beta) + 0.5 * vR**2.0 + 0.5 * vT**2.0, vT * R) def axipotential(R, beta=0.0): """ Return the axisymmetric potential at R/Ro. Parameters ---------- R : float Galactocentric radius. beta : float, optional Rotation curve power-law. Returns ------- float Pot(R)/vo**2. Notes ----- - 2010-03-01 - Written - Bovy (NYU) """ if beta == 0.0: if numpy.any(R == 0.0): out = numpy.empty(R.shape) out[R == 0.0] = numpylog(_RMIN) out[R != 0.0] = numpylog(R[R != 0.0]) return out else: return numpylog(R) else: # non-flat rotation curve return R ** (2.0 * beta) / 2.0 / beta def _ars_hx(x, args): """ h(x) for ARS sampling of the input surfacemass profile Parameters ---------- x : float R(/ro) args : tuple surfaceSigma - surfaceSigmaProfile instance dfcorr - DFcorrection instance Returns ------- float log(x)+log surface(x) + log(correction) Notes ----- - 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): """ h'(x) for ARS sampling of the input surfacemass profile Parameters ---------- x : float R(/ro) args : tuple surfaceSigma - surfaceSigmaProfile instance dfcorr - DFcorrection instance Returns ------- float derivative of log(x)+log surface(x) + log(correction) wrt x Notes ----- - 2010-07-11 - Written - Bovy (NYU) """ surfaceSigma, dfcorr = args if dfcorr is None: return 1.0 / x + surfaceSigma.surfacemassDerivative(x, log=True) else: return ( 1.0 / x + surfaceSigma.surfacemassDerivative(x, log=True) + dfcorr.derivLogcorrect(x)[0] ) def _kappa(R, beta): """Internal function to give kappa(r)""" return numpy.sqrt(2.0 * (1.0 + beta)) * R ** (beta - 1) def _dlToRphi(d, l): """Convert d and l to R and phi, l is in radians""" R = numpy.sqrt(1.0 + d**2.0 - 2.0 * d * numpy.cos(l)) if R == 0.0: R += 0.0001 d += 0.0001 if 1.0 / numpy.cos(l) < d and numpy.cos(l) > 0.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.0: E = vT**2.0 / 2.0 + numpylog(R) xE = numpy.exp(E - 0.5) OE = xE**-1.0 LCE = xE dxEdvT = xE * vT else: # non-flat rotation curve E = vT**2.0 / 2.0 + 1.0 / 2.0 / diskdf._beta * R ** (2.0 * diskdf._beta) xE = (2.0 * E / (1.0 + 1.0 / diskdf._beta)) ** (1.0 / 2.0 / diskdf._beta) OE = xE ** (diskdf._beta - 1.0) LCE = xE ** (diskdf._beta + 1.0) dxEdvT = xE / 2.0 / 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.0 + OE * (L - LCE) / sigma2xE) * diskdf._surfaceSigmaProfile.sigma2Derivative(xE, log=True) + (L - LCE) / sigma2xE * (diskdf._beta - 1.0) * xE ** (diskdf._beta - 2.0) - OE * (diskdf._beta + 1.0) / 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=1741018131.0 galpy-1.10.2/galpy/df/eddingtondf.py0000644000175100001660000001430414761352023016663 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): """ Initialize an isotropic distribution function computed using the Eddington inversion. Parameters ---------- pot : Potential instance or list thereof Represents the gravitational potential (assumed to be spherical). denspot : Potential instance or list thereof, optional Represents the density of the tracers (assumed to be spherical; if None, set equal to pot). rmax : float or Quantity, optional Maximum radius to consider. DF is cut off at E = Phi(rmax). scale : float or Quantity, optional Characteristic scale radius to aid sampling calculations. Optional and will also be overridden by value from pot if available. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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, 0) # Build interpolator r(pot) self._rphi = self._setup_rphi_interpolator() def sample(self, R=None, z=None, phi=None, n=1, return_orbit=True, rmin=0.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.0 - 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): """ Calculate the energy portion of a DF computed using the Eddington inversion Parameters ---------- E : float or Quantity The energy. Returns ------- fE : ndarray The value of the energy portion of the DF. Notes ----- - 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.0, numpy.sqrt(self._rphi(tE)), points=[0.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, 0.5 / self._rphi(tE), )[0] for tE in Eint[indx] ] ) return -out / (numpy.sqrt(8.0) * numpy.pi**2.0) 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.0 / 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.0 * t * _fEintegrand_raw(t**2.0 + 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.0 / t**2 * _fEintegrand_raw(1.0 / t, pot, E, dnudr, d2nudr2) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/evolveddiskdf.py0000644000175100001660000040106514761352023017233 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.0 _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.0 _RADTODEG = 180.0 / 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.0): """ Initialize the evolved disk distribution function. Parameters ---------- initdf : galpy.df.df instance The distribution function at the start of the evolution (at to) (units are transferred). pot : galpy.potential.Potential instance Potential to integrate orbits in. to : float or Quantity, optional Initial time (time at which initdf is evaluated; orbits are integrated from current t back to to). Notes ----- - 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): """ Evaluate the distribution function Parameters ---------- *args : tuple Either: 1) Orbit instance alone: use initial state and t=0 2) 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 : bool, optional marginalize over perpendicular velocity (only supported with 1a) for single orbits above) marginalizeVlos : bool, optional marginalize over line-of-sight velocity (only supported with 1a) for single orbits above) integrate_method : str, optional orbit.integrate method argument log : bool, optional if True, return the log (not for deriv, bc that can be negative) deriv : str, optional None, 'R', or 'phi': calculates derivative of the moment wrt R or phi **not with the marginalize options** **kwargs: dict, optional scipy.integrate.quad keywords Returns ------- float or numpy.ndarray value of DF Notes ----- - 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.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: # pragma: no cover raise RuntimeError( "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: # pragma: no cover raise RuntimeError( "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.0**-10.0 tmp = o.R(use_physical=False) + dderiv dderiv = tmp - o.R(use_physical=False) msg = o.integrate_dxdv( [dderiv, 0.0, 0.0, 0.0], ts, self._pot, method=integrate_method ) elif deriv.lower() == "phi": dderiv = 10.0**-10.0 tmp = o.phi(use_physical=False) + dderiv dderiv = tmp - o.phi(use_physical=False) msg = o.integrate_dxdv( [0.0, 0.0, 0.0, dderiv], ts, self._pot, method=integrate_method ) if not msg is None and msg > 0.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.0 elif not isinstance(retval, float) and len(retval.shape) > 0: retval[(numpy.isnan(retval))] = 0.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.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.0**-10.0 tmp = o.R(use_physical=False) + dderiv dderiv = tmp - o.R(use_physical=False) o.integrate_dxdv( [dderiv, 0.0, 0.0, 0.0], ts, self._pot, method=integrate_method ) elif deriv.lower() == "phi": dderiv = 10.0**-10.0 tmp = o.phi(use_physical=False) + dderiv dderiv = tmp - o.phi(use_physical=False) o.integrate_dxdv( [0.0, 0.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, use_physical=False) <= 0.0: return ( -numpy.finfo(numpy.dtype(numpy.float64)).max if kwargs.get("log", False) else numpy.finfo(numpy.dtype(numpy.float64)).eps ) # start= time.time() retval = self._initdf(o(self._to, use_physical=False), use_physical=False) # print( int_time/(time.time()-start)) if not deriv is None: thisorbit = o(self._to).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) 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.atleast_1d(numpy.log(retval)) out[retval == 0.0] = -numpy.finfo(numpy.dtype(numpy.float64)).max else: out = ( numpy.log(retval) if retval != 0.0 else -numpy.finfo(numpy.dtype(numpy.float64)).max ) return out else: return retval def vmomentsurfacemass( self, R, n, m, t=0.0, nsigma=None, deg=False, epsrel=1.0e-02, epsabs=1.0e-05, phi=0.0, grid=None, gridpoints=101, returnGrid=False, hierarchgrid=False, nlevels=2, print_progress=False, integrate_method="dopr54_c", deriv=None, ): """ Calculate the an arbitrary moment of the velocity distribution at (R,phi) times the surfacmass Parameters ---------- R : float Radius at which to calculate the moment (in natural units). phi : float, optional Azimuth (rad unless deg=True). n : int vR^n. m : int vT^m. t : float, optional 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 : float, optional Number of sigma to integrate the velocities over (based on an estimate, so be generous, but not too generous). deg : bool, optional Azimuth is in degree (default=False). epsrel : float, optional scipy.integrate keyword (the integration calculates the ratio of this vmoment to that of the initial DF). epsabs : float, optional scipy.integrate keyword (the integration calculates the ratio of this vmoment to that of the initial DF). grid : bool, optional 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 : int, optional Number of points to use for the grid in 1D (default=101). returnGrid : bool, optional If True, return the grid object (default=False). hierarchgrid : bool, optional If True, use a hierarchical grid (default=False). nlevels : int, optional Number of hierarchical levels for the hierarchical grid. print_progress : bool, optional If True, print progress updates. integrate_method : str, optional orbit.integrate method argument. deriv : str, optional None, 'R', or 'phi': calculates derivative of the moment wrt R or phi **onnly with grid options**. Returns ------- float or numpy.ndarray at R,phi (no support for units). Notes ----- - grid-based calculation is the only one that is heavily tested (although the test suite also tests the direct calculation) - 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.0: initvmoment = 1.0 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.0 - (x - meanvT / sigmaT1) ** 2.0), lambda x: meanvR / sigmaR1 + numpy.sqrt(nsigma**2.0 - (x - meanvT / sigmaT1) ** 2.0), (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.0, nsigma=None, deg=False, epsrel=1.0e-02, epsabs=1.0e-05, phi=0.0, grid=None, gridpoints=101, returnGrid=False, sigmaR2=None, sigmaT2=None, sigmaRT=None, surfacemass=None, hierarchgrid=False, nlevels=2, integrate_method="dopr54_c", ): """ Calculate the vertex deviation of the velocity distribution at (R,phi) Parameters ---------- R : float radius at which to calculate the moment (can be Quantity) t : float, optional 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), by default 0.0 nsigma : float, optional number of sigma to integrate the velocities over (based on an estimate, so be generous), by default None deg : bool, optional azimuth is in degree (default=False); do not set this when giving phi as a Quantity, by default False epsrel : float, optional scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-02 epsabs : float, optional scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-05 phi : float, optional azimuth (rad unless deg=True; can be Quantity), by default 0.0 grid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None gridpoints : int, optional number of points to use for the grid in 1D (default=101), by default 101 returnGrid : bool, optional if True, return the grid object (default=False), by default False sigmaR2 : float, optional if set the vertex deviation is simply calculated using these, by default None sigmaT2 : float, optional if set the vertex deviation is simply calculated using these, by default None sigmaRT : float, optional if set the vertex deviation is simply calculated using these, by default None surfacemass : None, optional Not used, by default None hierarchgrid : bool, optional if True, use a hierarchical grid (default=False), by default False nlevels : int, optional number of hierarchical levels for the hierarchical grid, by default 2 integrate_method : str, optional orbit.integrate method argument, by default "dopr54_c" Returns ------- float or tuple vertex deviation in rad or tuple of vertex deviation in rad and grid object Notes ----- - 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.0 * sigmaRT / (sigmaR2 - sigmaT2)) / 2.0, grido) else: return -numpy.arctan(2.0 * sigmaRT / (sigmaR2 - sigmaT2)) / 2.0 @potential_physical_input @physical_conversion("velocity", pop=True) def meanvR( self, R, t=0.0, nsigma=None, deg=False, phi=0.0, epsrel=1.0e-02, epsabs=1.0e-05, grid=None, gridpoints=101, returnGrid=False, surfacemass=None, hierarchgrid=False, nlevels=2, integrate_method="dopr54_c", ): """ Calculate the mean vR of the velocity distribution at (R,phi) Parameters ---------- R : float radius at which to calculate the moment(/ro) (can be Quantity) phi : float, optional azimuth (rad unless deg=True; can be Quantity) (default=0.0) t : float or array, optional 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) (default=0.0) surfacemass : float, optional if set use this pre-calculated surfacemass (default=None) nsigma : float, optional number of sigma to integrate the velocities over (based on an estimate, so be generous) (default=None) deg : bool, optional azimuth is in degree (default=False); do not set this when giving phi as a Quantity epsrel : float, optional scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF) (default=1.0e-02) epsabs : float, optional scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF) (default=1.0e-05) grid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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 (default=None) gridpoints : int, optional number of points to use for the grid in 1D (default=101) returnGrid : bool, optional if True, return the grid object (default=False) hierarchgrid : bool, optional if True, use a hierarchical grid (default=False) nlevels : int, optional number of hierarchical levels for the hierarchical grid (default=2) integrate_method : str, optional orbit.integrate method argument (default="dopr54_c") Returns ------- float or tuple mean vR Notes ----- -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.0, nsigma=None, deg=False, phi=0.0, epsrel=1.0e-02, epsabs=1.0e-05, grid=None, gridpoints=101, returnGrid=False, surfacemass=None, hierarchgrid=False, nlevels=2, integrate_method="dopr54_c", ): """ Calculate the mean vT of the velocity distribution at (R,phi) Parameters ---------- R : float radius at which to calculate the moment(/ro) (can be Quantity) phi : float, optional azimuth (rad unless deg=True; can be Quantity) (default=0.0) t : float or array, optional 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) (default=0.0) surfacemass : float, optional if set use this pre-calculated surfacemass (default=None) nsigma : float, optional number of sigma to integrate the velocities over (based on an estimate, so be generous) (default=None) deg : bool, optional azimuth is in degree (default=False); do not set this when giving phi as a Quantity epsrel : float, optional scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF) (default=1.0e-02) epsabs : float, optional scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF) (default=1.0e-05) grid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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 (default=None) gridpoints : int, optional number of points to use for the grid in 1D (default=101) returnGrid : bool, optional if True, return the grid object (default=False) hierarchgrid : bool, optional if True, use a hierarchical grid (default=False) nlevels : int, optional number of hierarchical levels for the hierarchical grid (default=2) integrate_method : str, optional orbit.integrate method argument (default="dopr54_c") Returns ------- float or tuple mean vT Notes ----- -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.0, nsigma=None, deg=False, phi=0.0, epsrel=1.0e-02, epsabs=1.0e-05, grid=None, gridpoints=101, returnGrid=False, surfacemass=None, meanvR=None, hierarchgrid=False, nlevels=2, integrate_method="dopr54_c", ): """ Calculate the radial variance of the velocity distribution at (R,phi) Parameters ---------- R : float Radius at which to calculate the moment (can be Quantity) t : float, optional 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 : float, optional Number of sigma to integrate the velocities over (based on an estimate, so be generous) deg : bool, optional Azimuth is in degree (default=False); do not set this when giving phi as a Quantity phi : float, optional Azimuth (rad unless deg=True; can be Quantity) epsrel : float, optional Scipy.integrate keyword (the integration calculates the ratio of this vmoment to that of the initial DF) epsabs : float, optional Scipy.integrate keyword (the integration calculates the ratio of this vmoment to that of the initial DF) grid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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 : int, optional Number of points to use for the grid in 1D (default=101) returnGrid : bool, optional If True, return the grid object (default=False) surfacemass : float, optional If set use this pre-calculated surfacemass meanvR : float, optional If set use this pre-calculated mean vR hierarchgrid : bool, optional If True, use a hierarchical grid (default=False) nlevels : int, optional Number of hierarchical levels for the hierarchical grid integrate_method : str, optional orbit.integrate method argument Returns ------- float or tuple Variance of vR. If returnGrid is True, return a tuple with the variance of vR and the grid object. Notes ----- - 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.0 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.0, nsigma=None, deg=False, phi=0.0, epsrel=1.0e-02, epsabs=1.0e-05, grid=None, gridpoints=101, returnGrid=False, surfacemass=None, meanvT=None, hierarchgrid=False, nlevels=2, integrate_method="dopr54_c", ): """ Calculate the rotational-velocity variance of the velocity distribution at (R,phi) Parameters ---------- R : float Radius at which to calculate the moment (can be Quantity) phi : float, optional Azimuth (rad unless deg=True; can be Quantity), by default 0.0 t : float or array, optional 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), by default 0.0 surfacemass : float, optional If set use this pre-calculated surfacemass and mean rotational velocity, by default None meanvT : float, optional If set use this pre-calculated surfacemass and mean rotational velocity, by default None nsigma : float, optional Number of sigma to integrate the velocities over (based on an estimate, so be generous), by default None deg : bool, optional Azimuth is in degree (default=False); do not set this when giving phi as a Quantity, by default False epsrel : float, optional Scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-02 epsabs : float, optional Scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-05 grid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None gridpoints : int, optional Number of points to use for the grid in 1D (default=101), by default 101 returnGrid : bool, optional If True, return the grid object (default=False), by default False hierarchgrid : bool, optional If True, use a hierarchical grid (default=False), by default False nlevels : int, optional Number of hierarchical levels for the hierarchical grid, by default 2 integrate_method : str, optional orbit.integrate method argument, by default "dopr54_c" Returns ------- float or tuple Variance of vT or tuple of variance of vT and grid object Notes ----- - 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.0 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.0, nsigma=None, deg=False, epsrel=1.0e-02, epsabs=1.0e-05, phi=0.0, grid=None, gridpoints=101, returnGrid=False, surfacemass=None, meanvR=None, meanvT=None, hierarchgrid=False, nlevels=2, integrate_method="dopr54_c", ): """ Calculate the radial-rotational co-variance of the velocity distribution at (R,phi) Parameters ---------- R : float Radius at which to calculate the moment (can be Quantity) phi : float, optional Azimuth (rad unless deg=True; can be Quantity), by default 0.0 t : float or array, optional 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), by default 0.0 surfacemass : float, optional If set use this pre-calculated surfacemass and mean rotational velocity, by default None meanvT : float, optional If set use this pre-calculated surfacemass and mean rotational velocity, by default None nsigma : float, optional Number of sigma to integrate the velocities over (based on an estimate, so be generous), by default None deg : bool, optional Azimuth is in degree (default=False); do not set this when giving phi as a Quantity, by default False epsrel : float, optional Scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-02 epsabs : float, optional Scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-05 grid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None gridpoints : int, optional Number of points to use for the grid in 1D (default=101), by default 101 returnGrid : bool, optional If True, return the grid object (default=False), by default False hierarchgrid : bool, optional If True, use a hierarchical grid (default=False), by default False nlevels : int, optional Number of hierarchical levels for the hierarchical grid, by default 2 integrate_method : str, optional orbit.integrate method argument, by default "dopr54_c" Returns ------- float or tuple Covariance of vR and vT or tuple of covariance of vR and vT and grid object Notes ----- - 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.0, nsigma=None, deg=False, phi=0.0, epsrel=1.0e-02, epsabs=1.0e-05, grid=None, gridpoints=101, returnGrids=False, derivRGrid=None, derivphiGrid=None, derivGridpoints=101, derivHierarchgrid=False, hierarchgrid=False, nlevels=2, integrate_method="dopr54_c", ): """ Calculate the Oort function A at (R,phi,t) Parameters ---------- R : float Radius at which to calculate the moment (can be Quantity) phi : float, optional Azimuth (rad unless deg=True; can be Quantity), by default 0.0 t : float or array, optional 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), by default 0.0 surfacemass : float, optional If set use this pre-calculated surfacemass and mean rotational velocity, by default None meanvT : float, optional If set use this pre-calculated surfacemass and mean rotational velocity, by default None nsigma : float, optional Number of sigma to integrate the velocities over (based on an estimate, so be generous), by default None deg : bool, optional Azimuth is in degree (default=False); do not set this when giving phi as a Quantity, by default False epsrel : float, optional Scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-02 epsabs : float, optional Scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-05 grid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None gridpoints : int, optional Number of points to use for the grid in 1D (default=101), by default 101 returnGrids : bool, optional If True, return the grid objects (default=False), by default False derivRGrid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None derivphiGrid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None derivGridpoints : int, optional Number of points to use for the grid in 1D (default=101), by default 101 derivHierarchgrid : bool, optional If True, use a hierarchical grid (default=False), by default False hierarchgrid : bool, optional If True, use a hierarchical grid (default=False), by default False nlevels : int, optional Number of hierarchical levels for the hierarchical grid, by default 2 integrate_method : str, optional orbit.integrate method argument, by default "dopr54_c" Returns ------- float or tuple Oort A at R,phi,t or tuple of Oort A at R,phi,t and grid objects Notes ----- - 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.0 * 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.0 * 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.0, nsigma=None, deg=False, phi=0.0, epsrel=1.0e-02, epsabs=1.0e-05, grid=None, gridpoints=101, returnGrids=False, derivRGrid=None, derivphiGrid=None, derivGridpoints=101, derivHierarchgrid=False, hierarchgrid=False, nlevels=2, integrate_method="dopr54_c", ): """ Calculate the Oort function B at (R,phi,t) Parameters ---------- R : float Radius at which to calculate the moment (can be Quantity) phi : float, optional Azimuth (rad unless deg=True; can be Quantity), by default 0.0 t : float or array, optional 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), by default 0.0 surfacemass : float, optional If set use this pre-calculated surfacemass and mean rotational velocity, by default None meanvT : float, optional If set use this pre-calculated surfacemass and mean rotational velocity, by default None nsigma : float, optional Number of sigma to integrate the velocities over (based on an estimate, so be generous), by default None deg : bool, optional Azimuth is in degree (default=False); do not set this when giving phi as a Quantity, by default False epsrel : float, optional Scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-02 epsabs : float, optional Scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-05 grid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None gridpoints : int, optional Number of points to use for the grid in 1D (default=101), by default 101 returnGrids : bool, optional If True, return the grid objects (default=False), by default False derivRGrid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None derivphiGrid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None derivGridpoints : int, optional Number of points to use for the grid in 1D (default=101), by default 101 derivHierarchgrid : bool, optional If True, use a hierarchical grid (default=False), by default False hierarchgrid : bool, optional If True, use a hierarchical grid (default=False), by default False nlevels : int, optional Number of hierarchical levels for the hierarchical grid, by default 2 integrate_method : str, optional orbit.integrate method argument, by default "dopr54_c" Returns ------- float or tuple Oort B at R,phi,t or tuple of Oort A at R,phi,t and grid objects Notes ----- - 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.0 * 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.0 * 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.0, nsigma=None, deg=False, phi=0.0, epsrel=1.0e-02, epsabs=1.0e-05, grid=None, gridpoints=101, returnGrids=False, derivRGrid=None, derivphiGrid=None, derivGridpoints=101, derivHierarchgrid=False, hierarchgrid=False, nlevels=2, integrate_method="dopr54_c", ): """ Calculate the Oort function C at (R,phi,t) Parameters ---------- R : float Radius at which to calculate the moment (can be Quantity) phi : float, optional Azimuth (rad unless deg=True; can be Quantity), by default 0.0 t : float or array, optional 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), by default 0.0 surfacemass : float, optional If set use this pre-calculated surfacemass and mean rotational velocity, by default None meanvT : float, optional If set use this pre-calculated surfacemass and mean rotational velocity, by default None nsigma : float, optional Number of sigma to integrate the velocities over (based on an estimate, so be generous), by default None deg : bool, optional Azimuth is in degree (default=False); do not set this when giving phi as a Quantity, by default False epsrel : float, optional Scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-02 epsabs : float, optional Scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-05 grid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None gridpoints : int, optional Number of points to use for the grid in 1D (default=101), by default 101 returnGrids : bool, optional If True, return the grid objects (default=False), by default False derivRGrid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None derivphiGrid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None derivGridpoints : int, optional Number of points to use for the grid in 1D (default=101), by default 101 derivHierarchgrid : bool, optional If True, use a hierarchical grid (default=False), by default False hierarchgrid : bool, optional If True, use a hierarchical grid (default=False), by default False nlevels : int, optional Number of hierarchical levels for the hierarchical grid, by default 2 integrate_method : str, optional orbit.integrate method argument, by default "dopr54_c" Returns ------- float or tuple Oort C at R,phi,t or tuple of Oort A at R,phi,t and grid objects Notes ----- - 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.0 * 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.0 * 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.0, nsigma=None, deg=False, phi=0.0, epsrel=1.0e-02, epsabs=1.0e-05, grid=None, gridpoints=101, returnGrids=False, derivRGrid=None, derivphiGrid=None, derivGridpoints=101, derivHierarchgrid=False, hierarchgrid=False, nlevels=2, integrate_method="dopr54_c", ): """ Calculate the Oort function K at (R,phi,t) Parameters ---------- R : float Radius at which to calculate the moment (can be Quantity) phi : float, optional Azimuth (rad unless deg=True; can be Quantity), by default 0.0 t : float or array, optional 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), by default 0.0 surfacemass : float, optional If set use this pre-calculated surfacemass and mean rotational velocity, by default None meanvT : float, optional If set use this pre-calculated surfacemass and mean rotational velocity, by default None nsigma : float, optional Number of sigma to integrate the velocities over (based on an estimate, so be generous), by default None deg : bool, optional Azimuth is in degree (default=False); do not set this when giving phi as a Quantity, by default False epsrel : float, optional Scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-02 epsabs : float, optional Scipy.integrate keywords (the integration calculates the ratio of this vmoment to that of the initial DF), by default 1.0e-05 grid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None gridpoints : int, optional Number of points to use for the grid in 1D (default=101), by default 101 returnGrids : bool, optional If True, return the grid objects (default=False), by default False derivRGrid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None derivphiGrid : bool or evolveddiskdfGrid or evolveddiskdfHierarchicalGrid, optional 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, by default None derivGridpoints : int, optional Number of points to use for the grid in 1D (default=101), by default 101 derivHierarchgrid : bool, optional If True, use a hierarchical grid (default=False), by default False hierarchgrid : bool, optional If True, use a hierarchical grid (default=False), by default False nlevels : int, optional Number of hierarchical levels for the hierarchical grid, by default 2 integrate_method : str, optional orbit.integrate method argument, by default "dopr54_c" Returns ------- float or tuple Oort K at R,phi,t or tuple of Oort A at R,phi,t and grid objects Notes ----- - 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.0 * 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.0 * 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.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, ) out.df[numpy.isnan(out.df)] = 0.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)): # pragma: no cover raise RuntimeError("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.0, 0.0], ro=1.0) * _DEGTORAD vlos = o.vlos(ro=1.0, vo=1.0, obs=[1.0, 0.0, 0.0, 0.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.0 / 2.0): 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.0 / 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.0, 0.0], ro=1.0) * _DEGTORAD vperp = o.vll(ro=1.0, vo=1.0, obs=[1.0, 0.0, 0.0, 0.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.0 + 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.0 / 2.0): 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.0 / 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): """ Plot the velocity distribution. Parameters ---------- tt : int, optional Time index. Default is 0. Returns ------- None Other Parameters ---------------- xrange : list Range of vRgrid. yrange : list Range of vTgrid. plotthis : array Array to be plotted. Notes ----- - 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, ): """ Initialize a hierarchical grid Parameters ---------- edf : evolveddiskdf instance R : float Radius. phi : float Azimuth. nsigma : int Number of sigma to integrate over. t : float Time. sigmaR1 : float Radial dispersion. sigmaT1 : float Rotational-velocity dispersion. meanvR : float Mean of radial velocity. meanvT : float Mean of rotational velocity. gridpoints : int Number of gridpoints. nlevels : int Number of levels to build. deriv : {None,'R','phi'} Calculates derivative of the moment wrt R or phi. upperdxdy : float, optional Area element of previous hierarchical level. Default is None. print_progress : bool, optional If True, print progress on building the grid. Default is False. nlevelsTotal : int, optional Number of levels to build. Default is nlevels. Notes ----- - 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.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) # 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 self.df[numpy.isnan(self.df)] = 0.0 # BOVY: 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): """ Plot the velocity distribution. Parameters ---------- tt : int, optional Time index. Default is 0. vmax : float, optional Maximum value for the plot. Default is None. aspect : float, optional Aspect ratio of the plot. Default is None. extent : list, optional Extent of the plot. Default is None. Returns ------- None Notes ----- - 2011-06-27 - Written - Bovy (NYU). """ if vmax is None: vmax = self.max(tt=tt) * 2.0 # 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.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.0, self.vRgrid[len(self.vRgrid) - 1] - dvR / 2.0, ] yrange = [ self.vTgrid[0] + dvT / 2.0, self.vTgrid[len(self.vTgrid) - 1] - dvT / 2.0, ] 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.0, vmax=vmax, ) else: plot.dens2d( plotthis.T, cmap="gist_yarg", origin="lower", aspect=aspect, extent=extent, interpolation="nearest", overplot=True, vmin=0.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=1741018131.0 galpy-1.10.2/galpy/df/isotropicHernquistdf.py0000644000175100001660000000712314761352023020627 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): """ Initialize an isotropic Hernquist distribution function. Parameters ---------- pot : HernquistPotential instance Hernquist potential instance. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 # first factor = mass to make the DF that of mass density self._fEnorm = ( self._psi0 * self._pot.a / numpy.sqrt(2.0) / (2 * numpy.pi) ** 3 / self._GMa**1.5 ) def fE(self, E): """ Calculate the energy portion of an isotropic Hernquist distribution function Parameters ---------- E : float or Quantity The energy. Returns ------- numpy.ndarray The value of the energy portion of the DF. Notes ----- - 2020-08-09 - Written - James Lane (UofT) """ Etilde = -numpy.atleast_1d(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.0 * ( (1.0 - 2.0 * Etilde) * (8.0 * Etilde**2.0 - 8.0 * Etilde - 3.0) + ( (3.0 * numpy.arcsin(sqrtEtilde)) / numpy.sqrt(Etilde * (1.0 - Etilde)) ) ) ) # Fix out of bounds values if len(Etilde_out) > 0: fE[Etilde_out] = 0 return fE.reshape(E.shape) def _dMdE(self, E): # E already in internal units here fE = self.fE(E) A = -self._psi0 / E[fE > 0.0] out = numpy.zeros_like(E) out[fE > 0.0] = ( (4.0 * numpy.pi) ** 2.0 * fE[fE > 0.0] * self._pot.a**3.0 * numpy.sqrt(-2.0 * E[fE > 0.0]) * ( numpy.sqrt(A - 1.0) * (0.125 * A**2.0 - 5.0 / 12.0 * A - 1.0 / 3.0) + 0.125 * A * (A**2.0 - 4.0 * A + 8.0) * numpy.arccos(A**-0.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.a * numpy.sqrt(ms) / (1 - numpy.sqrt(ms)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/isotropicNFWdf.py0000644000175100001660000000701014761352023017272 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): """ Initialize an isotropic NFW distribution function Parameters ---------- pot : NFWPotential instance NFW Potential instance widrow : bool, optional If True, use the approximate form from Widrow (2000), otherwise use improved fit that has <~1e-5 relative density errors rmax : float or Quantity, optional Maximum radius to consider; set to numpy.inf to evaluate NFW w/o cut-off ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * 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): """ Calculate the energy portion of an isotropic NFW distribution function Parameters ---------- E : float or Quantity The energy. Returns ------- ndarray The value of the energy portion of the DF. Notes ----- - 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.0) if self._widrow: out[indx] = ( self._fEnorm * Etilde[indx] ** 1.5 * (1 - Etilde[indx]) ** -2.5 * (-numpy.log(Etilde[indx]) / (1.0 - Etilde[indx])) ** -2.7419 * numpy.exp( 0.3620 * Etilde[indx] - 0.5639 * Etilde[indx] ** 2.0 - 0.0859 * Etilde[indx] ** 3.0 - 0.4912 * Etilde[indx] ** 4.0 ) ) else: out[indx] = ( self._fEnorm * Etilde[indx] ** 1.5 * (1 - Etilde[indx]) ** -2.5 * (-numpy.log(Etilde[indx]) / (1.0 - Etilde[indx])) ** -2.75 * numpy.polyval(_COEFFS, Etilde[indx]) ) return out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/isotropicPlummerdf.py0000644000175100001660000000461614761352023020272 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): """ Initialize an isotropic Plummer distribution function Parameters ---------- pot : Potential object Plummer Potential instance ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * numpy.sqrt(2.0) / 7.0 / numpy.pi**3.0 * pot._b**2.0 / pot._amp**4.0 ) def fE(self, E): """ Calculate the energy portion of an isotropic Plummer distribution function. Parameters ---------- E : float or Quantity The energy. Returns ------- ndarray The value of the energy portion of the DF. Notes ----- - 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.0 / 3.0) - 1.0) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/jeans.py0000644000175100001660000001113114761352023015471 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.0 / numpy.sqrt(2.0) @potential_physical_input @physical_conversion("velocity", pop=True) def sigmar(Pot, r, dens=None, beta=0.0): """ Compute the radial velocity dispersion using the spherical Jeans equation Parameters ---------- Pot : potential or list of potentials Gravitational potential; evaluated at R=r/sqrt(2),z=r/sqrt(2), sphericity not checked. r : float or Quantity Galactocentric radius dens : function, optional tracer density profile (function of r); if None, the density is assumed to be that corresponding to the potential beta : float or function, optional anisotropy; can be a constant or a function of r Returns ------- float sigma_r(r) Notes ----- - 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.0, use_physical=False, ) if callable(beta): intFactor = lambda x: numpy.exp( 2.0 * integrate.quad(lambda y: beta(y) / y, 1.0, x)[0] ) else: # assume to be number intFactor = lambda x: x ** (2.0 * beta) return numpy.sqrt( integrate.quad( lambda x: -intFactor(x) * dens(x) * evaluaterforces( Pot, x * _INVSQRTTWO, x * _INVSQRTTWO, phi=numpy.pi / 4.0, 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.0, sigma_r=None): """ Compute the line-of-sight velocity dispersion using the spherical Jeans equation Parameters ---------- Pot : potential or list of potentials Gravitational potential; evaluated at R=r/sqrt(2),z=r/sqrt(2), sphericity not checked. R : float or Quantity Galactocentric projected radius dens : function, optional tracer density profile (function of r); if None, the density is assumed to be that corresponding to the potential surfdens : float or function, optional 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 : float or function, optional anisotropy; can be a constant or a function of r sigma_r : float or function, optional if given, the solution of the spherical Jeans equation sigma_r(r) (used instead of solving the Jeans equation as part of this routine) Returns ------- float sigma_los(R) Notes ----- - 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.0 * integrate.quad( lambda x: dens(numpy.sqrt(R**2.0 + x**2.0)), 0.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, use_physical=False ) elif not callable(sigma_r): call_sigma_r = lambda x: sigma_r else: call_sigma_r = sigma_r return numpy.sqrt( 2.0 * integrate.quad( lambda x: (1.0 - call_beta(x) * R**2.0 / x**2.0) * x * dens(x) * call_sigma_r(x) ** 2.0 / numpy.sqrt(x**2.0 - R**2.0), R, numpy.inf, )[0] / called_surfdens ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/kingdf.py0000644000175100001660000001703314761352023015642 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.0 * numpy.pi _TWOOVERSQRTPI = 2.0 / 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.0, rt=1.0, npt=1001, ro=None, vo=None): """ Initialize a King DF Parameters ---------- W0 : float Dimensionless central potential :math:`W_0 = \\Psi(0)/\\sigma^2` (in practice, needs to be :math:`\\lesssim 200`, where the DF is essentially isothermal). M : float or Quantity, optional Total mass. rt : float or Quantity, optional Tidal radius. npt : int, optional Number of points to use to solve for :math:`\\Psi(r)`. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 # 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.0 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.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=1, ) # Setup velocity DF interpolator for velocity sampling here self._rmin_sampling = 0.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) > 0: out[varE > 0.0] = ( (numpy.exp(varE[varE > 0.0] / self._sigma2) - 1.0) * (2.0 * numpy.pi * self._sigma2) ** -1.5 * self.rho1 ) return out.reshape(E.shape) # 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.0 / 4.0 / 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.0: rbreak = self.r0 / 100.0 else: rbreak = self.r0 # Using linspace focuses on what happens ~rbreak rather than on < 0.0 else 0.0), ], [0.0, rbreak], [self.W0, 0.0], method="DOP853", 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.0, npt - npt // 2 + 1) sol = integrate.solve_ivp( lambda t, y: [ 1.0 / y[1], -1.0 / y[1] * (_FOURPI * self._dens_W(t) + 2.0 * y[1] / y[0]), ], [sol.y[0, -1], 0.0], [rbreak, sol.y[1, -1]], method="DOP853", 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, adjust small decreases to zero self._cumul_mass = -self._dWdr * self._r**2.0 for ii in range(1, npt): if self._cumul_mass[ii] < self._cumul_mass[ii - 1]: self._cumul_mass[ii] = ( self._cumul_mass[ii - 1] + 2.0 * numpy.finfo(float).eps ) 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.0 + 2.0 / 3.0 * W ) def dens(self, r): return self._dens_W(self._W_from_r(r)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/osipkovmerrittHernquistdf.py0000644000175100001660000000702714761352023021720 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 anisotropy radius. """ def __init__(self, pot=None, ra=1.4, ro=None, vo=None): """ Initialize a Hernquist DF with Osipkov-Merritt anisotropy Parameters ---------- pot : potential.HernquistPotential Hernquist potential which determines the DF ra : float or Quantity, optional Anisotropy radius ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 self._a2overra2 = self._pot.a**2.0 / 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.0) / (2 * numpy.pi) ** 3 / self._GMa**1.5 ) def fQ(self, Q): """ Calculate the f(Q) portion of an Osipkov-Merritt Hernquist distribution function Parameters ---------- Q : float or numpy.ndarray The Osipkov-Merritt 'energy' E-L^2/[2ra^2] (can be Quantity) Returns ------- float or numpy.ndarray The value of the f(Q) portion of the DF Notes ----- - 2020-11-12 - Written - Bovy (UofT) """ Qtilde = numpy.atleast_1d(conversion.parse_energy(Q, vo=self._vo) / self._psi0) # Handle potential Q outside of bounds Qtilde_out = numpy.where(numpy.logical_or(Qtilde < 0.0, Qtilde > 1.0))[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.0 - Qtilde) ** 2.0 * ( (1.0 - 2.0 * Qtilde) * (8.0 * Qtilde**2.0 - 8.0 * Qtilde - 3.0) + ( (3.0 * numpy.arcsin(sqrtQtilde)) / numpy.sqrt(Qtilde * (1.0 - Qtilde)) ) ) ) # The other part fQ += 8.0 * self._a2overra2 * sqrtQtilde * (1.0 - 2.0 * Qtilde) if len(Qtilde_out) > 0: fQ[Qtilde_out] = 0.0 return self._fQnorm * fQ.reshape(Q.shape) 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=1741018131.0 galpy-1.10.2/galpy/df/osipkovmerrittNFWdf.py0000644000175100001660000000625214761352023020367 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 anisotropy radius. """ def __init__(self, pot=None, ra=1.4, rmax=1e4, ro=None, vo=None): """ Initialize a NFW DF with Osipkov-Merritt anisotropy Parameters ---------- pot : potential.NFWPotential NFW potential which determines the DF ra : float or Quantity, optional Anisotropy radius rmax : float or Quantity, optional Maximum radius to consider (set to numpy.inf to evaluate NFW w/o cut-off) ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 / self._ra2 self._fQnorm = self._a2overra2 / (4.0 * 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): """ Calculate the f(Q) portion of an Osipkov-Merritt NFW distribution function Parameters ---------- Q : float or Quantity The Osipkov-Merritt 'energy' E-L^2/[2ra^2] Returns ------- ndarray The value of the f(Q) portion of the DF Notes ----- - 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.0) # 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.0 / 3.0) * (numpy.log(Qtilde[indx]) / (1 - Qtilde[indx])) ** 2.0 ) ) return out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/osipkovmerrittdf.py0000644000175100001660000002675114761352023020022 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 ): """ Initialize a DF with Osipkov-Merritt anisotropy. Parameters ---------- pot : Potential instance or list thereof, optional Default: None denspot : Potential instance or list thereof that represent the density of the tracers (assumed to be spherical; if None, set equal to pot), optional Default: None ra : float or Quantity, optional Anisotropy radius. Default: 1.4 rmax : float or Quantity, optional Maximum radius to consider; DF is cut off at E = Phi(rmax). Default: None scale : float or Quantity, optional Characteristic scale radius to aid sampling calculations. Not necessary, and will also be overridden by value from pot if available. Default: None ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 def _call_internal(self, *args): """ Evaluate the DF for an Osipkov-Merritt-anisotropy DF Parameters ---------- E : float The energy L : float The angular momentum Returns ------- float The value of the DF Notes ----- - 2020-11-12 - Written - Bovy (UofT) """ E, L, _ = args return self.fQ(-E - 0.5 * L**2.0 / self._ra2) def _dMdE(self, E): if not hasattr(self, "_rphi"): self._rphi = self._setup_rphi_interpolator() def Lintegrand(t, L2lim, E): return self((E, numpy.sqrt(L2lim - t**2.0)), use_physical=False) # Integrate where Q > 0 out = ( 16.0 * numpy.pi**2.0 * numpy.array( [ integrate.quad( lambda r: r * integrate.quad( Lintegrand, numpy.sqrt( numpy.amax( [ (0.0), ( 2.0 * r**2.0 * ( tE - _evaluatePotentials(self._pot, r, 0.0) ) + 2.0 * tE * self._ra2 ), ] ) ), numpy.sqrt( 2.0 * r**2.0 * (tE - _evaluatePotentials(self._pot, r, 0.0)) ), args=( 2.0 * r**2.0 * (tE - _evaluatePotentials(self._pot, r, 0.0)), tE, ), )[0], 0.0, self._rphi(tE), )[0] for ii, tE in enumerate(E) ] ) ) # Numerical issues can make the integrand's sqrt argument negative, only # happens at dMdE ~ 0, so just set to zero out[numpy.isnan(out)] = 0.0 return out.reshape(E.shape) 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.0 / self._ra2) / numpy.sqrt(r**2.0 / self._ra2 * c**2.0 + 1) ) x *= numpy.random.choice([1.0, -1.0], 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.0 ) ) * v**2.0 ) else: return ( self.fQ(-_evaluatePotentials(self._pot, r, 0) - 0.5 * v**2.0) * v**2.0 ) 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.0 + r**2.0 / self._ra2 * numpy.sin(eta) ** 2.0) def _vmomentdensity(self, r, n, m): if m % 2 == 1 or n % 2 == 1: return 0.0 return ( 2.0 * numpy.pi * integrate.quad( lambda v: v ** (2.0 + m + n) * self.fQ(-_evaluatePotentials(self._pot, r, 0) - 0.5 * v**2.0), 0.0, self._vmax_at_r(self._pot, r), )[0] * special.gamma(m / 2.0 + 1.0) * special.gamma((n + 1) / 2.0) / special.gamma(0.5 * (m + n + 3.0)) / (1 + r**2.0 / 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 anisotropy 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 ): """ Initialize a DF with Osipkov-Merritt anisotropy. Parameters ---------- pot : Potential instance or list thereof, optional Default: None denspot : Potential instance or list thereof that represent the density of the tracers (assumed to be spherical; if None, set equal to pot), optional Default: None ra : float or Quantity, optional Anisotropy radius. Default: 1.4 rmax : float or Quantity, optional Maximum radius to consider; DF is cut off at E = Phi(rmax). Default: None scale : float or Quantity, optional Characteristic scale radius to aid sampling calculations. Not necessary, and will also be overridden by value from pot if available. Default: None ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 + r**2.0 / self._ra2) + 2.0 * 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.0 + r**2.0 / self._ra2) + 2.0 * evaluateDensities(self._denspot, r, 0, use_physical=False) * r / self._ra2 ) ) self._edf._d2nudr2 = ( ( lambda r: self._denspot._d2densdr2(r) * (1.0 + r**2.0 / self._ra2) + 4.0 * self._denspot._ddensdr(r) * r / self._ra2 + 2.0 * 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.0 + r**2.0 / self._ra2) + 4.0 * numpy.sum([p._ddensdr(r) for p in self._denspot]) * r / self._ra2 + 2.0 * 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.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.0 - 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): """ Calculate the f(Q) portion of an Osipkov-Merritt Hernquist distribution function Parameters ---------- Q : float The Osipkov-Merritt 'energy' E-L^2/[2ra^2] Returns ------- float The value of the f(Q) portion of the DF Notes ----- - 2021-02-07 - Written - Bovy (UofT) """ return self._edf.fE(-Q) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/quasiisothermaldf.py0000644000175100001660000032016114761352023020123 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.0, lo=10.0 / 220.0 / 8.0, ro=None, vo=None, ): """ Initialize a quasi-isothermal DF Parameters ---------- hr : float or Quantity Radial scale length. sr : float or Quantity Radial velocity dispersion at the solar radius. sz : float or Quantity Vertical velocity dispersion at the solar radius. hsr : float or Quantity Radial-velocity-dispersion scale length. hsz : float or Quantity Vertial-velocity-dispersion scale length. pot : Potential or list thereof Potential or list of potentials that represents the underlying potential. aA : actionAngle instance 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 : bool, optional If True, set counter-rotating stars' DF to zero. refr : float or Quantity, optional Reference radius for dispersions (can be different from ro). lo : float or Quantity, optional Reference angular momentum below where there are significant numbers of retrograde stars. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). _precomputerg : bool, optional If True (default), pre-compute the rL(L). _precomputergrmax : float or Quantity, optional If set, this is the maximum R for which to pre-compute rg (default: 5*hr). _precomputergnLz : int, optional If set, number of Lz to pre-compute rg for (default: 51). Notes ----- - 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.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): """ Evaluate the DF Parameters ---------- args: tuple or Orbit 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: bool, optional If True, return the natural log. func: function of (jr,lz,jz), optional Function of the actions to multiply the DF with (useful for moments). _return_actions: bool, optional If True, return the actions as well. _return_freqs: bool, optional If True, return the frequencies as well. _return_rgr: bool, optional If True, return the rg as well. kwargs: dict, optional scipy.integrate.quadrature kwargs. Returns ------- float Value of DF. Notes ----- - 2012-07-25 - Written - Bovy (IAS@MPIA) """ # 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.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.0: if log: return -numpy.finfo(numpy.dtype(numpy.float64)).max else: return 0.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.0 else: funcFactor = 1.0 if log: lnfsr = ( numpy.log(Omega) + lnsurfmass - 2.0 * lnsr - numpy.log(numpy.pi) - numpy.log(kappa) + numpy.log(1.0 + numpy.tanh(lz / self._lo)) - kappa * jr * numpy.exp(-2.0 * lnsr) ) lnfsz = ( numpy.log(nu) - numpy.log(2.0 * numpy.pi) - 2.0 * lnsz - nu * jz * numpy.exp(-2.0 * 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.0)] = -numpy.finfo(numpy.dtype(numpy.float64)).max elif numpy.isnan(out): # pragma: no cover out = -numpy.finfo(numpy.dtype(numpy.float64)).max else: srm2 = numpy.exp(-2.0 * lnsr) fsr = ( Omega * numpy.exp(lnsurfmass) * srm2 / numpy.pi / kappa * (1.0 + numpy.tanh(lz / self._lo)) * numpy.exp(-kappa * jr * srm2) ) szm2 = numpy.exp(-2.0 * lnsz) fsz = nu / 2.0 / numpy.pi * szm2 * numpy.exp(-nu * jz * szm2) out = fsr * fsz * funcFactor if isinstance(lz, numpy.ndarray): out[numpy.isnan(out)] = 0.0 if self._cutcounter: out[(lz < 0.0)] = 0.0 elif numpy.isnan(out): # pragma: no cover out = 0.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.0, dR=10.0**-8.0, **kwargs): """ Estimate the exponential scale length at R. Parameters ---------- R : float or Quantity Galactocentric radius. z : float or Quantity, optional Height (default: 0 pc). dR : float or Quantity, optional Range in R to use. **kwargs Density kwargs. Returns ------- float or Quantity Estimated hR. Notes ----- - 2012-09-11 - Written - Bovy (IAS) - 2013-01-28 - Re-written - Bovy """ Rs = [R - dR / 2.0, R + dR / 2.0] 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.0**-8.0, **kwargs): """ Estimate the exponential scale height at R. Parameters ---------- R : float or Quantity Galactocentric radius. z : float or Quantity Height above the Galactic plane. dz : float or Quantity, optional z range to use. **kwargs density kwargs. Returns ------- float or Quantity Estimated hz. Notes ----- - 2012-08-30 - Written - Bovy (IAS) - 2013-01-28 - Re-written - Bovy """ if z == 0.0: zs = [z, z + dz] else: zs = [z - dz / 2.0, z + dz / 2.0] 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.0, dR=10.0**-8.0, **kwargs): """ Estimate the exponential scale length of the radial dispersion at R. Parameters ---------- R : float or Quantity Galactocentric radius. z : float or Quantity, optional Height (default: 0 pc). dR : float or Quantity, optional Range in R to use. **kwargs Density kwargs. Returns ------- float or Quantity Estimated hsR. Notes ----- - 2013-03-08 - Written - Bovy (IAS) """ Rs = [R - dR / 2.0, R + dR / 2.0] sf = numpy.array([self.sigmaR2(r, z, use_physical=False, **kwargs) for r in Rs]) lsf = numpy.log(sf) / 2.0 return -dR / (lsf[1] - lsf[0]) @potential_physical_input @physical_conversion("position", pop=True) def estimate_hsz(self, R, z=0.0, dR=10.0**-8.0, **kwargs): """ Estimate the exponential scale length of the vertical dispersion at R. Parameters ---------- R : float or Quantity Galactocentric radius. z : float or Quantity, optional Height (default: 0 pc). dR : float or Quantity, optional Range in R to use. **kwargs Density kwargs. Returns ------- float or Quantity Estimated hsz. Notes ----- - 2013-03-08 - Written - Bovy (IAS) """ Rs = [R - dR / 2.0, R + dR / 2.0] sf = numpy.array([self.sigmaz2(r, z, use_physical=False, **kwargs) for r in Rs]) lsf = numpy.log(sf) / 2.0 return -dR / (lsf[1] - lsf[0]) @potential_physical_input @physical_conversion("numbersurfacedensity", pop=True) def surfacemass_z( self, R, nz=7, zmax=1.0, fixed_quad=True, fixed_order=8, **kwargs ): """ Calculate the vertically-integrated surface density. Parameters ---------- R : float or Quantity Galactocentric radius. nz : int, optional Number of zs to use to estimate. Default is 7. zmax : float or Quantity, optional Maximum z to use. Default is 1.0. fixed_quad : bool, optional If True (default), use Gauss-Legendre integration. fixed_order : int, optional Order of GL integration to use. Default is 8. **kwargs : dict Density kwargs. Returns ------- float or Quantity Surface density at R. Notes ----- - 2012-08-30 - Written - Bovy (IAS) """ if fixed_quad: return ( 2.0 * integrate.fixed_quad( lambda x: self.density( R * numpy.ones(fixed_order), x, use_physical=False ), 0.0, 0.5, n=fixed_order, )[0] ) zs = numpy.linspace(0.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.0 * integrate.quad((lambda x: numpy.exp(lsfInterp(x))), 0.0, 1.0)[0] def vmomentdensity(self, *args, **kwargs): """ Calculate the an arbitrary moment of the velocity distribution at R times the density Parameters ---------- R : float radius at which to calculate the moment(/ro) z : float height at which to calculate the moment(/ro) n : int vR^n m : int vT^m o : int vz^o nsigma : int, optional number of sigma to integrate the vR and vz velocities over (when doing explicit numerical integral; default: 4) vTmax : float, optional upper limit for integration over vT (default: 1.5) mc : bool, optional if True, calculate using Monte Carlo integration nmc : int, optional if mc, use nmc samples gl : bool, optional use Gauss-Legendre _returngl : bool, optional if True, return the evaluated DF _return_actions : bool, optional if True, return the evaluated actions (does not work with _returngl currently) _return_freqs : bool, optional if True, return the evaluated frequencies and rg (does not work with _returngl currently) Returns ------- float at R,z (no support for units) Notes ----- - 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.0 or o % 2 == 1.0: return 0.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.0 / 2.0 / thisvc * ( gamma**2.0 - 1.0 # Assume close to flat rotation curve, sigphi2/sigR2 =~ 0.5 + R * (1.0 / self._hr + 2.0 / self._hsr) ) ) if numpy.fabs(va) > sigmaR1: va = 0.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.0 * (glx + 1.0) vzgl = nsigma * sigmaz1 / 2.0 * (glx + 1.0) vRglw = glw vzglw = glw else: vRgl = nsigma * sigmaR1 / 2.0 * (glx12 + 1.0) # vRgl= 1.5/2.*(glx12+1.) vRgl = list(vRgl) vRgl.extend(-nsigma * sigmaR1 / 2.0 * (glx12 + 1.0)) # vRgl.extend(-1.5/2.*(glx12+1.)) vRgl = numpy.array(vRgl) vzgl = nsigma * sigmaz1 / 2.0 * (glx12 + 1.0) # vzgl= 1.5/2.*(glx12+1.) vzgl = list(vzgl) vzgl.extend(-nsigma * sigmaz1 / 2.0 * (glx12 + 1.0)) # 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) vTmax = kwargs.get("vTmax", 1.5) vTgl = vTmax / 2.0 * (glx + 1.0) # 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.0 + n + m) * gamma ** (1.0 + m) * sigmaz1 ** (1.0 + o), vrs, vts - mvT, vzs, ) else: return ( numpy.mean(Is) * sigmaR1 ** (2.0 + n + m) * gamma ** (1.0 + m) * sigmaz1 ** (1.0 + o), vrs, vts, vzs, ) else: return ( numpy.mean(Is) * sigmaR1 ** (2.0 + n + m) * gamma ** (1.0 + m) * sigmaz1 ** (1.0 + 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.0 / gamma * (thisvc - va) / sigmaR1 - nsigma, 1.0 / gamma * (thisvc - va) / sigmaR1 + nsigma, lambda x: 0.0, lambda x: nsigma, lambda x, y: 0.0, lambda x, y: nsigma, (R, z, self, sigmaR1, gamma, sigmaz1, n, m, o), **kwargs, )[0] * sigmaR1 ** (2.0 + n + m) * gamma ** (1.0 + m) * sigmaz1 ** (1.0 + o) ) def jmomentdensity(self, *args, **kwargs): """ Calculate the an arbitrary moment of an action of the velocity distribution at R times the surfacmass. Parameters ---------- R : float radius at which to calculate the moment(/ro) z : float height at which to calculate the moment(/ro) n : int jr^n m : int lz^m o : int jz^o nsigma : int, optional Number of sigma to integrate the velocities over (when doing explicit numerical integral). Default is None. mc : bool, optional If True, calculate using Monte Carlo integration. Default is False. nmc : int, optional If mc is True, use nmc samples. Default is None. Returns ------- float or Quantity at R (no support for units) Notes ----- - 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.0 / 2.0 / thisvc * ( gamma**2.0 - 1.0 # Assume close to flat rotation curve, sigphi2/sigR2 =~ 0.5 + R * (1.0 / self._hr + 2.0 / self._hsr) ) ) if numpy.fabs(va) > sigmaR1: va = 0.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.0 * gamma * sigmaz1, vrs, vts, vzs, ) else: return numpy.mean(Is) * sigmaR1**2.0 * 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.0 / gamma * (thisvc - va) / sigmaR1 - nsigma, 1.0 / gamma * (thisvc - va) / sigmaR1 + nsigma, lambda x: 0.0, lambda x: nsigma, lambda x, y: 0.0, lambda x, y: nsigma, (R, z, self, sigmaR1, gamma, sigmaz1, n, m, o), **kwargs, )[0] * sigmaR1**2.0 * 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 ): """ Calculate the density at R,z by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate the density. z : float or Quantity Height at which to calculate the density. nsigma : float, optional Number of sigma to integrate the velocities over. mc : bool, optional If True, calculate using Monte Carlo integration. nmc : int, optional If mc, use nmc samples. gl : bool, optional If True, calculate using Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. **kwargs : dict, optional scipy.integrate.tplquad kwargs epsabs and epsrel. Returns ------- float Density at (R,z). Notes ----- - 2012-07-26 - Written - Bovy (IAS@MPIA) """ return self._vmomentdensity( R, z, 0.0, 0.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 ): """ Calculate sigma_R^2 by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate this. z : float or Quantity Height at which to calculate this. nsigma : int, optional Number of sigma to integrate the velocities over. mc : bool, optional If True, calculate using Monte Carlo integration. nmc : int, optional If mc, use nmc samples. gl : bool, optional If True, calculate using Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. **kwargs : dict, optional scipy.integrate.tplquad kwargs epsabs and epsrel. Returns ------- float sigma_R^2. Notes ----- - 2012-07-30 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=True, **kwargs, ) return ( self._vmomentdensity( R, z, 2.0, 0.0, 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.0, 0.0, gl=gl, ngl=ngl, _returngl=True, **kwargs ) return ( self._vmomentdensity( R, z, 2.0, 0.0, 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.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) / self._vmomentdensity( R, z, 0.0, 0.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 ): """ Calculate sigma_RZ^2 by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate this. z : float or Quantity Height at which to calculate this. nsigma : int, optional Number of sigma to integrate the velocities over. mc : bool, optional If True, calculate using Monte Carlo integration. nmc : int, optional If mc, use nmc samples. gl : bool, optional If True, calculate using Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. **kwargs scipy.integrate.tplquad kwargs epsabs and epsrel. Returns ------- float sigma_Rz^2. Notes ----- - 2012-07-30 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=True, **kwargs, ) return ( self._vmomentdensity( R, z, 1.0, 0.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.0, 0.0, gl=gl, ngl=ngl, _returngl=True, **kwargs ) return ( self._vmomentdensity( R, z, 1.0, 0.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, 1.0, 0.0, 1.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) / self._vmomentdensity( R, z, 0.0, 0.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 ): """ Calculate the tilt of the velocity ellipsoid by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate this. z : float or Quantity Height at which to calculate this. nsigma : int, optional Number of sigma to integrate the velocities over. mc : bool, optional If True, calculate using Monte Carlo integration. nmc : int, optional If mc, use nmc samples. gl : bool, optional If True, calculate using Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. Returns ------- float Tilt in radians. Notes ----- - 2012-12-23 - Written - Bovy (IAS) - 2017-10-28 - Changed return unit to rad - Bovy (UofT) """ if mc: surfmass, vrs, vts, vzs = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=True, **kwargs, ) tsigmar2 = ( self._vmomentdensity( R, z, 2.0, 0.0, 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, 0.0, 2.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=False, _vrs=vrs, _vts=vts, _vzs=vzs, **kwargs, ) / surfmass ) tsigmarz = ( self._vmomentdensity( R, z, 1.0, 0.0, 1.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=False, _vrs=vrs, _vts=vts, _vzs=vzs, **kwargs, ) / surfmass ) return 0.5 * numpy.arctan(2.0 * tsigmarz / (tsigmar2 - tsigmaz2)) elif gl: surfmass, glqeval = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, gl=gl, ngl=ngl, _returngl=True, **kwargs ) tsigmar2 = ( self._vmomentdensity( R, z, 2.0, 0.0, 0.0, ngl=ngl, gl=gl, _glqeval=glqeval, **kwargs ) / surfmass ) tsigmaz2 = ( self._vmomentdensity( R, z, 0.0, 0.0, 2.0, ngl=ngl, gl=gl, _glqeval=glqeval, **kwargs ) / surfmass ) tsigmarz = ( self._vmomentdensity( R, z, 1.0, 0.0, 1.0, ngl=ngl, gl=gl, _glqeval=glqeval, **kwargs ) / surfmass ) return 0.5 * numpy.arctan(2.0 * 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 ): """ Calculate sigma_z^2 by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate this. z : float or Quantity Height at which to calculate this. nsigma : int, optional Number of sigma to integrate the velocities over. mc : bool, optional If True, calculate using Monte Carlo integration. nmc : int, optional If mc, use nmc samples. gl : bool, optional If True, calculate using Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. **kwargs : dict, optional scipy.integrate.tplquad kwargs epsabs and epsrel. Returns ------- float sigma_z^2. Notes ----- - 2012-07-30 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=True, **kwargs, ) return ( self._vmomentdensity( R, z, 0.0, 0.0, 2.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.0, 0.0, gl=gl, ngl=ngl, _returngl=True, **kwargs ) return ( self._vmomentdensity( R, z, 0.0, 0.0, 2.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.0, 0.0, 2.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) / self._vmomentdensity( R, z, 0.0, 0.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 ): """ Calculate the mean rotational velocity by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate this. z : float or Quantity Height at which to calculate this. nsigma : float, optional Number of sigma to integrate the velocities over. mc : bool, optional If True, calculate using Monte Carlo integration. nmc : int, optional If mc, use nmc samples. gl : bool, optional If True, calculate using Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. **kwargs : dict, optional scipy.integrate.tplquad kwargs epsabs and epsrel. Returns ------- float Mean rotational velocity. Notes ----- - 2012-07-30 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=True, **kwargs, ) return ( self._vmomentdensity( R, z, 0.0, 1.0, 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.0, 0.0, gl=gl, ngl=ngl, _returngl=True, **kwargs ) return ( self._vmomentdensity( R, z, 0.0, 1.0, 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, 0.0, 1.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) / self._vmomentdensity( R, z, 0.0, 0.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 ): """ Calculate the mean radial velocity by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate this. z : float or Quantity Height at which to calculate this. nsigma : float, optional Number of sigma to integrate the velocities over. mc : bool, optional If True, calculate using Monte Carlo integration. nmc : int, optional If mc, use nmc samples. gl : bool, optional If True, calculate using Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. **kwargs : dict, optional scipy.integrate.tplquad kwargs epsabs and epsrel. Returns ------- float Mean radial velocity. Notes ----- - 2012-12-23 - Written - Bovy (IAS) """ if mc: surfmass, vrs, vts, vzs = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=True, **kwargs, ) return ( self._vmomentdensity( R, z, 1.0, 0.0, 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.0, 0.0, gl=gl, ngl=ngl, _returngl=True, **kwargs ) return ( self._vmomentdensity( R, z, 1.0, 0.0, 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.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) / self._vmomentdensity( R, z, 0.0, 0.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 ): """ Calculate the mean vertical velocity by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate this. z : float or Quantity Height at which to calculate this. nsigma : float, optional Number of sigma to integrate the velocities over. mc : bool, optional If True, calculate using Monte Carlo integration. nmc : int, optional If mc, use nmc samples. gl : bool, optional If True, calculate using Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. **kwargs : dict, optional scipy.integrate.tplquad kwargs epsabs and epsrel. Returns ------- float Mean vertical velocity Notes ----- - 2012-12-23 - Written - Bovy (IAS) """ if mc: surfmass, vrs, vts, vzs = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=True, **kwargs, ) return ( self._vmomentdensity( R, z, 0.0, 0.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.0, 0.0, gl=gl, ngl=ngl, _returngl=True, **kwargs ) return ( self._vmomentdensity( R, z, 0.0, 0.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.0, 0.0, 1.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) / self._vmomentdensity( R, z, 0.0, 0.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 ): """ Calculate sigma_T^2 by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate this. z : float or Quantity Height at which to calculate this. nsigma : int, optional Number of sigma to integrate the velocities over. mc : bool, optional If True, calculate using Monte Carlo integration. nmc : int, optional If mc is True, use nmc samples. gl : bool, optional If True, calculate using Gauss-Legendre integration. ngl : int, optional If gl is True, use ngl-th order Gauss-Legendre integration for each dimension. **kwargs scipy.integrate.tplquad kwargs epsabs and epsrel. Returns ------- float sigma_T^2. Notes ----- - 2012-07-30 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=True, **kwargs, ) mvt = ( self._vmomentdensity( R, z, 0.0, 1.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=False, _vrs=vrs, _vts=vts, _vzs=vzs, **kwargs, ) / surfmass ) return ( self._vmomentdensity( R, z, 0.0, 2.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=False, _vrs=vrs, _vts=vts, _vzs=vzs, **kwargs, ) / surfmass - mvt**2.0 ) elif gl: surfmass, glqeval = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, gl=gl, ngl=ngl, _returngl=True, **kwargs ) mvt = ( self._vmomentdensity( R, z, 0.0, 1.0, 0.0, ngl=ngl, gl=gl, _glqeval=glqeval, **kwargs ) / surfmass ) return ( self._vmomentdensity( R, z, 0.0, 2.0, 0.0, ngl=ngl, gl=gl, _glqeval=glqeval, **kwargs ) / surfmass - mvt**2.0 ) else: # pragma: no cover because this is too slow; a warning is shown surfmass = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) return ( self._vmomentdensity( R, z, 0.0, 2.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) / surfmass - ( self._vmomentdensity( R, z, 0.0, 2.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) / surfmass ) ** 2.0 ) @potential_physical_input @physical_conversion("action", pop=True) def meanjr(self, R, z, nsigma=None, mc=True, nmc=10000, **kwargs): """ Calculate the mean radial action by marginalizing over velocity Parameters ---------- R : float or Quantity Radius at which to calculate this z : float or Quantity Height at which to calculate this nsigma : float, optional Number of sigma to integrate the velocities over mc : bool, optional If True, calculate using Monte Carlo integration nmc : int, optional If mc, use nmc samples **kwargs : dict scipy.integrate.tplquad kwargs epsabs and epsrel Returns ------- float Mean jr Notes ----- - 2012-08-09 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=True, **kwargs, ) return ( self._jmomentdensity( R, z, 1.0, 0.0, 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.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) / self._vmomentdensity( R, z, 0.0, 0.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): """ Calculate the mean angular momentum by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate this. z : float or Quantity Height at which to calculate this. nsigma : float, optional Number of sigma to integrate the velocities over. mc : bool, optional If True, calculate using Monte Carlo integration. nmc : int, optional If mc, use nmc samples. **kwargs scipy.integrate.tplquad kwargs epsabs and epsrel. Returns ------- float Mean angular momentum. Notes ----- - 2012-08-09 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=True, **kwargs, ) return ( self._jmomentdensity( R, z, 0.0, 1.0, 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, 0.0, 1.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) / self._vmomentdensity( R, z, 0.0, 0.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): """ Calculate the mean vertical action by marginalizing over velocity. Parameters ---------- R : float or Quantity Radius at which to calculate this. z : float or Quantity Height at which to calculate this. nsigma : float, optional Number of sigma to integrate the velocities over. mc : bool, optional If True, calculate using Monte Carlo integration. nmc : int, optional If mc, use nmc samples. **kwargs : dict scipy.integrate.tplquad kwargs epsabs and epsrel. Returns ------- float Mean jz. Notes ----- - 2012-08-09 - Written - Bovy (IAS@MPIA) """ if mc: surfmass, vrs, vts, vzs = self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, _returnmc=True, **kwargs, ) return ( self._jmomentdensity( R, z, 0.0, 0.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.0, 0.0, 1.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) / self._vmomentdensity( R, z, 0.0, 0.0, 0.0, nsigma=nsigma, mc=mc, nmc=nmc, **kwargs ) @potential_physical_input def sampleV(self, R, z, n=1, **kwargs): """ Sample a radial, azimuthal, and vertical velocity at R,z Parameters ---------- R : float or Quantity Galactocentric distance. z : float or Quantity Height. n : int, optional Number of distances to sample. Returns ------- list List of samples. Notes ----- - 2012-12-17 - Written - Bovy (IAS@MPIA) """ 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.0 maxVz = 0.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.0, x, z, 0.0, log=True, use_physical=False)), 1.0 ) ) 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.0 * self._sr propvT = numpy.random.normal(size=nmore) * 2.0 * self._sr + maxVT propvz = numpy.random.normal(size=nmore) * 2.0 * 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.0 / 4.0 / self._sr**2.0 + propvz**2.0 / 4.0 / self._sz**2.0 + (propvT - maxVT) ** 2.0 / 4.0 / self._sr**2.0 ) 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, ): """ Sample radial, azimuthal, and vertical velocity at R,z using interpolation. Parameters ---------- R : numpy.ndarray or Quantity Galactocentric distance. z : numpy.ndarray or Quantity Height. R_pixel : float The pixel size for creating the grid for interpolation (in natural units). z_pixel : float The pixel size for creating the grid for interpolation (in natural units). num_std : float, optional Number of standard deviation to be considered outliers sampled separately from interpolation. R_min : float, optional Minimum R value for the grid. R_max : float, optional Maximum R value for the grid. z_max : float, optional Maximum z value for the grid. Returns ------- numpy.ndarray A numpy array containing the sampled velocity, (vR, vT, vz), where each row corresponds to the row of (R,z). Notes ----- - 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.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.0, x, z, 0.0, log=True, use_physical=False ) ), 1.0, ) ) # 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): """ Sample a radial, azimuthal, and vertical velocity at R,z. Parameters ---------- R : float or numpy.ndarray Galactocentric distance. z : float or numpy.ndarray Height. maxVT : numpy.ndarray An array of pre-optimized maximum vT at corresponding R,z. Returns ------- numpy.ndarray A numpy array containing the sampled velocity, (vR, vT, vz), where each row correspond to the row of (R,z). Notes ----- - 2018-08-10 - 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.0 * self._sr propvT = ( numpy.random.normal(size=nmore) * 2.0 * self._sr + maxVT[remain_indx] ) propvz = numpy.random.normal(size=nmore) * 2.0 * 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.0 / 4.0 / self._sr**2.0 + propvz**2.0 / 4.0 / self._sz**2.0 + (propvT - maxVT[remain_indx]) ** 2.0 / 4.0 / self._sr**2.0 ) 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.0, vTmax=1.5): """ Calculate the marginalized vR probability at this location (NOT normalized by the density). Parameters ---------- vR : float or Quantity Radial velocity. R : float or Quantity Radius. z : float or Quantity Height. gl : bool, optional If True, use Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. nsigma : float, optional Number of sigma to integrate the velocities over. vTmax : float, optional Sets integration limits to [0,vTmax] for integration over vT. Returns ------- float p(vR,R,z). Notes ----- - 2012-12-22 - Written - Bovy (IAS@MPIA) """ 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.0 * (glx + 1.0) vzglw = glw vzfac = nsigma * sigmaz1 # 2 x integration over [0,nsigma*sigmaz1] else: vzgl = nsigma * sigmaz1 / 2.0 * (glx12 + 1.0) vzgl = list(vzgl) vzgl.extend(-nsigma * sigmaz1 / 2.0 * (glx12 + 1.0)) 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.0 * (glx + 1.0) 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.0): """ Calculate the marginalized vT probability at this location (NOT normalized by the density). Parameters ---------- vT : float or Quantity Azimuthal velocity. R : float or Quantity Radius. z : float or Quantity Height. gl : bool, optional If True, use Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. nsigma : float, optional Number of sigma to integrate the velocities over. Returns ------- float p(vT,R,z). Notes ----- - 2012-12-22 - Written - Bovy (IAS@MPIA) - 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.0 * (glx + 1.0) vzgl = nsigma * sigmaz1 / 2.0 * (glx + 1.0) 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.0 * (glx12 + 1.0) vRgl = list(vRgl) vRgl.extend(-nsigma * sigmaR1 / 2.0 * (glx12 + 1.0)) vRgl = numpy.array(vRgl) vzgl = nsigma * sigmaz1 / 2.0 * (glx12 + 1.0) vzgl = list(vzgl) vzgl.extend(-nsigma * sigmaz1 / 2.0 * (glx12 + 1.0)) 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.0, 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, ): """ Calculate the marginalized vz probability at this location (NOT normalized by the density). Parameters ---------- vz : float or Quantity Vertical velocity. R : float or Quantity Radius. z : float or Quantity Height. gl : bool, optional If True, use Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. nsigma : float, optional Number of sigma to integrate the velocities over. vTmax : float, optional Sets integration limits to [0,vTmax] for integration over vT. Returns ------- float p(vz,R,z). Notes ----- - 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.0 vRglw = glw vRfac = nsigma * sigmaR1 # 2 x integration over [0,nsigma*sigmaR1] else: vRgl = glx12 + 1.0 vRgl = list(vRgl) vRgl.extend(-(glx12 + 1.0)) 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.0 * (glx + 1.0) 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.0, (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.0): """ Calculate the marginalized (vR,vT) probability at this location (NOT normalized by the density). Parameters ---------- vR : float or Quantity Radial velocity. vT : float or Quantity Azimuthal velocity. R : float or Quantity Radius. z : float or Quantity Height. gl : bool, optional If True, use Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. nsigma : float, optional Number of sigma to integrate the velocities over. Returns ------- float p(vR,vT,R,z). Notes ----- - 2012-12-22 - 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.0 * (glx + 1.0) vzglw = glw vzfac = nsigma * sigmaz1 # 2 x integration over [0,nsigma*sigmaz1] else: vzgl = nsigma * sigmaz1 / 2.0 * (glx12 + 1.0) vzgl = list(vzgl) vzgl.extend(-nsigma * sigmaz1 / 2.0 * (glx12 + 1.0)) 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.0): """ Calculate the marginalized (vT,vz) probability at this location (NOT normalized by the density). Parameters ---------- vT : float or Quantity Azimuthal velocity. vz : float or Quantity Vertical velocity. R : float or Quantity Radius. z : float or Quantity Height. gl : bool, optional If True, use Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. nsigma : float, optional Number of sigma to integrate the velocities over. Returns ------- float or Quantity p(vT,vz,R,z). Notes ----- - 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.0 * (glx + 1.0) vRglw = glw vRfac = nsigma * sigmaR1 # 2 x integration over [0,nsigma*sigmaR1] else: vRgl = nsigma * sigmaR1 / 2.0 * (glx12 + 1.0) vRgl = list(vRgl) vRgl.extend(-nsigma * sigmaR1 / 2.0 * (glx12 + 1.0)) 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): """ Calculate the marginalized (vR,vz) probability at this location (NOT normalized by the density). Parameters ---------- vR : float or Quantity Radial velocity. vz : float or Quantity Vertical velocity. R : float or Quantity Radius. z : float or Quantity Height. gl : bool, optional If True, use Gauss-Legendre integration. ngl : int, optional If gl, use ngl-th order Gauss-Legendre integration for each dimension. vTmax : float, optional Sets integration limits to [0,vTmax] for integration over vT. Returns ------- float or Quantity p(vR,vz,R,z). Notes ----- - 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.0 * (glx + 1.0) 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): """ Calculate the epicycle frequency at r. Parameters ---------- r : float Radius. Returns ------- float Epicycle frequency. Notes ----- - 2012-07-25 - Written - Bovy (IAS@MPIA) """ return potential.epifreq(self._pot, r) def _calc_verticalfreq(self, r): """ Calculate the vertical frequency at r. Parameters ---------- r : float Radius. Returns ------- float Vertical frequency. Notes ----- - 2012-07-25 - Written - Bovy (IAS@MPIA) """ return potential.verticalfreq(self._pot, r) def _rg(self, lz): """ Calculate the radius of a circular orbit of Lz. Parameters ---------- lz : float Angular momentum. Returns ------- float Radius. Notes ----- - 2012-07-25 - Written - Bovy (IAS@MPIA) """ 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.0 / 2.0 + (vT - mvT) ** 2.0 / 2.0 + vz**2.0 / 2.0) ) 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.0 / 2.0 + (vT - mvT) ** 2.0 / 2.0 + vz**2.0 / 2.0) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/sphericaldf.py0000644000175100001660000007623614761352023016676 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, interpolate, special from ..orbit import Orbit from ..potential import interpSphericalPotential, 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): """ Initializes a spherical DF Parameters ---------- pot : Potential instance or list thereof The potential. Default is None. denspot : Potential instance or list thereof, optional The potential that represents the density of the tracers (assumed to be spherical). If None, set equal to pot. Default is None. rmax : float or Quantity, optional The maximum radius to consider. DF is cut off at E = Phi(rmax). Default is None. scale : float or Quantity, optional The length-scale parameter to be used internally. Default is None. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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 compatible) 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.0 ) # Check that interpolated potential has appropriate grid range for DF if isinstance(pot, interpSphericalPotential) and pot._rmax < self._rmax: warnings.warn( "The interpolated potential's rmax is smaller than the DF's rmax", galpyWarning, ) ############################## EVALUATING THE DF############################### @physical_conversion("massphasespacedensity", pop=True) def __call__(self, *args, **kwargs): """ Evaluate the DF Parameters ---------- *args: tuple 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: cylindrical coordinates (can be Quantity) c) Orbit instance: orbit.Orbit instance and if specific time then orbit.Orbit(t) Returns ------- ndarray or Quantity Value of DF Notes ----- - 2020-07-22 - Written - Lane (UofT) - 2024-10-29 - Fixed to return mass/phase-space volume units for physical-unit output - Bovy (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.0)) 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.0 + vT**2.0 + vz**2.0 E = numpy.atleast_1d(0.5 * vtotSq + _evaluatePotentials(self._pot, R, z)) Lz = numpy.atleast_1d(R * vT) r = numpy.sqrt(R**2.0 + z**2.0) vrad = (R * vR + z * vz) / r L = numpy.atleast_1d(numpy.sqrt(vtotSq - vrad**2.0) * r) return self._call_internal(E, L, Lz).reshape( args[0].shape if len(args) == 1 and hasattr(args[0], "shape") else ( args[0][0].shape if len(args) == 1 and hasattr(args[0], "__len__") and hasattr(args[0][0], "shape") else (args[0].shape if hasattr(args[0], "shape") else ()) ) ) @physical_conversion("massenergydensity", pop=True) def dMdE(self, E): """ Compute the differential energy distribution dM/dE: the amount of mass per unit energy Parameters ---------- E : float or numpy.ndarray Energy; can be a Quantity Returns ------- float, numpy.ndarray, or Quantity The differential energy distribution Notes ----- - 2023-05-23 - Written - Bovy (UofT) """ return self._dMdE( numpy.atleast_1d(conversion.parse_energy(E, vo=self._vo)) ).reshape(E.shape if isinstance(E, numpy.ndarray) else ()) def vmomentdensity(self, r, n, m, **kwargs): """ Calculate an arbitrary moment of the velocity distribution at r times the density. Parameters ---------- r : float Spherical radius at which to calculate the moment. n : float vr^n, where vr = v x cos eta. m : float vt^m, where vt = v x sin eta. Returns ------- float or Quantity at r. Notes ----- - 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 vo is not None and ro is not None: fac = conversion.mass_in_msol(vo, ro) * vo ** (n + m) / ro**3 if _optional_deps._APY_UNITS: u = units.Msun / 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.0 * numpy.pi * integrate.dblquad( lambda eta, v: v ** (2.0 + m + n) * numpy.sin(eta) ** (1 + m) * numpy.cos(eta) ** n * self( r, v * numpy.cos(eta), v * numpy.sin(eta), 0.0, 0.0, use_physical=False, ), 0.0, self._vmax_at_r(self._pot, r), lambda x: 0.0, lambda x: numpy.pi, )[0] ) @physical_conversion("velocity", pop=True) def sigmar(self, r): """ Calculate the radial velocity dispersion at radius r. Parameters ---------- r : float Spherical radius at which to calculate the radial velocity dispersion. Returns ------- float or Quantity The radial velocity dispersion at radius r. Notes ----- - 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): """ Calculate the tangential velocity dispersion at radius r. Parameters ---------- r : float Spherical radius at which to calculate the tangential velocity dispersion. Returns ------- float or Quantity The tangential velocity dispersion at radius r. Notes ----- - 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): """ Calculate the anisotropy at radius r. Parameters ---------- r : float Spherical radius at which to calculate the anisotropy. Returns ------- float Anisotropy at radius r. Notes ----- - 2020-09-04 - Written - Bovy (UofT) """ r = conversion.parse_length(r, ro=self._ro) return 1.0 - self._vmomentdensity(r, 0, 2) / 2.0 / self._vmomentdensity(r, 2, 0) ############################### SAMPLING THE DF################################ def sample(self, R=None, z=None, phi=None, n=1, return_orbit=True, rmin=0.0): """ Sample the DF Parameters ---------- R : float, numpy.ndarray, Quantity, or None, optional If set, sample velocities at this radius. If array, sample velocities at these radii, ignoring n. z : float, numpy.ndarray, Quantity, or None, optional If set, sample velocities at this height. If array, sample velocities at these heights, ignoring n. phi : float, numpy.ndarray, Quantity, or None, optional If set, sample velocities at this azimuth. If array, sample velocities at these azimuths, ignoring n. n : int, optional Number of samples to generate. Default is 1. return_orbit : bool, optional If True, return an orbit.Orbit instance. If False, return a tuple of (R,vR,vT,z,vz,phi). Default is True. rmin : float, Quantity, optional Minimum radius at which to sample. Default is 0. Returns ------- orbit.Orbit instance or tuple If return_orbit is True, an orbit.Orbit instance. Otherwise, a tuple of (R,vR,vT,z,vz,phi). Notes ----- - When specifying position, it is necessary to specify both R and z; if phi is not set in this case, it is sampled - 2020-07-22 - Written - Lane (UofT) """ rmin = conversion.parse_length(rmin, ro=self._ro) 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.0 + z**2.0) 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, TypeError): 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, use_physical=False) mnorm -= mass(self._denspot, self._rmin_sampling, use_physical=False) 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=1) def _sample_position_angles(self, n=1): """Generate spherical angle samples""" phi_samples = numpy.random.uniform(size=n) * 2 * numpy.pi theta_samples = numpy.arccos(1.0 - 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.0 * ( _evaluatePotentials(self._pot, self._rmax + 1e-10, 0) - _evaluatePotentials(self._pot, r, 0.0) ) ) def _make_pvr_interpolator(self, r_a_start=-3, r_a_end=3, n_r_a=120, n_v_vesc=100): """ 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 Parameters ---------- r_a_start : float, optional Radius grid start location in units of log10(r/a). Default is -3. r_a_end : float, optional Radius grid end location in units of log10(r/a). Default is 3. n_r_a : int, optional Number of radius grid points to use. Default is 120. n_v_vesc : int, optional Number of velocity grid points to use. Default is 100. Returns ------- scipy.interpolate.RectBivariateSpline Interpolator for v/vesc given an input r/a. Notes ----- - 2020-07-24 - Written - Lane (UofT) """ # Check that interpolated potential has appropriate grid range if ( isinstance(self._pot, interpSphericalPotential) and self._rmin_sampling < self._pot._rmin ): warnings.warn( "Interpolated potential grid rmin is larger than the rmin to be used for the v_vesc_interpolator grid. This may adversely affect the generated samples. Proceed with care!", galpyWarning, ) # 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.0 ** 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.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, ) def _setup_rphi_interpolator(self, r_a_min=1e-6, r_a_max=1e6, nra=10001): """ Set up the interpolator for r(phi) Parameters ---------- r_a_min : float, optional Minimum r/a. Default is 1e-6. r_a_max : float, optional Maximum r/a. Default is 1e6. nra : int, optional Number of points to use in the r/a grid. Default is 10001. Returns ------- scipy.interpolate.InterpolatedUnivariateSpline Interpolator for r(phi). Notes ----- - 2023-02-23 - Written - Lane (UofT) """ r_a_values = numpy.concatenate( (numpy.array([0.0]), numpy.geomspace(r_a_min, r_a_max, nra)) ) phis = numpy.array( [_evaluatePotentials(self._pot, r * self._scale, 0) for r in r_a_values] ) # Ensure phi is monotonic (required if coming from interpolated pot) if numpy.any(numpy.diff(phis) <= 0): phim = numpy.maximum.accumulate(phis) indx_rm = numpy.where(numpy.diff(phim) == 0)[0] phis = numpy.delete(phim, indx_rm) r_a_values = numpy.delete(r_a_values, indx_rm) return interpolate.InterpolatedUnivariateSpline( phis, r_a_values * self._scale, k=3 ) class isotropicsphericaldf(sphericaldf): """Superclass for isotropic spherical distribution functions""" def __init__(self, pot=None, denspot=None, rmax=None, scale=None, ro=None, vo=None): """ Initialize an isotropic distribution function Parameters ---------- pot : Potential instance or list thereof Default: None denspot : Potential instance or list thereof that represent the density of the tracers (assumed to be spherical; if None, set equal to pot), optional Default: None rmax : float or Quantity, optional Maximum radius to consider; DF is cut off at E = Phi(rmax) Default: None scale : float, optional Scale parameter to be used internally ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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): """ Calculate the distribution function for an isotropic DF. Parameters ---------- *args : tuple of (E,L,Lz) with L and Lz optionalA Returns ------- float The distribution function evaluated at E. Notes ----- - 2020-07 - Written - Lane (UofT) """ return self.fE(args[0]) def _dMdE(self, E): if not hasattr(self, "_rphi"): self._rphi = self._setup_rphi_interpolator() fE = self.fE(E) out = numpy.zeros_like(E) out[fE > 0.0] = ( 16.0 * numpy.pi**2.0 * numpy.sqrt(2.0) * fE[fE > 0.0] * numpy.array( [ integrate.quad( lambda r: r**2.0 * numpy.sqrt(tE - _evaluatePotentials(self._pot, r, 0.0)), 0.0, self._rphi(tE), )[0] for ii, tE in enumerate(E) if fE[ii] > 0.0 ] ) ) # Numerical issues can make the integrand's sqrt argument negative, only # happens at dMdE ~ 0, so just set to zero out[numpy.isnan(out)] = 0.0 return out def _vmomentdensity(self, r, n, m): if m % 2 == 1 or n % 2 == 1: return 0.0 return ( 2.0 * numpy.pi * integrate.quad( lambda v: v ** (2.0 + m + n) * self.fE(_evaluatePotentials(self._pot, r, 0) + 0.5 * v**2.0), 0.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.0 - 2.0 * 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.0) * v**2.0 ) else: return self.fE(_evaluatePotentials(self._pot, r, 0) + 0.5 * v**2.0) * v**2.0 class anisotropicsphericaldf(sphericaldf): """Superclass for anisotropic spherical distribution functions""" def __init__(self, pot=None, denspot=None, rmax=None, scale=None, ro=None, vo=None): """ Initialize an anisotropic distribution function Parameters ---------- pot : Potential instance or list thereof The potential. Default: None. denspot : Potential instance or list thereof, optional The potential representing the density of the tracers (assumed to be spherical). If None, set equal to pot. Default: None. rmax : float or Quantity, optional Maximum radius to consider. DF is cut off at E = Phi(rmax). Default: None. scale : float, optional Length-scale parameter to be used internally. Default: None. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 2020-07-22 - Written - Lane (UofT) """ sphericaldf.__init__( self, pot=pot, denspot=denspot, rmax=rmax, scale=scale, ro=ro, vo=vo ) def _dMdE(self, E): if not hasattr(self, "_rphi"): self._rphi = self._setup_rphi_interpolator() def Lintegrand(t, L2lim, E): return self((E, numpy.sqrt(L2lim - t**2.0)), use_physical=False) out = ( 16.0 * numpy.pi**2.0 * numpy.array( [ integrate.quad( lambda r: r * integrate.quad( Lintegrand, 0.0, numpy.sqrt( 2.0 * r**2.0 * (tE - _evaluatePotentials(self._pot, r, 0.0)) ), args=( 2.0 * r**2.0 * (tE - _evaluatePotentials(self._pot, r, 0.0)), tE, ), )[0], 0.0, self._rphi(tE), )[0] for ii, tE in enumerate(E) ] ) ) # Numerical issues can make the integrand's sqrt argument negative, only # happens at dMdE ~ 0, so just set to zero out[numpy.isnan(out)] = 0.0 return out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/streamdf.py0000644000175100001660000046414414761352023016216 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.0 * 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.0, leading=True, sigangle=None, deltaAngleTrack=None, nTrackChunks=None, nTrackIterations=None, progIsTrack=False, ro=None, vo=None, Vnorm=None, Rnorm=None, R0=8.0, Zsun=0.0208, vsun=[-11.1, 8.0 * 30.24, 7.25], multi=None, interpTrack=_INTERPDURINGSETUP, useInterp=_USEINTERP, nosetup=False, nospreadsetup=False, approxConstTrackFreq=False, useTMHessian=False, custom_transform=None, ): """ Initialize the DF of a tidal stream Parameters ---------- sigv : float or Quantity Radial velocity dispersion of the progenitor. progenitor : galpy.orbit.Orbit Progenitor orbit as Orbit instance (will be re-integrated, so don't bother integrating the orbit before). pot : galpy.potential.Potential or list thereof, optional Potential instance or list thereof. aA : actionAngle instance ActionAngle instance used to convert (x,v) to actions. Generally a actionAngleIsochroneApprox instance. useTM : bool, optional If set to an actionAngleTorus instance, use this to speed up calculations. tdisrupt : float or Quantity, optional Time since start of disruption (default: 5 Gyr). sigMeanOffset : float, optional 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 (default: 6.0). leading : bool, optional If True, model the leading part of the stream; if False, model the trailing part (default: True). sigangle : float or Quantity, optional Estimate of the angle spread of the debris initially (default: sigv/122/[1km/s]=1.8sigv in natural coordinates). deltaAngleTrack : float or Quantity, optional Angle to estimate the stream track over (rad; or can be Quantity) (default: None). nTrackChunks : int, optional Number of chunks to divide the progenitor track in (default: floor(deltaAngleTrack/0.15)+1). nTrackIterations : int, optional 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 (default: None). progIsTrack : bool, optional 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 (default: False). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Vnorm : float or Quantity, optional Deprecated. Use vo instead (default: None). Rnorm : float or Quantity, optional Deprecated. Use ro instead (default: None). R0 : float or Quantity, optional Galactocentric radius of the Sun (kpc) (can be different from ro) (default: 8.0). Zsun : float or Quantity, optional Sun's height above the plane (kpc) (default: 0.0208). vsun : numpy.ndarray or Quantity, optional Sun's motion in cylindrical coordinates (vR positive away from center) (can be Quantity array, but not a list of Quantities) (default: [-11.1, 8.0 * 30.24, 7.25]). multi : int, optional If set, use multi-processing (default: None). interpTrack : bool, optional 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()) (default: _INTERPDURINGSETUP). useInterp : bool, optional Use interpolation by default when calculating approximated frequencies and angles (default: _USEINTERP). nosetup : bool, optional If True, don't setup the stream track and anything else that is expensive (default: False). nospreadsetup : bool, optional If True, don't setup the spread around the stream track (only for nosetup is False) (default: False). approxConstTrackFreq : bool, optional 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) (default: False). useTMHessian : bool, optional If True, compute the basic Hessian dO/dJ_prog using TM; otherwise use aA (default: False). custom_transform : numpy.ndarray, optional Matrix implementing the rotation from (ra,dec) to a custom set of sky coordinates (default: None). Notes ----- - 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.0 / 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( numpy.array(vsun) if isinstance(vsun, list) else 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.0 * 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.0, self._siglz**2.0, self._sigjz**2.0] ) 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.0 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.0 if self._leading and self._progenitor_Omega_along_dOmega < 0.0: self._sigMeanSign = -1.0 elif not self._leading and self._progenitor_Omega_along_dOmega > 0.0: self._sigMeanSign = -1.0 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.0)) self._sigomatrixinv, self._sigomatrixLogdet = fast_cholesky_invert( self._sigomatrix / self._sigomatrixNorm, tiny=10.0**-15.0, logdet=True ) self._sigomatrixinv /= self._sigomatrixNorm deltaAngleTrackLim = ( (self._sigMeanOffset + 4.0) * 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.0**-0.8 or numpy.fabs(R0 - progenitor._ro) > 10.0**-8.0 ): 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.0**-8.0: 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.0**-8.0: 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.0, self._vo, 0.0]) - progenitor._solarmotion ) > 10.0**-8.0 ): 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.0 # 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.0, # time = 0 self._progenitor_angle, self._sigMeanSign, self._dsigomeanProgDirection, lambda x: self.meanOmega(x, use_physical=False), 0.0, ) # angle = 0 # Setup the new progenitor orbit progenitor = Orbit(prog_stream_offset[3]) # Flip the offset sign again self._sigMeanSign *= -1.0 # 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): """ Calculate the misalignment between the progenitor's frequency and the direction along which the stream disrupts. Parameters ---------- isotropic : bool, optional If True, return the misalignment assuming an isotropic action distribution. Returns ------- float Misalignment in radians. Warnings -------- In versions >1.3, the output unit of streamdf.misalignment has been changed to radian (from degree before). Notes ----- - 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.0)) ) if out > numpy.pi / 2.0: return out - numpy.pi else: return out def freqEigvalRatio(self, isotropic=False): """ 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) Parameters ---------- isotropic : bool, optional If True, return the ratio assuming an isotropic action distribution (i.e., just of dO/dJ) Returns ------- float Ratio between eigenvalues of fabs(dO / dJ) Notes ----- - 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): """ Estimate the time of disruption. Parameters ---------- deltaAngle : float Spread in angle since disruption. Returns ------- float or Quantity Time of disruption. Notes ----- - 2013-11-27 - Written - Bovy (IAS) """ return deltaAngle / numpy.sqrt(numpy.sum(self._dsigomeanProg**2.0)) def subhalo_encounters( self, venc=numpy.inf, sigma=150.0 / 220.0, nsubhalo=0.3, bmax=0.025, yoon=False ): """ Estimate the number of encounters with subhalos over the lifetime of this stream, using a formalism similar to that of Yoon et al. (2011) Parameters ---------- venc : float, optional Count encounters with (relative) speeds less than this (relative radial velocity in cylindrical stream frame, unless yoon is True) (can be Quantity) sigma : float, optional Velocity dispersion of the DM subhalo population (can be Quantity) nsubhalo : float, optional Spatial number density of subhalos (can be Quantity) bmax : float, optional Maximum impact parameter (if larger than width of stream) (can be Quantity) yoon : bool, optional If True, use erroneous Yoon et al. formula Returns ------- float Number of encounters Notes ----- - 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.0 + self._progenitor.orbit[0, :, 3] ** 2.0 ) ) if numpy.isinf(venc): vencFac = 1.0 elif yoon: vencFac = 1.0 - (1.0 + venc**2.0 / 4.0 / sigma**2.0) * numpy.exp( -(venc**2.0) / 4.0 / sigma**2.0 ) else: vencFac = 1.0 - numpy.exp(-(venc**2.0) / 2.0 / sigma**2.0) if yoon: yoonFac = 2 * numpy.sqrt(2.0) else: yoonFac = 1.0 # Figure out width of stream w = self.sigangledAngle( self._meandO * self._tdisrupt, simple=True, use_physical=False ) if bmax < w * Ravg / 2.0: bmax = w * Ravg / 2.0 return ( yoonFac / numpy.sqrt(2.0) * numpy.sqrt(numpy.pi) * Ravg * sigma * self._tdisrupt**2.0 * self._meandO * bmax * nsubhalo * vencFac ) ############################STREAM TRACK FUNCTIONS############################# def plotTrack( self, d1="x", d2="z", interp=True, spread=0, simple=_USESIMPLE, *args, **kwargs ): """ Plot the stream track. Parameters ---------- d1 : str, optional Plot this on the X axis ('x','y','z','R','phi','vx','vy','vz','vR','vt','ll','bb','dist','pmll','pmbb','vlos'). Default is 'x'. d2 : str, optional Plot this on the Y axis (same list as for d1). Default is 'z'. interp : bool, optional If True, use the interpolated stream track. Default is True. spread : int, optional If int > 0, also plot the spread around the track as spread x sigma. Default is 0. simple : bool, optional If True, use a simple estimate for the spread in perpendicular angle. Default is _USESIMPLE. *args : tuple Arguments passed to galpy.util.plot.plot. **kwargs : dict Keyword arguments passed to galpy.util.plot.plot. Returns ------- None Notes ----- - 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.0) 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): """ Plot the progenitor orbit. Parameters ---------- d1 : str, optional Plot this on the X axis ('x','y','z','R','phi','vx','vy','vz','vR','vt','ll','bb','dist','pmll','pmbb','vlos'). Default is 'x'. d2 : str, optional Plot this on the Y axis (same list as for d1). Default is 'z'. scaleToPhysical : bool, optional If True, plot positions in kpc and velocities in km/s. Default is False. *args : tuple Arguments passed to `galpy.util.plot.plot`. **kwargs : dict Keyword arguments passed to `galpy.util.plot.plot`. Returns ------- None Notes ----- - 2013-12-09 - Written - Bovy (IAS) """ tts = self._progenitor.t[ self._progenitor.t < self._trackts[self._nTrackChunks - 1] ] obs = [self._R0, 0.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.0] ) 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, 0.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.0: minEigvec *= -1.0 # 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, 0.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.0: minEigvec *= -1.0 # 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.0) * (slerpts <= 1.0) 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): """ 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) Parameters ---------- **kwargs: dict galpy.util.plot.plot kwargs Returns ------- None Notes ----- - 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] = numpy.array( self._aA.actionsFreqsAngles( Orbit(self._ObsTrack[ii, :]), use_physical=False )[3:] ).flatten() 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.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.0 / 180.0 * numpy.pi: self.nTrackIterations = 0 elif ( numpy.fabs(self.misalignment(quantity=False)) >= 1.0 / 180.0 * numpy.pi and numpy.fabs(self.misalignment(quantity=False)) < 3.0 / 180.0 * numpy.pi ): self.nTrackIterations = 1 elif numpy.fabs(self.misalignment(quantity=False)) >= 3.0 / 180.0 * 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 self._nTrackChunks = self._nTrackChunks if self._nTrackChunks >= 4 else 4 else: self._nTrackChunks = nTrackChunks if not hasattr(self, "nInterpolatedTrackChunks"): self.nInterpolatedTrackChunks = 1001 dt = self._deltaAngleTrack / self._progenitor_Omega_along_dOmega self._trackts = numpy.linspace( 0.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.0, # time = 0 self._progenitor_angle, self._sigMeanSign, self._dsigomeanProgDirection, lambda x: self.meanOmega(x, use_physical=False), 0.0, ) # angle = 0 auxiliaryTrack = Orbit(prog_stream_offset[3]) if dt < 0.0: self._trackts = numpy.linspace(0.0, -2.0 * dt, 2 * self._nTrackChunks - 1) # Flip velocities before integrating auxiliaryTrack = auxiliaryTrack.flip() auxiliaryTrack.integrate(self._trackts, self._pot) if dt < 0.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.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.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.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.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.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, 0.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.0: teig[1][:, sortIndx[jj]] *= -1.0 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.0) * (slerpts <= 1.0) 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.0, Zsun] obs.extend(vsun) obskwargs = {} obskwargs["ro"] = ro obskwargs["vo"] = vo obskwargs["obs"] = obs obskwargs["quantity"] = False self._ErrCovsLBScale = [ 180.0, 90.0, self._progenitor.dist(**obskwargs), numpy.fabs(self._progenitor.vlos(**obskwargs)), numpy.sqrt( self._progenitor.pmll(**obskwargs) ** 2.0 + self._progenitor.pmbb(**obskwargs) ** 2.0 ), numpy.sqrt( self._progenitor.pmll(**obskwargs) ** 2.0 + self._progenitor.pmbb(**obskwargs) ** 2.0 ), ] allErrCovsEigvalLB = numpy.empty((len(self._thetasTrack), 6)) allErrCovsEigvecLB = numpy.empty_like(self._allErrCovs) eigDir = numpy.array( [numpy.array([1.0, 0.0, 0.0, 0.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.0: teig[1][:, sortIndx[jj]] *= -1.0 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.0) * (slerpts <= 1.0) 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.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.0 * numpy.pi ) return None def calc_stream_lb(self, vo=None, ro=None, R0=None, Zsun=None, vsun=None): """ Convert the stream track to observational coordinates and store Parameters ---------- ro : float or Quantity, optional Distance scale for translation into internal units (object-wide default. vo : float or Quantity, optional Velocity scale for translation into internal units (object-wide default. R0 : float or Quantity, optional Distance to the Galactic center (object-wide default. Zsun : float or Quantity, optional Distance to the Galactic plane (object-wide default. vsun : float or Quantity, optional Velocity of the Sun (object-wide default. Returns ------- None Notes ----- - 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 ): """ Find the closest point on the stream track to a given point Parameters ---------- R,vR,vT,z,vz,phi : float Phase-space coordinates of the given point interp : bool, optional If True, return the index of the interpolated track xy : bool, optional 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 : bool, optional If True, also use velocities to find the closest point Returns ------- int Index into the track of the closest track point Notes ----- - 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.0 if Y is None: Y = 0.0 if Z is None: Z = 0.0 if usev and vX is None: vX = 0.0 if usev and vY is None: vY = 0.0 if usev and vZ is None: vZ = 0.0 if interp: dist2 = ( present[0] * (X - self._interpolatedObsTrackXY[:, 0]) ** 2.0 + present[1] * (Y - self._interpolatedObsTrackXY[:, 1]) ** 2.0 + present[2] * (Z - self._interpolatedObsTrackXY[:, 2]) ** 2.0 ) if usev: dist2 += ( present[3] * (vX - self._interpolatedObsTrackXY[:, 3]) ** 2.0 + present[4] * (vY - self._interpolatedObsTrackXY[:, 4]) ** 2.0 + present[5] * (vZ - self._interpolatedObsTrackXY[:, 5]) ** 2.0 ) else: dist2 = ( present[0] * (X - self._ObsTrackXY[:, 0]) ** 2.0 + present[1] * (Y - self._ObsTrackXY[:, 1]) ** 2.0 + present[2] * (Z - self._ObsTrackXY[:, 2]) ** 2.0 ) if usev: dist2 += ( present[3] * (vX - self._ObsTrackXY[:, 3]) ** 2.0 + present[4] * (vY - self._ObsTrackXY[:, 4]) ** 2.0 + present[5] * (vZ - self._ObsTrackXY[:, 5]) ** 2.0 ) 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 ): """ Find the closest point on the stream track to a given point in (l,b,...) coordinates Parameters ---------- l : float Galactic longitude in degrees b : float Galactic latitude in degrees D : float Distance in kpc vlos : float Line-of-sight velocity in km/s pmll : float Proper motion in Galactic longitude in mas/yr pmbb : float Proper motion in Galactic latitude in mas/yr interp : bool, optional If True, return the closest index on the interpolated track (default is True) usev : bool, optional If True, also use the velocity components (default is False) Returns ------- int Index of closest track point on the interpolated or not-interpolated track Notes ----- - 2013-12-17 - Written - Bovy (IAS) """ if interp: nTrackPoints = len(self._interpolatedThetasTrack) else: nTrackPoints = len(self._thetasTrack) if l is None: l = 0.0 trackL = numpy.zeros(nTrackPoints) elif interp: trackL = self._interpolatedObsTrackLB[:, 0] else: trackL = self._ObsTrackLB[:, 0] if b is None: b = 0.0 trackB = numpy.zeros(nTrackPoints) elif interp: trackB = self._interpolatedObsTrackLB[:, 1] else: trackB = self._ObsTrackLB[:, 1] if D is None: D = 1.0 trackD = numpy.ones(nTrackPoints) elif interp: trackD = self._interpolatedObsTrackLB[:, 2] else: trackD = self._ObsTrackLB[:, 2] if usev: if vlos is None: vlos = 0.0 trackVlos = numpy.zeros(nTrackPoints) elif interp: trackVlos = self._interpolatedObsTrackLB[:, 3] else: trackVlos = self._ObsTrackLB[:, 3] if pmll is None: pmll = 0.0 trackPmll = numpy.zeros(nTrackPoints) elif interp: trackPmll = self._interpolatedObsTrackLB[:, 4] else: trackPmll = self._ObsTrackLB[:, 4] if pmbb is None: pmbb = 0.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.0 + (XYZ[1] - trackXYZ[:, 1]) ** 2.0 + (XYZ[2] - trackXYZ[:, 2]) ** 2.0 ) if usev: dist2 += ( (vxvyvz[0] - trackvxvyvz[:, 0]) ** 2.0 + (vxvyvz[1] - trackvxvyvz[:, 1]) ** 2.0 + (vxvyvz[2] - trackvxvyvz[:, 2]) ** 2.0 ) return numpy.argmin(dist2) def _find_closest_trackpointaA(self, Or, Op, Oz, ar, ap, az, interp=True): """ Find the closest point on the stream track to a given point in frequency-angle coordinates Parameters ---------- Or : float Radial frequency Op : float Azimuthal frequency Oz : float Vertical frequency ar : float Radial angle ap : float Azimuthal angle az : float Vertical angle interp : bool, optional If True, return the index of the interpolated track (default is True) Returns ------- int Index into the track of the closest track point Notes ----- - 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): """ Return the probability of a given parallel (frequency,angle) offset pair. Parameters ---------- Opar : numpy.ndarray or Quantity Parallel frequency offset. apar : float or Quantity Parallel angle offset along the stream. tdisrupt : float, optional Time since the start of the simulation at which the stream is disrupted (in galpy time units). If None, the value set at the initialization of the object is used. Returns ------- ndarray Probability of a given parallel (frequency,angle) offset pair. Notes ----- - 2015-11-17 - 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 Opar = numpy.array(Opar) out = numpy.zeros_like(Opar) # Compute ts ts = apar / Opar # Evaluate out[(ts < tdisrupt) * (ts >= 0.0)] = numpy.exp( -0.5 * (Opar[(ts < tdisrupt) * (ts >= 0.0)] - self._meandO) ** 2.0 / self._sortedSigOEig[2] ) / numpy.sqrt(self._sortedSigOEig[2]) return out def density_par(self, dangle, coord="apar", tdisrupt=None, **kwargs): """ Calculate the density as a function of a parallel coordinate Parameters ---------- dangle : float Parallel angle offset for this coordinate value coord : str, optional Coordinate to return the density in ('apar' [default], 'll','ra','customra','phi') tdisrupt : float, optional Time since the disruption to calculate the density at (in galpy natural units). If None, use the current time (default: None). Returns ------- float Density(angle) Notes ----- - 2015-11-17 - Written - Bovy (UofT) """ if coord.lower() != "apar": # Need to compute the Jacobian for this coordinate value ddangle = dangle + 10.0**-7.0 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.0 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.0 + special.erf( (self._meandO - dOmin) / numpy.sqrt(2.0 * self._sortedSigOEig[2]) ) ) def length(self, threshold=0.2, phys=False, ang=False, tdisrupt=None, **kwargs): """ Calculate the length of the stream. Parameters ---------- threshold : float, optional Threshold down from the density near the progenitor at which to define the 'end' of the stream. Default is 0.2. phys : bool, optional If True, return the length in physical kpc. Default is False. ang : bool, optional If True, return the length in sky angular arc length in degree. Default is False. tdisrupt : float, optional Time since disruption in natural units. Default is None. Returns ------- float Length (rad for parallel angle; kpc for physical length; deg for sky arc length). Notes ----- - 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.0 + dYda(da) ** 2.0 + dZda(da) ** 2.0 ), 0.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.0 ): ll = ( dePeriod( self._interpolatedObsTrackLB[:, 0][:, numpy.newaxis].T * numpy.pi / 180.0 ).T * 180.0 / numpy.pi ) else: ll = ( dePeriod( self._interpolatedObsTrackLB[::-1, 0][:, numpy.newaxis].T * numpy.pi / 180.0 ).T[::-1] * 180.0 / numpy.pi ) if ( numpy.median( numpy.roll(self._interpolatedObsTrackLB[:, 1], -1) - self._interpolatedObsTrackLB[:, 1] ) > 0.0 ): bb = ( dePeriod( self._interpolatedObsTrackLB[:, 1][:, numpy.newaxis].T * numpy.pi / 180.0 ).T * 180.0 / numpy.pi ) else: bb = ( dePeriod( self._interpolatedObsTrackLB[::-1, 1][:, numpy.newaxis].T * numpy.pi / 180.0 ).T[::-1] * 180.0 / 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.0 + dbda(da) ** 2.0), 0.0, result )[0] return result @physical_conversion("frequency", pop=True) def meanOmega(self, dangle, oned=False, offset_sign=None, tdisrupt=None): """ Calculate the mean frequency as a function of angle, assuming a uniform time distribution up to a maximum time. Parameters ---------- dangle : float Angle offset. oned : bool, optional If True, return the 1D offset from the progenitor (along the direction of disruption). Default is False. offset_sign : None, optional Sign of the frequency offset. Default is None. tdisrupt : float, optional Maximum time. Default is None. Returns ------- float or Quantity Mean Omega. Notes ----- - 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.0 / numpy.pi) * numpy.sqrt(self._sortedSigOEig[2]) * numpy.exp(-0.5 * (meandO - dOmin) ** 2.0 / self._sortedSigOEig[2]) / ( 1.0 + special.erf( (meandO - dOmin) / numpy.sqrt(2.0 * 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): """ Calculate the 1D sigma in frequency as a function of angle, assuming a uniform time distribution up to a maximum time Parameters ---------- dangle : float Angle offset Returns ------- float or Quantity Sigma Omega Notes ----- - 2013-12-01 - Written - Bovy (IAS) """ dOmin = dangle / self._tdisrupt meandO = self._meandO sO1D2 = ( ( numpy.sqrt(2.0 / numpy.pi) * numpy.sqrt(self._sortedSigOEig[2]) * (meandO + dOmin) * numpy.exp(-0.5 * (meandO - dOmin) ** 2.0 / self._sortedSigOEig[2]) / ( 1.0 + special.erf( (meandO - dOmin) / numpy.sqrt(2.0 * self._sortedSigOEig[2]) ) ) ) + meandO**2.0 + self._sortedSigOEig[2] ) mO = self.meanOmega(dangle, oned=True, use_physical=False) return numpy.sqrt(sO1D2 - mO**2.0) def ptdAngle(self, t, dangle): """ Return the probability of a given stripping time at a given angle along the stream. Parameters ---------- t : numpy.ndarray Stripping time. dangle : float Angle offset along the stream. Returns ------- numpy.ndarray p(td|dangle) Notes ----- - 2013-12-05 - Written - Bovy (IAS). """ t = numpy.array(t) out = numpy.zeros_like(t) dO = dangle / t[(t > 0.0) * (t < self._tdisrupt)] # 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 > 0.0) * (t < self._tdisrupt)] = ( dO**2.0 / dangle * numpy.exp(-0.5 * (dO - self._meandO) ** 2.0 / self._sortedSigOEig[2]) / numpy.sqrt(self._sortedSigOEig[2]) ) return out @physical_conversion("time", pop=True) def meantdAngle(self, dangle): """ Calculate the mean stripping time at a given angle. Parameters ---------- dangle : float Angle offset along the stream. Returns ------- float or Quantity Mean stripping time at this dangle. Notes ----- - 2013-12-05 - Written - Bovy (IAS) """ Tlow = dangle / (self._meandO + 3.0 * numpy.sqrt(self._sortedSigOEig[2])) Thigh = dangle / (self._meandO - 3.0 * 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] # Denom is 0 at the progenitor and very far from it, # so distinguish between those two cases if denom == 0.0 and dangle / self._meandO < self._tdisrupt / 10.0: return 0.0 elif denom == 0.0 and dangle / self._meandO > self._tdisrupt / 10.0: return self._tdisrupt else: return num / denom @physical_conversion("time", pop=True) def sigtdAngle(self, dangle): """ Calculate the dispersion in the stripping times at a given angle. Parameters ---------- dangle : float Angle offset along the stream. Returns ------- float or Quantity Dispersion in the stripping times at this angle. Notes ----- - 2013-12-05 - Written - Bovy (IAS) """ Tlow = dangle / (self._meandO + 3.0 * numpy.sqrt(self._sortedSigOEig[2])) Thigh = dangle / (self._meandO - 3.0 * numpy.sqrt(self._sortedSigOEig[2])) numsig2 = integrate.quad( lambda x: x**2.0 * 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.0: return 0.0 else: return numpy.sqrt(numsig2 / denom - (nummean / denom) ** 2.0) def pangledAngle(self, angleperp, dangle, smallest=False): """ Return the probability of a given perpendicular angle at a given angle along the stream. Parameters ---------- angleperp : numpy.ndarray Perpendicular angle. dangle : float Angle offset along the stream. smallest : bool, optional Calculate for smallest eigenvalue direction rather than for middle. Default is False. Returns ------- numpy.ndarray p(angle_perp|dangle) Notes ----- - 2013-12-06 - Written - Bovy (IAS) """ angleperp = numpy.array(angleperp) out = numpy.zeros_like(angleperp) out = numpy.array( [ integrate.quad( self._pangledAnglet, 0.0, self._tdisrupt, (ap, dangle, smallest) )[0] for ap in numpy.atleast_1d(angleperp) ] ) return out.reshape(angleperp.shape) @physical_conversion("angle", pop=True) def meanangledAngle(self, dangle, smallest=False): """ Calculate the mean perpendicular angle at a given angle. Parameters ---------- dangle : float Angle offset along the stream. smallest : bool, optional Calculate for smallest eigenvalue direction rather than for middle. Default is False. Returns ------- float or Quantity Mean perpendicular angle. Notes ----- - 2013-12-06 - Written - Bovy (IAS) """ if smallest: eigIndx = 0 else: eigIndx = 1 aplow = numpy.amax( [ numpy.sqrt(self._sortedSigOEig[eigIndx]) * self._tdisrupt * 5.0, 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.0 or numpy.isnan(denom): return 0.0 else: return num / denom @physical_conversion("angle", pop=True) def sigangledAngle(self, dangle, assumeZeroMean=True, smallest=False, simple=False): """ Calculate the dispersion in the perpendicular angle at a given angle. Parameters ---------- dangle : float Angle offset along the stream. assumeZeroMean : bool, optional If True, assume that the mean is zero (should be). Default is True. smallest : bool, optional Calculate for smallest eigenvalue direction rather than for middle. Default is False. simple : bool, optional If True, return an even simpler estimate. Default is False. Returns ------- float or Quantity Dispersion in the perpendicular angle at this angle. Notes ----- - 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.0) aplow = numpy.amax( [ numpy.sqrt(self._sortedSigOEig[eigIndx]) * self._tdisrupt * 5.0, self._sigangle, ] ) numsig2 = integrate.quad( lambda x: x**2.0 * 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.0 denom = integrate.quad(self.pangledAngle, aplow, -aplow, (dangle,))[0] if denom == 0.0 or numpy.isnan(denom): return 0.0 else: return numpy.sqrt(numsig2 / denom - (nummean / denom) ** 2.0) def _pangledAnglet(self, t, angleperp, dangle, smallest): """p(angle_perp|angle_par,time)""" if smallest: eigIndx = 0 else: eigIndx = 1 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.0 / (t[tindx] ** 2.0 * self._sortedSigOEig[eigIndx] + self._sigangle2) ) / numpy.sqrt( t[tindx] ** 2.0 * 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): """ Return action-angle coordinates for a point based on the linear approximation around the stream track Parameters ---------- R : float Radius vR : float Radial velocity vT : float Tangential velocity z : float Vertical height vz : float Vertical velocity phi : float Azimuth interp : bool, optional If True, use the interpolated track. Default is True. cindx : int, optional Index of the closest point on the (interpolated) stream track. If not given, determined from the dimensions given. Returns ------- tuple (Or,Op,Oz,ar,ap,az) Notes ----- - 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.0 + (Y[ii] - self._ObsTrackXY[jacIndx, 1]) ** 2.0 + (Z[ii] - self._ObsTrackXY[jacIndx, 2]) ** 2.0 ) if jacIndx == 0: jacIndx2 = jacIndx + 1 dmJacIndx2 = ( (X[ii] - self._ObsTrackXY[jacIndx + 1, 0]) ** 2.0 + (Y[ii] - self._ObsTrackXY[jacIndx + 1, 1]) ** 2.0 + (Z[ii] - self._ObsTrackXY[jacIndx + 1, 2]) ** 2.0 ) elif jacIndx == self._nTrackChunks - 1: jacIndx2 = jacIndx - 1 dmJacIndx2 = ( (X[ii] - self._ObsTrackXY[jacIndx - 1, 0]) ** 2.0 + (Y[ii] - self._ObsTrackXY[jacIndx - 1, 1]) ** 2.0 + (Z[ii] - self._ObsTrackXY[jacIndx - 1, 2]) ** 2.0 ) else: dm1 = ( (X[ii] - self._ObsTrackXY[jacIndx - 1, 0]) ** 2.0 + (Y[ii] - self._ObsTrackXY[jacIndx - 1, 1]) ** 2.0 + (Z[ii] - self._ObsTrackXY[jacIndx - 1, 2]) ** 2.0 ) dm2 = ( (X[ii] - self._ObsTrackXY[jacIndx + 1, 0]) ** 2.0 + (Y[ii] - self._ObsTrackXY[jacIndx + 1, 1]) ** 2.0 + (Z[ii] - self._ObsTrackXY[jacIndx + 1, 2]) ** 2.0 ) 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 dxv[5] = (dxv[5] + numpy.pi) % (2.0 * numpy.pi) - numpy.pi # Apply closest jacobians out[:, ii] = numpy.dot( (1.0 - 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): """ Return R,vR,... coordinates for a point based on the linear approximation around the stream track Parameters ---------- Or : float Radial action Op : float Azimuthal action Oz : float Vertical action ar : float Radial angle ap : float Azimuthal angle az : float Vertical angle interp : bool, optional If True, use the interpolated track. Default is True. Returns ------- tuple (R,vR,vT,z,vz,phi) Notes ----- - 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 dOa[3] = (dOa[3] + numpy.pi) % (2.0 * numpy.pi) - numpy.pi dOa[4] = (dOa[4] + numpy.pi) % (2.0 * numpy.pi) - numpy.pi dOa[5] = (dOa[5] + numpy.pi) % (2.0 * numpy.pi) - numpy.pi # Apply closest jacobian out[:, ii] = numpy.dot( (1.0 - 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): """ Evaluate the distribution function (DF). Parameters ---------- *args : tuple 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 : bool, optional If True, return the natural log. Default is True. aaInput : bool, optional If True, option b above. Default is False. Returns ------- ndarray Value of DF. Notes ----- - 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.0, axis=0) dOmega2 = numpy.sum(dOmega**2.0, axis=0) dOmegaAngle = numpy.sum(dOmega * dangle, axis=0) logdfA = ( -0.5 / self._sigangle2 * (dangle2 - dOmegaAngle**2.0 / dOmega2) - 2.0 * self._lnsigangle - 0.5 * numpy.log(dOmega2) ) # Finite stripping part a0 = dOmegaAngle / numpy.sqrt(2.0) / self._sigangle / numpy.sqrt(dOmega2) ad = ( numpy.sqrt(dOmega2) / numpy.sqrt(2.0) / self._sigangle * (self._tdisrupt - dOmegaAngle / dOmega2) ) loga = numpy.log( (special.erf(a0) + special.erf(ad)) / 2.0 ) # 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): """ Prepare stream data for the __call__ method. Parameters ---------- *args : tuple 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 : bool, optional If True, return the natural log. Default is True. aaInput : bool, optional If True, option b above. Default is False. Returns ------- tuple Tuple containing two arrays (dOmega, dangle), each of shape [3, nobj], wrt the progenitor. Notes ----- - 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.0)] += 2.0 * numpy.pi dangle[(dangle > 4.0)] -= 2.0 * 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): """ Evaluate the distribution function (DF), marginalizing over some directions, in Galactocentric rectangular coordinates (or in observed l,b,D,vlos,pmll,pmbb) coordinates) Parameters ---------- xy : numpy.ndarray Phase-space point [X,Y,Z,vX,vY,vZ]; the distribution of the dimensions set to None is returned. interp : bool, optional If True, use the interpolated stream track. Default is True. cindx : int, optional Index of the closest point on the (interpolated) stream track if not given, determined from the dimensions given. nsigma : int, optional Number of sigma to marginalize the DF over (approximate sigma). Default is 3. ngl : int, optional Order of Gauss-Legendre integration. Default is 5. lb : bool, optional 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. Default is False. ro : float or Quantity, optional Distance scale for translation into internal units (object-wide default). vo : float or Quantity, optional Velocity scale for translation into internal units (object-wide default). R0 : float or Quantity, optional Distance to the Galactic center (object-wide default). Zsun : float or Quantity, optional Sun's height above the plane (object-wide default). vsun : float or Quantity, optional Sun's motion in cylindrical coordinates (object-wide default). Returns ------- ndarray Value of DF. Notes ----- - 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.0 baseX = list(baseX) baseX.extend(-(glx + 1) / 2.0) 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.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): """ 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) Parameters ---------- xy : numpy.ndarray Phase-space point [X,Y,Z,vX,vY,vZ]; the distribution of the dimensions set to None is returned. interp : bool, optional If True, use the interpolated stream track. Default is True. cindx : int, optional Index of the closest point on the (interpolated) stream track if not given, determined from the dimensions given. lb : bool, optional 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. Default is False. Returns ------- tuple (mean,variance) of the approximate Gaussian DF for the missing directions in xy. Notes ----- - 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 ): """ Sample from the DF. Parameters ---------- n : int Number of points to return. returnaAdt : bool, optional If True, return (Omega,angle,dt). Default is False. returndt : bool, optional If True, also return the time since the star was stripped. Default is False. interp : object-wide default, optional Use interpolation of the stream track. Default is object-wide default. xy : bool, optional If True, return Galactocentric rectangular coordinates. Default is False. lb : bool, optional If True, return Galactic l,b,d,vlos,pmll,pmbb coordinates. Default is False. Returns ------- ndarray or tuple (R,vR,vT,z,vz,phi) of points on the stream in 6,N array. Or (X,Y,Z,vX,vY,vZ) if xy is True. Or (l,b,d,vlos,pmll,pmbb) if lb is True. Or (Omega,angle,dt) if returnaAdt is True. If returndt is set, also return the time since the star was stripped. Notes ----- - 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, 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): """ Sample the time since the progenitor was stripped Parameters ---------- n : int Number of points to return Returns ------- ndarray Array of n stripping times Notes ----- - 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.0 / 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.0 / 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][0] 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.0 * numpy.pi, ) ObsTrackAA[3:] = theseAngles diffAngles = theseAngles - allAcfsTrack[6:] diffAngles[(diffAngles > numpy.pi)] = ( diffAngles[(diffAngles > numpy.pi)] - 2.0 * numpy.pi ) diffAngles[(diffAngles < -numpy.pi)] = ( diffAngles[(diffAngles < -numpy.pi)] + 2.0 * 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 numpy.array( [allAcfsTrack, alljacsTrack, allinvjacsTrack, ObsTrack, ObsTrackAA, detdOdJ], dtype="object", ) 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.0 * 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 numpy.array( [alljacsTrack, allinvjacsTrack, ObsTrack, ObsTrackAA, detdOdJ], dtype="object" ) 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.0 * 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.0 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 sigangle2 = ( sigAngle(thetasTrack) ** 2.0 if hasattr(sigAngle, "__call__") else sigAngle**2.0 ) tsigadiag = numpy.ones(3) * sigangle2 tsigadiag[numpy.argmax(tsigOdiag)] = 1.0 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.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.0, ro=8.0, R0=8.0, Zsun=0.0208, vsun=[-11.1, 8.0 * 30.24, 7.25], _initacfs=None, ): """ Calculate the Jacobian d(J,theta)/d(x,v) Parameters ---------- xv: numpy.ndarray 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: float, optional Infinitesimal to use (rescaled for lb, so think fractionally)) freqs: bool, optional If True, go to frequencies rather than actions dOdJ: bool, optional Actually calculate d Frequency / d action actionsFreqsAngles: bool, optional If True, calculate d(action,freq.,angle)/d (xv) lb: bool, optional If True, start with (l,b,D,vlos,pmll,pmbb) in (deg,deg,kpc,km/s,mas/yr,mas/yr) vo: float, optional Circular velocity to normalize with when lb=True ro: float, optional Galactocentric radius to normalize with when lb=True R0: float, optional Galactocentric radius of the Sun (kpc) Zsun: float, optional Sun's height above the plane (kpc) vsun: list, optional Sun's motion in cylindrical coordinates (vR positive away from center) coordFunc: function, optional 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) _initacfs: list, optional If set, this is a list of the actions, frequencies, and angles at xv Returns ------- jac: numpy.ndarray Jacobian matrix Notes ----- - 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.0**-8.0 * numpy.ones(6) if lb: # Re-scale some of the differences, to be more natural dxv[0] *= 180.0 / numpy.pi dxv[1] *= 180.0 / 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)[0] / dxv[ii] jac[1, ii] = (tlz - lz)[0] / dxv[ii] jac[2, ii] = (tjz - jz)[0] / dxv[ii] jac[3, ii] = (tOr - Or)[0] / dxv[ii] jac[4, ii] = (tOphi - Ophi)[0] / dxv[ii] jac[5, ii] = (tOz - Oz)[0] / dxv[ii] angleIndx = 6 elif freqs: jac[0, ii] = (tOr - Or)[0] / dxv[ii] jac[1, ii] = (tOphi - Ophi)[0] / dxv[ii] jac[2, ii] = (tOz - Oz)[0] / dxv[ii] else: jac[0, ii] = (tjr - jr)[0] / dxv[ii] jac[1, ii] = (tlz - lz)[0] / dxv[ii] jac[2, ii] = (tjz - jz)[0] / dxv[ii] if dOdJ: jac2[0, ii] = (tOr - Or)[0] / dxv[ii] jac2[1, ii] = (tOphi - Ophi)[0] / dxv[ii] jac2[2, ii] = (tOz - Oz)[0] / dxv[ii] # For the angles, make sure we do not hit a turning point jac[angleIndx, ii] = ((tar - ar + numpy.pi) % (2.0 * numpy.pi) - numpy.pi)[ 0 ] / dxv[ii] jac[angleIndx + 1, ii] = ( (taphi - aphi + numpy.pi) % (2.0 * numpy.pi) - numpy.pi )[0] / dxv[ii] jac[angleIndx + 2, ii] = ((taz - az + numpy.pi) % (2.0 * numpy.pi) - numpy.pi)[ 0 ] / 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=1741018131.0 galpy-1.10.2/galpy/df/streamgapdf.py0000644000175100001660000024275414761352023016707 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.0) out[goodIndx] = func(args[0], args[1][goodIndx]) return out elif args[1] >= args[0]._deltaAngleTrackImpact or args[1] <= 0.0: return 0.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): """ Initialize the DF of a gap in a stellar stream Parameters ---------- sigv : float or Quantity Radial velocity dispersion of the progenitor. progenitor : galpy.orbit.Orbit Progenitor orbit as Orbit instance (will be re-integrated, so don't bother integrating the orbit before). pot : galpy.potential.Potential or list thereof, optional Potential instance or list thereof. aA : actionAngle instance ActionAngle instance used to convert (x,v) to actions. Generally a actionAngleIsochroneApprox instance. useTM : bool, optional If set to an actionAngleTorus instance, use this to speed up calculations. tdisrupt : float or Quantity, optional Time since start of disruption (default: 5 Gyr). sigMeanOffset : float, optional 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 (default: 6.0). leading : bool, optional If True, model the leading part of the stream; if False, model the trailing part (default: True). sigangle : float or Quantity, optional Estimate of the angle spread of the debris initially (default: sigv/122/[1km/s]=1.8sigv in natural coordinates). deltaAngleTrack : float or Quantity, optional Angle to estimate the stream track over (rad; or can be Quantity) (default: None). nTrackChunks : int, optional Number of chunks to divide the progenitor track in (default: floor(deltaAngleTrack/0.15)+1). nTrackIterations : int, optional 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 (default: None). progIsTrack : bool, optional 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 (default: False). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Vnorm : float or Quantity, optional Deprecated. Use vo instead (default: None). Rnorm : float or Quantity, optional Deprecated. Use ro instead (default: None). R0 : float or Quantity, optional Galactocentric radius of the Sun (kpc) (can be different from ro) (default: 8.0). Zsun : float or Quantity, optional Sun's height above the plane (kpc) (default: 0.0208). vsun : numpy.ndarray or Quantity, optional Sun's motion in cylindrical coordinates (vR positive away from center) (can be Quantity array, but not a list of Quantities) (default: [-11.1, 8.0 * 30.24, 7.25]). multi : int, optional If set, use multi-processing (default: None). interpTrack : bool, optional 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()) (default: _INTERPDURINGSETUP). useInterp : bool, optional Use interpolation by default when calculating approximated frequencies and angles (default: _USEINTERP). nosetup : bool, optional If True, don't setup the stream track and anything else that is expensive (default: False). nospreadsetup : bool, optional If True, don't setup the spread around the stream track (only for nosetup is False) (default: False). approxConstTrackFreq : bool, optional 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) (default: False). useTMHessian : bool, optional If True, compute the basic Hessian dO/dJ_prog using TM; otherwise use aA (default: False). custom_transform : numpy.ndarray, optional Matrix implementing the rotation from (ra,dec) to a custom set of sky coordinates (default: None). impactb : float or Quantity, optional Impact parameter (can be Quantity) (default: 1.0). subhalovel : numpy.ndarray or Quantity, optional Velocity of the subhalo shape=(3) (default: [0.0, 1.0, 0.0]). timpact : float or Quantity, optional Time since impact (can be Quantity) (default: 1.0). impact_angle : float or Quantity, optional Angle offset from progenitor at which the impact occurred (rad) (can be Quantity) (default: 1.0). GM : float or Quantity, optional Mass of the subhalo when using a Plummer or Hernquist model. rs : float or Quantity, optional Scale parameter of the subhalo when using a Plummer or Hernquist model. hernquist : bool, optional If True, use Hernquist kicks for GM/rs (default: False --> Plummer). subhalopot : Potential or list thereof, optional Gravitational potential of the subhalo (alternative to specifying GM and rs) deltaAngleTrackImpact : float or Quantity, optional Angle to estimate the stream track over to determine the effect of the impact [similar to deltaAngleTrack] (rad) (default: None). nTrackChunksImpact : int, optional Number of chunks to divide the progenitor track in near the impact [similar to nTrackChunks] (default: floor(deltaAngleTrack/0.15)+1). nKickPoints : int, optional 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% (default: 30xnTrackChunksImpact). nokicksetup : bool, optional If True, only run as far as setting up the coordinate transformation at the time of impact (useful when using this in streampepperdf) (default: False). spline_order : int, optional Order of the spline to interpolate the kicks with (default: 3). higherorderTrack : bool, optional If True, calculate the track using higher-order terms (default: False). nTrackChunks : int, optional Number of chunks to divide the progenitor track into (default: 8). interpTrack : bool, optional If True, interpolate the track (default: True). useInterp : bool, optional If True, use the interpolated track to calculate actions and angles (default: True). Notes ----- - Parameters above up to impactb are streamdf parameters used to setup the underlying smooth stream. - 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.0), ro=self._ro) subhalovel = conversion.parse_velocity( kwargs.pop("subhalovel", numpy.array([0.0, 1.0, 0.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.0), ro=self._ro, vo=self._vo ) impact_angle = conversion.parse_angle(kwargs.pop("impact_angle", 1.0)) 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: # pragma: no cover 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): """ Return the probability of a given parallel (frequency,angle) offset pair. Parameters ---------- Opar : numpy.ndarray or Quantity Parallel frequency offset. apar : float or Quantity Parallel angle offset along the stream. Returns ------- numpy.ndarray Probability of a given parallel (frequency,angle) offset pair. Notes ----- - 2015-11-17 - Written - Bovy (UofT). """ Opar = conversion.parse_frequency(Opar, ro=self._ro, vo=self._vo) apar = conversion.parse_angle(apar) Opar = numpy.array(Opar) out = numpy.zeros_like(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.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.0 * self.pOparapar( T / (1 - T * T) * numpy.sqrt(self._sortedSigOEig[2]) + self._meandO, dangle, ), -1.0, 1.0, )[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.0 + self._kick_interpdOpar_poly.c[-2] * self._timpact) * ( special.erf( 1.0 / numpy.sqrt(2.0 * self._sortedSigOEig[2]) * (Oparb[:-1] - self._kick_interpdOpar_poly.c[-1] - self._meandO) ) - special.erf( 1.0 / numpy.sqrt(2.0 * 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.0 + special.erf( (self._meandO - Oparb[0]) / numpy.sqrt(2.0 * 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: # pragma: no cover return 0.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.0 * self._sortedSigOEig[2]) ul = ( Oparb[:-1] - self._kick_interpdOpar_poly.c[-1] - self._meandO ) / numpy.sqrt(2.0 * 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.0)) ** (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.0 + 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.0 / numpy.sqrt(numpy.pi) * (numpy.exp(-(ll**2.0)) - numpy.exp(-(ul**2.0))) ) gaussxpolyInt[-2] = 1.0 / numpy.sqrt(numpy.pi) * ( numpy.exp(-(ll**2.0)) * ll - numpy.exp(-(ul**2.0)) * ul ) + 0.5 * (special.erf(ul) - special.erf(ll)) for jj in range(maxj - 2): gaussxpolyInt[-jj - 3] = ( 1.0 / numpy.sqrt(numpy.pi) * ( numpy.exp(-(ll**2.0)) * ll ** (jj + 2) - numpy.exp(-(ul**2.0)) * ul ** (jj + 2) ) + 0.5 * (jj + 2) * gaussxpolyInt[-jj - 1] ) return gaussxpolyInt def minOpar(self, dangle, tdisrupt=None, _return_raw=False): """ Return the approximate minimum parallel frequency at a given angle Parameters ---------- dangle : float Parallel angle tdisrupt : float, optional Disruption time (default is the value passed at initialization) _return_raw : bool, optional If True, return the index of the minimum frequency and the value of the minimum frequency (default is False) Returns ------- float or tuple Minimum frequency that gets to this parallel angle or a tuple with the index of the minimum frequency and the value of the minimum frequency Notes ----- - 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.0 + self._kick_interpdOpar_poly.c[-2] * self._timpact) + self._timpact ) lowx[lowx < 0.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 ): """ Calculate the mean frequency as a function of angle, assuming a uniform time distribution up to a maximum time. Parameters ---------- dangle : float Angle offset. oned : bool, optional If True, return the 1D offset from the progenitor (along the direction of disruption). Default is False. tdisrupt : float, optional Maximum time. Default is None. approx : bool, optional If True, compute the mean Omega by direct integration of the spline representation. Default is True. higherorder : object, optional Higher-order spline terms in the approximate computation. Default is object-wide default higherorderTrack. Returns ------- float Mean Omega. Notes ----- - 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.0 * self.pOparapar( T / (1 - T * T) * numpy.sqrt(self._sortedSigOEig[2]) + self._meandO, dangle, ), -1.0, 1.0, )[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.0 + self._kick_interpdOpar_poly.c[-2] * self._timpact) ) * self._density_par_approx(dangle, tdisrupt, _return_array=True) + numpy.sqrt(self._sortedSigOEig[2] / 2.0 / numpy.pi) / (1.0 + self._kick_interpdOpar_poly.c[-2] * self._timpact) ** 2.0 * ( numpy.exp( -0.5 * ( Oparb[:-1] - self._kick_interpdOpar_poly.c[-1] - (1.0 + self._kick_interpdOpar_poly.c[-2] * self._timpact) * (Oparb - numpy.roll(Oparb, -1))[:-1] - self._meandO ) ** 2.0 / self._sortedSigOEig[2] ) - numpy.exp( -0.5 * ( Oparb[:-1] - self._kick_interpdOpar_poly.c[-1] - self._meandO ) ** 2.0 / 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.0 / numpy.pi) * numpy.sqrt(self._sortedSigOEig[2]) * numpy.exp( -0.5 * (self._meandO - Oparb[0]) ** 2.0 / self._sortedSigOEig[2] ) + self._meandO * ( 1.0 + special.erf( (self._meandO - Oparb[0]) / numpy.sqrt(2.0 * 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: # pragma: no cover return 0.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.0 * self._sortedSigOEig[2]) ul = ( Oparb[:-1] - self._kick_interpdOpar_poly.c[-1] - self._meandO ) / numpy.sqrt(2.0 * 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.0)) ** (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.0 + 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.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.0 * numpy.pi ) return None def _determine_deltaAngleTrackImpact(self, deltaAngleTrackImpact, timpact): self._timpact = timpact deltaAngleTrackLim = ( (self._sigMeanOffset + 4.0) * 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.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.0 if ( self._gap_leading and self._progenitor_Omega_along_dOmega / self._sigMeanSign < 0.0 ) or ( not self._gap_leading and self._progenitor_Omega_along_dOmega / self._sigMeanSign > 0.0 ): self._gap_sigMeanSign = -1.0 # 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 ) self._nTrackChunksImpact = ( self._nTrackChunksImpact if self._nTrackChunksImpact >= 4 else 4 ) else: self._nTrackChunksImpact = nTrackChunksImpact dt = ( self._deltaAngleTrackImpact / self._progenitor_Omega_along_dOmega / self._sigMeanSign * self._gap_sigMeanSign ) self._gap_trackts = numpy.linspace( 0.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.0, ) # angle = 0 auxiliaryTrack = Orbit(prog_stream_offset[3]) if dt < 0.0: self._gap_trackts = numpy.linspace( 0.0, -2.0 * dt, 2 * self._nTrackChunksImpact - 1 ) # Flip velocities before integrating auxiliaryTrack = auxiliaryTrack.flip() auxiliaryTrack.integrate(self._gap_trackts, self._pot) if dt < 0.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.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.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.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.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.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): """ 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 Parameters ---------- v : numpy.ndarray velocity of the stream (nstar,3) y : numpy.ndarray position along the stream (nstar) b : float impact parameter w : numpy.ndarray velocity of the Plummer sphere (3) GM : float mass of the Plummer sphere (in natural units) rs : float size of the Plummer sphere Returns ------- numpy.ndarray velocity kick deltav (nstar,3) Notes ----- - 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.0 + tilew[:, 2] ** 2.0) wpar = numpy.sqrt(numpy.sum(v**2.0, axis=1)) - tilew[:, 1] wmag2 = wpar**2.0 + wperp**2.0 wmag = numpy.sqrt(wmag2) out = numpy.empty_like(v) denom = wmag * ((b**2.0 + rs**2.0) * wmag2 + wperp**2.0 * y**2.0) out[:, 0] = (b * wmag2 * tilew[:, 2] / wperp - y * wpar * tilew[:, 0]) / denom out[:, 1] = -(wperp**2.0) * y / denom out[:, 2] = -(b * wmag2 * tilew[:, 0] / wperp + y * wpar * tilew[:, 2]) / denom # deal w/ perpendicular impacts wperp0Indx = numpy.fabs(wperp) < 10.0**-10.0 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): """ 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 Parameters ---------- v : numpy.ndarray velocity of the stream (nstar,3) x : numpy.ndarray position along the stream (nstar,3) b : float impact parameter w : numpy.ndarray velocity of the Plummer sphere (3) x0 : numpy.ndarray point of closest approach v0 : numpy.ndarray velocity of point of closest approach GM : float mass of the Plummer sphere (in natural units) rs : float size of the Plummer sphere Returns ------- numpy.ndarray velocity kick deltav (nstar,3) Notes ----- - 2015-05-04 - Written based on above - Sanders (Cambridge) """ 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.0 / 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.0: raise ValueError("s must be positive in Hernquist X function") elif s < 1.0: return numpy.log((1 + numpy.sqrt(1 - s * s)) / s) / numpy.sqrt(1 - s * s) elif s == 1.0: return 1.0 else: return numpy.arccos(1.0 / s) / numpy.sqrt(s * s - 1) def impulse_deltav_hernquist(v, y, b, w, GM, rs): """ 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 Parameters ---------- v : numpy.ndarray velocity of the stream (nstar,3) y : numpy.ndarray position along the stream (nstar) b : float impact parameter w : numpy.ndarray velocity of the Hernquist sphere (3) GM : float mass of the Hernquist sphere (in natural units) rs : float size of the Hernquist sphere Returns ------- numpy.ndarray velocity kick deltav (nstar,3) Notes ----- - 2015-08-13 - Written using Wyn Evans calculation - Sanders (Cambridge) """ 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.0 + tilew[:, 2] ** 2.0) wpar = numpy.sqrt(numpy.sum(v**2.0, axis=1)) - tilew[:, 1] wmag2 = wpar**2.0 + wperp**2.0 wmag = numpy.sqrt(wmag2) B = numpy.sqrt(b**2.0 + wperp**2.0 * y**2.0 / wmag2) denom = wmag * (B**2 - rs**2) denom = 1.0 / denom s = numpy.sqrt(2.0 * B / (rs + B)) HernquistXv = numpy.vectorize(HernquistX) Xfac = 1.0 - 2.0 * 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.0) * 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.0**-10.0 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): """ 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 Parameters ---------- v : numpy.ndarray velocity of the stream (nstar,3) x : numpy.ndarray position along the stream (nstar,3) b : float impact parameter w : numpy.ndarray velocity of the Hernquist sphere (3) x0 : numpy.ndarray point of closest approach v0 : numpy.ndarray velocity of point of closest approach GM : float mass of the Hernquist sphere (in natural units) rs : float size of the Hernquist sphere Returns ------- numpy.ndarray velocity kick deltav (nstar,3) Notes ----- - 2015-08-13 - Written using Wyn Evans calculation - Sanders (Cambridge) """ 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.0 / denom s = numpy.sqrt(2.0 * B / (rs + B)) HernquistXv = numpy.vectorize(HernquistX) Xfac = 1.0 - 2.0 * 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.0) * X[compt] / r def _deltav_integrate(y, b, w, pot): return numpy.array( [ integrate.quad(_a_integrand, -1.0, 1.0, args=(y, b, w, pot, i))[0] for i in range(3) ] ) def impulse_deltav_general(v, y, b, w, pot): """ 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 Parameters ---------- v : numpy.ndarray velocity of the stream (nstar,3) y : numpy.ndarray position along the stream (nstar) b : float impact parameter w : numpy.ndarray velocity of the subhalo (3) pot : Potential object or list thereof Potential object or list thereof (should be spherical) Returns ------- numpy.ndarray velocity kick deltav (nstar,3) Notes ----- - 2015-05-04 - Written - Sanders (Cambridge) - 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.0, 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): """ Calculate the delta velocity to due an encounter with a general spherical potential in the impulse approximation; allows for arbitrary velocity vectors, and arbitrary position along the stream Parameters ---------- v : numpy.ndarray velocity of the stream (nstar,3) x : numpy.ndarray position along the stream (nstar,3) b : float impact parameter w : numpy.ndarray velocity of the subhalo (3) x0 : numpy.ndarray point of closest approach v0 : numpy.ndarray velocity of point of closest approach pot : Potential object or list thereof Potential object or list thereof (should be spherical) Returns ------- numpy.ndarray velocity kick deltav (nstar,3) Notes ----- - 2015-05-04 - Written - Sanders (Cambridge) - 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.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.0, nsamp=1000, integrate_method="symplec4_c", ): """ 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. Parameters ---------- v : numpy.ndarray velocity of the stream (nstar,3) x : numpy.ndarray position along the stream (nstar,3) b : float impact parameter w : numpy.ndarray velocity of the subhalo (3) x0 : numpy.ndarray position of closest approach v0 : numpy.ndarray velocity of point of closest approach pot : Potential object or list thereof Potential object or list thereof (should be spherical) tmax : float maximum integration time galpot : Potential object or list thereof galpy Potential object or list thereof tmaxfac : float multiple of rs/fabs(w - v0) to use for time integration interval nsamp : int number of forward integration points integrate_method : str orbit integrator to use (see Orbit.integrate) Returns ------- numpy.ndarray velocity kick deltav (nstar,3) Notes ----- - 2015-08-17 - Written - Sanders (Cambridge) """ 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.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.0), (nstar, nsamp)) / r)[ :, :, numpy.newaxis ] * X return integrate.simpson(acc, x=times, axis=1) def impulse_deltav_general_fullplummerintegration( v, x, b, w, x0, v0, galpot, GM, rs, tmaxfac=10.0, N=1000, integrate_method="symplec4_c", ): """ Calculate the delta velocity to due an encounter with a moving Plummer sphere and galactic potential relative to just in galactic potential by integrating each particle in the underlying galactic potential; allows for arbitrary velocity vectors and arbitrary shaped streams. Parameters ---------- v : numpy.ndarray velocity of the stream (nstar,3) x : numpy.ndarray position along the stream (nstar,3) b : float impact parameter w : numpy.ndarray velocity of the subhalo (3) x0 : numpy.ndarray position of closest approach v0 : numpy.ndarray velocity of point of closest approach galpot : Potential object or list thereof galpy Potential object or list thereof GM : float mass of Plummer rs : float scale of Plummer tmaxfac : float multiple of rs/fabs(w - v0) to use for time integration interval N : int number of forward integration points integrate_method : str orbit integrator to use (see Orbit.integrate) Returns ------- numpy.ndarray velocity kick deltav (nstar,3) Notes ----- - 2015-08-18 - Written - Sanders (Cambridge) """ 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.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.0) ) 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.0) 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.0) ) def impulse_deltav_plummerstream(v, y, b, w, GSigma, rs, tmin=None, tmax=None): """ 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 Parameters ---------- v : numpy.ndarray velocity of the stream (nstar,3) y : numpy.ndarray position along the stream (nstar) b : float impact parameter w : numpy.ndarray velocity of the Plummer sphere (3) GSigma : function surface density of the Plummer-softened stream (in natural units); should be a function of time rs : float size of the Plummer sphere tmin : float minimum time to consider for GSigma (need to be set) tmax : float maximum time to consider for GSigma (need to be set) Returns ------- numpy.ndarray velocity kick deltav (nstar,3) Notes ----- - 2015-11-14 - Written - Bovy (UofT) """ if len(v.shape) == 1: v = numpy.reshape(v, (1, 3)) y = numpy.atleast_1d(y) 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.0, 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.0 + tilew[:, 2] ** 2.0) wpar = numpy.sqrt(numpy.sum(v**2.0, axis=1)) - tilew[:, 1] wmag2 = wpar**2.0 + wperp**2.0 wmag = numpy.sqrt(wmag2) b2 = b**2.0 rs2 = rs**2.0 wperp2 = wperp**2.0 out = numpy.empty_like(v) out[:, 0] = [ 1.0 / 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.0 / 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.0 / 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 ): """ Calculate the delta velocity to due an encounter with a Plummer-softened stream 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 Parameters ---------- v : numpy.ndarray velocity of the stream (nstar,3) x : numpy.ndarray position along the stream (nstar,3) t : numpy.ndarray times at which (v,x) are reached, wrt the closest impact t=0 (nstar) b : float impact parameter w : numpy.ndarray velocity of the Plummer sphere (3) x0 : numpy.ndarray point of closest approach v0 : numpy.ndarray velocity of point of closest approach GSigma : function surface density of the Plummer-softened stream (in natural units); should be a function of time rs : float size of the Plummer sphere galpot : Potential object or list thereof galpy Potential object or list thereof tmin : float minimum time to consider for GSigma (need to be set) tmax : float maximum time to consider for GSigma (need to be set) Returns ------- numpy.ndarray velocity kick deltav (nstar,3) Notes ----- - 2015-11-14 - Written - 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.0, numpy.fabs(numpy.amin(t) + tmin), 101) o.integrate(ts, galpot) o = o(ts[-1]).flip() ts = numpy.linspace(0.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.0, 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=1741018131.0 galpy-1.10.2/galpy/df/streamspraydf.py0000644000175100001660000006577314761352023017302 0ustar00runnerdockerimport warnings import numpy from ..df.df import df from ..orbit import Orbit from ..potential import MovingObjectPotential, 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 basestreamspraydf(df): def __init__( self, progenitor_mass, progenitor=None, pot=None, rtpot=None, tdisrupt=None, leading=True, center=None, centerpot=None, progpot=None, ro=None, vo=None, ): """ Initialize a stream spray DF model of a tidal stream Parameters ---------- progenitor_mass : float or Quantity Mass of the progenitor. progenitor : galpy.orbit.Orbit, optional Progenitor orbit as Orbit instance (will be re-integrated, so don't bother integrating the orbit before). pot : galpy.potential.Potential or list of such instances, optional Potential for integrating orbits. rtpot : galpy.potential.Potential or list of such instances, optional 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). tdisrupt : float or Quantity, optional Time since start of disruption. Default is 5 Gyr. leading : bool, optional If True, model the leading part of the stream. If False, model the trailing part. Default is True. center : galpy.orbit.Orbit, optional 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 : galpy.potential.Potential or list of such instances, optional 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). progpot : galpy.potential.Potential or list of such instances or None, optional Potential for the progenitor. Ignored if None. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 2018-07-31 - Written - Bovy (UofT) - 2021-05-05 - Added center keyword - Yansong Qian (UofT) - 2024-08-11 - Generalized to allow different particle-spray methods - Yingtian Chen (UMich) """ super().__init__(ro=ro, vo=vo) self._progenitor_mass = conversion.parse_mass( progenitor_mass, ro=self._ro, vo=self._vo ) self._tdisrupt = ( 5.0 / 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 basestreamspraydf object being initialized" ) assert conversion.physical_compatible(self, self._rtpot), ( "Physical conversion for the rt potential is not consistent with that of the basestreamspraydf 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 basestreamspraydf 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.0, -self._tdisrupt, 10001) self._progenitor.integrate(self._progenitor_times, self._pot) self._leading = leading # 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 basestreamspraydf object being initialized" ) self._center = center() self._center.turn_physical_off() self._center.integrate(self._progenitor_times, self._centerpot) else: self._center = None if progpot is not None: progtrajpot = MovingObjectPotential( orbit=self._progenitor, pot=progpot, ro=self._ro, vo=self._vo, ) self._pot = self._pot + progtrajpot return None def sample(self, n, return_orbit=True, returndt=False, integrate=True): """ Sample from the DF Parameters ---------- n : int Number of points to return. return_orbit : bool, optional If True, the output phase-space positions is an orbit.Orbit object. If False, the output is (R,vR,vT,z,vz,phi). Default is True. returndt : bool, optional If True, also return the time since the star was stripped. Default is False. integrate : bool, optional 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!). Default is True. Returns ------- Orbit, numpy.ndarray, or tuple 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 Notes ----- - 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) - 2024-08-11 - Include the progenitor's potential - Yingtian Chen (Umich) """ # 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 ) # generate the initial conditions xst, yst, zst, vxst, vyst, vzst = self.spray_df(xyzpt, vxyzpt, dt) 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.0, 10001), self._pot) o = o(0.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.0, axis=1)), (3, 1)).T z_rot = numpy.swapaxes( _rotate_to_arbitrary_vector( numpy.atleast_2d(Lnorm), [0.0, 0.0, 1], inv=True ), 1, 2, ) z_rot_inv = numpy.swapaxes( _rotate_to_arbitrary_vector( numpy.atleast_2d(Lnorm), [0.0, 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.0 + xyzt[:, 1] ** 2.0) 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) def _calc_rtide(self, Rpt, phipt, Zpt, dt): try: rtides = rtide( self._rtpot, Rpt, Zpt, phi=phipt, t=-dt, M=self._progenitor_mass, 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)) ] ) return rtides def _calc_vc(self, Rpt, phipt, Zpt, dt): try: vcs = numpy.sqrt( -Rpt * evaluateRforces( self._rtpot, Rpt, Zpt, phi=phipt, t=-dt, use_physical=False ) ) except (ValueError, TypeError): 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)) ] ) return vcs def spray_df(self, xyzpt, vxyzpt, dt): """ Sample the positions and velocities around the progenitor Must be implemented in a subclass Parameters ---------- xyzpt : array, shape (N,3) Positions of progenitor in the progenitor coordinates. vxyzpt : array, shape (N,3) Velocities of progenitor in the progenitor coordinates. dt : array, shape (N,) Time of sampling. Returns ------- xst, yst, zst : array, shape (N,) Positions of points on the stream in the progenitor coordinates. vxst, vyst, vzst : array, shape (N,) Velocities of points on the stream in the progenitor coordinates. """ raise NotImplementedError class chen24spraydf(basestreamspraydf): def __init__( self, progenitor_mass, progenitor=None, pot=None, rtpot=None, tdisrupt=None, leading=True, center=None, centerpot=None, progpot=None, mean=None, cov=None, ro=None, vo=None, ): """ Initialize a `Chen et al. (2024) `_ stream spray DF model of a tidal stream. Parameters ---------- progenitor_mass : float or Quantity Mass of the progenitor. progenitor : galpy.orbit.Orbit, optional Progenitor orbit as Orbit instance (will be re-integrated, so don't bother integrating the orbit before). pot : galpy.potential.Potential or list of such instances, optional Potential for integrating orbits. rtpot : galpy.potential.Potential or list of such instances, optional 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). tdisrupt : float or Quantity, optional Time since start of disruption. Default is 5 Gyr. leading : bool, optional If True, model the leading part of the stream. If False, model the trailing part. Default is True. center : galpy.orbit.Orbit, optional 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 : galpy.potential.Potential or list of such instances, optional 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). progpot : galpy.potential.Potential or list of such instances or None, optional Potential for the progenitor. Ignored if None. mean : None or array, shape (6,), optional Means of the multivariate Gaussian distribution (angles in radians). If None, use the default values. cov : None or array, shape (6,6), optional Covariance of the multivariate Gaussian distribution (angles in radians). If None, use the default values. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 2024-08-11 - Written - Yingtian Chen (UMich) """ super().__init__( progenitor_mass=progenitor_mass, progenitor=progenitor, pot=pot, rtpot=rtpot, tdisrupt=tdisrupt, leading=leading, center=center, centerpot=centerpot, progpot=progpot, ro=ro, vo=vo, ) if mean is None: self._mean = numpy.array([1.6, -0.523599, 0, 1, 0.349066, 0]) else: self._mean = mean if cov is None: self._cov = numpy.array( [ [0.1225, 0, 0, 0, -0.085521, 0], [0, 0.161143, 0, 0, 0, 0], [0, 0, 0.043865, 0, 0, 0], [0, 0, 0, 0, 0, 0], [-0.085521, 0, 0, 0, 0.121847, 0], [0, 0, 0, 0, 0, 0.147435], ] ) else: self._cov = cov return None def spray_df(self, xyzpt, vxyzpt, dt): """ Sample the positions and velocities around the progenitor Parameters ---------- xyzpt : array, shape (N,3) Positions of progenitor in the progenitor coordinates. vxyzpt : array, shape (N,3) Velocities of progenitor in the progenitor coordinates. dt : array, shape (N,) Time of sampling. Returns ------- xst, yst, zst : array, shape (N,) Positions of points on the stream in the progenitor coordinates. vxst, vyst, vzst : array, shape (N,) Velocities of points on the stream in the progenitor coordinates. """ Rpt, phipt, Zpt = coords.rect_to_cyl(xyzpt[:, 0], xyzpt[:, 1], xyzpt[:, 2]) rtides = self._calc_rtide(Rpt, phipt, Zpt, dt) # Sample positions and velocities in the instantaneous frame posvel = numpy.random.multivariate_normal(self._mean, self._cov, size=len(dt)) Dr = posvel[:, 0] * rtides v_esc = numpy.sqrt(2 * self._progenitor_mass / Dr) Dv = posvel[:, 3] * v_esc if self._leading: Dr *= -1.0 Dv *= -1.0 dR, dz, dp = coords.spher_to_cyl( r=Dr, theta=0.5 * numpy.pi - posvel[:, 2], phi=posvel[:, 1] ) dx, dy, dz = coords.cyl_to_rect(R=dR, phi=dp, Z=dz) dvR, dvz, dvp = coords.spher_to_cyl( r=Dv, theta=0.5 * numpy.pi - posvel[:, 5], phi=posvel[:, 4] ) dvx, dvy, dvz = coords.cyl_to_rect(R=dvR, phi=dvp, Z=dvz) return ( xyzpt[:, 0] + dx, xyzpt[:, 1] + dy, xyzpt[:, 2] + dz, vxyzpt[:, 0] + dvx, vxyzpt[:, 1] + dvy, vxyzpt[:, 2] + dvz, ) class fardal15spraydf(basestreamspraydf): def __init__( self, progenitor_mass, progenitor=None, pot=None, rtpot=None, tdisrupt=None, leading=True, center=None, centerpot=None, progpot=None, meankvec=[2.0, 0.0, 0.3, 0.0, 0.0, 0.0], sigkvec=[0.4, 0.0, 0.4, 0.5, 0.5, 0.0], ro=None, vo=None, ): """ Initialize a `Fardal et al. (2015) `_ stream spray DF model of a tidal stream. Parameters ---------- progenitor_mass : float or Quantity Mass of the progenitor. progenitor : galpy.orbit.Orbit, optional Progenitor orbit as Orbit instance (will be re-integrated, so don't bother integrating the orbit before). pot : galpy.potential.Potential or list of such instances, optional Potential for integrating orbits. rtpot : galpy.potential.Potential or list of such instances, optional 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). tdisrupt : float or Quantity, optional Time since start of disruption. Default is 5 Gyr. leading : bool, optional If True, model the leading part of the stream. If False, model the trailing part. Default is True. center : galpy.orbit.Orbit, optional 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 : galpy.potential.Potential or list of such instances, optional 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). progpot : galpy.potential.Potential or list of such instances or None, optional Potential for the progenitor. Ignored if None. meankvec : list or array, optional Mean of the action-angle distribution. Default is [2.0, 0.0, 0.3, 0.0, 0.0, 0.0]. sigkvec : list or array, optional Dispersion of the action-angle distribution. Default is [0.4, 0.0, 0.4, 0.5, 0.5, 0.0]. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 2018-07-31 - Written - Bovy (UofT) - 2021-05-05 - Added center keyword - Yansong Qian (UofT) """ super().__init__( progenitor_mass=progenitor_mass, progenitor=progenitor, pot=pot, rtpot=rtpot, tdisrupt=tdisrupt, leading=leading, center=center, centerpot=centerpot, progpot=progpot, ro=ro, vo=vo, ) self._meankvec = numpy.array(meankvec) self._sigkvec = numpy.array(sigkvec) if leading: self._meankvec *= -1.0 return None def spray_df(self, xyzpt, vxyzpt, dt): """ Sample the positions and velocities around the progenitor Parameters ---------- xyzpt : array, shape (N,3) Positions of progenitor in the progenitor coordinates. vxyzpt : array, shape (N,3) Velocities of progenitor in the progenitor coordinates. dt : array, shape (N,) Time of sampling. Returns ------- xst, yst, zst : array, shape (N,) Positions of points on the stream in the progenitor coordinates. vxst, vyst, vzst : array, shape (N,) Velocities of points on the stream in the progenitor coordinates. """ Rpt, phipt, Zpt = coords.rect_to_cyl(xyzpt[:, 0], xyzpt[:, 1], xyzpt[:, 2]) rtides = self._calc_rtide(Rpt, phipt, Zpt, dt) vcs = self._calc_vc(Rpt, phipt, Zpt, dt) rtides_as_frac = rtides / Rpt 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=(len(dt), 6)) * self._sigkvec RpZst = numpy.array( [ Rpt + k[:, 0] * rtides, phipt + k[:, 5] * rtides_as_frac, k[:, 3] * rtides_as_frac, ] ).T vRTZst = numpy.array( [ vRpt * (1.0 + 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] ) return xst, yst, zst, vxst, vyst, vzst class streamspraydf(fardal15spraydf): def __init__(self, args, **kwargs): """ For backward compatibility """ super().__init__(args, **kwargs) warnings.warn( "Class `streamspraydf` will be deprecated in version 1.11. " "Please use class `fardal15spraydf` for the Fardal+15 particle spray model.", DeprecationWarning, stacklevel=1, ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/df/surfaceSigmaProfile.py0000644000175100001660000001403514761352023020331 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): """ Return the format-strings to use when writing the parameters of this profile. Returns ------- tuple of str Tuple of format-strings to use for each parameter in self._params. Notes ----- This function defaults to '%6.4f' for each parameter in self._params. - 2010-03-28 - Written - Bovy (NYU) """ out = [] for param in self._params: out.append("%6.4f") return out def outputParams(self): """ Return a tuple of parameters of this profile, to create filenames. Returns ------- tuple Tuple of parameters (given to % ...). Notes ----- - 2010-03-28 - Written - Bovy """ return tuple(self._params) def surfacemass(self, R, log=False): """ Return the surface density profile at this R. Parameters ---------- R : float Galactocentric radius (/ro). log : bool, optional If True, return the log (default: False). Returns ------- float Sigma(R) Notes ----- - 2010-03-26 - Written - Bovy (NYU) """ raise NotImplementedError( "'surfacemass' function not implemented for this surfaceSigmaProfile class" ) def sigma2(self, R, log=False): """ Return the radial velocity variance at this R. Parameters ---------- R : float Galactocentric radius (/ro). log : bool, optional If True, return the log (default: False). Returns ------- float sigma^2(R). Notes ----- - 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.0 / 3.0, 1.0, 0.2)): """ Initialize an exponential surface density and sigma_R^2 profile. Parameters ---------- params : tuple/list Tuple/list of (surface scale-length, sigma scale-length, sigma(ro)) (note: *not* sigma2 scale-length) Notes ----- - 2010-03-26 - Written - Bovy (NYU) """ surfaceSigmaProfile.__init__(self) self._params = params def surfacemass(self, R, log=False): """ Return the surface density profile at this R. Parameters ---------- R : float Galactocentric radius (/ro). log : bool, optional If True, return the log (default: False). Returns ------- Sigma(R) : float Surface density profile at this R. Notes ----- - 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): """ Return the derivative wrt R of the surface density profile at this R. Parameters ---------- R : float Galactocentric radius (/ro). log : bool, optional If True, return the derivative of the log (default: False). Returns ------- float Sigma'(R) or (log Sigma(r) )'. Notes ----- - 2010-03-26 - Written - Bovy (NYU). """ if log: return -1.0 / self._params[0] else: return -numpy.exp(-R / self._params[0]) / self._params[0] def sigma2(self, R, log=False): """ Return the radial velocity variance at this R. Parameters ---------- R : float Galactocentric radius (/ro). log : bool, optional If True, return the log (default: False). Returns ------- float sigma^2(R). Notes ----- - 2010-03-26 - Written - Bovy (NYU) """ if log: return 2.0 * numpy.log(self._params[2]) - 2.0 * (R - 1.0) / self._params[1] else: return self._params[2] ** 2.0 * numpy.exp( -2.0 * (R - 1.0) / self._params[1] ) def sigma2Derivative(self, R, log=False): """ Return the derivative wrt R of the sigma_R^2 profile at this R Parameters ---------- R : float Galactocentric radius (/ro) log : bool, optional If True, return the derivative of the log (default: False) Returns ------- float Sigma_R^2'(R) or (log Sigma_R^2(r) )' Notes ----- - 2011-03-24 - Written - Bovy (NYU) """ if log: return -2.0 / self._params[1] else: return ( self._params[2] ** 2.0 * numpy.exp(-2.0 * (R - 1.0) / self._params[1]) * (-2.0 / self._params[1]) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6498842 galpy-1.10.2/galpy/orbit/0000755000175100001660000000000014761352040014547 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/orbit/Orbits.py0000644000175100001660000122425114761352023016373 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.plotEscapecurve import _INF 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, _AQ_GT_47, _ASTROQUERY_LOADED, _NUMEXPR_LOADED, ) from ..util.conversion import ( physical_compatible, physical_conversion, physical_conversion_tuple, ) from ..util.coords import _K from .integrateFullOrbit import ( integrateFullOrbit, integrateFullOrbit_c, integrateFullOrbit_sos, integrateFullOrbit_sos_c, ) from .integrateLinearOrbit import ( _ext_loaded, integrateLinearOrbit, integrateLinearOrbit_c, ) from .integratePlanarOrbit import ( integratePlanarOrbit, integratePlanarOrbit_c, integratePlanarOrbit_dxdv, integratePlanarOrbit_sos, integratePlanarOrbit_sos_c, ) 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() # Plot labeling dictionaries _labeldict_physical = { "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)$", } _labeldict_internal = { "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_radec = { "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})$", } # 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, ): """ Initialize an Orbit instance. Parameters ---------- vxvv : numpy.ndarray, optional Initial conditions (must all have the same phase-space dimension); can be either: - astropy (>v3.0) SkyCoord with arbitrary shape, including velocities (note that this turns *on* physical output even if ro and vo are not given) - 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); 3. [ra,dec,d,U,V,W] in [deg,deg,kpc,km/s,km/s,kms]; (for Quantity input; see 'list' option below); ICRS frame 4. (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) 5. [l,b,d,U,V,W] in [deg,deg,kpc,km/s,km/s,kms]; (for Quantity input; see 'list' option below) 6. And 5) also work when leaving out b and mu_b/W - lists of initial conditions, entries can be: 1. Individual Orbit instances (of single objects) 2. Regular or 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 ro : float or Quantity, optional Distance from vantage point to Galactic center (kpc; can be an array with the same shape as the Orbit itself). vo : float or Quantity, optional Circular velocity at ro (km/s; can be an array with the same shape as the Orbit itself). zo : float or Quantity, optional Offset toward the NGP of the Sun wrt the plane in kpc; default = 20.8 pc from Bennett & Bovy 2019). Can be an array with the same shape as the Orbit itself solarmotion : str, numpy.ndarray or Quantity, optional 'hogg' or 'dehnen', or 'schoenrich', or value in [-U,V,W] in km/s. Can be an array with the same shape as the Orbit itself radec : bool, optional If set, treat input as being in ICRS coordinates [ra,dec,d,mu_ra, mu_dec,vlos] in [deg,deg,kpc,mas/yr,mas/yr,km/s] (mu_ra = mu_ra * cos dec). lb : bool, optional If set, treat input as being in Galactic coordinates (l,b,d,mu_l, mu_b, vlos) in [deg,deg,kpc,mas/yr,mas/yr,km/s) (mu_l = mu_l * cos b). uvw : bool, optional If set, treat velocity part of radec or lb input as [U,V,W] in km/s. Returns ------- instance Notes ----- - 2010-07-XX - Original version started - Bovy (NYU) - 2018-10-13 - Start of re-write to allow multiple orbits - 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-03-19 - Allow array vxvv and arbitrary shapes - Bovy (UofT) - 2023-07-20 - Allowed ro/zo/vo/solarmotion input to be arrays with the same shape as the Orbit itself - 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, 0.0, 0.0, 0.0]) radec = True self._name = numpy.char.array(["Sun"]) elif isinstance(vxvv, (list, tuple)): # Robust way to check for None in case of a list of arrays (None in # doesn't work then for some reason) if any(elem is None for elem in vxvv): vxvv = [ [0.0, 0.0, 0.0, 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)): raise_diffphasedim_error = False if isinstance(vxvv[0], Orbit): try: vxvv = self._setup_parse_listofOrbits(vxvv, ro, vo, zo, solarmotion) input_shape = (len(vxvv),) vxvv = numpy.array(vxvv) except ValueError: raise_diffphasedim_error = True 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) elif isinstance(vxvv[0], numpy.ndarray): input_shape = vxvv[0].shape vxvv = numpy.array(vxvv).T else: input_shape = (len(vxvv),) try: vxvv = numpy.array(vxvv) except ValueError: raise_diffphasedim_error = True if ( isinstance(vxvv, numpy.ndarray) and vxvv.dtype == "object" ) or raise_diffphasedim_error: # 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 # Check that ro/zo/vo/solarmotion have the same shape as the vxvv inputs (if they are arrays) for attr in ["_ro", "_zo", "_vo"]: if ( isinstance(self.__dict__[attr], numpy.ndarray) and self.__dict__[attr].ndim > 0 ): if self.__dict__[attr].shape != self.shape: raise RuntimeError( f"{attr[1:]} must have the same shape as the input orbits for an array of orbits" ) else: self.__dict__[attr] = self.__dict__[attr].flatten() if isinstance(self._solarmotion, numpy.ndarray) and self._solarmotion.ndim > 1: if self._solarmotion.shape[1:] != self.shape: raise RuntimeError( "solarmotion must have the shape [3,...] where the ... matches the shape of the input orbits for an array of orbits" ) else: self._solarmotion = self._solarmotion.reshape( ( self._solarmotion.shape[0], numpy.prod(self._solarmotion.shape[1:]), ) ) 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" ) # Print warning when SkyCoord doesn't come with values for galcen_distance, z_sun, and galcen_v_sun (issue #709) if ( vxvv.z_sun is None or vxvv.galcen_distance is None or vxvv.galcen_v_sun is None ): apy_coordframe_warning = "Supplied SkyCoord does not contain (" apy_coordframe_warning_keywords = "" print_apy_coordframe_warning = 0 print_apy_coordframe_warning_dict = { "are": "is", "these": "this", "were": "was", "values": "value", } if vxvv.galcen_distance is None and ro is None: apy_coordframe_warning += "galcen_distance, " apy_coordframe_warning_keywords += "ro, " print_apy_coordframe_warning += 1 if vxvv.z_sun is None and zo is None: apy_coordframe_warning += "z_sun, " apy_coordframe_warning_keywords += "zo, " print_apy_coordframe_warning += 1 if vxvv.galcen_v_sun is None and solarmotion is None: apy_coordframe_warning += "galcen_v_sun, " apy_coordframe_warning_keywords += "vo, solarmotion, " print_apy_coordframe_warning += 1 if print_apy_coordframe_warning: if print_apy_coordframe_warning > 1: print_apy_coordframe_warning_dict = { "are": "are", "these": "these", "were": "were", "values": "values", } warnings.warn( apy_coordframe_warning[:-2] + f") and {print_apy_coordframe_warning_dict['these']} {print_apy_coordframe_warning_dict['were']} not explicitly set in the Orbit initialization using the keywords ({apy_coordframe_warning_keywords[:-2]}); {print_apy_coordframe_warning_dict['these']} {print_apy_coordframe_warning_dict['are']} required for Orbit initialization; proceeding with default {print_apy_coordframe_warning_dict['values']}", galpyWarning, ) 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.0 if ro is None and not vxvv.galcen_distance is None: ro = numpy.sqrt( vxvv.galcen_distance.to(units.kpc).value ** 2.0 - zo**2.0 ) elif ( not vxvv.galcen_distance is None and numpy.fabs( ro**2.0 + zo**2.0 - vxvv.galcen_distance.to(units.kpc).value ** 2.0 ) > 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.0, 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( numpy.array(solarmotion) if isinstance(solarmotion, list) else 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 when initializing with a list of Orbits 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 when initializing with a list of Orbits 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 when initializing with a list of Orbits 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 when initializing with a list of Orbits 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.0 + self._zo**2.0) * 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( [ self._solarmotion[0] / self._vo, 1.0 + self._solarmotion[1] / self._vo, self._solarmotion[2] / 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.0, 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): """ Construct an orbit from the name of an object or a list of names. Parameters ---------- name : str or list The name of the object or list of names. When loading a collection of objects (like 'mwglobularclusters'), lists are not allowed. ro : float or Quantity, optional Distance from vantage point to Galactic center (kpc). vo : float or Quantity, optional Circular velocity at ro (km/s; can be Quantity). zo : float or Quantity, optional Offset toward the NGP of the Sun wrt the plane in kpc; default = 20.8 pc from Bennett & Bovy 2019). solarmotion : str, numpy.ndarray or Quantity, optional Solar motion. Can be 'hogg' or 'dehnen', or 'schoenrich', or value in [-U,V,W] in km/s. Returns ------- Orbit An orbit containing the phase space coordinates of the named object. Notes ----- - 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, ): """ Initialize an Orbit using a fit to data. Parameters ---------- init_vxvv : numpy.ndarray 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 : numpy.ndarray [:,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 : numpy.ndarray, optional [:,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, DissipativeForce, or list of such instances, optional Gravitational field to integrate orbits in. radec : bool, optional If set, treat input as being in ICRS coordinates [ra,dec,d,mu_ra, mu_dec,vlos] in [deg,deg,kpc,mas/yr,mas/yr,km/s] (mu_ra = mu_ra * cos dec). lb : bool, optional If set, treat input as being in Galactic coordinates (l,b,d,mu_l, mu_b, vlos) in [deg,deg,kpc,mas/yr,mas/yr,km/s) (mu_l = mu_l * cos b). customsky : bool, optional 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. lb_to_customsky : function, optional 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, optional 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. tintJ : float, optional Time to integrate orbits for fitting the orbit (can be Quantity). ntintJ : int, optional Number of time-integration points. integrate_method : str, optional Integration method to use (default: 'dopr54_c'; see galpy.orbit.Orbit.integrate). ro : float or Quantity, optional Distance from vantage point to Galactic center (kpc). vo : float or Quantity, optional Circular velocity at ro (km/s; can be Quantity). zo : float or Quantity, optional Offset toward the NGP of the Sun wrt the plane in pc; default = 20.8 pc from Bennett & Bovy 2019). solarmotion : str, numpy.ndarray or Quantity, optional 'hogg' or 'dehnen', or 'schoenrich', or value in [-U,V,W] in km/s. disp : bool, optional Display the optimizer's convergence message. Returns ------- Orbit An orbit that is the best fit to the given data. Notes ----- - 2014-06-17 - Written - Bovy (IAS) - 2019-05-22 - Incorporated into new Orbit class as from_fit - Bovy (UofT) See Also -------- galpy.orbit.Orbit.integrate """ 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): """ Return the dimension of the Orbit. Returns ------- int Dimension of the orbit. Notes ----- - 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): """ Return the phase-space dimension of the problem. Returns ------- int Phase-space dimension (2 for 1D, 3 for 2D-axi, 4 for 2D, 5 for 3D-axi, 6 for 3D). Notes ----- - 2018-12-20: Written by Bovy (UofT). """ return self.vxvv.shape[-1] def __getattr__(self, name): """ Get or evaluate an attribute for this Orbit instance. Parameters ---------- name : str Name of the attribute. Returns ------- function or list If the attribute is callable, a function to evaluate the attribute for each Orbit; otherwise a list of attributes. Notes ----- - 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, DissipativeForce or list of such instances, optional Gravitational field to use. Default is the gravitational field used to integrate the orbit. normed : bool, optional if set, plot {quant}(t)/{quant}(0) rather than {quant}(t) """.format(quant=name.split("plot")[1]) else: Estring = "" _plot.__doc__ = """Plot {quant}(t) along the orbit. Parameters ---------- d1 : str or callable, optional First dimension to plot. Can be a string ('x', 'y', 'R', 'vR', 'vT', 'z', 'vz', ...), an expression like 'R*vR', or a user-defined function of time (e.g., lambda t: o.R(t) for R). Default is determined by the number of dimensions in the orbit. {Estring}ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. *args : optional Additional arguments to pass to galpy.util.plot.plot. **kwargs : optional Additional keyword arguments to pass to galpy.util.plot.plot. Returns ------- None Sends plot to output device. Notes ----- - 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): """ Get a subset of this instance's orbits. Parameters ---------- key : slice The slice of the orbits to get. Returns ------- Orbit A new Orbit instance with the subset of orbits. Notes ----- - 2018-12-31: Written by 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 = {} # Single vs. individual time arrays if len(self.t.shape) < len(self.orbit.shape) - 1: integrate_kwargs["t"] = self.t else: integrate_kwargs["t"] = self.t[flat_indx_array] 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): """ Change the shape of the Orbit instance. Parameters ---------- newshape : int or tuple of ints New shape (see numpy.reshape). Returns ------- None Re-shaping is done in-place. Notes ----- - 2019-03-20: Written by 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): """ Turn off automatic returning of outputs in physical units. Parameters ---------- None Returns ------- None Notes ----- - 2019-02-28 - Written - Bovy (UofT) """ self._roSet = False self._voSet = False return None def turn_physical_on(self, ro=None, vo=None): """ Turn on automatic returning of outputs in physical units. Parameters ---------- ro : float, Quantity or bool, optional Physical scale in kpc for distances to use to convert. If False, do not set. vo : float, Quantity or bool, optional Physical scale for velocities in km/s to use to convert. If False, do not set. Returns ------- None Notes ----- - 2019-02-28: Written by 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 ro = conversion.parse_length_kpc(ro) if not ro is None: self._ro = ro if not vo is False: self._voSet = True vo = conversion.parse_velocity_kms(vo) if not vo is None: self._vo = vo return None @staticmethod def check_integrator(method, no_symplec=False): valid_methods = [ "odeint", "leapfrog", "dop853", "leapfrog_c", "symplec4_c", "symplec6_c", "rk4_c", "rk6_c", "dopr54_c", "dop853_c", "ias15_c", ] if no_symplec: symplec_methods = [ "leapfrog", "leapfrog_c", "symplec4_c", "symplec6_c", "ias15_c", # practically speaking, ias15 has the same limitations as symplectic integrators in galpy ] [valid_methods.remove(symplec_method) for symplec_method in symplec_methods] if method.lower() not in valid_methods: raise ValueError(f"{method:s} is not a valid `method`") return None @staticmethod def _check_method_c_compatible(method, pot): if "_c" in method: if not ext_loaded or not _check_c(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, ) return method @staticmethod def _check_method_dissipative_compatible(method, pot): if _isDissipative(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, ) return method @staticmethod def _check_array_evenlyspaced(t, method): # for ODE integrators if method in ["odeint", "dop853", "dop853_c"]: return True else: diffs = numpy.diff(t, axis=-1) return numpy.all( numpy.isclose(diffs, numpy.expand_dims(diffs[..., 0], axis=-1)) ) def integrate( self, t, pot, method="symplec4_c", progressbar=True, dt=None, numcores=_NUMCORES, force_map=False, ): """ Integrate the orbit instance with multiprocessing. Parameters ---------- t : list, numpy.ndarray or Quantity List of equispaced times at which to compute the orbit. The initial condition is t[0]. (note that for method='odeint', method='dop853', and method='dop853_c', the time array can be non-equispaced). pot : Potential, DissipativeForce or list of such instances Gravitational field to integrate the orbit in. method : str, optional Integration method to use. Default is 'symplec4_c'. See Notes for more information. progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!). Default is True. dt : int or Quantity, optional 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 : int, optional Number of cores to use for Python-based multiprocessing (pure Python or using force_map=True). Default is OMP_NUM_THREADS. force_map : bool, optional If True, force use of Python-based multiprocessing (not recommended). Default is False. Returns ------- None Get the actual orbit using getOrbit() or access the individual attributes (e.g., R, vR, etc.). Notes ----- - Possible integration methods are: - '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 - 'ias15_c' for an adaptive 15th order integrator using Gauß-Radau quadrature (see IAS15 paper) in C - 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) """ self.check_integrator(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 # Check that t is evenly spaced if not using odeint if not self._check_array_evenlyspaced(t, method): raise ValueError( f"Input time array must be equally spaced for method {method}, use method='dop853_c', method='dop853', or method='odeint' instead for non-equispaced time arrays" ) 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 self.dim() == 2: thispot = toPlanarPotential(pot) else: thispot = pot self.t = numpy.array(t) self._pot = thispot method = self._check_method_c_compatible(method, self._pot) method = self._check_method_dissipative_compatible(method, self._pot) # 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_SOS( self, psi, pot, surface=None, t0=0.0, method="dop853_c", progressbar=True, numcores=_NUMCORES, force_map=False, ): """ Integrate this Orbit instance using an independent variable suitable to creating surfaces-of-section. Parameters ---------- psi : list, numpy.ndarray or Quantity Equispaced list of increment angles over which to integrate [increments wrt initial angle]. (note that for method='odeint', method='dop853', and method='dop853_c', the psi array can be non-equispaced). pot : Potential, DissipativeForce or list of such instances Gravitational field to integrate the orbit in. surface : str, optional Surface to punch through (this has no effect in 3D, where the surface is always z=0, but in 2D it can be 'x' or 'y' for x=0 or y=0). t0 : float or Quantity, optional Initial time. method : {'odeint', 'dop853_c', 'dop853', 'rk4_c', 'rk6_c', 'dop54_c'}, optional Integration method to use. Default is 'dop853_c'. See Notes for more information. progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!). numcores : int, optional Number of cores to use for Python-based multiprocessing (pure Python or using force_map=True); default = OMP_NUM_THREADS. force_map : bool, optional If True, force use of Python-based multiprocessing (not recommended). Returns ------- None Get the actual orbit using getOrbit() or access the individual attributes (e.g., R, vR, etc.). Notes ----- - Possible integration methods are: - '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 - 'dop853' for a 8-5-3 Dormand-Prince integrator in Python - 'dop853_c' for a 8-5-3 Dormand-Prince integrator in C - 2023-03-16 - Written - Bovy (UofT) """ if self.dim() == 1: raise NotImplementedError("SOS integration is not supported for 1D orbits") self.check_integrator(method, no_symplec=True) pot = flatten_potential(pot) _check_potential_dim(self, pot) _check_consistent_units(self, pot) # Parse psi if _APY_LOADED and isinstance(psi, units.Quantity): psi = conversion.parse_angle(psi) # Check that psi is evenly spaced if not self._check_array_evenlyspaced(psi, method): raise ValueError( f"Input psi array must be equally spaced for method {method}, use method='dop853_c', method='dop853', or method='odeint' instead for non-equispaced psi arrays" ) if _APY_LOADED and isinstance(t0, units.Quantity): t0 = conversion.parse_time(t0, ro=self._ro, vo=self._vo) self._integrate_t_asQuantity = False 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, ) # Delete attributes for interpolation and rperi etc. determination if hasattr(self, "_orbInterp"): delattr(self, "_orbInterp") if self.dim() == 2: thispot = toPlanarPotential(pot) else: thispot = pot self._psi = numpy.array(psi) self._pot = thispot method = self._check_method_c_compatible(method, self._pot) method = self._check_method_dissipative_compatible(method, self._pot) # Implementation with parallel_map in Python if not "_c" in method or not ext_loaded or force_map: if self.dim() == 2: out, msg = integratePlanarOrbit_sos( self._pot, self.vxvv, self._psi, t0, method, surface=surface, progressbar=progressbar, numcores=numcores, ) else: out, msg = integrateFullOrbit_sos( self._pot, self.vxvv, self._psi, t0, method, progressbar=progressbar, numcores=numcores, ) else: warnings.warn( "Using C implementation to integrate orbits", galpyWarningVerbose ) 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_sos_c( self._pot, vxvvs, self._psi, t0, method, surface=surface, progressbar=progressbar, ) else: out, msg = integrateFullOrbit_sos_c( self._pot, vxvvs, self._psi, t0, method, progressbar=progressbar ) if self.phasedim() == 3 or self.phasedim() == 5: phi_mask = numpy.ones(out.shape[2], dtype="bool") phi_mask[3 + 2 * (self.phasedim() == 5)] = False out = out[:, :, phi_mask] # Store orbit internally self.orbit = out[:, :, :-1] self.t = out[:, :, -1] # Quick check that all is well in terms of psi increasing with time assert numpy.all( (numpy.roll(self.t, -1, axis=1) - self.t)[:, :-1] * (numpy.roll(self._psi.T, -1, axis=0) - self._psi.T)[:-1].T > 0.0 ), ( "SOS integration failed (time does not monotonically increase with increasing psi)" ) 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, ): r""" Integrate the orbit and a small area of phase space. Parameters ---------- dxdv : numpy.ndarray Initial conditions for the orbit in cylindrical or rectangular coordinates. The shape of the array should be (\*input_shape, 4). t : list, numpy.ndarray or Quantity List of equispaced times at which to compute the orbit. The initial condition is t[0]. (note that for method='odeint', method='dop853', and method='dop853_c', the time array can be non-equispaced). pot : Potential, DissipativeForce or list of such instances Gravitational field to integrate the orbit in. method : str, optional Integration method. Default is 'dopr54_c'. See Notes for more information. progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!). Default is True. dt : float, optional 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 : int, optional Number of cores to use for Python-based multiprocessing (pure Python or using force_map=True); default = OMP_NUM_THREADS. force_map : bool, optional If True, force use of Python-based multiprocessing (not recommended). Default is False. rectIn : bool, optional If True, input dxdv is in rectangular coordinates. Default is False. rectOut : bool, optional If True, output dxdv (that in orbit_dxdv) is in rectangular coordinates. Default is False. Returns ------- 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! Notes ----- - Possible integration methods are the non-symplectic ones in galpy: - '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 - 'dop853' for a 8-5-3 Dormand-Prince integrator in Python - 'dop853_c' for a 8-5-3 Dormand-Prince integrator in C - 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" ) self.check_integrator(method, no_symplec=True) 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 # Check that t is evenly spaced if not self._check_array_evenlyspaced(t, method): raise ValueError( f"Input time array must be equally spaced for method {method}, use method='dop853_c', method='dop853', or method='odeint' instead for non-equispaced time arrays" ) 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 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): """ Flip an orbit's initial conditions such that the velocities are minus the original velocities. Parameters ---------- inplace : bool, optional 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). Default is False. Returns ------- Orbit If inplace=False, returns a new Orbit instance that has the velocities of the current orbit flipped. If inplace=True, flips all velocities of current instance. Notes ----- - 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): r""" Return previously calculated orbits. Returns ------- numpy.ndarray [\*input_shape,nt,nphasedim] Integrated orbit. Notes ----- - 2019-03-02 - Written - Bovy (UofT) """ return self.orbit.copy() @shapeDecorator def getOrbit_dxdv(self): r""" Return a previously calculated integration of a small phase-space volume (with integrate_dxdv). Returns ------- numpy.ndarray [\*input_shape,nt,nphasedim] Integrated orbit's phase-space volume. Notes ----- - 2019-05-21: Written by Bovy (UofT) """ return self.orbit_dxdv[..., 4:].copy() @physical_conversion("energy") @shapeDecorator def E(self, *args, **kwargs): r""" Calculate the energy. Parameters ---------- t : numeric, numpy.ndarray, or Quantity, optional Time at which to get the energy. Default is the initial time. pot : Potential, DissipativeForce or list of such instances, optional Gravitational potential to use to compute the energy (DissipativeForce instances are ignored). Default is the gravitational field used to integrate the orbit. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Energy. Notes ----- - 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.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.0 / 2.0 ).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.0 / 2.0).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.0 / 2.0 + thiso[2] ** 2.0 / 2.0 ).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.0 / 2.0 + thiso[2] ** 2.0 / 2.0).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.0 / 2.0 + thiso[2] ** 2.0 / 2.0 ).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.0 / 2.0 + thiso[2] ** 2.0 / 2.0).T ) elif self.phasedim() == 5: z = kwargs.get("_z", 1.0) * thiso[3] # For ER and Ez vz = kwargs.get("_vz", 1.0) * 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.0 / 2.0 + thiso[2] ** 2.0 / 2.0 + vz**2.0 / 2.0 ).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.0 / 2.0 + thiso[2] ** 2.0 / 2.0 + vz**2.0 / 2.0).T ) elif self.phasedim() == 6: z = kwargs.get("_z", 1.0) * thiso[3] # For ER and Ez vz = kwargs.get("_vz", 1.0) * 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.0 / 2.0 + thiso[2] ** 2.0 / 2.0 + vz**2.0 / 2.0 ).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.0 / 2.0 + thiso[2] ** 2.0 / 2.0 + vz**2.0 / 2.0).T ) if onet: return out[:, 0] else: return out @physical_conversion("action") @shapeDecorator def L(self, *args, **kwargs): r""" Calculate the angular momentum at time t. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the angular momentum. Default is the initial time. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt,3] Angular momentum. Notes ----- - 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): r""" Calculate the z-component of the angular momentum at time t. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the angular momentum. Default is the initial time. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] z-component of the angular momentum. Notes ----- - 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): r""" Calculate the radial energy. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the radial energy. Default is the initial time. pot : Potential, DissipativeForce or list of such instances Gravitational potential to use for the calculation (DissipativeForce instances are ignored). Default is the gravitational field used to integrate the orbit. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Radial energy. Notes ----- - 2019-03-01 - Written - Bovy (UofT) """ old_physical = kwargs.get("use_physical", None) kwargs["use_physical"] = False kwargs["_z"] = 0.0 kwargs["_vz"] = 0.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): r""" Calculate the vertical energy. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the vertical energy. Default is the initial time. pot : Potential, DissipativeForce or list of such instances Gravity potential to use for the calculation (DissipativeForce instances are ignored). Default is the gravitational field used to integrate the orbit. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Vertical energy. Notes ----- - 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.0 kwargs["_vz"] = 0.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): r""" Calculate the Jacobi integral E - Omega L. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the Jacobi integral. Default is the initial time. OmegaP : numeric or Quantity, optional Pattern speed. pot : Potential, DissipativeForce or list of such instances Gravity potential to use for the calculation (DissipativeForce instances are ignored). Default is the gravitational field used to integrate the orbit. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Jacobi integral. Notes ----- - 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.0 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( numpy.array(OmegaP) if isinstance(OmegaP, list) else 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: 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): """ Set up an actionAngle module for this Orbit. Parameters ---------- pot : Potential or list of Potentials, optional Gravitational potential to use for the calculation (DissipativeForce instances are ignored). Default is the gravitational field used to integrate the orbit. type : {'staeckel', 'adiabatic', 'spherical', 'isochroneApprox'}, optional Type of actionAngle module to use. Default is 'staeckel'. Options are: 1) 'adiabatic' 2) 'staeckel' 3) 'isochroneApprox' 4) 'spherical' Returns ------- None Notes ----- - 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.0 * (self.z(use_physical=False, dontreshape=True) >= 0) - 1.0) * 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 _unbound_indx_and_aAkwargs(self): """Internal function to compute the index of unbound orbits""" if self.dim() == 2: Einf = evaluateplanarPotentials( toPlanarPotential(self._aAPot), _INF, use_physical=False ) elif self.dim() == 3: Einf = evaluatePotentials(self._aAPot, _INF, 0.0, use_physical=False) if numpy.isnan(Einf): Einf = numpy.inf # Just try to proceed as best as possible, don't make assumptions about the potential indx = self.E(pot=self._aAPot, use_physical=False, dontreshape=True) < Einf if hasattr(self._aA, "_delta"): if hasattr(self._aA._delta, "__len__"): aAkwargs = {"delta": self._aA._delta[indx]} else: aAkwargs = {"delta": self._aA._delta} else: aAkwargs = {} return indx, aAkwargs def _setup_EccZmaxRperiRap(self, pot=None, **kwargs): """Internal function to compute e,zmax,rperi,rap and cache it for reuse""" 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.0 * (self.z(use_physical=False, dontreshape=True) >= 0) - 1.0) * 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 # Exclude unbound orbits (aAkwargs deals with delta processing) indx, aAkwargs = self._unbound_indx_and_aAkwargs() ( self._aA_ecc, self._aA_zmax, self._aA_rperi, self._aA_rap, ) = ( numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, ) if numpy.sum(indx) > 0: ( self._aA_ecc[indx], self._aA_zmax[indx], self._aA_rperi[indx], self._aA_rap[indx], ) = self._aA.EccZmaxRperiRap( self.R(use_physical=False, dontreshape=True)[indx], self.vR(use_physical=False, dontreshape=True)[indx], self.vT(use_physical=False, dontreshape=True)[indx], tz[indx], tvz[indx], use_physical=False, **aAkwargs, ) return None def _setup_actionsFreqsAngles(self, pot=None, **kwargs): """Internal function to compute the actions, frequencies, and angles and cache them for reuse""" 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.0 * (self.z(use_physical=False, dontreshape=True) >= 0) - 1.0) * 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 # Exclude unbound orbits (aAkwargs deals with delta processing) indx, aAkwargs = self._unbound_indx_and_aAkwargs() ( 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, ) = ( numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, ) if numpy.sum(indx) > 0: ( self._aA_jr[indx], self._aA_jp[indx], self._aA_jz[indx], self._aA_Or[indx], self._aA_Op[indx], self._aA_Oz[indx], self._aA_wr[indx], self._aA_wp[indx], self._aA_wz[indx], ) = self._aA.actionsFreqsAngles( self.R(use_physical=False, dontreshape=True)[indx], self.vR(use_physical=False, dontreshape=True)[indx], self.vT(use_physical=False, dontreshape=True)[indx], tz[indx], tvz[indx], self.phi(use_physical=False, dontreshape=True)[indx], use_physical=False, **aAkwargs, ) return None def _setup_actions(self, pot=None, **kwargs): """Internal function to compute the actions and cache them for reuse (used for methods that don't support frequencies and angles)""" self._setupaA(pot=pot, **kwargs) # Caching effectively checked in _setup_actionsFreqsAngles, because always called first # 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.0 * (self.z(use_physical=False, dontreshape=True) >= 0) - 1.0) * 1e-10 ) tvz = self.vz(use_physical=False, dontreshape=True) # dim = 2 never reached currently, bc adiabatic is the only method that uses # this and for 2D orbits that just uses spherical # elif self.dim() == 2: # tz = numpy.zeros(self.size) # tvz = numpy.zeros(self.size) # self.dim() == 1 error caught by _setupaA # Exclude unbound orbits indx, aAkwargs = self._unbound_indx_and_aAkwargs() ( self._aA_jr, self._aA_jp, self._aA_jz, ) = ( numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, numpy.zeros_like(self.R(use_physical=False, dontreshape=True)) + numpy.nan, ) if numpy.sum(indx) > 0: self._aA_jr[indx], self._aA_jp[indx], self._aA_jz[indx] = self._aA( self.R(use_physical=False, dontreshape=True)[indx], self.vR(use_physical=False, dontreshape=True)[indx], self.vT(use_physical=False, dontreshape=True)[indx], tz[indx], tvz[indx], self.phi(use_physical=False, dontreshape=True)[indx], use_physical=False, **aAkwargs, ) return None @shapeDecorator def e(self, analytic=False, pot=None, **kwargs): r""" Calculate the eccentricity, either numerically from the numerical orbit integration or using analytical means. Parameters ---------- analytic : bool, optional If True, compute this analytically. Default is False. pot : Potential or list of Potential instances, optional Gravitational potential to use for analytical calculation. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'spherical'}, optional Type of actionAngle module to use when analytic=True. Default is 'staeckel'. Returns ------- float or numpy.ndarray [\*input_shape] Eccentricity of the orbit. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-25 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleSpherical """ 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): r""" Calculate the apocenter radius, either numerically from the numerical orbit integration or using analytical means. Parameters ---------- analytic : bool, optional If True, compute this analytically. Default is False. pot : Potential or list of Potential instances, optional Gravity potential to use for analytical calculation. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'spherical'}, optional Type of actionAngle module to use when analytic=True. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Apocenter radius of the orbit. Notes ----- - 2019-02-25 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleSpherical """ 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): r""" Calculate the pericenter radius, either numerically from the numerical orbit integration or using analytical means. Parameters ---------- analytic : bool, optional If True, compute this analytically. Default is False. pot : Potential or list of Potential instances, optional Gravity potential to use for analytical calculation. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'spherical'}, optional Type of actionAngle module to use when analytic=True. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Pericenter radius of the orbit. Notes ----- - 2019-02-25 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleSpherical """ 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): r""" Calculate the guiding-center radius (the radius of a circular orbit with the same angular momentum). Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Guiding-center radius of the orbit. Notes ----- - 2019-03-02 - Written as thin wrapper around Potential.rl - Bovy (UofT) See Also -------- galpy.potential.Potential.rl """ 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): r""" Calculate the radius of a circular orbit with the same energy. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the radius. Default is the initial time. pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Radius of a circular orbit with the same energy. Notes ----- - 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): r""" Calculate the angular momentum of a circular orbit with the same energy. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the radius. Default is the initial time. pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Angular momentum of a circular orbit with the same energy. Notes ----- - 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): r""" Calculate the maximum vertical height, either numerically from the numerical orbit integration or using analytical means. Parameters ---------- analytic : bool, optional Compute this analytically. Default is False. pot : Potential or list of Potential instances, optional Gravitational potential for the analytical calculation. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'spherical'}, optional Type of actionAngle module to use for 3D orbits when analytic=True. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Maximum vertical height. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-25 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleSpherical """ 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): r""" Calculate the radial action. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Radial action. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-27 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ 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): r""" Calculate the azimuthal action. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Azimuthal action. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-26 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ 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): r""" Calculate the vertical action. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Vertical action. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-27 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ 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): r""" Calculate the radial angle. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Radial angle. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-27 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ self._setup_actionsFreqsAngles(pot=pot, **kwargs) return self._aA_wr @physical_conversion("angle") @shapeDecorator def wp(self, pot=None, **kwargs): r""" Calculate the azimuthal angle. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Azimuthal angle. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-27 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ self._setup_actionsFreqsAngles(pot=pot, **kwargs) return self._aA_wp @physical_conversion("angle") @shapeDecorator def wz(self, pot=None, **kwargs): r""" Calculate the vertical angle. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Vertical angle. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-27 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ self._setup_actionsFreqsAngles(pot=pot, **kwargs) return self._aA_wz @physical_conversion("time") @shapeDecorator def Tr(self, pot=None, **kwargs): r""" Calculate the radial period. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Radial period. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-27 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ self._setup_actionsFreqsAngles(pot=pot, **kwargs) return 2.0 * numpy.pi / self._aA_Or @physical_conversion("time") @shapeDecorator def Tp(self, pot=None, **kwargs): r""" Calculate the azimuthal period. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Azimuthal period. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-27 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ self._setup_actionsFreqsAngles(pot=pot, **kwargs) return 2.0 * numpy.pi / self._aA_Op @shapeDecorator def TrTp(self, pot=None, **kwargs): r""" Calculate the ratio between the radial and azimuthal period Tr/Tphi*pi. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. +actionAngle module setup kwargs Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Ratio between the radial and azimuthal period Tr/Tphi*pi Notes ----- - 2019-02-27 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ 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): r""" Calculate the vertical period. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale in km/s for velocities to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Vertical period. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-27 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ self._setup_actionsFreqsAngles(pot=pot, **kwargs) return 2.0 * numpy.pi / self._aA_Oz @physical_conversion("frequency") @shapeDecorator def Or(self, pot=None, **kwargs): r""" Calculate the radial frequency. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Radial frequency. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-27 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ self._setup_actionsFreqsAngles(pot=pot, **kwargs) return self._aA_Or @physical_conversion("frequency") @shapeDecorator def Op(self, pot=None, **kwargs): r""" Calculate the azimuthal frequency. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Azimuthal frequency. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-27 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ self._setup_actionsFreqsAngles(pot=pot, **kwargs) return self._aA_Op @physical_conversion("frequency") @shapeDecorator def Oz(self, pot=None, **kwargs): r""" Calculate the vertical frequency. Parameters ---------- pot : Potential or list of Potential instances, optional Gravitational potential. Default is the gravitational field used for the orbit integration. type : {'staeckel', 'adiabatic', 'isochroneApprox', 'spherical'}, optional Type of actionAngle module to use. Default is 'staeckel'. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape] Vertical frequency. Notes ----- - Keyword arguments also include the actionAngle module setup kwargs for the corresponding actionAngle modules - 2019-02-27 - Written - Bovy (UofT) See Also -------- galpy.actionAngle.actionAngleStaeckel galpy.actionAngle.actionAngleAdiabatic galpy.actionAngle.actionAngleIsochroneApprox galpy.actionAngle.actionAngleSpherical """ self._setup_actionsFreqsAngles(pot=pot, **kwargs) return self._aA_Oz @physical_conversion("time") def time(self, *args, **kwargs): r""" Return the times at which the orbit is sampled. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional 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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Times at which the orbit is sampled. Notes ----- - 2019-02-28 - Written - Bovy (UofT) """ if len(args) == 0: try: return self.t.copy() except AttributeError: return 0.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): r""" Return cylindrical radius at time t. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the radius. Default is the initial time. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Cylindrical radius. Notes ----- - 2019-02-01 - Written - Bovy (UofT) """ return self._call_internal(*args, **kwargs)[0].T @physical_conversion("position") @shapeDecorator def r(self, *args, **kwargs): r""" Return spherical radius at time t. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the radius. Default is the initial time. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Spherical radius. Notes ----- - 2019-02-20: Written by Bovy (UofT). """ thiso = self._call_internal(*args, **kwargs) if self.dim() == 3: return numpy.sqrt(thiso[0] ** 2.0 + thiso[3] ** 2.0).T else: return numpy.fabs(thiso[0]).T @physical_conversion("velocity") @shapeDecorator def vR(self, *args, **kwargs): r""" Return radial velocity at time t. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the radial velocity. Default is the initial time. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Radial velocity. Notes ----- - 2019-02-20 - Written - Bovy (UofT) """ return self._call_internal(*args, **kwargs)[1].T @physical_conversion("velocity") @shapeDecorator def vT(self, *args, **kwargs): r""" Return rotational velocity at time t. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the rotational velocity. Default is the initial time. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Rotational velocity. Notes ----- - 2019-02-20 - Written by Bovy (UofT). """ return self._call_internal(*args, **kwargs)[2].T @physical_conversion("position") @shapeDecorator def z(self, *args, **kwargs): r""" Return vertical height. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the vertical height. Default is the initial time. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Vertical height. Notes ----- - 2019-02-20: Written by 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): r""" Return vertical velocity. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the vertical velocity. Default is the initial time. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Vertical velocity. Notes ----- - 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): r""" Return azimuth. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the azimuth. Default is the initial time. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Azimuth in [-pi,pi]. Notes ----- - 2019-02-20: Written by 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): r""" Return x. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the x-coordinate. Default is the initial time. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] x-coordinate. Notes ----- - 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): r""" Return y. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the y-coordinate. Default is the initial time. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] y-coordinate. Notes ----- - 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): r""" Return x velocity at time t. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the x-velocity. Default is the initial time. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] x-velocity. Notes ----- - 2019-02-20: Written by 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): r""" Return y velocity at time t. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the y-velocity. Default is the initial time. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] y-velocity. Notes ----- - 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): r""" Return angular velocity. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the angular velocity. Default is the initial time. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Angular velocity. Notes ----- - 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): r""" Return spherical radial velocity. For < 3 dimensions returns vR. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the radial velocity. Default is the initial time. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Radial velocity. Notes ----- - 2020-07-01: Written by James Lane (UofT). """ thiso = self._call_internal(*args, **kwargs) if self.dim() == 3: r = numpy.sqrt(thiso[0] ** 2.0 + thiso[3] ** 2.0) 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): r""" Return spherical polar velocity. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the theta velocity. Default is the initial time. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Spherical polar velocity. Notes ----- - 2020-07-01: Written by 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.0 + thiso[3] ** 2.0) return ((thiso[1] * thiso[3] - thiso[0] * thiso[4]) / r).T @physical_conversion("angle") @shapeDecorator def theta(self, *args, **kwargs): r""" Return spherical polar angle. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the angle. Default is the initial time. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Spherical polar angle. Notes ----- - 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]).T @physical_conversion("angle_deg") @shapeDecorator def ra(self, *args, **kwargs): r""" Return the right ascension. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the right ascension. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position of observer (in kpc, arranged as [X,Y,Z]; 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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Right ascension. Notes ----- - 2019-02-21: Written by 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): r""" Return the declination. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the declination. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position of observer (in kpc, arranged as [X,Y,Z]; 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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Declination. Notes ----- - 2019-02-21: Written by 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): r""" Return Galactic longitude. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the Galactic longitude. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position of observer (in kpc, arranged as [X,Y,Z]; 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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Galactic longitude. Notes ----- - 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): r""" Return Galactic latitude. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the Galactic latitude. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position of observer (in kpc, arranged as [X,Y,Z]; 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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Galactic latitude. Notes ----- - 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): r""" Return distance from the observer in kpc. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the distance. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position of observer (in kpc, arranged as [X,Y,Z]; 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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Distance in kpc. Notes ----- - 2019-02-21: Written by 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): r""" Return proper motion in right ascension (in mas/yr). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the proper motion. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer (in kpc and km/s, arranged as [X,Y,Z,vx,vy,vz]; 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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Proper motion in right ascension in mas/yr. Notes ----- - 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): r""" Return proper motion in declination (in mas/yr). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the proper motion. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer (in kpc and km/s, arranged as [X,Y,Z,vx,vy,vz]; 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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Proper motion in declination in mas/yr. Notes ----- - 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): r""" Return proper motion in Galactic longitude (in mas/yr). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the proper motion. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer (in kpc and km/s, arranged as [X,Y,Z,vx,vy,vz]; 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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Proper motion in Galactic longitude in mas/yr. Notes ----- - 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): r""" Return proper motion in Galactic latitude (in mas/yr). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the proper motion. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer (in kpc and km/s, arranged as [X,Y,Z,vx,vy,vz]; 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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Proper motion in Galactic latitude in mas/yr. Notes ----- This method returns the proper motion in Galactic latitude (in mas/yr). - 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): r""" Return the line-of-sight velocity (in km/s). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the line-of-sight velocity. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer (in kpc and km/s, arranged as [X,Y,Z,vx,vy,vz]; 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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] Line-of-sight velocity in km/s. Notes ----- - 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): r""" Return velocity in right ascension (km/s). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get vra. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer in the Galactocentric frame (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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- numpy.ndarray or Quantity [\*input_shape] v_ra(t) in km/s. Notes ----- - 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): r""" Return velocity in declination (km/s). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get vdec. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer in the Galactocentric frame (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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- numpy.ndarray or Quantity [\*input_shape] v_dec(t) in km/s. Notes ----- - 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): r""" Return the velocity in Galactic longitude (km/s). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get vll. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer in the Galactocentric frame (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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- numpy.ndarray or Quantity [\*input_shape] v_l(t) in km/s. Notes ----- - 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): r""" Return velocity in Galactic latitude (km/s). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get vbb. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer in the Galactocentric frame (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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- numpy.ndarray or Quantity [\*input_shape] v_b(t) in km/s. Notes ----- - Written on 2019-02-28 by 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): r""" Return Heliocentric Galactic rectangular x-coordinate (aka "X"). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get X. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer in the Galactocentric frame (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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] helioX(t) in kpc. Notes ----- - 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): r""" Return Heliocentric Galactic rectangular y-coordinate (aka "Y"). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get Y. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer in the Galactocentric frame (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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] helioY(t) in kpc. Notes ----- - 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): r""" Return Heliocentric Galactic rectangular z-coordinate (aka "Z"). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get Z. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer in the Galactocentric frame (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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] helioZ(t) in kpc. Notes ----- - Written on 2019-02-21 by 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): r""" Return Heliocentric Galactic rectangular x-velocity (aka "U"). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get U. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer in the Galactocentric frame (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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] U(t) in km/s. Notes ----- - 2019-02-21: Written by 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): r""" Return Heliocentric Galactic rectangular y-velocity (aka "V"). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get V. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer in the Galactocentric frame (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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] V(t) in km/s. Notes ----- - 2019-02-21: Written. """ _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): r""" Return Heliocentric Galactic rectangular z-velocity (aka "W"). Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get W. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer in the Galactocentric frame (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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- float, numpy.ndarray or Quantity [\*input_shape,nt] W(t) in km/s. Notes ----- - Written by Bovy (UofT) on 2019-02-21. """ _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): r""" Return the positions and velocities as an astropy SkyCoord. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the position. Default is the initial time. obs : numpy.ndarray, Quantity or Orbit, optional Position and velocity of observer in the Galactocentric frame (in kpc and km/s; arranged as [x,y,z,vx,vy,vz]) (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 : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. quantity : bool, optional If True, return an Astropy Quantity object. Default from configuration file. Returns ------- SkyCoord [\*input_shape,nt] SkyCoord(t). Notes ----- - 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.0 + self._zo**2.0) * units.kpc, z_sun=self._zo * units.kpc, galcen_v_sun=v_sun, ).T @physical_conversion_tuple(["position", "velocity"]) def SOS( self, pot, ncross=500, surface=None, t0=0.0, method="dop853_c", skip=100, progressbar=True, numcores=_NUMCORES, force_map=False, **kwargs, ): """ Calculate the surface of section of the orbit. Parameters ---------- pot : Potential, DissipativeForce, or list of such instances Gravitational field to integrate the orbit in. ncross : int, optional Number of times to cross the surface. Default is 500. surface : str, optional Surface to punch through. This has no effect in 3D, where the surface is always z=0, but in 2D it can be 'x' or 'y' for x=0 or y=0. Default is None. Other Parameters ---------------- t0 : float or Quantity, optional Time of the initial condition. Default is 0. method : {'odeint', 'dop853_c', 'dop853', 'rk4_c', 'rk6_c', 'dop54_c'}, optional Integration method. Default is 'dop853_c'. See Notes for more information. skip : int, optional For non-adaptive integrators, the number of basic steps to take between crossings (these are further refined in the code, but only up to a maximum refinement, so you can use skip to get finer integration in cases where more accuracy is needed). Default is 100. progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!). Default is True. numcores : int, optional Number of cores to use for Python-based multiprocessing (pure Python or using force_map=True). Default is OMP_NUM_THREADS. force_map : bool, optional If True, force use of Python-based multiprocessing (not recommended). Default is False. Returns ------- tuple (R,vR) for 3D orbits, (y,vy) for 2D orbits when surface=='x', (x,vx) for 2D orbits when surface=='y'. Notes ----- - Possible integration methods are: - '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 - 'dop853' for a 8-5-3 Dormand-Prince integrator in Python - 'dop853_c' for a 8-5-3 Dormand-Prince integrator in C - 2023-03-16 - Written - Bovy (UofT) """ if self.dim() == 3: init_psis = numpy.arctan2( self.z(use_physical=False), self.vz(use_physical=False) ) elif self.phasedim() == 4: if not surface is None and surface.lower() == "y": init_psis = numpy.arctan2( self.y(use_physical=False), self.vy(use_physical=False) ) else: init_psis = numpy.arctan2( self.x(use_physical=False), self.vx(use_physical=False) ) else: raise NotImplementedError( "SOS not implemented for 1D orbits or 2D orbits without phi" ) # Let's check that v(x/y/z) != 0 for orbits that are already on the SOS if ( ( self.dim() == 3 and not numpy.all( (self.vz() != 0.0) + (numpy.fabs(init_psis % numpy.pi) > 1e-10) ) ) or ( self.dim() == 2 and not surface is None and surface.lower() == "y" and not numpy.all( (self.vy() != 0.0) + (numpy.fabs(init_psis % numpy.pi) > 1e-10) ) ) or ( self.dim() == 2 and (surface is None or surface.lower() == "x") and not numpy.all( (self.vx() != 0.0) + (numpy.fabs(init_psis % numpy.pi) > 1e-10) ) ) ): raise RuntimeError( "An orbit appears to be within the SOS surface. Refusing to perform specialized SOS integration, please use normal integration instead" ) if numpy.any(numpy.fabs(init_psis) > 1e-10): # Integrate to the next crossing init_psis = numpy.atleast_1d( (init_psis + 2.0 * numpy.pi) % (2.0 * numpy.pi) ) psis = numpy.array( [ numpy.linspace(0.0, 2.0 * numpy.pi - init_psi, 101) for init_psi in init_psis ] ) self.integrate_SOS( psis, pot, surface=surface, t0=t0, method=method, progressbar=progressbar, numcores=numcores, force_map=force_map, ) old_vxvv = self.vxvv self.vxvv = self.orbit[:, -1] if method == "rk4_c" or method == "rk6_c": # Because these are non-adaptive, we need to make sure we # integrate finely enough iskip = skip else: iskip = 1 psis = numpy.arange(ncross * iskip) * 2 * numpy.pi / iskip self.integrate_SOS( psis, pot, surface=surface, t0=t0, method=method, progressbar=progressbar, numcores=numcores, force_map=force_map, ) self.t = self.t[:, ::iskip] self.orbit = self.orbit[:, ::iskip] if self.dim() == 3: out = ( self.R(self.t, use_physical=False), self.vR(self.t, use_physical=False), ) elif not surface is None and surface.lower() == "y": out = ( self.x(self.t, use_physical=False), self.vx(self.t, use_physical=False), ) else: out = ( self.y(self.t, use_physical=False), self.vy(self.t, use_physical=False), ) if numpy.any(numpy.fabs(init_psis) > 1e-7): self.vxvv = old_vxvv return out @physical_conversion_tuple(["position", "velocity"]) def bruteSOS( self, t, pot, surface=None, method="dop853_c", dt=None, progressbar=True, numcores=_NUMCORES, force_map=False, ): """ Calculate the surface of section of the orbit using a brute-force integration approach. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the position. Default is the initial time. pot : Potential, DissipativeForce or list of such instances Gravitational field to integrate the orbit in. surface : str, optional Surface to punch through (this has no effect in 3D, where the surface is always z=0, but in 2D it can be 'x' or 'y' for x=0 or y=0), by default None. method : str, optional Integration method to use. Default is 'dop853_c'. See Notes for more information. dt : float, optional 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), by default None. progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!), by default True. numcores : int, optional Number of cores to use for Python-based multiprocessing (pure Python or using force_map=True); default = OMP_NUM_THREADS, by default _NUMCORES. force_map : bool, optional If True, force use of Python-based multiprocessing (not recommended), by default False. Returns ------- tuple (R,vR) for 3D orbits, (y,vy) for 2D orbits when surface=='x', (x,vx) for 2D orbits when surface=='y' Notes ----- - Possible integration methods are: - '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 - 'ias15_c' for an adaptive 15th order integrator using Gauß-Radau quadrature (see IAS15 paper) in C - 2023-05-31 - Written - Bovy (UofT) """ if not self.dim() == 3 and not self.phasedim() == 4: raise NotImplementedError( "SOS not implemented for 1D orbits or 2D orbits without phi" ) # Integrate the Orbit self.integrate( t, pot, method=method, progressbar=progressbar, dt=dt, numcores=numcores, force_map=force_map, ) # Find the crossings if self.dim() == 3: cross = self.z(self.t, use_physical=False, dontreshape=True) else: # phasedim == 4 from check about if not surface is None and surface.lower() == "y": cross = self.y(self.t, use_physical=False, dontreshape=True) else: cross = self.x(self.t, use_physical=False, dontreshape=True) shifts = numpy.roll(cross, -1, axis=1) crossindx = (cross[:, :-1] < 0.0) * (shifts[:, :-1] > 0.0) anycrossindx = numpy.sum(crossindx, axis=0).astype("bool") self.t = numpy.tile(self.t, (self.size, 1))[:, :-1] self.t = self.t[:, anycrossindx] self.orbit = self.orbit[:, :-1][:, anycrossindx] self.t[~crossindx[:, anycrossindx]] = numpy.nan self.orbit[~crossindx[:, anycrossindx]] = numpy.nan if self.dim() == 3: return ( self.R(self.t, use_physical=False), self.vR(self.t, use_physical=False), ) else: if not surface is None and surface.lower() == "y": return ( self.x(self.t, use_physical=False), self.vx(self.t, use_physical=False), ) else: return ( self.y(self.t, use_physical=False), self.vy(self.t, use_physical=False), ) def __call__(self, *args, **kwargs): """ Return the orbits at time t. Parameters ---------- t : numeric, numpy.ndarray or Quantity Desired time. Default is the initial time. Returns ------- Orbit An Orbit instance with initial conditions set to the phase-space at time t; shape of new Orbit is (shape_old,nt). Notes ----- - 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): """ Return the orbits vector at time t Parameters ---------- t : numeric, numpy.ndarray or Quantity Desired time. Default is the initial time. Returns ------- ndarray [R,vR,vT,z,vz(,phi)] or [R,vR,vT(,phi)] depending on the orbit; shape = [phasedim,nt,norb] Notes ----- - 2019-02-01 - Started - Bovy (UofT) - 2019-02-18 - Written interpolation part - Bovy (UofT) """ if len(args) == 0 and "t" in kwargs: args = [kwargs.pop("t")] if len(args) == 0 or (not hasattr(self, "t") and args[0] == 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 # Remove NaN times from consideration, these are used in internally in bruteSOS t_exact_integration_times = ( not (_APY_LOADED and isinstance(t, units.Quantity)) and hasattr(t, "__len__") and (len(t) == len(self.t)) and numpy.all((t == self.t)[~numpy.isnan(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)[~numpy.isnan(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): """ Convert 3D orbits into 2D orbits. Returns ------- Orbit Planar Orbit instance. Notes ----- - 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): """ Convert 3D orbits into 1D orbits (z phase space). Returns ------- Orbit Linear Orbit instance. Notes ----- - 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.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.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.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 out = self.time(self.t, **kwargs) if len(self.t.shape) < len(self.orbit.shape) - 1: out = numpy.tile(out, (len(self.vxvv), 1)) return out elif q == "Enorm": return (self.E(self.t, **kwargs).T / self.E(0.0, **kwargs)).T elif q == "Eznorm": return (self.Ez(self.t, **kwargs).T / self.Ez(0.0, **kwargs)).T elif q == "ERnorm": return (self.ER(self.t, **kwargs).T / self.ER(0.0, **kwargs)).T elif q == "Jacobinorm": return (self.Jacobi(self.t, **kwargs).T / self.Jacobi(0.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): """ Plot a previously calculated orbit. Parameters ---------- d1 : str or callable, optional First dimension to plot. Can be a string ('x', 'y', 'R', 'vR', 'vT', 'z', 'vz', ...), an expression like 'R*vR', or a user-defined function of time (e.g., lambda t: o.R(t) for R). Default is determined by the number of dimensions in the orbit. d2 : str or callable, optional Second dimension to plot. Same format as d1. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is the object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is the object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. *args : optional Additional arguments to pass to galpy.util.plot.plot. **kwargs : optional Additional keyword arguments to pass to galpy.util.plot.plot. Returns ------- list A list of matplotlib.lines.Line2D objects representing the plotted data. Notes ----- - 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 = _labeldict_physical.copy() else: labeldict = _labeldict_internal.copy() labeldict.update(_labeldict_radec.copy()) # 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, rf"${d1}$") if not "ylabel" in kwargs: kwargs["ylabel"] = labeldict.get(d2, rf"${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): """ Plot 3D aspects of an Orbit. Parameters ---------- d1 : str or callable 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 : str or callable Second dimension to plot. Same format as d1. d3 : str or callable Third dimension to plot. Same format as d1. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is the object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is the object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. *args : optional Additional arguments to pass to galpy.util.plot.plot3d. **kwargs : optional Additional keyword arguments to pass to galpy.util.plot.plot3d. Returns ------- list A list of matplotlib.lines.Line3D objects representing the plotted data. Notes ----- - 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 = _labeldict_physical.copy() else: labeldict = _labeldict_internal.copy() labeldict.update(_labeldict_radec.copy()) # 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, rf"${d1}$") if not "ylabel" in kwargs: kwargs["ylabel"] = labeldict.get(d2, rf"${d2}$") if not "zlabel" in kwargs: kwargs["zlabel"] = labeldict.get(d3, rf"${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 plotSOS( self, pot, *args, ncross=500, surface=None, t0=0.0, method="dop853_c", skip=100, progressbar=True, **kwargs, ): """ Calculate and plot a surface of section of the orbit. Parameters ---------- pot : Potential, DissipativeForce, or list of such instances Gravitational field to integrate the orbit in. ncross : int, optional Number of times to cross the surface. The default is 500. surface : str, optional Surface to punch through (this has no effect in 3D, where the surface is always z=0, but in 2D it can be 'x' or 'y' for x=0 or y=0). The default is None. t0 : float or Quantity, optional Time of the initial condition. The default is 0.0. method : {'odeint', 'dop853_c', 'dop853', 'dop54_c', 'rk4_c', 'rk6_c'}, optional Method to integrate the orbit. The default is 'dop853_c'. skip : int, optional For non-adaptive integrators, the number of basic steps to take between crossings (these are further refined in the code, but only up to a maximum refinement, so you can use skip to get finer integration in cases where more accuracy is needed). The default is 100. progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!). The default is True. *args : optional Additional arguments to pass to galpy.util.plot.plot. **kwargs : optional Additional keyword arguments to pass to galpy.util.plot.plot. Notes ----- - 2023-03-16 - Written - 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 = _labeldict_physical.copy() else: labeldict = _labeldict_internal.copy() labeldict.update(_labeldict_radec.copy()) if self.dim() == 3: d1 = "R" d2 = "vR" elif not surface is None and surface.lower() == "y": d1 = "x" d2 = "vx" else: d1 = "y" d2 = "vy" kwargs["quantity"] = False x, y = self.SOS( pot, ncross=ncross, surface=surface, t0=t0, method=method, skip=skip, progressbar=progressbar, **kwargs, ) x = numpy.atleast_2d(x) y = numpy.atleast_2d(y) kwargs.pop("ro", None) kwargs.pop("vo", None) kwargs.pop("use_physical", 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, rf"${d1}$") if not "ylabel" in kwargs: kwargs["ylabel"] = labeldict.get(d2, rf"${d2}$") if not "ls" in kwargs: kwargs["ls"] = "none" if not "marker" in kwargs: kwargs["marker"] = "." 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 plotBruteSOS( self, t, pot, *args, surface=None, method="dop853_c", progressbar=True, **kwargs, ): """ Calculate and plot a surface of section of the orbit computed using bruteSOS. Parameters ---------- t : numeric, numpy.ndarray or Quantity, optional Time at which to get the position. Default is the initial time. pot : Potential, DissipativeForce or list of such instances Gravitational field to integrate the orbit in. surface : str, optional Surface to punch through (this has no effect in 3D, where the surface is always z=0, but in 2D it can be 'x' or 'y' for x=0 or y=0), by default None. method : str, optional Integration method to use. Default is 'dop853_c'. See Notes for more information. progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!) (default is True). *args : optional Additional arguments to pass to galpy.util.plot.plot. **kwargs : dict Additional keyword arguments to pass to galpy.util.plot.plot. Returns ------- list A list of the plotted line objects. Other Parameters ---------------- matplotlib.plot inputs+galpy.util.plot.plot inputs Notes ----- - Possible integration methods are: - '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 - 'ias15_c' for an adaptive 15th order integrator using Gauß-Radau quadrature (see IAS15 paper) in C - 2023-05-31 - Written - Bovy (UofT) """ x, y = self.bruteSOS( t, pot, surface=surface, method=method, progressbar=progressbar ) return self._base_plotSOS(x, y, surface, *args, **kwargs) def _base_plotSOS(self, x, y, surface, *args, **kwargs): """Shared code between plotSOS and plotBruteSOS""" 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 = _labeldict_physical.copy() else: labeldict = _labeldict_internal.copy() labeldict.update(_labeldict_radec.copy()) if self.dim() == 3: d1 = "R" d2 = "vR" elif not surface is None and surface.lower() == "y": d1 = "x" d2 = "vx" else: d1 = "y" d2 = "vy" kwargs["quantity"] = False x = numpy.atleast_2d(x) y = numpy.atleast_2d(y) kwargs.pop("ro", None) kwargs.pop("vo", None) kwargs.pop("use_physical", 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, rf"${d1}$") if not "ylabel" in kwargs: kwargs["ylabel"] = labeldict.get(d2, rf"${d2}$") if not "ls" in kwargs: kwargs["ls"] = "none" if not "marker" in kwargs: kwargs["marker"] = "." 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 animate(self, **kwargs): # pragma: no cover """ Animate a previously calculated orbit. Parameters ---------- d1 : str or callable or list First dimension to plot ('x', 'y', 'R', 'vR', 'vT', 'z', 'vz', ...). Can be a list with up to three entries for three subplots. Each entry 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 : str or callable or list Second dimension to plot. Same format as d1. width : int, optional Width of output div in pixels. Default is 600. height : int, optional Height of output div in pixels. Default is 400. xlabel : str or list, optional 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 : str or list, optional Label for the second dimension. Same format as xlabel. json_filename : str, optional 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 : bool, optional If True, create a static plot that doesn't allow zooming, panning, etc. Default is False. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert. Default is the object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert. Default is the object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. Returns ------- 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. Notes ----- - 2017-09-17 - 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 = [] tlabel = labeldict.get("t") if hasattr(self, "name"): # name for display names = ( self.name if isinstance(self.name, list) or isinstance(self.name, numpy.ndarray) else [self.name] ) else: names = [f"Object {i}" for i in range(self.size)] 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) # 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() jsonDict["time"] = self._parse_plot_quantity("t", **kwargs)[0].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 = "".join(choice(ascii_lowercase) for i in range(24)) button_width = 419.51 + 4.0 * 10.0 button_margin_left = int(numpy.round((width - button_width) / 2.0)) 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]) hovertemplate = ( lambda name, xlabel, ylabel, tlabel: f"""'{name}' + '
{xlabel}: %{{x:.2f}}' + '
{ylabel}: %{{y:.2f}}' + '
{tlabel}: %{{customdata:.2f}}'""" ) hovertemplate_current = ( lambda name, xlabel, ylabel, tlabel: f"""'{name} (Current location)' + '
{xlabel}: %{{x:.2f}}' + '
{ylabel}: %{{y:.2f}}' + '
{tlabel}: %{{customdata:.2f}}'""" ) 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), customdata: data.time, hovertemplate: {hovertemplate}, name: '', mode: 'lines', line: {{ shape: 'spline', width: 0.8, color: '{line_color}', }}, type: "scattergl", }}; let trace2= {{ x: data.x1_0.slice(0,numPerFrame), y: data.y1_0.slice(0,numPerFrame), customdata: data.time, hovertemplate: {hovertemplate_current}, name: '', mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, type: "scattergl", }}; """.format( line_color=line_colors[0], hovertemplate=hovertemplate(names[0], xlabels[0], ylabels[0], tlabel), hovertemplate_current=hovertemplate_current( names[0], xlabels[0], ylabels[0], tlabel ), ) 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), customdata: data.time, hovertemplate: {hovertemplate}, name: '', mode: 'lines', line: {{ shape: 'spline', width: 0.8, color: '{line_color}', }}, type: "scattergl", }}; let trace{trace_num_2}= {{ x: data.x1_{trace_indx}.slice(0,numPerFrame), y: data.y1_{trace_indx}.slice(0,numPerFrame), customdata: data.time, hovertemplate: {hovertemplate_current}, name: '', mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, type: "scattergl", }}; """.format( trace_indx=str(ii), trace_num_1=str(2 * ii + 1), trace_num_2=str(2 * ii + 2), line_color=line_colors[ii], hovertemplate=hovertemplate(names[ii], xlabels[0], ylabels[0], tlabel), hovertemplate_current=hovertemplate_current( names[ii], xlabels[0], ylabels[0], tlabel ), ) traces_cumul += f""",trace{str(2 * ii + 1)},trace{str(2 * ii + 2)}""" x_data_list = """""" y_data_list = """""" t_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, trace_indx=str(ii) ) y_data_list += """data.y{jj}_{trace_indx}.slice(trace_slice_begin,trace_slice_end), """.format( jj=jj + 1, trace_indx=str(ii) ) t_data_list += ( """data.time.slice(trace_slice_begin,trace_slice_end), """ ) 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), customdata: data.time, hovertemplate: {hovertemplate}, name: '', xaxis: 'x2', yaxis: 'y2', mode: 'lines', line: {{ shape: 'spline', width: 0.8, color: '{line_color}', }}, type: "scattergl", }}; let trace{trace_num_2}= {{ x: data.x2_0.slice(0,numPerFrame), y: data.y2_0.slice(0,numPerFrame), customdata: data.time, hovertemplate: {hovertemplate_current}, name: '', xaxis: 'x2', yaxis: 'y2', mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, type: "scattergl", }}; """.format( line_color=line_colors[0], trace_num_1=str(2 * self.size + 1), trace_num_2=str(2 * self.size + 2), hovertemplate=hovertemplate(names[0], xlabels[1], ylabels[1], tlabel), hovertemplate_current=hovertemplate_current( names[0], xlabels[1], ylabels[1], tlabel ), ) 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), customdata: data.time, hovertemplate: {hovertemplate}, name: '', xaxis: 'x2', yaxis: 'y2', mode: 'lines', line: {{ shape: 'spline', width: 0.8, color: '{line_color}', }}, type: "scattergl", }}; let trace{trace_num_2}= {{ x: data.x2_{trace_indx}.slice(0,numPerFrame), y: data.y2_{trace_indx}.slice(0,numPerFrame), customdata: data.time, hovertemplate: {hovertemplate_current}, name: '', xaxis: 'x2', yaxis: 'y2', mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, type: "scattergl", }}; """.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), hovertemplate=hovertemplate( names[ii], xlabels[1], ylabels[1], tlabel ), hovertemplate_current=hovertemplate_current( names[ii], xlabels[1], ylabels[1], tlabel ), ) 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), customdata: data.time, hovertemplate: {hovertemplate}, name: '', xaxis: 'x3', yaxis: 'y3', mode: 'lines', line: {{ shape: 'spline', width: 0.8, color: '{line_color}', }}, type: "scattergl", }}; let trace{trace_num_2}= {{ x: data.x3_0.slice(0,numPerFrame), y: data.y3_0.slice(0,numPerFrame), customdata: data.time, hovertemplate: {hovertemplate_current}, name: '', xaxis: 'x3', yaxis: 'y3', mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, type: "scattergl", }}; """.format( line_color=line_colors[0], trace_num_1=str(4 * self.size + 1), trace_num_2=str(4 * self.size + 2), hovertemplate=hovertemplate(names[0], xlabels[2], ylabels[2], tlabel), hovertemplate_current=hovertemplate_current( names[0], xlabels[2], ylabels[2], tlabel ), ) 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), customdata: data.time, hovertemplate: {hovertemplate}, name: '', xaxis: 'x3', yaxis: 'y3', mode: 'lines', line: {{ shape: 'spline', width: 0.8, color: '{line_color}', }}, type: "scattergl", }}; let trace{trace_num_2}= {{ x: data.x3_{trace_indx}.slice(0,numPerFrame), y: data.y3_{trace_indx}.slice(0,numPerFrame), customdata: data.time, hovertemplate: {hovertemplate_current}, name: '', xaxis: 'x3', yaxis: 'y3', mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, type: "scattergl", }}; """.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), hovertemplate=hovertemplate( names[ii], xlabels[2], ylabels[2], tlabel ), hovertemplate_current=hovertemplate_current( names[ii], xlabels[2], ylabels[0], tlabel ), ) 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( """
""".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, x_data_list=x_data_list, y_data_list=y_data_list, t_data_list=t_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))], ) ) def animate3d(self, mw_plane_bg=False, **kwargs): # pragma: no cover """ Animate a previously calculated orbit in 3D (with reasonable defaults). Parameters ---------- d1 : str First dimension to plot ('x', 'y', 'R', 'vR', 'vT', 'z', 'vz', ...), can only be a single entry. d2 : str Second dimension to plot. Same format as d1. d3 : str Third dimension to plot. Same format as d1. width : int, optional Width of output div in px, by default 800. height : int, optional Height of output div in px, by default 600. mw_plane_bg : bool, optional Whether to add a Milky Way plane when plotting x, y, z, by default False. xlabel : str or list, optional Label for the first dimension; should only have to be specified when using a function as d1. ylabel : str or list, optional Label for the second dimension; should only have to be specified when using a function as d2. zlabel : str or list, optional Label for the third dimension; should only have to be specified when using a function as d3. json_filename : str, optional 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, by default None. ro : float or Quantity, optional Physical scale in kpc for distances to use to convert, Default is the object-wide default. vo : float or Quantity, optional Physical scale for velocities in km/s to use to convert, by default None. Default is the object-wide default. use_physical : bool, optional Use to override object-wide default for using a physical scale for output. Returns ------- 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. Notes ----- - 2022-12-09 - Written - Henry Leung (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 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 do 3D animation with 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: 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") d3 = kwargs.pop("d3") xs = [] ys = [] zs = [] xlabels = [] ylabels = [] zlabels = [] tlabel = labeldict.get("t") if hasattr(self, "name"): # name for display names = ( self.name if isinstance(self.name, list) or isinstance(self.name, numpy.ndarray) else [self.name] ) else: names = [f"Object {i}" for i in range(self.size)] if isinstance(d1, str) or callable(d1): d1s = [d1] d2s = [d2] d3s = [d3] else: d1s = d1 d2s = d2 d3s = d3 if len(d1s) > 1: raise ValueError("Orbit.animate3d only works for one subplot") all_xlabel = kwargs.get("xlabel", [None for d in d1]) all_ylabel = kwargs.get("ylabel", [None for d in d2]) all_zlabel = kwargs.get("zlabel", [None for d in d3]) for d1, d2, d3, xlabel, ylabel, zlabel in zip( d1s, d2s, d3s, all_xlabel, all_ylabel, all_zlabel ): # Get x and y for each subplot kwargs["dontreshape"] = True x = self._parse_plot_quantity(d1, **kwargs) y = self._parse_plot_quantity(d2, **kwargs) z = self._parse_plot_quantity(d3, **kwargs) kwargs.pop("dontreshape") xs.append(x) ys.append(y) zs.append(z) 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) if zlabel is None: zlabels.append(labeldict.get(d3, r"\mathrm{No\ ylabel\ specified}")) else: zlabels.append(zlabel) 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", 800) height = kwargs.pop("height", 600) # 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() jsonDict["z%i_%i" % (ii + 1, jj)] = zs[ii][jj].tolist() jsonDict["time"] = self._parse_plot_quantity("t", **kwargs)[0].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.divid3d = "".join(choice(ascii_lowercase) for i in range(24)) button_width = 419.51 + 4.0 * 10.0 button_margin_left = int(numpy.round((width - button_width) / 2.0)) if button_margin_left < 0: button_margin_left = 0 # 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)) ] ) hovertemplate = ( lambda name, xlabel, ylabel, zlabel, tlabel: f"""'{name}' + '
{xlabel}: %{{x:.2f}}' + '
{ylabel}: %{{y:.2f}}' + '
{zlabel}: %{{z:.2f}}' + '
{tlabel}: %{{customdata:.2f}}'""" ) hovertemplate_current = ( lambda name, xlabel, ylabel, zlabel, tlabel: f"""'{name} (Current location)' + '
{xlabel}: %{{x:.2f}}' + '
{ylabel}: %{{y:.2f}}' + '
{zlabel}: %{{z:.2f}}' + '
{tlabel}: %{{customdata:.2f}}'""" ) layout = """{{ scene:{{ // force the scene always look like a cube aspectmode: 'cube', xaxis: {{ title: '{xlabel}', domain: [{xmin},{xmax}], }}, yaxis: {{title: '{ylabel}'}}, zaxis: {{title: '{zlabel}'}},}}, margin: {{t: 20}}, hovermode: 'closest', showlegend: false, """.format( xlabel=xlabels[0], ylabel=ylabels[0], zlabel=zlabels[0], xmin=xmin[0], xmax=xmax[0], ) layout += """}""" # First plot setup_trace1 = """ let trace1= {{ x: data.x1_0.slice(0,numPerFrame), y: data.y1_0.slice(0,numPerFrame), z: data.z1_0.slice(0,numPerFrame), customdata: data.time, hovertemplate: {hovertemplate}, mode: 'lines', name: '', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, type: "scatter3d", }}; let trace2= {{ x: data.x1_0.slice(0,numPerFrame), y: data.y1_0.slice(0,numPerFrame), z: data.z1_0.slice(0,numPerFrame), customdata: data.time, hovertemplate: {hovertemplate_current}, mode: 'lines', name: '', line: {{ shape: 'spline', width: 8., color: '{line_color}', }}, type: "scatter3d", }}; """.format( line_color=line_colors[0], hovertemplate=hovertemplate( names[0], xlabels[0], ylabels[0], zlabels[0], tlabel ), hovertemplate_current=hovertemplate_current( names[0], xlabels[0], ylabels[0], zlabels[0], tlabel ), ) traces_cumul = """trace1,trace2""" # milkyway plane surface # kpc or internal unit, because we need to scale the img correctly is_kpc = ( True if "kpc" in labeldict.get(d1, r"\mathrm{No\ xlabel\ specified}") else False ) mw_bg_surface_scale = 20.775 if not is_kpc: mw_bg_surface_scale /= self._ro mw_bg_surface = f"""let mw_bg = {{ x: {json.dumps((numpy.linspace(-1, 1, 50) * mw_bg_surface_scale).tolist())}, y: {json.dumps((numpy.linspace(-1, 1, 50) * mw_bg_surface_scale).tolist())}, z: {json.dumps((numpy.zeros((50, 50))).tolist())}, colorscale: [[0.0,"rgba(0, 0, 0, 1)"],[0.09090909090909091,"rgba(16, 16, 16, 1)"],[0.18181818181818182,"rgba(38, 38, 38, 0.9)"],[0.2727272727272727,"rgba(59, 59, 59, 0.8)"],[0.36363636363636365,"rgba(81, 80, 80, 0.7)"],[0.45454545454545453,"rgba(102, 101, 101, 0.6)"],[0.5454545454545454,"rgba(124, 123, 122, 0.5)"],[0.6363636363636364,"rgba(146, 146, 145, 0.4)"],[0.7272727272727273,"rgba(171, 171, 170, 0.3)"],[0.8181818181818182,"rgba(197, 197, 195, 0.2)"],[0.9090909090909091,"rgba(224, 224, 223, 0.1)"],[1.0,"rgba(254, 254, 253, 0.05)"]], surfacecolor: [ [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 253, 252, 254, 252, 251, 251, 249, 249, 253, 254, 253, 251, 251, 249, 250, 253, 255, 254, 254, 255, 255, 254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 253, 252, 251, 247, 242, 238, 236, 235, 228, 230, 229, 224, 225, 228, 227, 236, 239, 233, 227, 239, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 253, 251, 247, 243, 239, 233, 230, 229, 225, 228, 227, 229, 230, 220, 218, 223, 225, 235, 239, 231, 223, 214, 233, 248, 254, 253, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 252, 250, 247, 241, 236, 232, 231, 235, 235, 229, 229, 229, 228, 228, 232, 233, 233, 233, 236, 236, 238, 236, 220, 202, 227, 248, 250, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 251, 248, 245, 241, 238, 236, 236, 237, 232, 227, 221, 217, 217, 215, 217, 222, 225, 227, 230, 232, 235, 235, 236, 216, 212, 235, 245, 248, 251, 252, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 254, 253, 250, 247, 245, 241, 240, 238, 235, 231, 231, 226, 220, 217, 213, 210, 209, 208, 212, 215, 218, 222, 226, 230, 232, 233, 220, 207, 233, 247, 246, 249, 251, 253, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 253, 252, 249, 246, 243, 239, 236, 233, 229, 227, 225, 223, 222, 218, 216, 216, 211, 205, 203, 205, 202, 206, 215, 221, 223, 228, 228, 211, 219, 241, 244, 246, 248, 251, 254, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 254, 253, 251, 247, 243, 239, 234, 230, 227, 224, 222, 220, 218, 216, 214, 209, 205, 210, 208, 202, 196, 192, 193, 197, 198, 207, 218, 224, 225, 209, 223, 240, 244, 245, 248, 252, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 254, 255, 255, 254, 253, 249, 244, 239, 234, 229, 225, 222, 219, 216, 212, 212, 210, 209, 208, 200, 200, 205, 201, 194, 189, 186, 183, 178, 173, 189, 208, 222, 214, 215, 235, 239, 243, 246, 249, 253, 254, 255, 254, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 254, 251, 246, 241, 234, 227, 224, 221, 217, 216, 212, 209, 206, 203, 199, 196, 201, 200, 199, 197, 196, 192, 185, 181, 176, 168, 160, 175, 203, 221, 226, 228, 230, 236, 244, 247, 252, 253, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 252, 248, 244, 237, 230, 225, 220, 217, 212, 203, 197, 189, 181, 180, 173, 162, 169, 178, 175, 189, 197, 194, 195, 189, 177, 175, 147, 136, 177, 209, 226, 226, 217, 225, 243, 245, 249, 252, 254, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 253, 249, 245, 238, 231, 226, 219, 215, 211, 200, 178, 180, 177, 175, 178, 172, 161, 154, 159, 156, 155, 177, 186, 185, 192, 186, 173, 159, 140, 141, 189, 216, 225, 210, 212, 236, 242, 246, 252, 254, 255, 254, 255, 255], [255, 255, 255, 255, 255, 254, 251, 246, 241, 234, 227, 222, 216, 211, 205, 193, 190, 192, 191, 188, 183, 179, 181, 180, 176, 177, 159, 149, 168, 162, 178, 193, 183, 167, 152, 123, 142, 198, 221, 219, 210, 226, 242, 244, 249, 253, 255, 255, 255, 255], [255, 255, 255, 255, 255, 253, 248, 242, 236, 229, 223, 218, 209, 194, 189, 197, 191, 182, 172, 161, 151, 142, 142, 144, 148, 169, 180, 169, 173, 170, 153, 171, 186, 178, 166, 140, 100, 152, 211, 224, 206, 211, 239, 244, 247, 252, 255, 254, 255, 255], [255, 255, 255, 255, 254, 251, 245, 238, 233, 225, 218, 200, 186, 189, 191, 187, 170, 152, 132, 122, 120, 113, 105, 98, 89, 103, 125, 154, 171, 176, 158, 137, 173, 185, 168, 154, 127, 138, 188, 219, 207, 205, 235, 240, 245, 250, 254, 255, 255, 255], [255, 255, 255, 255, 253, 247, 243, 235, 227, 216, 197, 184, 192, 197, 189, 161, 134, 121, 117, 119, 129, 135, 132, 126, 119, 105, 92, 95, 124, 162, 176, 136, 151, 189, 174, 155, 149, 140, 170, 209, 213, 211, 229, 237, 243, 248, 253, 255, 255, 255], [255, 255, 255, 255, 251, 245, 239, 231, 221, 205, 193, 198, 200, 191, 158, 104, 100, 124, 134, 145, 160, 160, 162, 166, 165, 154, 134, 102, 90, 105, 151, 156, 155, 180, 177, 165, 148, 124, 153, 199, 208, 205, 226, 235, 241, 248, 252, 255, 255, 255], [255, 255, 255, 254, 248, 243, 235, 227, 214, 201, 202, 205, 197, 165, 112, 103, 127, 151, 159, 169, 161, 153, 161, 147, 161, 171, 172, 151, 120, 73, 110, 158, 160, 170, 177, 169, 153, 115, 137, 190, 199, 204, 223, 232, 239, 245, 251, 254, 255, 255], [255, 255, 255, 253, 247, 241, 233, 222, 207, 203, 206, 201, 177, 135, 116, 145, 161, 162, 156, 143, 139, 132, 152, 174, 173, 161, 162, 179, 156, 108, 90, 139, 158, 158, 175, 169, 151, 134, 143, 176, 199, 204, 219, 230, 237, 243, 250, 254, 255, 255], [255, 255, 255, 251, 246, 238, 231, 216, 206, 208, 206, 186, 125, 120, 154, 163, 167, 163, 139, 129, 124, 78, 92, 167, 177, 179, 151, 171, 176, 139, 92, 116, 147, 145, 175, 166, 144, 138, 140, 171, 196, 200, 216, 228, 234, 242, 249, 254, 255, 255], [255, 255, 255, 250, 244, 235, 228, 214, 209, 209, 200, 150, 112, 133, 161, 170, 170, 148, 144, 142, 60, 25, 68, 124, 147, 172, 182, 162, 171, 165, 116, 109, 135, 138, 172, 166, 143, 117, 130, 173, 195, 204, 216, 228, 234, 241, 247, 252, 255, 255], [255, 255, 255, 249, 242, 233, 224, 213, 208, 208, 179, 126, 131, 156, 162, 175, 154, 125, 148, 94, 67, 59, 38, 76, 119, 128, 154, 173, 161, 172, 117, 91, 133, 130, 164, 158, 141, 118, 126, 168, 196, 210, 216, 227, 234, 240, 246, 252, 255, 255], [255, 255, 255, 249, 242, 233, 220, 211, 211, 205, 158, 131, 151, 164, 173, 173, 133, 132, 133, 81, 129, 115, 39, 15, 31, 64, 106, 165, 170, 163, 115, 79, 132, 142, 163, 148, 146, 120, 127, 174, 191, 210, 220, 226, 232, 241, 246, 252, 255, 255], [255, 254, 253, 248, 242, 232, 215, 208, 215, 191, 140, 134, 154, 168, 177, 165, 130, 144, 91, 93, 162, 153, 89, 17, 0, 8, 54, 127, 170, 156, 122, 99, 131, 158, 147, 144, 142, 123, 145, 174, 194, 208, 219, 226, 232, 240, 246, 252, 255, 255], [255, 255, 253, 247, 242, 231, 214, 211, 215, 182, 131, 139, 157, 170, 175, 156, 129, 144, 99, 115, 170, 175, 127, 59, 4, 0, 21, 83, 145, 156, 121, 117, 125, 149, 140, 137, 142, 131, 150, 178, 198, 209, 217, 227, 233, 240, 246, 252, 255, 255], [255, 255, 253, 248, 242, 231, 216, 214, 213, 184, 136, 137, 161, 171, 177, 158, 142, 139, 97, 136, 165, 175, 155, 120, 70, 27, 12, 40, 113, 145, 100, 111, 126, 138, 146, 153, 119, 118, 156, 187, 196, 208, 219, 226, 232, 241, 248, 252, 255, 255], [255, 255, 253, 249, 241, 231, 219, 216, 211, 183, 140, 137, 168, 171, 176, 160, 145, 145, 88, 119, 162, 173, 183, 149, 136, 113, 67, 40, 81, 95, 79, 113, 131, 140, 154, 141, 102, 123, 167, 192, 192, 201, 220, 227, 235, 241, 247, 253, 255, 255], [255, 254, 253, 249, 242, 231, 221, 220, 215, 181, 133, 140, 164, 173, 176, 155, 149, 156, 92, 97, 160, 166, 184, 184, 164, 148, 126, 67, 33, 49, 106, 137, 146, 162, 156, 125, 120, 154, 184, 190, 180, 199, 222, 229, 235, 242, 249, 252, 255, 255], [255, 255, 254, 250, 243, 232, 222, 223, 216, 190, 125, 128, 166, 176, 173, 165, 161, 160, 108, 83, 139, 164, 155, 180, 195, 183, 169, 120, 50, 86, 127, 140, 160, 155, 126, 107, 124, 167, 184, 163, 178, 215, 226, 229, 236, 243, 248, 255, 255, 255], [254, 255, 254, 250, 244, 234, 223, 222, 219, 193, 140, 141, 157, 175, 174, 173, 171, 165, 142, 97, 102, 148, 156, 146, 166, 179, 191, 172, 128, 107, 127, 148, 144, 131, 114, 117, 141, 168, 175, 176, 205, 221, 225, 232, 238, 245, 250, 253, 255, 255], [255, 255, 254, 251, 245, 237, 224, 219, 222, 199, 161, 134, 148, 169, 175, 179, 176, 168, 157, 132, 95, 104, 150, 156, 140, 145, 147, 153, 154, 144, 147, 143, 121, 91, 108, 158, 171, 174, 181, 205, 215, 221, 228, 233, 240, 245, 250, 255, 255, 255], [255, 255, 255, 252, 246, 239, 229, 223, 222, 210, 165, 128, 150, 165, 178, 185, 181, 170, 150, 154, 133, 87, 91, 123, 141, 151, 147, 144, 145, 139, 129, 106, 91, 113, 149, 180, 191, 186, 192, 211, 218, 224, 229, 236, 242, 248, 253, 255, 255, 255], [255, 255, 254, 252, 247, 241, 233, 227, 225, 217, 184, 145, 140, 159, 176, 187, 184, 180, 143, 150, 164, 134, 96, 86, 91, 97, 112, 116, 104, 90, 84, 108, 138, 170, 189, 190, 179, 169, 199, 215, 221, 226, 232, 239, 244, 250, 254, 255, 255, 255], [255, 255, 254, 252, 248, 243, 236, 231, 227, 221, 201, 161, 142, 159, 170, 184, 188, 186, 168, 149, 158, 164, 154, 129, 113, 94, 96, 108, 111, 105, 123, 168, 185, 187, 180, 170, 170, 188, 211, 220, 225, 229, 235, 242, 247, 252, 255, 255, 255, 255], [255, 254, 254, 253, 249, 245, 239, 234, 231, 226, 213, 180, 152, 158, 168, 179, 188, 188, 190, 165, 143, 145, 161, 168, 169, 170, 159, 161, 175, 179, 190, 187, 170, 163, 163, 181, 201, 214, 218, 221, 227, 232, 239, 245, 250, 254, 255, 255, 255, 255], [255, 255, 255, 254, 252, 249, 243, 237, 234, 230, 222, 204, 164, 135, 163, 175, 183, 189, 193, 187, 169, 169, 170, 148, 159, 177, 174, 178, 180, 185, 185, 181, 177, 179, 194, 210, 212, 216, 219, 225, 230, 236, 243, 248, 253, 255, 255, 255, 255, 255], [255, 255, 255, 255, 254, 252, 248, 241, 236, 234, 229, 219, 191, 151, 152, 163, 175, 182, 188, 194, 188, 183, 190, 177, 158, 165, 173, 161, 161, 173, 184, 188, 199, 207, 209, 211, 214, 220, 224, 228, 234, 241, 246, 251, 254, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 254, 252, 246, 240, 236, 234, 229, 214, 187, 144, 147, 173, 176, 180, 186, 186, 182, 187, 195, 194, 188, 188, 184, 181, 191, 198, 202, 205, 208, 212, 215, 222, 223, 229, 233, 239, 245, 249, 253, 254, 255, 255, 255, 255, 255], [255, 254, 255, 255, 255, 255, 254, 251, 245, 241, 237, 235, 227, 207, 173, 151, 151, 162, 177, 178, 180, 185, 187, 192, 197, 199, 198, 201, 203, 202, 205, 207, 210, 214, 216, 221, 223, 228, 233, 239, 243, 248, 252, 254, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 251, 245, 241, 239, 236, 226, 206, 184, 149, 141, 168, 176, 180, 185, 189, 190, 193, 197, 200, 205, 207, 209, 210, 211, 214, 215, 217, 221, 227, 233, 237, 242, 246, 251, 254, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 253, 250, 247, 244, 241, 236, 226, 214, 198, 177, 169, 166, 175, 186, 194, 191, 194, 196, 198, 205, 212, 215, 215, 213, 213, 214, 218, 225, 232, 238, 241, 245, 249, 253, 254, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 252, 249, 246, 242, 238, 231, 223, 215, 201, 190, 188, 184, 186, 196, 199, 201, 202, 207, 212, 214, 215, 215, 217, 218, 225, 234, 239, 242, 245, 248, 252, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 253, 251, 248, 245, 240, 234, 228, 222, 218, 217, 211, 203, 205, 207, 206, 204, 206, 211, 216, 220, 223, 226, 228, 234, 240, 244, 246, 249, 251, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 255, 254, 252, 250, 247, 244, 238, 234, 231, 227, 225, 224, 219, 217, 216, 214, 216, 221, 226, 230, 232, 235, 238, 242, 245, 248, 250, 252, 253, 254, 255, 255, 255, 255, 255, 255, 254, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 253, 252, 250, 247, 244, 241, 240, 236, 235, 232, 230, 230, 231, 232, 234, 236, 238, 239, 242, 246, 249, 250, 252, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 252, 253, 250, 247, 246, 245, 244, 244, 243, 243, 243, 244, 245, 247, 247, 250, 252, 253, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 255, 254, 253, 252, 251, 252, 250, 251, 250, 251, 252, 253, 253, 254, 254, 254, 255, 255, 255, 255, 255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ], hoverinfo:"skip", showscale:false, cmax:255, cmin:0, type: 'surface', }};""" setup_trace1 += mw_bg_surface 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), z: data.z1_{trace_indx}.slice(0,numPerFrame), customdata: data.time, hovertemplate: {hovertemplate}, name: '', mode: 'lines', line: {{ shape: 'spline', width: 3., color: '{line_color}', }}, type: "scatter3d", }}; let trace{trace_num_2}= {{ x: data.x1_{trace_indx}.slice(0,numPerFrame), y: data.y1_{trace_indx}.slice(0,numPerFrame), z: data.z1_{trace_indx}.slice(0,numPerFrame), customdata: data.time, hovertemplate: {hovertemplate_current}, name: '', mode: 'lines', line: {{ shape: 'spline', width: 8., color: '{line_color}', }}, type: "scatter3d", }}; """.format( trace_indx=str(ii), trace_num_1=str(2 * ii + 1), trace_num_2=str(2 * ii + 2), line_color=line_colors[ii], hovertemplate=hovertemplate( names[ii], xlabels[0], ylabels[0], zlabels[0], tlabel ), hovertemplate_current=hovertemplate_current( names[ii], xlabels[0], ylabels[0], zlabels[0], tlabel ), ) traces_cumul += f""",trace{str(2 * ii + 1)},trace{str(2 * ii + 2)}""" x_data_list = """""" y_data_list = """""" z_data_list = """""" t_data_list = """""" trace_num_10_list = """""" trace_num_20_list = """""" if ( mw_plane_bg and d1 == "x" and d2 == "y" and d3 == "z" ): # only add when its true traces_cumul += """,mw_bg""" 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, trace_indx=str(ii) ) y_data_list += """data.y{jj}_{trace_indx}.slice(trace_slice_begin,trace_slice_end), """.format( jj=jj + 1, trace_indx=str(ii) ) z_data_list += """data.z{jj}_{trace_indx}.slice(trace_slice_begin,trace_slice_end), """.format( jj=jj + 1, trace_indx=str(ii) ) t_data_list += ( """data.time.slice(trace_slice_begin,trace_slice_end), """ ) 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)}, """ return HTML( """
""".format( json_code=json_code, close_json_code=close_json_code, divid3d=self.divid3d, width=width, height=height, button_margin_left=button_margin_left, layout=layout, x_data_list=x_data_list, y_data_list=y_data_list, z_data_list=z_data_list, t_data_list=t_data_list, trace_num_10_list=trace_num_10_list, trace_num_20_list=trace_num_20_list, setup_trace1=setup_trace1, traces_cumul=traces_cumul, 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): """ Query Simbad for the phase-space coordinates of one object. Parameters ---------- name : str Name of the object. obs : numpy.ndarray, optional Array of [ro, vo, zo, solarmotion] that can be altered. Returns ------- list A list of [ra, dec, dist, pmra, pmdec, vlos]. Notes ----- - 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() # Make sure to make an HTTPS request so this code works in the browser simbad.SIMBAD_URL = simbad.SIMBAD_URL.replace("http://", "https://") simbad.add_votable_fields( "ra" if _AQ_GT_47 else "ra(d)", "dec" if _AQ_GT_47 else "dec(d)", "pmra", "pmdec", "rvz_radvel" if _AQ_GT_47 else "rv_value", ) if not _AQ_GT_47: # pragma: no cover simbad.add_votable_fields("plx", "distance") # 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") if _AQ_GT_47: # check that the necessary coordinates have been found missing = simbad_table.mask if any(missing["ra", "dec", "pmra", "pmdec", "rvz_radvel"][0]): raise ValueError(f"failed to find some coordinates for {name} in SIMBAD") ra, dec, pmra, pmdec, vlos = simbad_table[ "ra", "dec", "pmra", "pmdec", "rvz_radvel" ][0] # get a distance value simbad.add_votable_fields("plx_value") simbad_table = simbad.query_object(name) if not simbad_table or ( hasattr(simbad_table["plx_value"][0], "mask") and simbad_table["plx_value"][0].mask ): # No parallax, try to find a distance simbad.reset_votable_fields() simbad.add_votable_fields("mesdistance") simbad_table = simbad.query_object(name) if not simbad_table or ( hasattr(simbad_table["mesdistance.dist"][0], "mask") and simbad_table["mesdistance.dist"][0].mask ): # pragma: no cover # I can't find an example of a case without both a plx and a distance, but with a pmra and pmdec raise ValueError(f"Failed to find a distance for {name} in SIMBAD") dist = simbad_table["mesdistance.dist"][0] else: dist = 1.0 / simbad_table["plx_value"][0] else: # pragma: no cover # 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(f"failed to find some coordinates for {name} in SIMBAD") 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.0 / 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.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.0) * (Y == 0.0) * (Z == 0.0) if True in bad_indx: # pragma: no cover X[bad_indx] += ro / 10000.0 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.0 for ii in range(vxvv.shape[0]): sub_vxvv = (orb_vxvv - vxvv[ii, :].flatten()) ** 2.0 # print(sub_vxvv[numpy.argmin(numpy.sum(sub_vxvv,axis=1))]) if not vxvv_err is None: sub_vxvv /= vxvv_err[ii, :] ** 2.0 else: sub_vxvv /= 0.01**2.0 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, thiso=thiso) 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]), numpy.zeros_like(thiso[0]), Xsun=numpy.sqrt(obs[0] ** 2.0 + obs[1] ** 2.0) / 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.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.0 + obs[1] ** 2.0) / 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.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, thiso=thiso) X, Y, Z = _helioXYZ(orb, thiso, *args, **kwargs) bad_indx = (X == 0.0) * (Y == 0.0) * (Z == 0.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, thiso=thiso) 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.0 + obs[1] ** 2.0) 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 / vo + obs[4] * obs[1] / Xsun / vo, -obs[3] * obs[1] / Xsun / vo + obs[4] * obs[0] / Xsun / vo, obs[5] * ( numpy.ones_like(Xsun) if isinstance(Xsun, numpy.ndarray) and obs[5].ndim == 0 else 1.0 ) / 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.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), numpy.zeros_like(obs.vR(*args, **kwargs)), ] ), Xsun=obs.R(*args, **kwargs), Zsun=0.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.0 + obs[1] ** 2.0) 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 / vo + obs[4] * obs[1] / Xsun / vo, -obs[3] * obs[1] / Xsun / vo + obs[4] * obs[0] / Xsun / vo, obs[5] * ( numpy.ones_like(Xsun) if isinstance(Xsun, numpy.ndarray) and obs[5].ndim == 0 else 1.0 ) / 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.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.0] ), Xsun=obs.R(*args, **kwargs), Zsun=0.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, thiso=thiso) X, Y, Z, vX, vY, vZ = _XYZvxvyvz(orb, thiso, *args, **kwargs) bad_indx = (X == 0.0) * (Y == 0.0) * (Z == 0.0) if True in bad_indx: X[bad_indx] += ro / 10000.0 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, thiso=None): 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.0] elif len(obs) == 4: obs = [obs[0], obs[1], 0.0, obs[2], obs[3], 0.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.0, orb._zo, orb._solarmotion[0], orb._solarmotion[1] + orb._vo, orb._solarmotion[2], ] else: obs = [orb._ro, 0.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 # Tile everything when thiso includes a time axis if isinstance(obs, list) and not thiso is None and thiso.shape[1] > orb.size: nt = thiso.shape[1] // orb.size obs = [ ( numpy.tile(obs[ii], nt) if isinstance(obs[ii], numpy.ndarray) and obs[ii].ndim > 0 else obs[ii] ) for ii in range(len(obs)) ] ro = numpy.tile(ro, nt) if isinstance(ro, numpy.ndarray) and ro.ndim > 0 else ro vo = numpy.tile(vo, nt) if isinstance(vo, numpy.ndarray) and vo.ndim > 0 else 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.0**-10.0: 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=1741018131.0 galpy-1.10.2/galpy/orbit/__init__.py0000644000175100001660000000012214761352023016654 0ustar00runnerdockerfrom . import Orbits # # Functions # # none # # Classes # Orbit = Orbits.Orbit ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/orbit/integrateFullOrbit.py0000644000175100001660000013502114761352023020731 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.0]) # 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.0 * 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, 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.0 * numpy.pi * p._glw[ii] * p._b * p._c / numpy.sqrt( (1.0 + (p._b2 - 1.0) * p._glx[ii] ** 2.0) * (1.0 + (p._c2 - 1.0) * p._glx[ii] ** 2.0) ) 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.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.0 * numpy.pi * Sigma.get("amp", 1.0) * p._amp, Sigma.get("h", 1.0 / 3.0), ] ) elif stype == "expwhole" or (stype == "exp" and "Rhole" in Sigma): pot_args.extend( [ 4, 1, 4.0 * numpy.pi * Sigma.get("amp", 1.0) * p._amp, Sigma.get("h", 1.0 / 3.0), 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, 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.0, 0.0, 0.0, 0.0]) else: if p._omegaz_only: pot_args.extend([0.0, 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, 0.0, p._Omegadot]) elif not p._const_freq: pot_args.extend(p._Omegadot) else: pot_args.extend([0.0, 0.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, 0.0, 0.0, 0.0, 0.0]) # for caching pot_args.extend( [ p._ms, p._rhm, p._gamma**2.0, -1 if not p._lnLambda else p._lnLambda, p._minr**2.0, ] ) 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, 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.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) elif isinstance(p, potential.KuzminLikeWrapperPotential): pot_type.append(-10) # 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._a, p._b2]) 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 ): """ Integrate an ode for a FullOrbit. Parameters ---------- pot : Potential or list of such instances The potential (or list thereof) to evaluate the orbit in. yo : numpy.ndarray Initial condition [q,p], can be [N,6] or [6]. t : numpy.ndarray Set of times at which one wants the result. int_method : str Integration method. One of 'leapfrog_c', 'rk4_c', 'rk6_c', 'symplec4_c'. rtol : float, optional Relative tolerance. atol : float, optional Absolute tolerance. progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!). dt : float, optional Force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators). Returns ------- tuple (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 : int or array of ints Error message, if not zero: 1 means maximum step reduction happened for adaptive integrators. Notes ----- - 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 """ Integrate an ode for a planarOrbit+phase space volume dxdv. Parameters ---------- pot : Potential or list of such instances The potential (or list thereof) to evaluate the orbit in. yo : numpy.ndarray Initial condition [q,p]. dyo : numpy.ndarray Initial condition [dq,dp]. t : numpy.ndarray Set of times at which one wants the result. int_method : str Integration method. One of 'leapfrog_c', 'rk4_c', 'rk6_c', 'symplec4_c'. rtol : float, optional Relative tolerance. atol : float, optional Absolute tolerance. Returns ------- tuple (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 : int Error message if not zero, 1: maximum step reduction happened for adaptive integrators. Notes ----- - 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 ): """ Integrate an ode for a FullOrbit Parameters ---------- pot : Potential or list of such instances The potential (or list thereof) to evaluate the orbit in. yo : numpy.ndarray Initial condition [q,p], shape [N,5] or [N,6] t : numpy.ndarray Set of times at which one wants the result. int_method : str Integration method. One of 'leapfrog', 'odeint', 'dop853'. rtol : float, optional Relative tolerance. atol : float, optional Absolute tolerance. numcores : int, optional Number of cores to use for multi-processing. progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!). dt : float, optional Force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators). Returns ------- tuple (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 : int or array of ints Error message, if not zero: 1 means maximum step reduction happened for adaptive integrators. Notes ----- - 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.0 + out[:, 1] ** 2.0) phi = numpy.arccos(out[:, 0] / R) phi[(out[:, 1] < 0.0)] = 2.0 * numpy.pi - phi[(out[:, 1] < 0.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.0 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.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.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 integrateFullOrbit_sos_c( pot, yo, psi, t0, int_method, rtol=None, atol=None, progressbar=True, dpsi=None ): """ Integrate an ode for a FullOrbit for integrate_sos in C Parameters ---------- pot : Potential or list of such instances The potential (or list thereof) to evaluate the orbit in. yo : numpy.ndarray initial condition [q,p] psi : numpy.ndarray set of increment angles at which one wants the result [increments wrt initial angle] t0 : float or numpy.ndarray initial time int_method : str 'rk4_c', 'rk6_c', 'dopr54_c', or 'dop853_c' rtol : float, optional tolerances (not always used...) atol : float, optional tolerances (not always used...) progressbar : bool, optional if True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!) dpsi : float, optional force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators) Returns ------- tuple (y,err) y : array, shape (N,len(psi),7) where the last of the last dimension is the time Array containing the value of y for each desired angle in psi, \ with the initial value y0 in the first row. err : int error message, always zero for now Notes ----- - 2023-03-17 - Written based on integrateFullOrbit_c - 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 dpsi is None: dpsi = -9999.99 t0 = numpy.atleast_1d(t0) yoo = numpy.empty((nobj, 7)) yoo[:, :6] = yo[:, :6] if len(t0) == 1: yoo[:, 6] = t0[0] else: yoo[:, 6] = t0 npsi = len(psi.T) # .T to make npsi always the first dim # Set up result array result = numpy.empty((nobj, npsi, 7)) 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_sos integrationFunc.argtypes = [ ctypes.c_int, ndpointer(dtype=numpy.float64, flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.float64, flags=ndarrayFlags), ctypes.c_int, 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 = [yoo.flags["F_CONTIGUOUS"], psi.flags["F_CONTIGUOUS"]] yoo = numpy.require(yoo, dtype=numpy.float64, requirements=["C", "W"]) psi = numpy.require(psi, 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), yoo, ctypes.c_int(npsi), psi, ctypes.c_int(len(psi.shape) > 1), ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(dpsi), 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]: yoo = numpy.asfortranarray(yoo) if f_cont[1]: psi = numpy.asfortranarray(psi) if single_obj: return (result[0], err[0]) else: return (result, err) def integrateFullOrbit_sos( pot, yo, psi, t0, int_method, rtol=None, atol=None, numcores=1, progressbar=True, dpsi=None, ): """ Integrate an ode for a FullOrbit for integrate_sos Parameters ---------- pot : Potential or list of such instances The potential (or list thereof) to evaluate the orbit in. yo : numpy.ndarray Initial condition [q,p], shape [N,5] or [N,6] psi : numpy.ndarray Set of increment angles at which one wants the result [increments wrt initial angle] t0 : float or numpy.ndarray Initial time int_method : str Integration method. One of 'leapfrog', 'odeint', or 'dop853' rtol : float, optional Relative tolerance. Default is None. atol : float, optional Absolute tolerance. Default is None. numcores : int, optional Number of cores to use for multi-processing. Default is 1. progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!). Default is True. dpsi : float, optional Force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators). Default is None. Returns ------- tuple (y,err) y : array, shape (N,len(psi),6/7) where the last of the last dimension is the time Array containing the value of y for each desired angle in psi, with the initial value y0 in the first row. err : float Error message, always zero for now Notes ----- - 2023-03-16 - Written based on integrateFullOrbit - Bovy (UofT) """ nophi = False 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 not "_c" in int_method: 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, psi, t0): # go to the transformed plane: (x,vx,y,vy,A,t) init_psi = numpy.arctan2(vxvv[3], vxvv[4]) init = numpy.array( [ vxvv[0] * numpy.cos(vxvv[5]), vxvv[1] * numpy.cos(vxvv[5]) - vxvv[2] * numpy.sin(vxvv[5]), vxvv[0] * numpy.sin(vxvv[5]), vxvv[2] * numpy.cos(vxvv[5]) + vxvv[1] * numpy.sin(vxvv[5]), numpy.sqrt(vxvv[3] ** 2.0 + vxvv[4] ** 2.0), t0, ] ) # integrate intOut = integrator( _SOSEOM, init, t=psi + init_psi, args=(pot,), **extra_kwargs ) # go back to the cylindrical frame out = numpy.zeros((len(psi), 7)) out[:, 0] = numpy.sqrt(intOut[:, 0] ** 2.0 + intOut[:, 2] ** 2.0) out[:, 5] = numpy.arctan2(intOut[:, 2], intOut[:, 0]) out[:, 1] = intOut[:, 1] * numpy.cos(out[:, 5]) + intOut[:, 3] * numpy.sin( out[:, 5] ) out[:, 2] = intOut[:, 3] * numpy.cos(out[:, 5]) - intOut[:, 1] * numpy.sin( out[:, 5] ) out[:, 3] = intOut[:, 4] * numpy.sin(psi + init_psi) out[:, 4] = intOut[:, 4] * numpy.cos(psi + init_psi) out[:, 6] = intOut[:, 5] return out else: # Assume we are forcing parallel_mapping of a C integrator... def integrate_for_map(vxvv, psi, t0): return integrateFullOrbit_sos_c( pot, numpy.copy(vxvv), psi, t0, int_method, dpsi=dpsi )[0] if len(yo) == 1: # Can't map a single value... out = numpy.atleast_3d(integrate_for_map(yo[0], psi.flatten(), t0).T).T else: out = numpy.array( parallel_map( lambda ii: integrate_for_map( yo[ii], psi[ii] if len(psi.shape) > 1 else psi, t0[0] if len(t0) == 1 else t0[ii], ), range(len(yo)), numcores=numcores, progressbar=progressbar, ) ) if nophi: phi_mask = numpy.ones(out.shape[2], dtype="bool") phi_mask[5] = False out = out[:, :, phi_mask] return out, numpy.zeros(len(yo)) def _RZEOM(y, t, pot, l2): """ Implements the EOM, i.e., the right-hand side of the differential equation, for a 3D orbit assuming conservation of angular momentum. Parameters ---------- y : list or numpy.ndarray Current phase-space position. t : float Current time. pot : list of Potential instance(s) Potential instance(s). l2 : float Angular momentum squared. Returns ------- list Derivative of the phase-space position. Notes ----- - 2010-04-16 - Written - Bovy (NYU). """ return [ y[1], l2 / y[0] ** 3.0 + _evaluateRforces(pot, y[0], y[2], t=t), y[3], _evaluatezforces(pot, y[0], y[2], t=t), ] def _EOM(y, t, pot): """ Implements the EOM, i.e., the right-hand side of the differential equation, for a 3D orbit. Parameters ---------- y : list or numpy.ndarray Current phase-space position. t : float Current time. pot : list of Potential instance(s) Potential instance(s). Returns ------- list Derivative of the phase-space position. Notes ----- - 2010-04-16 - Written - Bovy (NYU) """ l2 = (y[0] ** 2.0 * y[3]) ** 2.0 return [ y[1], l2 / y[0] ** 3.0 + _evaluateRforces(pot, y[0], y[4], phi=y[2], t=t, v=[y[1], y[0] * y[3], y[5]]), y[3], 1.0 / y[0] ** 2.0 * ( _evaluatephitorques( pot, y[0], y[4], phi=y[2], t=t, v=[y[1], y[0] * y[3], y[5]] ) - 2.0 * 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 _SOSEOM(y, psi, pot): """ Implements the EOM, i.e., the right-hand side of the differential equation, for the SOS integration of a 3D orbit Parameters ---------- y : numpy.ndarray Current phase-space position psi : float Current angle pot : list of Potential instance(s) Potential instance(s) Returns ------- numpy.ndarray dy/dpsi Notes ----- - 2023-03-16 - Written - Bovy (UofT) """ # y = (x,vx,y,vy,A,t) # Calculate z, vz sp, cp = numpy.sin(psi), numpy.cos(psi) z = y[4] * sp gxyz = _rectForce([y[0], y[2], z], pot, t=y[5], vx=[y[1], y[3], y[4] * cp]) psidot = cp**2.0 - sp / y[4] * gxyz[2] Adot = y[4] * cp * sp + gxyz[2] * cp return numpy.array([y[1], gxyz[0], y[3], gxyz[1], Adot, 1.0]) / psidot def _rectForce(x, pot, t=0.0, vx=None): """ Returns the force in the rectangular frame Parameters ---------- x : numpy.ndarray Current position t : float, optional Current time (default is 0.0) pot : (list of) Potential instance(s) The potential (or list thereof) to evaluate the force for vx : numpy.ndarray, optional If set, use this [vx,vy,vz] when evaluating dissipative forces (default is None) Returns ------- numpy.ndarray The force in the rectangular frame Notes ----- - 2011-02-02 - Written - Bovy (NYU) """ # x is rectangular so calculate R and phi R = numpy.sqrt(x[0] ** 2.0 + x[1] ** 2.0) phi = numpy.arccos(x[0] / R) sinphi = x[1] / R cosphi = x[0] / R if x[1] < 0.0: phi = 2.0 * numpy.pi - phi if not vx is None: vR = vx[0] * cosphi + vx[1] * sinphi vT = -vx[0] * sinphi + vx[1] * cosphi vx = [vR, vT, vx[2]] # calculate forces Rforce = _evaluateRforces(pot, R, x[2], phi=phi, t=t, v=vx) phitorque = _evaluatephitorques(pot, R, x[2], phi=phi, t=t, v=vx) return numpy.array( [ cosphi * Rforce - 1.0 / R * sinphi * phitorque, sinphi * Rforce + 1.0 / R * cosphi * phitorque, _evaluatezforces(pot, R, x[2], phi=phi, t=t, v=vx), ] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/orbit/integrateLinearOrbit.py0000644000175100001660000002721214761352023021243 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.0 * numpy.pi * Sigma.get("amp", 1.0) * p._Pot._amp, Sigma.get("h", 1.0 / 3.0), ] ) elif stype == "expwhole" or (stype == "exp" and "Rhole" in Sigma): pot_args.extend( [ 4, 1, 4.0 * numpy.pi * Sigma.get("amp", 1.0) * p._Pot._amp, Sigma.get("h", 1.0 / 3.0), 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.0 * p._F]) elif isinstance(p, potential.IsothermalDiskPotential): pot_type.append(32) pot_args.extend([p._amp * p._sigma2 / p._H, 2.0 * 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 ): """ C integrate an ode for a LinearOrbit Parameters ---------- pot : Potential or list of such instances yo : numpy.ndarray initial condition [q,p], shape [N,2] or [2] t : numpy.ndarray set of times at which one wants the result int_method : str integration method rtol : float, optional relative tolerance atol : float, optional absolute tolerance progressbar : bool, optional if True, display a tqdm progress bar dt : float, optional force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators) Returns ------- tuple (y,err) y : 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 Notes ----- - 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 ): """ Integrate an ode for a LinearOrbit Parameters ---------- pot : Potential or list of such instances yo : numpy.ndarray initial condition [q,p], shape [N,2] or [2] t : numpy.ndarray set of times at which one wants the result int_method : str integration method rtol : float, optional relative tolerance atol : float, optional absolute tolerance numcores : int, optional number of cores to use progressbar : bool, optional if True, display a tqdm progress bar dt : float, optional force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators) Returns ------- tuple (y,err) y : 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 Notes ----- - 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): """ The one-dimensional equation-of-motion Parameters ---------- y : list or numpy.ndarray Current phase-space position t : float Current time pot : list (list of) linearPotential instance(s) Returns ------- list dy/dt Notes ----- - 2010-07-13 - Bovy (NYU) """ return [y[1], _evaluatelinearForces(pot, y[0], t=t)] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/orbit/integratePlanarOrbit.py0000644000175100001660000017304314761352023021252 0ustar00runnerdockerimport ctypes import ctypes.util import numpy from numpy.ctypeslib import ndpointer from scipy import integrate from .. import potential from ..potential.planarDissipativeForce import ( planarDissipativeForceFromFullDissipativeForce, ) from ..potential.planarPotential import ( _evaluateplanarphitorques, _evaluateplanarPotentials, _evaluateplanarRforces, planarPotentialFromFullPotential, planarPotentialFromRZPotential, ) from ..potential.WrapperPotential import WrapperPotential, 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, WrapperPotential)) ) or isinstance(p, (parentWrapperPotential, WrapperPotential)): if not isinstance(p, (parentWrapperPotential, WrapperPotential)): 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.0] ) # 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.0 * 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, 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.0 * numpy.pi * p._Pot._glw[ii] * p._Pot._b * p._Pot._c / numpy.sqrt( (1.0 + (p._Pot._b2 - 1.0) * p._Pot._glx[ii] ** 2.0) * (1.0 + (p._Pot._c2 - 1.0) * p._Pot._glx[ii] ** 2.0) ) 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.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.0 * numpy.pi * Sigma.get("amp", 1.0) * p._Pot._amp, Sigma.get("h", 1.0 / 3.0), ] ) elif stype == "expwhole" or (stype == "exp" and "Rhole" in Sigma): pot_args.extend( [ 4, 1, 4.0 * numpy.pi * Sigma.get("amp", 1.0) * p._Pot._amp, Sigma.get("h", 1.0 / 3.0), 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, planarDissipativeForceFromFullDissipativeForce ) and isinstance(p._Pot, potential.NonInertialFrameForce): pot_type.append(39) pot_args.append(p._Pot._amp) pot_args.extend( [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] ) # for caching pot_args.extend( [ p._Pot._rot_acc, p._Pot._lin_acc, p._Pot._omegaz_only, p._Pot._const_freq, p._Pot._Omega_as_func, ] ) if p._Pot._Omega_as_func: pot_args.extend([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) else: if p._Pot._omegaz_only: pot_args.extend([0.0, 0.0, p._Pot._Omega]) else: pot_args.extend(p._Pot._Omega) pot_args.append(p._Pot._Omega2) if not p._Pot._const_freq and p._Pot._omegaz_only: pot_args.extend([0.0, 0.0, p._Pot._Omegadot]) elif not p._Pot._const_freq: pot_args.extend(p._Pot._Omegadot) else: pot_args.extend([0.0, 0.0, 0.0]) if p._Pot._lin_acc: pot_tfuncs.extend([p._Pot._a0[0], p._Pot._a0[1], p._Pot._a0[2]]) if p._Pot._rot_acc: pot_tfuncs.extend([p._Pot._x0[0], p._Pot._x0[1], p._Pot._x0[2]]) pot_tfuncs.extend([p._Pot._v0[0], p._Pot._v0[1], p._Pot._v0[2]]) if p._Pot._Omega_as_func: if p._Pot._omegaz_only: pot_tfuncs.extend([p._Pot._Omega, p._Pot._Omegadot]) else: pot_tfuncs.extend( [ p._Pot._Omega[0], p._Pot._Omega[1], p._Pot._Omega[2], p._Pot._Omegadot[0], p._Pot._Omegadot[1], p._Pot._Omegadot[2], ] ) 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) elif ( ( isinstance(p, planarPotentialFromFullPotential) or isinstance(p, planarPotentialFromRZPotential) ) and isinstance(p._Pot, potential.KuzminLikeWrapperPotential) ) or isinstance(p, potential.KuzminLikeWrapperPotential): if not isinstance(p, potential.KuzminLikeWrapperPotential): p = p._Pot pot_type.append(-10) # 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._a, p._b2]) 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 elif int_method.lower() == "ias15_c": int_method_c = 7 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.0 * numpy.log(10.0) else: # pragma: no cover rtol = numpy.log(rtol) if atol is None: atol = -12.0 * numpy.log(10.0) else: # pragma: no cover atol = numpy.log(atol) return (rtol, atol) def _parse_scf_pot(p, extra_amp=1.0): # Stand-alone parser for SCF, bc reused 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, 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, ctypes.c_double, # Return type ) # 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 ): """ Integrate an ode for a planarOrbit. Parameters ---------- pot : Potential or list of such instances yo : numpy.ndarray Initial condition [q,p], can be [N,4] or [4]. t : numpy.ndarray Set of times at which one wants the result. int_method : str Integration method. Options are 'leapfrog_c', 'rk4_c', 'rk6_c', 'symplec4_c', ... rtol : float, optional Relative tolerance. atol : float, optional Absolute tolerance. progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!). dt : float, optional Force integrator to use this stepsize (default is to automatically determine one). Returns ------- tuple (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 : int Error message, if not zero: 1 means maximum step reduction happened for adaptive integrators. Notes ----- - 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 ): """ Integrate an ode for a planarOrbit+phase space volume dxdv Parameters ---------- pot : Potential or list of such instances yo : numpy.ndarray Initial condition [q,p] dyo : numpy.ndarray Initial condition [dq,dp] t : numpy.ndarray Set of times at which one wants the result int_method : str Integration method. One of 'leapfrog_c', 'rk4_c', 'rk6_c', 'symplec4_c' rtol : float, optional Relative tolerance. Default is None atol : float, optional Absolute tolerance. Default is None dt : float, optional Force integrator to use this stepsize (default is to automatically determine one) Returns ------- tuple (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 Notes ----- - 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 ): """ Integrate an ode for a planarOrbit Parameters ---------- pot : Potential or list of such instances yo : numpy.ndarray Initial condition [q,p], shape [N,3] or [N,4] t : numpy.ndarray Set of times at which one wants the result int_method : str Integration method. One of 'leapfrog', 'odeint', 'dop853' rtol : float, optional Relative tolerance. Default is None atol : float, optional Absolute tolerance. Default is None numcores : int, optional Number of cores to use for multi-processing progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!). dt : float, optional Force integrator to use this stepsize (default is to automatically determine one) Returns ------- tuple (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 Notes ----- - 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.0 + tmp_out[:, 1] ** 2.0) phi = numpy.arccos(tmp_out[:, 0] / R) phi[(tmp_out[:, 1] < 0.0)] = 2.0 * numpy.pi - phi[(tmp_out[:, 1] < 0.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.0 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.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.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, ): """ Integrate an ode for a planarOrbit+phase space volume dxdv Parameters ---------- pot : Potential or list of such instances yo : numpy.ndarray Initial condition [q,p], shape [N,4] dyo : numpy.ndarray Initial condition [dq,dp], shape [N,4] t : numpy.ndarray Set of times at which one wants the result int_method : str Integration method. One of 'leapfrog', 'odeint', 'dop853' rectIn : bool If True, input dyo is in rectangular coordinates rectOut : bool If True, output dyo is in rectangular coordinates rtol : float, optional Relative tolerance. Default is None atol : float, optional Absolute tolerance. Default is None progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!). dt : float, optional Force integrator to use this stepsize (default is to automatically determine one) numcores : int, optional Number of cores to use for multi-processing Returns ------- tuple (y,err) y,dy : 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 if not zero, 1: maximum step reduction happened for adaptive integrators Notes ----- - 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.0 + out[..., 1] ** 2.0) phi = numpy.arccos(out[..., 0] / R) phi[(out[..., 1] < 0.0)] = 2.0 * numpy.pi - phi[(out[..., 1] < 0.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 integratePlanarOrbit_sos_c( pot, yo, psi, t0, int_method, surface="x", rtol=None, atol=None, progressbar=True, dpsi=None, ): """ Integrate an ode for a PlanarOrbit for integrate_sos in C Parameters ---------- pot : Potential or list of such instances yo : numpy.ndarray Initial condition [q,p], shape [N,5] or [N,6] psi : numpy.ndarray Set of increment angles at which one wants the result [increments wrt initial angle] t0 : float or numpy.ndarray Initial time int_method : str 'rk4_c', 'rk6_c', 'dopr54_c', or 'dop853_c' surface : str, optional Surface to use ('x' for finding x=0, vx>0; 'y' for finding y=0, vy>0), by default "x" rtol : float, optional Relative tolerance, by default None atol : float, optional Absolute tolerance, by default None progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!), by default True dpsi : float, optional Force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators), by default None Returns ------- tuple (y,err) y : array, shape (N,len(psi),5) where the last of the last dimension is the time Array containing the value of y for each desired angle in psi, with the initial value y0 in the first row. err : int Error message, always zero for now Notes ----- - 2023-03-17 - Written based on integrateFullOrbit_sos_c - 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 dpsi is None: dpsi = -9999.99 t0 = numpy.atleast_1d(t0) yoo = numpy.empty((nobj, 5)) yoo[:, :4] = yo[:, :4] if len(t0) == 1: yoo[:, 4] = t0[0] else: yoo[:, 4] = t0 npsi = len(psi.T) # .T to make npsi always the first dim # Set up result array result = numpy.empty((nobj, npsi, 5)) 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_sos integrationFunc.argtypes = [ ctypes.c_int, ndpointer(dtype=numpy.float64, flags=ndarrayFlags), ctypes.c_int, ndpointer(dtype=numpy.float64, flags=ndarrayFlags), ctypes.c_int, ctypes.c_int, 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 = [yoo.flags["F_CONTIGUOUS"], psi.flags["F_CONTIGUOUS"]] yoo = numpy.require(yoo, dtype=numpy.float64, requirements=["C", "W"]) psi = numpy.require(psi, 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), yoo, ctypes.c_int(npsi), psi, ctypes.c_int(len(psi.shape) > 1), ctypes.c_int(1 if surface == "y" else 0), ctypes.c_int(npot), pot_type, pot_args, pot_tfuncs, ctypes.c_double(dpsi), 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]: yoo = numpy.asfortranarray(yoo) if f_cont[1]: psi = numpy.asfortranarray(psi) if single_obj: return (result[0], err[0]) else: return (result, err) def integratePlanarOrbit_sos( pot, yo, psi, t0, int_method, surface="x", rtol=None, atol=None, numcores=1, progressbar=True, dpsi=None, ): """ Integrate an ode for a PlanarOrbit for integrate_sos Parameters ---------- pot : Potential or list of such instances yo : numpy.ndarray Initial condition [q,p], shape [N,5] or [N,6] psi : numpy.ndarray Set of increment angles at which one wants the result [increments wrt initial angle] t0 : float or numpy.ndarray Initial time surface : str, optional Surface to use ('x' for finding x=0, vx>0; 'y' for finding y=0, vy>0), by default "x" int_method : str Integration method to use. One of 'leapfrog', 'odeint', or 'dop853' rtol : float, optional Relative tolerance, by default None atol : float, optional Absolute tolerance, by default None numcores : int, optional Number of cores to use for multi-processing, by default 1 progressbar : bool, optional If True, display a tqdm progress bar when integrating multiple orbits (requires tqdm to be installed!), by default True dpsi : float, optional Force integrator to use this stepsize (default is to automatically determine one; only for C-based integrators), by default None Returns ------- tuple (y,err) y : array, shape (N,len(psi),4/5) where the last of the last dimension is the time Array containing the value of y for each desired angle in psi, with the initial value y0 in the first row. err : int Error message, always zero for now Notes ----- - 2023-03-24 - Written based on integrateFullOrbi_sos - Bovy (UofT) """ if surface is None: surface = "x" nophi = False 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 not "_c" in int_method: 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, psi, t0): # go to the transformed plane: (A,t,y,vy) or (x,vx,A,t) if surface.lower() == "x": x = vxvv[0] * numpy.cos(vxvv[3]) vx = vxvv[1] * numpy.cos(vxvv[3]) - vxvv[2] * numpy.sin(vxvv[3]) init_psi = numpy.arctan2(x, vx) this_vxvv = numpy.array( [ vxvv[0] * numpy.sin(vxvv[3]), vxvv[2] * numpy.cos(vxvv[3]) + vxvv[1] * numpy.sin(vxvv[3]), numpy.sqrt(x**2.0 + vx**2.0), t0, ] ) # integrate intOut = integrator( _planarSOSEOMx, this_vxvv, t=psi + init_psi, args=(pot,), **extra_kwargs, ) # go back to the cylindrical frame x = intOut[:, 2] * numpy.sin(psi + init_psi) vx = intOut[:, 2] * numpy.cos(psi + init_psi) y = intOut[:, 0] vy = intOut[:, 1] else: y = vxvv[0] * numpy.sin(vxvv[3]) vy = vxvv[2] * numpy.cos(vxvv[3]) + vxvv[1] * numpy.sin(vxvv[3]) init_psi = numpy.arctan2(y, vy) this_vxvv = numpy.array( [ vxvv[0] * numpy.cos(vxvv[3]), vxvv[1] * numpy.cos(vxvv[3]) - vxvv[2] * numpy.sin(vxvv[3]), numpy.sqrt(y**2.0 + vy**2.0), t0, ] ) # integrate intOut = integrator( _planarSOSEOMy, this_vxvv, t=psi + init_psi, args=(pot,), **extra_kwargs, ) # go back to the cylindrical frame x = intOut[:, 0] vx = intOut[:, 1] y = intOut[:, 2] * numpy.sin(psi + init_psi) vy = intOut[:, 2] * numpy.cos(psi + init_psi) out = numpy.zeros((len(psi), 5)) out[:, 0] = numpy.sqrt(x**2.0 + y**2.0) out[:, 3] = numpy.arctan2(y, x) out[:, 1] = vx * numpy.cos(out[:, 3]) + vy * numpy.sin(out[:, 3]) out[:, 2] = vy * numpy.cos(out[:, 3]) - vx * numpy.sin(out[:, 3]) out[:, 4] = intOut[:, 3] return out else: # Assume we are forcing parallel_mapping of a C integrator... def integrate_for_map(vxvv, psi, t0): return integratePlanarOrbit_sos_c( pot, numpy.copy(vxvv), psi, t0, int_method, surface=surface, dpsi=dpsi )[0] if len(yo) == 1: # Can't map a single value... out = numpy.atleast_3d(integrate_for_map(yo[0], psi.flatten(), t0).T).T else: out = numpy.array( parallel_map( lambda ii: integrate_for_map( yo[ii], psi[ii] if len(psi.shape) > 1 else psi, t0[0] if len(t0) == 1 else t0[ii], ), range(len(yo)), numcores=numcores, progressbar=progressbar, ) ) if nophi: phi_mask = numpy.ones(out.shape[2], dtype="bool") phi_mask[3] = False out = out[:, :, phi_mask] return out, numpy.zeros(len(yo)) def _planarREOM(y, t, pot, l2): """ Implements the EOM, i.e., the right-hand side of the differential equation, for integrating a planar Orbit assuming angular momentum conservation. Parameters ---------- y : numpy.ndarray Current phase-space position. t : float Current time. pot : list of Potential instance(s) Potential instance(s). l2 : float Angular momentum squared. Returns ------- numpy.ndarray dy/dt. Notes ----- - 2010-07-20 - Written - Bovy (NYU) """ return [y[1], l2 / y[0] ** 3.0 + _evaluateplanarRforces(pot, y[0], t=t)] def _planarEOM(y, t, pot): """ Implements the EOM, i.e., the right-hand side of the differential equation, for integrating a general planar Orbit Parameters ---------- y : numpy.ndarray Current phase-space position t : float Current time pot : (list of) Potential instance(s) Returns ------- numpy.ndarray dy/dt Notes ----- - 2010-07-20 - Written - Bovy (NYU) """ l2 = (y[0] ** 2.0 * y[3]) ** 2.0 return [ y[1], l2 / y[0] ** 3.0 + _evaluateplanarRforces(pot, y[0], phi=y[2], t=t, v=[y[1], y[0] * y[3]]), y[3], 1.0 / y[0] ** 2.0 * ( _evaluateplanarphitorques(pot, y[0], phi=y[2], t=t, v=[y[1], y[0] * y[3]]) - 2.0 * y[0] * y[1] * y[3] ), ] def _planarEOM_dxdv(x, t, pot): """ Implements the EOM, i.e., the right-hand side of the differential equation, for integrating phase space differences, rectangular Parameters ---------- x : numpy.ndarray Current phase-space position t : float Current time pot : (list of) Potential instance(s) Returns ------- numpy.ndarray dy/dt Notes ----- - 2011-10-18 - Written - Bovy (IAS) """ # x is rectangular so calculate R and phi R = numpy.sqrt(x[0] ** 2.0 + x[1] ** 2.0) phi = numpy.arccos(x[0] / R) sinphi = x[1] / R cosphi = x[0] / R if x[1] < 0.0: phi = 2.0 * 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.0) * R2deriv + 2.0 * cosphi * sinphi / R**2.0 * phitorque + sinphi**2.0 / R * Rforce + 2.0 * sinphi * cosphi / R * Rphideriv - sinphi**2.0 / R**2.0 * phi2deriv ) dFxdy = ( -sinphi * cosphi * R2deriv + (sinphi**2.0 - cosphi**2.0) / R**2.0 * phitorque - cosphi * sinphi / R * Rforce - (cosphi**2.0 - sinphi**2.0) / R * Rphideriv + cosphi * sinphi / R**2.0 * phi2deriv ) dFydx = ( -cosphi * sinphi * R2deriv + (sinphi**2.0 - cosphi**2.0) / R**2.0 * phitorque + (sinphi**2.0 - cosphi**2.0) / R * Rphideriv - sinphi * cosphi / R * Rforce + sinphi * cosphi / R**2.0 * phi2deriv ) dFydy = ( -(sinphi**2.0) * R2deriv - 2.0 * sinphi * cosphi / R**2.0 * phitorque - 2.0 * sinphi * cosphi / R * Rphideriv + cosphi**2.0 / R * Rforce - cosphi**2.0 / R**2.0 * phi2deriv ) return numpy.array( [ x[2], x[3], cosphi * Rforce - 1.0 / R * sinphi * phitorque, sinphi * Rforce + 1.0 / R * cosphi * phitorque, x[6], x[7], dFxdx * x[4] + dFxdy * x[5], dFydx * x[4] + dFydy * x[5], ] ) def _planarSOSEOMx(y, psi, pot): """ Implements the EOM, i.e., the right-hand side of the differential equation, for integrating a general planar Orbit in the SOS style Parameters ---------- y : numpy.ndarray Current phase-space position psi : float Current angle pot : (list of) Potential instance(s) Returns ------- numpy.ndarray dy/dt Notes ----- - 2023-03-24 - Written - Bovy (UofT) """ # y = (y,vy,A,t) # Calculate x, vx sp, cp = numpy.sin(psi), numpy.cos(psi) gxyz = _planarRectForce([y[2] * sp, y[0]], pot, t=y[3], vx=[y[2] * cp, y[1]]) psidot = cp**2.0 - sp / y[2] * gxyz[0] Adot = y[2] * cp * sp + gxyz[0] * cp return numpy.array([y[1], gxyz[1], Adot, 1.0]) / psidot def _planarSOSEOMy(y, psi, pot): """ Implements the EOM, i.e., the right-hand side of the differential equation, for integrating a general planar Orbit in the SOS style Parameters ---------- y : numpy.ndarray Current phase-space position psi : float Current angle pot : list of Potential instance(s) Potential instance(s) Returns ------- numpy.ndarray dy/dt Notes ----- - 2023-03-24 - Written - Bovy (UofT) """ # y = (x,vx,A,t) # Calculate y sp, cp = numpy.sin(psi), numpy.cos(psi) gxyz = _planarRectForce([y[0], y[2] * sp], pot, t=y[3], vx=[y[1], y[2] * cp]) psidot = cp**2.0 - sp / y[2] * gxyz[1] Adot = y[2] * cp * sp + gxyz[1] * cp return numpy.array([y[1], gxyz[0], Adot, 1.0]) / psidot def _planarRectForce(x, pot, t=0.0, vx=None): """ Returns the planar force in the rectangular frame. Parameters ---------- x : numpy.ndarray Current position. t : float, optional Current time (default is 0.0). pot : list or Potential instance(s) Potential instance(s). vx : numpy.ndarray, optional If set, use this [vx,vy] when evaluating dissipative forces (default is None). Returns ------- numpy.ndarray Force. Notes ----- - 2011-02-02 - Written - Bovy (NYU) """ # x is rectangular so calculate R and phi R = numpy.sqrt(x[0] ** 2.0 + x[1] ** 2.0) phi = numpy.arccos(x[0] / R) sinphi = x[1] / R cosphi = x[0] / R if x[1] < 0.0: phi = 2.0 * numpy.pi - phi if not vx is None: vR = vx[0] * cosphi + vx[1] * sinphi vT = -vx[0] * sinphi + vx[1] * cosphi vx = [vR, vT] # calculate forces Rforce = _evaluateplanarRforces(pot, R, phi=phi, t=t, v=vx) phitorque = _evaluateplanarphitorques(pot, R, phi=phi, t=t, v=vx) return numpy.array( [ cosphi * Rforce - 1.0 / R * sinphi * phitorque, sinphi * Rforce + 1.0 / R * cosphi * phitorque, ] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/orbit/named_objects.json0000644000175100001660000034163414761352023020253 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=1741018143.6508844 galpy-1.10.2/galpy/orbit/orbit_c_ext/0000755000175100001660000000000014761352040017050 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/orbit/orbit_c_ext/integrateFullOrbit.c0000644000175100001660000013111314761352023023022 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 #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 evalSOSDeriv(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; case -10: // KuzminLikeWrapperPotential potentialArgs->potentialEval= &KuzminLikeWrapperPotentialEval; potentialArgs->Rforce= &KuzminLikeWrapperPotentialRforce; potentialArgs->zforce= &KuzminLikeWrapperPotentialzforce; potentialArgs->phitorque= &ZeroForce; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; } 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; case 7: //ias15 odeint_func= &wez_ias15; odeint_deriv_func= &evalRectForce; dim= 3; 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! } EXPORT void integrateFullOrbit_sos( int nobj, double *yo, int npsi, double *psi, int indiv_psi, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double dpsi, 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 *); dim= 7; odeint_deriv_func= &evalSOSDeriv; switch ( odeint_type ) { // case 0: = leapfrog = not supported symplectic method case 1: //RK4 odeint_func= &bovy_rk4; break; case 2: //RK6 odeint_func= &bovy_rk6; break; // case 3: = symplec4 = not supported symplectic method // case 4: = symplec6 = not supported symplectic method case 5: //DOPR54 odeint_func= &bovy_dopr54; break; case 6: //DOP853 odeint_func= &dop853; break; } #pragma omp parallel for schedule(dynamic,ORBITS_CHUNKSIZE) private(ii,jj) num_threads(max_threads) for (ii=0; ii < nobj; ii++) { cyl_to_sos_galpy(yo+dim*ii); odeint_func(odeint_deriv_func,dim,yo+dim*ii,npsi,dpsi,psi+npsi*ii*indiv_psi, npot,potentialArgs+omp_get_thread_num()*npot,rtol,atol, result+dim*npsi*ii,err+ii); for (jj=0; jj < npsi; jj++) sos_to_cyl_galpy(result+dim*jj+dim*npsi*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; case 7: //ias15 odeint_func= &wez_ias15; odeint_deriv_func= &evalRectForce; dim= 6; 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; //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); phitorque= calcphitorque(R,z,phi,t,nargs,potentialArgs); *a++= cosphi*Rforce-1./R*sinphi*phitorque; *a++= sinphi*Rforce+1./R*cosphi*phitorque; *a= calczforce(R,z,phi,t,nargs,potentialArgs); } void evalRectDeriv(double t, double *q, double *a, int nargs, struct potentialArg * potentialArgs){ double sinphi, cosphi, x, y, phi,R,Rforce,phitorque,z,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)); 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= calczforce(R,z,phi,t,nargs,potentialArgs,vR,vT,*(q+5));; } void evalSOSDeriv(double psi, double *q, double *a, int nargs, struct potentialArg * potentialArgs){ // q= (x,y,vx,vy,A,t,psi); to save operations, we reuse a first for the // rectForce then for the actual RHS // Note also that we keep track of psi in q+6, not in psi! This is // such that we can avoid having to convert psi to psi+psi0 // q+6 starts as psi0 and then just increments as psi (exactly) double sinpsi,cospsi,psidot,x,y,z,R,phi,sinphi,cosphi,vR,vT,vz,Rforce,phitorque; sinpsi= sin( *(q+6) ); cospsi= cos( *(q+6) ); // Calculate forces, put them in a+3, a+4, a+5 //q is rectangular so calculate R and phi, vR and vT (for dissipative) x= *q; y= *(q+1); z= *(q+4) * sinpsi; R= sqrt(x*x+y*y); phi= atan2( y ,x ); sinphi= y/R; cosphi= x/R; vR= *(q+2) * cosphi + *(q+3) * sinphi; vT= -*(q+2) * sinphi + *(q+3) * cosphi; vz= *(q+4) * cospsi; //Calculate the forces Rforce= calcRforce(R,z,phi,*(q+5),nargs,potentialArgs,vR,vT,vz); phitorque= calcphitorque(R,z,phi,*(q+5),nargs,potentialArgs,vR,vT,vz); *(a+3)= cosphi*Rforce-1./R*sinphi*phitorque; *(a+4)= sinphi*Rforce+1./R*cosphi*phitorque; *(a+5)= calczforce(R,z,phi,*(q+5),nargs,potentialArgs,vR,vT,vz); // Now calculate the RHS of the ODE psidot= cospsi * cospsi - sinpsi * *(a+5) / ( *(q+4) ); *(a )= *(q+2) / psidot; *(a+1)= *(q+3) / psidot; *(a+2)= *(a+3) / psidot; *(a+3)= *(a+4) / psidot; *(a+4)= cospsi * ( *(q+4) * sinpsi + *(a+5) ) / psidot; *(a+5)= 1./psidot; *(a+6)= 1.; // dpsi / dpsi to keep track of psi } 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=1741018131.0 galpy-1.10.2/galpy/orbit/orbit_c_ext/integrateFullOrbit.h0000644000175100001660000000151314761352023023027 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=1741018131.0 galpy-1.10.2/galpy/orbit/orbit_c_ext/integrateLinearOrbit.c0000644000175100001660000001613714761352023023342 0ustar00runnerdocker/* Wrappers around the C integration code for linear 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 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; case 7: //ias15 odeint_func= &wez_ias15; odeint_deriv_func= &evalLinearForce; dim= 1; 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=1741018131.0 galpy-1.10.2/galpy/orbit/orbit_c_ext/integratePlanarOrbit.c0000644000175100001660000013033214761352023023337 0ustar00runnerdocker/* Wrappers around the C integration code for planar Orbits */ #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 /* Function Declarations */ void evalPlanarRectForce(double, double *, double *, int, struct potentialArg *); void evalPlanarRectDeriv(double, double *, double *, int, struct potentialArg *); void evalPlanarSOSDerivx(double, double *, double *, int, struct potentialArg *); void evalPlanarSOSDerivy(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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; break; case 2: //TransientLogSpiralPotential, 8 arguments potentialArgs->planarRforce= &TransientLogSpiralPotentialRforce; potentialArgs->planarphitorque= &TransientLogSpiralPotentialphitorque; potentialArgs->nargs= 8; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; break; case 3: //SteadyLogSpiralPotential, 8 arguments potentialArgs->planarRforce= &SteadyLogSpiralPotentialRforce; potentialArgs->planarphitorque= &SteadyLogSpiralPotentialphitorque; potentialArgs->nargs= 8; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; break; case 25: //SoftenedNeedleBarPotential, 13 arguments potentialArgs->potentialEval= &SoftenedNeedleBarPotentialEval; potentialArgs->planarRforce= &SoftenedNeedleBarPotentialPlanarRforce; potentialArgs->planarphitorque= &SoftenedNeedleBarPotentialPlanarphitorque; potentialArgs->nargs= 13; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; 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->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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; break; case 39: //NonInertialFrameForce, 22 arguments (10 caching ones) potentialArgs->planarRforceVelocity= &NonInertialFrameForcePlanarRforce; potentialArgs->planarphitorqueVelocity= &NonInertialFrameForcePlanarphitorque; 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->potentialEval= &ZeroForce; potentialArgs->planarRforce= &ZeroPlanarForce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &ZeroPlanarForce; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 0; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; break; case -6: //MovingObjectPotential potentialArgs->planarRforce= &MovingObjectPotentialPlanarRforce; potentialArgs->planarphitorque= &MovingObjectPotentialPlanarphitorque; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; 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; potentialArgs->requiresVelocity= false; break; case -10: //KuzminLikeWrapperPotential potentialArgs->potentialEval= &KuzminLikeWrapperPotentialEval; potentialArgs->planarRforce= &KuzminLikeWrapperPotentialPlanarRforce; potentialArgs->planarphitorque= &ZeroPlanarForce; potentialArgs->planarR2deriv= &KuzminLikeWrapperPotentialPlanarR2deriv; potentialArgs->planarphi2deriv= &ZeroPlanarForce; potentialArgs->planarRphideriv= &ZeroPlanarForce; potentialArgs->nargs= 3; potentialArgs->ntfuncs= 0; potentialArgs->requiresVelocity= false; 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; case 7: //ias15 odeint_func= &wez_ias15; odeint_deriv_func= &evalPlanarRectForce; dim= 2; 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_sos( int nobj, double *yo, int npsi, double *psi, int indiv_psi, int surface, int npot, int * pot_type, double * pot_args, tfuncs_type_arr pot_tfuncs, double dpsi, 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 *); dim= 5; switch ( odeint_type ) { // case 0: = leapfrog = not supported symplectic method case 1: //RK4 odeint_func= &bovy_rk4; break; case 2: //RK6 odeint_func= &bovy_rk6; break; // case 3: = symplec4 = not supported symplectic method // case 4: = symplec6 = not supported symplectic method case 5: //DOPR54 odeint_func= &bovy_dopr54; break; case 6: //DOP853 odeint_func= &dop853; break; } switch ( surface ) { case 0: // x=0 odeint_deriv_func= &evalPlanarSOSDerivx; break; case 1: // y=0 odeint_deriv_func= &evalPlanarSOSDerivy; break; } #pragma omp parallel for schedule(dynamic,ORBITS_CHUNKSIZE) private(ii,jj) num_threads(max_threads) for (ii=0; ii < nobj; ii++) { polar_to_sos_galpy(yo+dim*ii,surface); odeint_func(odeint_deriv_func,dim,yo+dim*ii,npsi,dpsi,psi+npsi*ii*indiv_psi, npot,potentialArgs+omp_get_thread_num()*npot,rtol,atol, result+dim*npsi*ii,err+ii); for (jj=0; jj < npsi; jj++) sos_to_polar_galpy(result+dim*jj+dim*npsi*ii,surface); 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,vR,vT; //first two derivatives are just the velocities *a++= *(q+2); *a++= *(q+3); //Rest is force //q is rectangular so calculate R and phi, vR and vT (for dissipative) 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; vR= *(q+2) * cosphi + *(q+3) * sinphi; vT= -*(q+2) * sinphi + *(q+3) * cosphi; //Calculate the forces Rforce= calcPlanarRforce(R,phi,t,nargs,potentialArgs,vR,vT); phitorque= calcPlanarphitorque(R,phi,t,nargs,potentialArgs,vR,vT); *a++= cosphi*Rforce-1./R*sinphi*phitorque; *a= sinphi*Rforce+1./R*cosphi*phitorque; } void evalPlanarSOSDerivx(double psi, double *q, double *a, int nargs, struct potentialArg * potentialArgs){ // q= (y,vy,A,t,psi); to save operations, we reuse a first for the // rectForce then for the actual RHS // Note also that we keep track of psi in q+4, not in psi! This is // such that we can avoid having to convert psi to psi+psi0 // q+4 starts as psi0 and then just increments as psi (exactly) double sinpsi,cospsi,psidot,x,y,R,phi,sinphi,cosphi,Rforce,phitorque,vR,vT; sinpsi= sin( *(q+4) ); cospsi= cos( *(q+4) ); // Calculate forces, put them in a+2, a+3 //q is rectangular so calculate R and phi x= *(q+2) * sinpsi; y= *(q ); R= sqrt(x*x+y*y); phi= atan2( y ,x ); sinphi= y/R; cosphi= x/R; vR= *(q+2) * cospsi * cosphi + *(q+1) * sinphi; vT= -*(q+2) * cospsi * sinphi + *(q+1) * cosphi; //Calculate the forces Rforce= calcPlanarRforce(R,phi,*(q+3),nargs,potentialArgs,vR,vT); phitorque= calcPlanarphitorque(R,phi,*(q+3),nargs,potentialArgs,vR,vT); *(a+2)= cosphi*Rforce-1./R*sinphi*phitorque; *(a+3)= sinphi*Rforce+1./R*cosphi*phitorque; // Now calculate the RHS of the ODE psidot= cospsi * cospsi - sinpsi * *(a+2) / ( *(q+2) ); *(a )= *(q+1) / psidot; *(a+1)= *(a+3) / psidot; *(a+2)= cospsi * ( *(q+2) * sinpsi + *(a+2) ) / psidot; *(a+3)= 1./psidot; *(a+4)= 1.; // dpsi / dpsi to keep track of psi } void evalPlanarSOSDerivy(double psi, double *q, double *a, int nargs, struct potentialArg * potentialArgs){ // q= (x,vx,A,t,psi); to save operations, we reuse a first for the // rectForce then for the actual RHS // Note also that we keep track of psi in q+4, not in psi! This is // such that we can avoid having to convert psi to psi+psi0 // q+4 starts as psi0 and then just increments as psi (exactly) double sinpsi,cospsi,psidot,x,y,R,phi,sinphi,cosphi,Rforce,phitorque,vR,vT; sinpsi= sin( *(q+4) ); cospsi= cos( *(q+4) ); // Calculate forces, put them in a+2, a+3 //q is rectangular so calculate R and phi x= *(q ); y= *(q+2) * sinpsi; R= sqrt(x*x+y*y); phi= atan2( y ,x ); sinphi= y/R; cosphi= x/R; vR= *(q+1 ) * cosphi + *(q+2) * cospsi * sinphi; vT= -*(q+1 ) * sinphi + *(q+2) * cospsi * cosphi; //Calculate the forces Rforce= calcPlanarRforce(R,phi,*(q+3),nargs,potentialArgs,vR,vT); phitorque= calcPlanarphitorque(R,phi,*(q+3),nargs,potentialArgs,vR,vT); *(a+2)= cosphi*Rforce-1./R*sinphi*phitorque; *(a+3)= sinphi*Rforce+1./R*cosphi*phitorque; // Now calculate the RHS of the ODE psidot= cospsi * cospsi - sinpsi * *(a+3) / ( *(q+2) ); *(a )= *(q+1) / psidot; *(a+1)= *(a+2) / psidot; *(a+2)= cospsi * ( *(q+2) * sinpsi + *(a+3) ) / psidot; *(a+3)= 1./psidot; *(a+4)= 1.; // dpsi / dpsi to keep track of psi } 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=1741018143.6638844 galpy-1.10.2/galpy/potential/0000755000175100001660000000000014761352040015427 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/AdiabaticContractionWrapperPotential.py0000644000175100001660000001665414761352023025304 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.0, pot=None, baryonpot=None, method="cautun", f_bar=0.157, rmin=None, rmax=50.0, ro=None, vo=None, ): """ Initialize a AdiabaticContractionWrapper Potential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1.). pot : Potential instance or list thereof, optional Representing the density that is adiabatically contracted. baryonpot : Potential instance or list thereof, optional Representing the density of baryons whose growth causes the contraction. method : {'cautun', 'blumenthal', 'gnedin'}, optional Type of adiabatic-contraction formula: - 'cautun' for that from Cautun et al. 2020 [1]_; - 'blumenthal' for that from Blumenthal et al. 1986 [2]_; - 'gnedin' for that from Gnedin et al. 2004 [3]_. (default: 'cautun') f_bar : float, optional 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 (default: 0.157). rmin : float, optional Minimum radius to consider (default: rmax/2500; don't set this to zero). rmax : float or Quantity, optional Maximum radius to consider (default: 50.). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 2021-03-21 - Started based on Marius Cautun's code - Bovy (UofT) References ---------- .. [1] Cautun, M et al. (2020), Mon. Not. Roy. Astron. Soc., 494, 4291. ADS: https://ui.adsabs.harvard.edu/abs/2020MNRAS.494.4291C .. [2] Blumenthal et al. (1986), Astrophys. J., 301, 27. ADS: https://ui.adsabs.harvard.edu/abs/1986ApJ...301...27B .. [3] Gnedin et al. (2004), Astrophys. J., 616, 16. ADS: https://ui.adsabs.harvard.edu/abs/2004ApJ...616...16G """ # 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.0 ) # 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.0, 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.0], rgrid)) new_rforce = numpy.concatenate(([0.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.0 - fbar) / (M + Mbar)) ** -0.54 ) M_DM = fixed_point(func_M_DM_contract, M_DMO) return M_DM / M_DMO * M_DMO / r**2.0 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.0 - 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.0 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.0 - 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.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/AnyAxisymmetricRazorThinDiskPotential.py0000644000175100001660000002174014761352023025470 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): """Class that implements the potential of an arbitrary axisymmetric, razor-thin disk with surface density :math:`\\Sigma(R)`""" def __init__( self, amp=1.0, surfdens=lambda R: 1.5 * numpy.exp(-3.0 * R), normalize=False, ro=None, vo=None, ): """ Potential of an arbitrary axisymmetric disk. Parameters ---------- amp : float, optional Amplitude to be applied to the potential. Default is 1.0. surfdens : callable, optional Function of a single variable that gives the surface density as a function of radius (can return a Quantity). Default is ``lambda R: 1.5 * numpy.exp(-3.0 * R)``. normalize : bool or float, optional 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. Default is False. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * units.kpc).to(units.Msun / units.pc**2) except (AttributeError, units.UnitConversionError): pass else: _sdens_unit_output = True else: try: surfdens(1.0).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.0 * 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.0, t=0.0): if R == 0 and z == 0: return self._pot_zero elif numpy.isinf(R**2 + z**2): return 0.0 potint = ( lambda a: a * self._sdens(a) / numpy.sqrt((R + a) ** 2.0 + z**2.0) * special.ellipk(4 * R * a / ((R + a) ** 2.0 + z**2.0)) ) 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.0, t=0.0): R2 = R**2 z2 = z**2 def rforceint(a): a2 = a**2 aRz = (a + R) ** 2.0 + 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.0, t=0.0): if z == 0: return 0.0 z2 = z**2 def zforceint(a): aRz = (a + R) ** 2.0 + 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.0, t=0.0): R2 = R**2 z2 = z**2 def r2derivint(a): a2 = a**2 aRz = (a + R) ** 2.0 + z2 faRoveraRz = 4 * a * R / aRz return ( a * self._sdens(a) * ( -( ( (a2 - 3.0 * R2) * (a2 - R2) ** 2 + (3.0 * a2**2 + 2.0 * a2 * R2 + 3.0 * R2**2) * z2 + (3.0 * a2 + 7.0 * R2) * z**4 + z**6 ) * special.ellipe(faRoveraRz) ) + ((a - R) ** 2 + z2) * ((a2 - R2) ** 2 + 2.0 * (a2 + 2.0 * R2) * z2 + z**4) * special.ellipk(faRoveraRz) ) / (2.0 * 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.0, t=0.0): R2 = R**2 z2 = z**2 def z2derivint(a): a2 = a**2 aRz = (a + R) ** 2.0 + z2 faRoveraRz = 4 * a * R / aRz return ( a * self._sdens(a) * ( -( ((a2 - R2) ** 2 - 2.0 * (a2 + R2) * z2 - 3.0 * 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.0, t=0.0): R2 = R**2 z2 = z**2 def rzderivint(a): a2 = a**2 aRz = (a + R) ** 2.0 + z2 faRoveraRz = 4 * a * R / aRz return ( a * self._sdens(a) * ( -( ( a**4 - 7.0 * R**4 - 6.0 * R2 * z2 + z**4 + 2.0 * a2 * (3.0 * 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.0, t=0.0): return self._sdens(R) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/AnySphericalPotential.py0000644000175100001660000001223514761352023022247 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, amp=1.0, dens=lambda r: 0.64 / r / (1 + r) ** 3, normalize=False, ro=None, vo=None, ): """ Initialize the potential of an arbitrary spherical density distribution. Parameters ---------- amp : float, optional Amplitude to be applied to the potential. Default is 1.0. dens : callable, optional A function of a single variable that gives the density as a function of radius (can return a Quantity). Default is ``lambda r: 0.64 / r / (1 + r) ** 3``. normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * units.kpc).to(units.Msun / units.pc**3) except (AttributeError, units.UnitConversionError): pass else: _dens_unit_output = True else: try: dens(1.0).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.0 * numpy.pi * integrate.quad( lambda a: a**2 * self._rawdens(a), 0, numpy.atleast_1d(r).flatten()[0] )[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.0 * 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.0 * self._rawdens(a), 0, numpy.inf, full_output=True )[-1] ) self._pot_inf = 0.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.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.0 * numpy.pi * integrate.quad( lambda a: self._rawdens(a) * a, numpy.atleast_1d(r).flatten()[0], numpy.inf, )[0] ) def _rforce(self, r, t=0.0): return -self._rawmass(r) / r**2 def _r2deriv(self, r, t=0.0): return -2 * self._rawmass(r) / r**3.0 + 4.0 * numpy.pi * self._rawdens(r) def _rdens(self, r, t=0.0): return self._rawdens(r) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/BurkertPotential.py0000644000175100001660000001112014761352023021273 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.0, a=2.0, normalize=False, ro=None, vo=None): """ Initialize a Burkert-density potential [1]_. Parameters ---------- amp : float or Quantity Amplitude to be applied to the potential. Can be a Quantity with units of mass density or Gxmass density. a : float or Quantity Scale radius. normalize : bool or float, optional 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. Default is False. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 2013-04-10 - Written - Bovy (IAS) - 2020-03-30 - Re-implemented using SphericalPotential - Bovy (UofT) References ---------- .. [1] Burkert (1995), Astrophysical Journal, 447, L25. ADS: https://ui.adsabs.harvard.edu/abs/1995ApJ...447L..25B. """ 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.0): """Potential as a function of r and time""" x = r / self.a return ( -(self.a**2.0) * numpy.pi * ( -numpy.pi / x + 2.0 * (1.0 / x + 1) * numpy.arctan(1 / x) + (1.0 / x + 1) * numpy.log((1.0 + 1.0 / x) ** 2.0 / (1.0 + 1 / x**2.0)) + special.xlogy(2.0 / x, 1.0 + x**2.0) ) ) # 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.0): x = r / self.a return ( self.a * numpy.pi / x**2.0 * ( numpy.pi - 2.0 * numpy.arctan(1.0 / x) - 2.0 * numpy.log(1.0 + x) - numpy.log(1.0 + x**2.0) ) ) def _r2deriv(self, r, t=0.0): x = r / self.a return ( 4.0 * numpy.pi / (1.0 + x**2.0) / (1.0 + x) + 2.0 * self._rforce(r) / x / self.a ) def _rdens(self, r, t=0.0): x = r / self.a return 1.0 / (1.0 + x) / (1.0 + x**2.0) def _surfdens(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) x = r / self.a Rpa = numpy.sqrt(R**2.0 + self.a**2.0) Rma = numpy.sqrt(R**2.0 - self.a**2.0 + 0j) if Rma == 0: za = z / self.a return ( self.a**2.0 / 2.0 * ( ( 2.0 - 2.0 * numpy.sqrt(za**2.0 + 1) + numpy.sqrt(2.0) * za * numpy.arctan(za / numpy.sqrt(2.0)) ) / z + numpy.sqrt(2 * za**2.0 + 2.0) * numpy.arctanh(za / numpy.sqrt(2.0 * (za**2.0 + 1))) / numpy.sqrt(self.a**2.0 + z**2.0) ) ) else: return ( self.a**2.0 * ( 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=1741018131.0 galpy-1.10.2/galpy/potential/Cautun20.py0000644000175100001660000000632714761352023017413 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.0 / sigo zd_thick = 0.9 / ro Rd_thick = 3.80 / ro Sigma0_thick = 101.0 / sigo # Cautun Gas Discs Rd_HI = 7.0 / ro Rm_HI = 4.0 / ro zd_HI = 0.085 / ro Sigma0_HI = 53 / sigo Rd_H2 = 1.5 / ro Rm_H2 = 12.0 / 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.0}, {"type": "exp", "h": Rd_thick, "amp": Sigma0_thick, "Rhole": 0.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.0 * 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.0e12, vo=vo, ro=ro, H=67.77, Om=0.307, overdens=200.0 * (1.0 - 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=1741018131.0 galpy-1.10.2/galpy/potential/ChandrasekharDynamicalFrictionForce.py0000644000175100001660000002700614761352023025044 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.0 / numpy.sqrt(2.0) _INVSQRTPI = 1.0 / 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 [2]_ and earlier work. """ def __init__( self, amp=1.0, GMs=0.1, gamma=1.0, rhm=0.0, dens=None, sigmar=None, const_lnLambda=False, minr=0.0001, maxr=25.0, nr=501, ro=None, vo=None, ): """ Initialize a Chandrasekhar Dynamical Friction force [1]_. Parameters ---------- amp : float Amplitude to be applied to the potential (default: 1). GMs : float or Quantity 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 : float or Quantity Half-mass radius of the satellite (set to zero for a black hole); can be adjusted after initialization by setting obj.rhm= where obj is your ChandrasekharDynamicalFrictionForce instance. gamma : float Free-parameter in :math:`\\Lambda`. dens : Potential instance or list thereof, optional Potential instance or list thereof that represents the density [default: LogarithmicHaloPotential(normalize=1.,q=1.)]. sigmar : callable, optional 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). const_lnLambda : bool, optional If set to a number, use a constant ln(Lambda) instead with this value. minr : float or Quantity, optional Minimum r at which to apply dynamical friction: at r < minr, friction is set to zero. maxr : float or Quantity, optional Maximum r for which sigmar gets interpolated; for best performance set this to the maximum r you will consider. nr : int, optional Number of radii to use in the interpolation of sigmar. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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) References ---------- .. [1] Chandrasekhar, S. (1943), Astrophysical Journal, 97, 255. ADS: http://adsabs.harvard.edu/abs/1943ApJ....97..255C. .. [2] Petts, J. A., Gualandris, A., Read, J. I., & Bovy, J. (2016), Monthly Notices of the Royal Astronomical Society, 463, 858. ADS: http://adsabs.harvard.edu/abs/2016MNRAS.463..858P. """ 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.0, q=1.0) 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.0, t=0.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.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.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.0 * 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): """ Evaluate the Coulomb logarithm ln Lambda. Parameters ---------- r : float Spherical radius (natural units). v : float Current velocity in cylindrical coordinates (natural units). Returns ------- lnLambda : float Coulomb logarithm. Notes ----- - 2018-03-18 - Started - Bovy (UofT) """ if self._lnLambda: lnLambda = self._lnLambda else: GMvs = self._ms / v**2.0 if GMvs < self._rhm: Lambda = r / self._gamma / self._rhm else: Lambda = r / self._gamma / GMvs lnLambda = 0.5 * numpy.log(1.0 + Lambda**2.0) return lnLambda def _calc_force(self, R, phi, z, v, t): r = numpy.sqrt(R**2.0 + z**2.0) if r < self._minr: self._cached_force = 0.0 else: vs = numpy.sqrt(v[0] ** 2.0 + v[1] ** 2.0 + v[2] ** 2.0) if r > self._maxr: sr = self.sigmar_orig(r) else: sr = self.sigmar(r) X = vs * _INVSQRTTWO / sr Xfactor = special.erf(X) - 2.0 * X * _INVSQRTPI * numpy.exp(-(X**2.0)) lnLambda = self.lnLambda(r, vs) self._cached_force = ( -self._dens(R, z, phi=phi, t=t) / vs**3.0 * Xfactor * lnLambda ) def _Rforce(self, R, z, phi=0.0, t=0.0, v=None): 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.0, t=0.0, v=None): 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.0, t=0.0, v=None): 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.0, t=0.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.0, use_physical=False ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/CorotatingRotationWrapperPotential.py0000644000175100001660000001233414761352023025057 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.0, pot=None, vpo=1.0, beta=0.0, to=0.0, pa=0.0, ro=None, vo=None ): """ Initialize a CorotatingRotationWrapper Potential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1.). pot : Potential instance or list thereof, optional This potential is made to rotate around the z axis by the wrapper. vpo : float or Quantity, optional Amplitude of the circular-velocity curve. beta : float, optional Power-law amplitude of the circular-velocity curve. to : float or Quantity, optional Reference time at which the potential == pot. pa : float or Quantity, optional The position angle. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0) - self._vpo * args[0] ** (self._beta - 1.0) * (kwargs.get("t", 0.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.0) - self._vpo * args[0] ** (self._beta - 1.0) * (kwargs.get("t", 0.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.0) * args[0] ** (self._beta - 2.0) * (kwargs.get("t", 0.0) - self._to) ) def _R2deriv(self, *args, **kwargs): kwargs["phi"] = ( kwargs.get("phi", 0.0) - self._vpo * args[0] ** (self._beta - 1.0) * (kwargs.get("t", 0.0) - self._to) - self._pa ) phiRderiv = ( -self._vpo * (self._beta - 1.0) * args[0] ** (self._beta - 2.0) * (kwargs.get("t", 0.0) - self._to) ) return ( self._wrap_pot_func("_R2deriv")(self._pot, *args, **kwargs) + 2.0 * self._wrap_pot_func("_Rphideriv")(self._pot, *args, **kwargs) * phiRderiv + self._wrap_pot_func("_phi2deriv")(self._pot, *args, **kwargs) * phiRderiv**2.0 + self._wrap_pot_func("_phitorque")(self._pot, *args, **kwargs) * ( self._vpo * (self._beta - 1.0) * (self._beta - 2.0) * args[0] ** (self._beta - 3.0) * (kwargs.get("t", 0.0) - self._to) ) ) def _Rphideriv(self, *args, **kwargs): kwargs["phi"] = ( kwargs.get("phi", 0.0) - self._vpo * args[0] ** (self._beta - 1.0) * (kwargs.get("t", 0.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.0) * args[0] ** (self._beta - 2.0) * ( kwargs.get("t", 0.0) - self._to ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/CosmphiDiskPotential.py0000644000175100001660000001751014761352023022103 0ustar00runnerdocker############################################################################### # CosmphiDiskPotential: cos(mphi) potential ############################################################################### import numpy from ..util import conversion from .planarPotential import planarPotential _degtorad = numpy.pi / 180.0 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.0, phib=25.0 * _degtorad, p=1.0, phio=0.01, m=4, r1=1.0, rb=None, cp=None, sp=None, ro=None, vo=None, ): """ Initialize a CosmphiDiskPotential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1.), degenerate with phio below, but kept for overall consistency with potentials. m : int, optional m in cos(m * phi - m * phib). p : float Power-law index of the phi(R) = (R/Ro)^p part. r1 : float or Quantity, optional 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 : float or Quantity, optional If set, break radius for power-law: potential R^p at R > Rb, R^-p at R < Rb, potential and force continuous at Rb. phib : float or Quantity, optional Angle (in rad; default=25 degree). phio : float or Quantity, optional Potential perturbation (in terms of phio/vo^2 if vo=1 at Ro=1; or can be Quantity). cp : float or Quantity, optional m * phio * cos(m * phib); can be Quantity with units of velocity-squared. sp : float or Quantity, optional m * phio * sin(m * phib); can be Quantity with units of velocity-squared. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - Either specify (phib, phio) or (cp, sp). - 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.0 and cp < 0.0: self._phib = numpy.pi + self._phib self._p = p if rb is None: self._rb = 0.0 self._rbp = 1.0 # never used, but for p < 0 general expr fails self._rb2p = 1.0 else: self._rb = rb self._rbp = self._rb**self._p self._rb2p = self._rbp**2.0 self._mphib = self._m * self._phib self.hasC = True self.hasC_dxdv = True def _evaluate(self, R, phi=0.0, t=0.0): if R < self._rb: return ( self._mphio / self._m * numpy.cos(self._m * phi - self._mphib) * self._rbp * (2.0 * 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.0, t=0.0): if R < self._rb: return ( -self._p * self._mphio / self._m * self._rb2p / R ** (self._p + 1.0) * numpy.cos(self._m * phi - self._mphib) ) else: return ( -self._p * self._mphio / self._m * R ** (self._p - 1.0) * numpy.cos(self._m * phi - self._mphib) ) def _phitorque(self, R, phi=0.0, t=0.0): if R < self._rb: return ( self._mphio * numpy.sin(self._m * phi - self._mphib) * self._rbp * (2.0 * 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.0, t=0.0): if R < self._rb: return ( -self._p * (self._p + 1.0) * self._mphio / self._m * self._rb2p / R ** (self._p + 2.0) * numpy.cos(self._m * phi - self._mphib) ) else: return ( self._p * (self._p - 1.0) / self._m * self._mphio * R ** (self._p - 2.0) * numpy.cos(self._m * phi - self._mphib) ) def _phi2deriv(self, R, phi=0.0, t=0.0): if R < self._rb: return ( -self._m * self._mphio * numpy.cos(self._m * phi - self._mphib) * self._rbp * (2.0 * 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.0, t=0.0): if R < self._rb: return ( -self._p * self._mphio / self._m * self._rb2p / R ** (self._p + 1.0) * numpy.sin(self._m * phi - self._mphib) ) else: return ( -self._p * self._mphio * R ** (self._p - 1.0) * 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.0, phib=25.0 * _degtorad, p=1.0, phio=0.01, r1=1.0, cp=None, sp=None, ro=None, vo=None, ): CosmphiDiskPotential.__init__( self, amp=amp, phib=phib, p=p, phio=phio, m=1.0, cp=cp, sp=sp, ro=ro, vo=vo ) self.hasC = True self.hasC_dxdv = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/DehnenBarPotential.py0000644000175100001660000006361414761352023021522 0ustar00runnerdocker############################################################################### # DehnenBarPotential: Dehnen (2000)'s bar potential ############################################################################### import numpy from ..util import conversion from .Potential import Potential _degtorad = numpy.pi / 180.0 class DehnenBarPotential(Potential): """Class that implements the Dehnen bar potential (`Dehnen 2000 `__; [1]_), generalized to 3D following `Monari et al. (2016) `__ [2]_ .. 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\\,. If the bar's pattern speed is zero, :math:`t_\\mathrm{form}` and :math:`t_\\mathrm{steady}` are straight times, not times divided by the bar period. """ normalize = property() # turn off normalize def __init__( self, amp=1.0, omegab=None, rb=None, chi=0.8, rolr=0.9, barphi=25.0 * _degtorad, tform=-4.0, tsteady=None, beta=0.0, alpha=0.01, Af=None, ro=None, vo=None, ): """ Initialize a Dehnen bar potential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1., see alpha or Ab below). omegab : float or Quantity, optional Rotation speed of the bar (can be Quantity). rb : float or Quantity, optional Bar radius (can be Quantity). Af : float or Quantity, optional Bar strength (can be Quantity). chi : float, optional Fraction R_bar / R_CR (corotation radius of bar). rolr : float or Quantity, optional Radius of the Outer Lindblad Resonance for a circular orbit (can be Quantity). barphi : float or Quantity, optional Angle between sun-GC line and the bar's major axis (in rad; default=25 degree; or can be Quantity). beta : float, optional Power law index of rotation curve (to calculate OLR, etc.). alpha : float or Quantity, optional Relative bar strength (default: 0.01). tform : float, optional Start of bar growth / bar period (default: -4). tsteady : float, optional Time from tform at which the bar is fully grown / bar period (default: -tform/2, so the perturbation is fully grown at tform/2). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - Either provide (omegab, rb, Af) or (chi, rolr, alpha, beta). - 2010-11-24 - Started - Bovy (NYU) - 2017-06-23 - Converted to 3D following Monari et al. (2016) - Bovy (UofT/CCA) References ---------- .. [1] Dehnen (1999). The Astrophysical Journal, 524, L35. ADS: https://ui.adsabs.harvard.edu/abs/1999ApJ...524L..35D/abstract .. [2] Monari, G., Famaey, B., Siebert, A., et al. (2016). Monthly Notices of the Royal Astronomical Society, 462(2), 2333-2346. ADS: https://ui.adsabs.harvard.edu/abs/2016MNRAS.462.2333M/abstract """ 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.0 / ( (self._rolr ** (1.0 - self._beta)) / (1.0 + numpy.sqrt((1.0 + self._beta) / 2.0)) ) self._rb = self._chi * self._omegab ** (1.0 / (self._beta - 1.0)) self._alpha = alpha self._af = self._alpha / 3.0 / self._rb**3.0 else: self._omegab = omegab self._rb = rb self._af = Af self._tb = 2.0 * numpy.pi / self._omegab if self._omegab != 0.0 else 1.0 self._tform = tform * self._tb if tsteady is None: self._tsteady = self._tform / 2.0 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.0 indx = (t < self._tsteady) * (t >= self._tform) deltat = t[indx] - self._tform xi = 2.0 * deltat / (self._tsteady - self._tform) - 1.0 smooth[indx] = ( 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 ) else: if t < self._tform: smooth = 0.0 elif t < self._tsteady: deltat = t - self._tform xi = 2.0 * deltat / (self._tsteady - self._tform) - 1.0 smooth = ( 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 ) else: # bar is fully on smooth = 1.0 return smooth def _evaluate(self, R, z, phi=0.0, t=0.0): # Calculate relevant time smooth = self._smooth(t) r2 = R**2.0 + z**2.0 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.0 - 2.0) * numpy.divide( R[indx] ** 2.0, r2[indx], numpy.ones_like(R[indx]), where=R[indx] != 0 ) indx = numpy.invert(indx) out[indx] = ( -((self._rb / r[indx]) ** 3.0) * 1.0 / (1.0 + z[indx] ** 2.0 / R[indx] ** 2.0) ) out *= ( self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) ) return out else: if r == 0: return ( -2.0 * self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) ) elif r <= self._rb: return ( self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * ((r / self._rb) ** 3.0 - 2.0) * R**2.0 / r2 ) else: return ( -self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * (self._rb / r) ** 3.0 * 1.0 / (1.0 + z**2.0 / R**2.0) ) def _Rforce(self, R, z, phi=0.0, t=0.0): # Calculate relevant time smooth = self._smooth(t) r = numpy.sqrt(R**2.0 + z**2.0) 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.0 * R[indx] * (3.0 * R[indx] ** 2.0 + 2.0 * z[indx] ** 2.0) - 4.0 * R[indx] * z[indx] ** 2.0 ) / r[indx] ** 4.0 ) indx = numpy.invert(indx) out[indx] = ( -((self._rb / r[indx]) ** 3.0) * R[indx] / r[indx] ** 4.0 * (3.0 * R[indx] ** 2.0 - 2.0 * z[indx] ** 2.0) ) out *= ( self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) ) return out else: if r <= self._rb: return ( -self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * ( (r / self._rb) ** 3.0 * R * (3.0 * R**2.0 + 2.0 * z**2.0) - 4.0 * R * z**2.0 ) / r**4.0 ) else: return ( -self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * (self._rb / r) ** 3.0 * R / r**4.0 * (3.0 * R**2.0 - 2.0 * z**2.0) ) def _phitorque(self, R, z, phi=0.0, t=0.0): # Calculate relevant time smooth = self._smooth(t) r2 = R**2.0 + z**2.0 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.0 - 2.0) * R[indx] ** 2.0 / r2[indx] indx = numpy.invert(indx) out[indx] = -((self._rb / r[indx]) ** 3.0) * R[indx] ** 2.0 / r2[indx] out *= ( 2.0 * self._af * smooth * numpy.sin(2.0 * (phi - self._omegab * t - self._barphi)) ) return out else: if r <= self._rb: return ( 2.0 * self._af * smooth * numpy.sin(2.0 * (phi - self._omegab * t - self._barphi)) * ((r / self._rb) ** 3.0 - 2.0) * R**2.0 / r2 ) else: return ( -2.0 * self._af * smooth * numpy.sin(2.0 * (phi - self._omegab * t - self._barphi)) * (self._rb / r) ** 3.0 * R**2.0 / r2 ) def _zforce(self, R, z, phi=0.0, t=0.0): # Calculate relevant time smooth = self._smooth(t) r = numpy.sqrt(R**2.0 + z**2.0) 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.0 + 4.0) * R[indx] ** 2.0 * z[indx] / r[indx] ** 4.0 ) indx = numpy.invert(indx) out[indx] = ( -5.0 * (self._rb / r[indx]) ** 3.0 * R[indx] ** 2.0 * z[indx] / r[indx] ** 4.0 ) out *= ( self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) ) return out else: if r <= self._rb: return ( -self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * ((r / self._rb) ** 3.0 + 4.0) * R**2.0 * z / r**4.0 ) else: return ( -5.0 * self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * (self._rb / r) ** 3.0 * R**2.0 * z / r**4.0 ) def _R2deriv(self, R, z, phi=0.0, t=0.0): # Calculate relevant time smooth = self._smooth(t) r = numpy.sqrt(R**2.0 + z**2.0) 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.0 * ( (9.0 * R[indx] ** 2.0 + 2.0 * z[indx] ** 2.0) / r[indx] ** 4.0 - R[indx] ** 2.0 / r[indx] ** 6.0 * (3.0 * R[indx] ** 2.0 + 2.0 * z[indx] ** 2.0) ) + 4.0 * z[indx] ** 2.0 / r[indx] ** 6.0 * ( 4.0 * R[indx] ** 2.0 - r[indx] ** 2.0 ) indx = numpy.invert(indx) out[indx] = ( (self._rb / r[indx]) ** 3.0 / r[indx] ** 6.0 * ( (r[indx] ** 2.0 - 7.0 * R[indx] ** 2.0) * (3.0 * R[indx] ** 2.0 - 2.0 * z[indx] ** 2.0) + 6.0 * R[indx] ** 2.0 * r[indx] ** 2.0 ) ) out *= ( self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) ) return out else: if r <= self._rb: return ( self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * ( (r / self._rb) ** 3.0 * ( (9.0 * R**2.0 + 2.0 * z**2.0) / r**4.0 - R**2.0 / r**6.0 * (3.0 * R**2.0 + 2.0 * z**2.0) ) + 4.0 * z**2.0 / r**6.0 * (4.0 * R**2.0 - r**2.0) ) ) else: return ( self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * (self._rb / r) ** 3.0 / r**6.0 * ( (r**2.0 - 7.0 * R**2.0) * (3.0 * R**2.0 - 2.0 * z**2.0) + 6.0 * R**2.0 * r**2.0 ) ) def _phi2deriv(self, R, z, phi=0.0, t=0.0): # Calculate relevant time smooth = self._smooth(t) r = numpy.sqrt(R**2.0 + z**2.0) 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.0 - 2.0) * R[indx] ** 2.0 / r[indx] ** 2.0 ) indx = numpy.invert(indx) out[indx] = (self._rb / r[indx]) ** 3.0 * R[indx] ** 2.0 / r[indx] ** 2.0 out *= ( 4.0 * self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) ) return out else: if r <= self._rb: return ( -4.0 * self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * ((r / self._rb) ** 3.0 - 2.0) * R**2.0 / r**2.0 ) else: return ( 4.0 * self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * (self._rb / r) ** 3.0 * R**2.0 / r**2.0 ) def _Rphideriv(self, R, z, phi=0.0, t=0.0): # Calculate relevant time smooth = self._smooth(t) r = numpy.sqrt(R**2.0 + z**2.0) 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.0 * R[indx] * (3.0 * R[indx] ** 2.0 + 2.0 * z[indx] ** 2.0) - 4.0 * R[indx] * z[indx] ** 2.0 ) / r[indx] ** 4.0 indx = numpy.invert(indx) out[indx] = ( (self._rb / r[indx]) ** 3.0 * R[indx] / r[indx] ** 4.0 * (3.0 * R[indx] ** 2.0 - 2.0 * z[indx] ** 2.0) ) out *= ( -2.0 * self._af * smooth * numpy.sin(2.0 * (phi - self._omegab * t - self._barphi)) ) return out else: if r <= self._rb: return ( -2.0 * self._af * smooth * numpy.sin(2.0 * (phi - self._omegab * t - self._barphi)) * ( (r / self._rb) ** 3.0 * R * (3.0 * R**2.0 + 2.0 * z**2.0) - 4.0 * R * z**2.0 ) / r**4.0 ) else: return ( -2.0 * self._af * smooth * numpy.sin(2.0 * (phi - self._omegab * t - self._barphi)) * (self._rb / r) ** 3.0 * R / r**4.0 * (3.0 * R**2.0 - 2.0 * z**2.0) ) def _z2deriv(self, R, z, phi=0.0, t=0.0): # Calculate relevant time smooth = self._smooth(t) r = numpy.sqrt(R**2.0 + z**2.0) 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.0 / r[indx] ** 6.0 * ( (r[indx] / self._rb) ** 3.0 * (r[indx] ** 2.0 - z[indx] ** 2.0) + 4.0 * (r[indx] ** 2.0 - 4.0 * z[indx] ** 2.0) ) ) indx = numpy.invert(indx) out[indx] = ( 5.0 * (self._rb / r[indx]) ** 3.0 * R[indx] ** 2.0 / r[indx] ** 6.0 * (r[indx] ** 2.0 - 7.0 * z[indx] ** 2.0) ) out *= ( self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) ) return out else: if r <= self._rb: return ( self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * R**2.0 / r**6.0 * ( (r / self._rb) ** 3.0 * (r**2.0 - z**2.0) + 4.0 * (r**2.0 - 4.0 * z**2.0) ) ) else: return ( 5.0 * self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * (self._rb / r) ** 3.0 * R**2.0 / r**6.0 * (r**2.0 - 7.0 * z**2.0) ) def _Rzderiv(self, R, z, phi=0.0, t=0.0): # Calculate relevant time smooth = self._smooth(t) r = numpy.sqrt(R**2.0 + z**2.0) 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.0 * ( (r[indx] / self._rb) ** 3.0 * (2.0 * r[indx] ** 2.0 - R[indx] ** 2.0) + 8.0 * (r[indx] ** 2.0 - 2.0 * R[indx] ** 2.0) ) ) indx = numpy.invert(indx) out[indx] = ( 5.0 * (self._rb / r[indx]) ** 3.0 * R[indx] * z[indx] / r[indx] ** 6.0 * (2.0 * r[indx] ** 2.0 - 7.0 * R[indx] ** 2.0) ) out *= ( self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) ) return out else: if r <= self._rb: return ( self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * R * z / r**6.0 * ( (r / self._rb) ** 3.0 * (2.0 * r**2.0 - R**2.0) + 8.0 * (r**2.0 - 2.0 * R**2.0) ) ) else: return ( 5.0 * self._af * smooth * numpy.cos(2.0 * (phi - self._omegab * t - self._barphi)) * (self._rb / r) ** 3.0 * R * z / r**6.0 * (2.0 * r**2.0 - 7.0 * R**2.0) ) def _phizderiv(self, R, z, phi=0.0, t=0.0): # Calculate relevant time smooth = self._smooth(t) r = numpy.sqrt(R**2.0 + z**2.0) 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.0 + 4.0) * R[indx] ** 2.0 * z[indx] / r[indx] ** 4.0 ) indx = numpy.invert(indx) out[indx] = ( -5.0 * (self._rb / r[indx]) ** 3.0 * R[indx] ** 2.0 * z[indx] / r[indx] ** 4.0 ) out *= ( self._af * smooth * numpy.sin(2.0 * (phi - self._omegab * t - self._barphi)) ) return 2.0 * out else: if r <= self._rb: return ( -2 * self._af * smooth * numpy.sin(2.0 * (phi - self._omegab * t - self._barphi)) * ((r / self._rb) ** 3.0 + 4.0) * R**2.0 * z / r**4.0 ) else: return ( -10.0 * self._af * smooth * numpy.sin(2.0 * (phi - self._omegab * t - self._barphi)) * (self._rb / r) ** 3.0 * R**2.0 * z / r**4.0 ) def tform(self): # pragma: no cover """ Return formation time of the bar. Returns ------- tform : float Formation time of the bar in normalized units. Other Parameters ---------------- none Notes ----- - 2011-03-08 - Written - Bovy (NYU) """ return self._tform def OmegaP(self): """ Return the pattern speed. Returns ------- float The pattern speed. Notes ----- - 2011-10-10 - Written - Bovy (IAS) """ return self._omegab ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/DehnenBinney98.py0000644000175100001660000001132014761352023020526 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.0 / ro zd_ISM = 0.040 / ro zd_thin = 0.180 / ro zd_thick = 1.0 / ro Rd_ISM_over_Rd = 2.0 Rd_thin_over_Rd = 1.0 Rd_thick_over_Rd = 1.0 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.0 / 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.0 Rd_star = 0.25 Sigma0 = 1905.0 rho0_bulge = 0.4271 rho0_halo = 0.7110 alpha_halo = -2.0 beta_halo = 2.959 r0_halo = 3.83 / ro elif model == 2: vo = 217.0 Rd_star = 0.30 Sigma0 = 1208.0 rho0_bulge = 0.7561 rho0_halo = 1.263 alpha_halo = -2.0 beta_halo = 2.207 r0_halo = 1.09 / ro elif model == 3: vo = 217.0 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.0 Rd_star = 0.40 Sigma0 = 536.0 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.0 * zd_ISM) / mwpot_helpers.expexp_dens_with_hole(1.0, 0.0, Rd_ISM, Rm_ISM, zd_ISM, 1.0) + SigmaR0_thin_over_SigmaR0 * mwpot_helpers.expexp_dens(0.0, 0.0, Rd_thin, zd_thin, 1.0) / mwpot_helpers.expexp_dens(1.0, 0.0, Rd_thin, zd_thin, 1.0) + SigmaR0_thick_over_SigmaR0 * mwpot_helpers.expexp_dens(0.0, 0.0, Rd_thick, zd_thick, 1.0) / mwpot_helpers.expexp_dens(1.0, 0.0, Rd_thick, zd_thick, 1.0) ) Sigma0_ISM = ( SigmaR0_ISM_over_SigmaR0 * SigmaR0 / (2.0 * zd_ISM) / mwpot_helpers.expexp_dens_with_hole(1.0, 0.0, Rd_ISM, Rm_ISM, zd_ISM, 1.0) ) Sigma0_thin = ( SigmaR0_thin_over_SigmaR0 * SigmaR0 * mwpot_helpers.expexp_dens(0.0, 0.0, Rd_thin, zd_thin, 1.0) / mwpot_helpers.expexp_dens(1.0, 0.0, Rd_thin, zd_thin, 1.0) ) Sigma0_thick = ( SigmaR0_thick_over_SigmaR0 * SigmaR0 * mwpot_helpers.expexp_dens(0.0, 0.0, Rd_thick, zd_thick, 1.0) / mwpot_helpers.expexp_dens(1.0, 0.0, Rd_thick, zd_thick, 1.0) ) # 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=1741018131.0 galpy-1.10.2/galpy/potential/DehnenSmoothWrapperPotential.py0000644000175100001660000000656114761352023023626 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.0, pot=None, tform=-4.0, tsteady=None, decay=False, ro=None, vo=None ): """ Initialize a DehnenSmoothWrapper Potential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1.). pot : Potential instance or list thereof, optional The amplitude of this will be grown by this wrapper. tform : float or Quantity, optional Start of growth (default: -4.0). tsteady : float or Quantity, optional Time from tform at which the potential is fully grown (default: -tform/2, so the perturbation is fully grown at tform/2). decay : bool, optional If True, decay the amplitude instead of growing it (as 1-grow). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 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.0 elif t < self._tsteady: deltat = t - self._tform xi = 2.0 * deltat / (self._tsteady - self._tform) - 1.0 smooth = 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 else: # bar is fully on smooth = 1.0 return smooth if self._grow else 1.0 - smooth def _wrap(self, attribute, *args, **kwargs): return self._smooth(kwargs.get("t", 0.0)) * self._wrap_pot_func(attribute)( self._pot, *args, **kwargs ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/DiskSCFPotential.py0000644000175100001660000004341714761352023021121 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.0, normalize=False, dens=lambda R, z: 13.5 * numpy.exp(-3.0 * R) * numpy.exp(-27.0 * numpy.fabs(z)), Sigma={"type": "exp", "h": 1.0 / 3.0, "amp": 1.0}, hz={"type": "exp", "h": 1.0 / 27.0}, Sigma_amp=None, dSigmadR=None, d2SigmadR2=None, Hz=None, dHzdz=None, N=10, L=10, a=1.0, radial_order=None, costheta_order=None, phi_order=None, ro=None, vo=None, ): """ Initialize a DiskSCFPotential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1); cannot have units currently. normalize : bool or float, optional 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. dens : callable Function of R,z[,phi optional] that gives the density [in natural units, cannot return a Quantity currently]. N : int, optional Number of radial basis functions to use in the SCF expansion. L : int, optional Number of angular basis functions to use in the SCF expansion. a : float or Quantity, optional Scale radius for the SCF expansion. radial_order : int, optional Order of the radial basis functions to use in the SCF expansion. costheta_order : int, optional Order of the angular basis functions to use in the SCF expansion. phi_order : int, optional Order of the azimuthal basis functions to use in the SCF expansion. Sigma : dict or callable Either a dictionary of surface density (example: {'type':'exp','h':1./3.,'amp':1.,'Rhole':0.} for amp x exp(-Rhole/R-R/h) ) or a function of R that gives the surface density. hz : dict or callable Either a 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]) or a function of z that gives the vertical profile. Sigma_amp : float, optional Amplitude to apply to all Sigma functions. dSigmadR : callable, optional Function that gives d Sigma / d R. d2SigmadR2 : callable, optional Function that gives d^2 Sigma / d R^2. Hz : callable, optional Function of z such that d^2 Hz(z) / d z^2 = hz. dHzdz : callable, optional Function of z that gives d Hz(z) / d z. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - Either specify (Sigma,hz) or (Sigma_amp,Sigma,dSigmadR,d2SigmadR2,hz,Hz,dHzdz) - Written - Bovy (UofT) - 2016-12-26 """ 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.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.0, 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): 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.0 / 3.0) ta = Sigma.get("amp", 1.0) 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.0 elif stype == "expwhole" or (stype == "exp" and "Rhole" in Sigma): rd = Sigma.get("h", 1.0 / 3.0) rm = Sigma.get("Rhole", 0.5) ta = Sigma.get("amp", 1.0) ts = lambda R, trd=rd, trm=rm: numpy.exp(-trm / R - R / trd) tds = lambda R, trd=rd, trm=rm: (trm / R**2.0 - 1.0 / trd) * numpy.exp( -trm / R - R / trd ) td2s = lambda R, trd=rd, trm=rm: ( (trm / R**2.0 - 1.0 / trd) ** 2.0 - 2.0 * trm / R**3.0 ) * numpy.exp(-trm / R - R / trd) return (ta, ts, tds, td2s) def _parse_hz(self, hz, Hz, dHzdz): 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.0 / 2.0 / tzd * numpy.exp(-numpy.fabs(z) / tzd) tH = ( lambda z, tzd=zd: ( numpy.exp(-numpy.fabs(z) / tzd) - 1.0 + numpy.fabs(z) / tzd ) * tzd / 2.0 ) tdH = ( lambda z, tzd=zd: 0.5 * numpy.sign(z) * (1.0 - 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.0) * numpy.ones_like(z)] ), axis=0, ) ) / tzd ) tH = lambda z, tzd=zd: tzd * ( logsumexp(numpy.array([z / 2.0 / tzd, -z / 2.0 / tzd]), axis=0) - numpy.log(2.0) ) tdH = lambda z, tzd=zd: numpy.tanh(z / 2.0 / tzd) / 2.0 return (th, tH, tdH) def _evaluate(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) 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.0 * numpy.pi * a * s(r) * H(z) return out def _Rforce(self, R, z, phi=0, t=0): r = numpy.sqrt(R**2.0 + z**2.0) 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.0 * numpy.pi * a * ds(r) * H(z) * R / r return out def _zforce(self, R, z, phi=0, t=0): r = numpy.sqrt(R**2.0 + z**2.0) 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.0 * numpy.pi * a * (ds(r) * H(z) * z / r + s(r) * dH(z)) return out def _phitorque(self, R, z, phi=0.0, t=0.0): return self._scf.phitorque(R, z, phi=phi, use_physical=False) def _R2deriv(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) 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.0 * numpy.pi * a * H(z) / r**2.0 * (d2s(r) * R**2.0 + z**2.0 / r * ds(r)) ) return out def _z2deriv(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) 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.0 * numpy.pi * a * ( H(z) / r**2.0 * (d2s(r) * z**2.0 + ds(r) * R**2.0 / r) + 2.0 * ds(r) * dH(z) * z / r + s(r) * h(z) ) ) return out def _Rzderiv(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) 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.0 * numpy.pi * a * (H(z) * R * z / r**2.0 * (d2s(r) - ds(r) / r) + ds(r) * dH(z) * R / r) ) return out def _phi2deriv(self, R, z, phi=0.0, t=0.0): return self._scf.phi2deriv(R, z, phi=phi, use_physical=False) def _dens(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) 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.0 / r * ds(r) * (H(z) + z * dH(z)) ) return out def _mass(self, R, z=None, t=0.0): if not z is None: # pragma: no cover 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.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.0 * numpy.pi * integrate.quad(_integrand, 0.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.0 + z**2.0) 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.0 / r * ds(r) * (H(z) + z * dH(z))) return out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/DissipativeForce.py0000644000175100001660000001461214761352023021251 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.0, ro=None, vo=None, amp_units=None): """ Initialize a DissipativeForce object. Parameters ---------- amp : float Amplitude to be applied when evaluating the potential and its forces. ro : float or Quantity, optional Distance from the Galactic center that corresponds to the zero point of the potential. Default is from galpy.util.const. vo : float or Quantity, optional Velocity that corresponds to the zero point of the velocity. Default is from galpy.util.const. amp_units : str or None, optional Units of the input amplitude. If None, default unit is used. Returns ------- None Notes ----- - 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.isDissipative = True 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.0, t=0.0, v=None): """ Evaluate cylindrical radial force F_R (R,z). Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. z : float or Quantity Vertical height. phi : float or Quantity, optional Azimuth. Default is 0.0. t : float or Quantity, optional Time. Default is 0.0. v : numpy.ndarray, optional 3D velocity. Default is None. Returns ------- float or Quantity Cylindrical radial force F_R (R,z,phi,t,v). Notes ----- - 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.0, t=0.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 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.0, t=0.0, v=None): """ Evaluate the vertical force F_z (R,z,t). Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. z : float or Quantity Vertical height. phi : float or Quantity, optional Azimuth. Default is 0.0. t : float or Quantity, optional Time. Default is 0.0. v : numpy.ndarray, optional 3D velocity. Default is None. Returns ------- float or Quantity Vertical force F_z (R,z,phi,t,v). Notes ----- - 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.0, t=0.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.0, t=0.0, v=None): """ Evaluate the azimuthal torque F_phi (R,z,phi,t,v). Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. z : float or Quantity Vertical height. phi : float or Quantity, optional Azimuth. Default is 0.0. t : float or Quantity, optional Time. Default is 0.0. v : numpy.ndarray, optional 3D velocity. Default is None. Returns ------- float or Quantity Azimuthal torque F_phi (R,z,phi,t,v). Notes ----- - 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.0, t=0.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.0 def _isDissipative(obj): """ Determine whether this combination of potentials and forces is Dissipative Parameters ---------- obj : Potential/DissipativeForce instance or list of such instances Returns ------- bool True or False depending on whether the object is dissipative Notes ----- - 2018-03-16 - Written - Bovy (UofT) - 2023-05-29 - Adjusted to also take planarDissipativeForces into account - Bovy (UofT) """ from .Potential import PotentialError, flatten obj = flatten(obj) isList = isinstance(obj, list) try: if isList: isCons = [not p.isDissipative for p in obj] nonCons = not numpy.prod(numpy.array(isCons)) else: nonCons = obj.isDissipative except AttributeError: raise PotentialError( "'isDissipative' attribute has not been set for this potential/force" ) return nonCons ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/DoubleExponentialDiskPotential.py0000644000175100001660000003213514761352023024122 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.0 * 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.0, hr=1.0 / 3.0, hz=1.0 / 16.0, normalize=False, ro=None, vo=None, de_h=1e-3, de_n=10000, ): """ Initialize a double exponential disk potential Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass density or Gxmass density. hr : float or Quantity, optional Disk scale-length. hz : float or Quantity, optional Scale-height. normalize : bool or float, optional 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 : float, optional Step used in numerical integration. de_n : int, optional Number of points used in numerical integration (use 1000 for a lower accuracy version that is typically still high accuracy enough, but faster). ro : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 / self._hr self._beta = 1.0 / 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.0 / ( numpy.pi * self._de_j0zeros * special.j1(numpy.pi * self._de_j0zeros) ** 2.0 ) * 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.0 / ( numpy.pi * self._de_j1zeros * special.jv(2, numpy.pi * self._de_j1zeros) ** 2.0 ) * 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.0 self._pot_zero = ( 2.0 * (_gamma - 1.0) * numpy.sqrt(1.0 + _gamma2) + 2.0 * numpy.arctanh(1.0 / numpy.sqrt(1.0 + _gamma2)) - numpy.log(1.0 - _gamma / numpy.sqrt(1.0 + _gamma2)) + numpy.log(1.0 + _gamma / numpy.sqrt(1.0 + _gamma2)) ) / (2.0 * (1.0 + _gamma2) ** 1.5) self._pot_zero *= -4.0 * numpy.pi / self._alpha**2.0 # 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.0, t=0.0, dR=0, dphi=0): """ Evaluate the potential at (R,z) Parameters ---------- R : float Cylindrical Galactocentric radius z : float Vertical height phi : float, optional Azimuth (default: 0.0) t : float, optional Time (default: 0.0) Returns ------- float Potential at (R,z) Notes ----- - 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.atleast_1d(R) z = numpy.atleast_1d(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.0 + (x / R[:, numpy.newaxis]) ** 2.0) ** -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.0 - (x / R[:, numpy.newaxis]) ** 2.0) ) out = ( -4.0 * 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.0, t=0.0): """ Evaluate radial force K_R (R,z) Parameters ---------- R : float Cylindrical Galactocentric radius z : float Vertical height phi : float, optional Azimuth (default: 0.0) t : float, optional Time (default: 0.0) Returns ------- float Radial force (R,z) Notes ----- - 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.0 + (x / R) ** 2.0) ** -1.5 * ( self._beta * numpy.exp(-x / R * numpy.fabs(z)) - x / R * numpy.exp(-self._beta * numpy.fabs(z)) ) / (self._beta**2.0 - (x / R) ** 2.0) ) return ( -4.0 * numpy.pi * self._alpha / R**2.0 * numpy.nansum(fun(self._de_j1_xs) * self._de_j1_weights) ) @check_potential_inputs_not_arrays def _zforce(self, R, z, phi=0.0, t=0.0): """ Evaluate vertical force K_z (R,z) Parameters ---------- R : float Cylindrical Galactocentric radius z : float Vertical height phi : float, optional Azimuth (default: 0.0) t : float, optional Time (default: 0.0) Returns ------- float Vertical force (R,z) Notes ----- - 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.0 + (x / R) ** 2.0) ** -1.5 * x / R * ( numpy.exp(-x / R * numpy.fabs(z)) - numpy.exp(-self._beta * numpy.fabs(z)) ) / (self._beta**2.0 - (x / R) ** 2.0) ) out = ( -4.0 * numpy.pi * self._alpha * self._beta / R * numpy.nansum(fun(self._de_j0_xs) * self._de_j0_weights) ) if z > 0.0: return out else: return -out @check_potential_inputs_not_arrays def _R2deriv(self, R, z, phi=0.0, t=0.0): """ Evaluate radial force K_R (R,z) R2 derivative Parameters ---------- R : float Cylindrical Galactocentric radius z : float Vertical height phi : float, optional Azimuth (default: 0.0) t : float, optional Time (default: 0.0) Returns ------- float -d K_R (R,z) d R Notes ----- - 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.0 + (x / R) ** 2.0) ** -1.5 * ( self._beta * numpy.exp(-x / R * numpy.fabs(z)) - x / R * numpy.exp(-self._beta * numpy.fabs(z)) ) / (self._beta**2.0 - (x / R) ** 2.0) ) return ( 4.0 * numpy.pi * self._alpha / R**3.0 * 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.0, t=0.0): """ Evaluate vertical force K_Z (R,z) Z2 derivative Parameters ---------- R : float Cylindrical Galactocentric radius z : float Vertical height phi : float, optional Azimuth (default: 0.0) t : float, optional Time (default: 0.0) Returns ------- float -d K_Z (R,z) d Z Notes ----- - 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.0 + (x / R) ** 2.0) ** -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.0 - (x / R) ** 2.0) ) return ( -4.0 * 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.0, t=0.0): """ Evaluate mixed R,z derivative d2phi/dR/dz. Parameters ---------- R : float Cylindrical Galactocentric radius z : float Vertical height phi : float, optional Azimuth (default: 0.0) t : float, optional Time (default: 0.0) Returns ------- float d2phi/dR/dz Notes ----- - 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.0 + (x / R) ** 2.0) ** -1.5 * (x / R) ** 2.0 * ( numpy.exp(-x / R * numpy.fabs(z)) - numpy.exp(-self._beta * numpy.fabs(z)) ) / (self._beta**2.0 - (x / R) ** 2.0) ) out = ( -4.0 * numpy.pi * self._alpha * self._beta / R * numpy.nansum(fun(self._de_j1_xs) * self._de_j1_weights) ) if z > 0.0: return out else: return -out def _dens(self, R, z, phi=0.0, t=0.0): return numpy.exp(-self._alpha * R - self._beta * numpy.fabs(z)) def _surfdens(self, R, z, phi=0.0, t=0.0): return ( 2.0 * numpy.exp(-self._alpha * R) / self._beta * (1.0 - numpy.exp(-self._beta * numpy.fabs(z))) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/EllipsoidalPotential.py0000644000175100001660000004256014761352023022132 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.0, b=1.0, c=1.0, zvec=None, pa=None, glorder=50, ro=None, vo=None, amp_units=None, ): """ Initialize an ellipsoidal potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units that depend on the specific spheroidal potential. b : float, optional y-to-x axis ratio of the density. c : float, optional z-to-x axis ratio of the density. zvec : numpy.ndarray, optional If set, a unit vector that corresponds to the z axis. pa : float or Quantity, optional If set, the position angle of the x axis (rad or Quantity). glorder : int, optional If set, compute the relevant force and potential integrals with Gaussian quadrature of this order. ro : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). amp_units : str, optional Type of units that amp should have if it has units (passed to Potential.__init__). Notes ----- - 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.0 self._c2 = self._c**2.0 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.0) > 10.0**-10.0: 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.0**-10.0): self._aligned = True else: self._aligned = False if not pa is None: pa_rot = numpy.array( [ [numpy.cos(pa), numpy.sin(pa), 0.0], [-numpy.sin(pa), numpy.cos(pa), 0.0], [0.0, 0.0, 1.0], ] ) 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.0)) zvec_rot = _rotate_to_arbitrary_vector( numpy.array([[0.0, 0.0, 1.0]]), 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.0, t=0.0): if not self.isNonAxi: phi = 0.0 x, y, z = coords.cyl_to_rect(R, phi, z) if numpy.isinf(R): y = 0.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.0 * 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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0 * 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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0 * phixx + numpy.sin(phi) ** 2.0 * phiyy + 2.0 * numpy.cos(phi) * numpy.sin(phi) * phixy ) @check_potential_inputs_not_arrays def _Rzderiv(self, R, z, phi=0.0, t=0.0): if not self.isNonAxi: phi = 0.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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0 * ( numpy.sin(phi) ** 2.0 * phixx + numpy.cos(phi) ** 2.0 * phiyy - 2.0 * 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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0 * phi) * phixy + numpy.sin(phi) * Fx - numpy.cos(phi) * Fy ) @check_potential_inputs_not_arrays def _phizderiv(self, R, z, phi=0.0, t=0.0): if not self.isNonAxi: phi = 0.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.0 * 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.0, t=0.0): 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.0 + yp**2.0 / self._b2 + zp**2.0 / self._c2) return self._mdens(m) def _mass(self, R, z=None, t=0.0): if not z is None: raise AttributeError # Hack to fall back to general return ( 4.0 * numpy.pi * self._b * self._c * integrate.quad(lambda m: m**2.0 * self._mdens(m), 0, R)[0] ) def OmegaP(self): return 0.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.0 - 1.0 return psi( numpy.sqrt(x**2.0 / (1.0 + t) + y**2.0 / (b2 + t) + z**2.0 / (c2 + t)) ) / numpy.sqrt((1.0 + (b2 - 1.0) * s**2.0) * (1.0 + (c2 - 1.0) * s**2.0)) if glx is None: return integrate.quad(integrand, 0.0, 1.0)[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.0 - 1.0 return ( dens(numpy.sqrt(x**2.0 / (1.0 + t) + y**2.0 / (b2 + t) + z**2.0 / (c2 + t))) * ( x / (1.0 + t) * (i == 0) + y / (b2 + t) * (i == 1) + z / (c2 + t) * (i == 2) ) / numpy.sqrt((1.0 + (b2 - 1.0) * s**2.0) * (1.0 + (c2 - 1.0) * s**2.0)) ) if glx is None: return integrate.quad(integrand, 0.0, 1.0)[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.0 - 1.0 m = numpy.sqrt(x**2.0 / (1.0 + t) + y**2.0 / (b2 + t) + z**2.0 / (c2 + t)) return ( densDeriv(m) * ( x / (1.0 + t) * (i == 0) + y / (b2 + t) * (i == 1) + z / (c2 + t) * (i == 2) ) * ( x / (1.0 + t) * (j == 0) + y / (b2 + t) * (j == 1) + z / (c2 + t) * (j == 2) ) / m + dens(m) * (i == j) * ( 1.0 / (1.0 + t) * (i == 0) + 1.0 / (b2 + t) * (i == 1) + 1.0 / (c2 + t) * (i == 2) ) ) / numpy.sqrt((1.0 + (b2 - 1.0) * s**2.0) * (1.0 + (c2 - 1.0) * s**2.0)) if glx is None: return integrate.quad(integrand, 0.0, 1.0)[0] else: return numpy.sum(glw * integrand(glx)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/EllipticalDiskPotential.py0000644000175100001660000002036014761352023022560 0ustar00runnerdocker############################################################################### # EllipticalDiskPotential: Kuijken & Tremaine (1994)'s elliptical disk # potential ############################################################################### import numpy from ..util import conversion from .planarPotential import planarPotential _degtorad = numpy.pi / 180.0 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.0, phib=25.0 * _degtorad, p=1.0, twophio=0.01, r1=1.0, tform=None, tsteady=None, cp=None, sp=None, ro=None, vo=None, ): """ Initialize an Elliptical disk potential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1.), see twophio below. tform : float or Quantity, optional Start of growth (to smoothly grow this potential). tsteady : float or Quantity, optional Time delay at which the perturbation is fully grown (default: 2.). p : float, optional Power-law index of the phi(R) = (R/Ro)^p part. r1 : float or Quantity, optional Normalization radius for the amplitude. phib : float or Quantity, optional Angle (in rad; default=25 degree). twophio : float or Quantity, optional Potential perturbation (in terms of 2phio/vo^2 if vo=1 at Ro=1). cp : float or Quantity, optional Twophio * cos(2phib). sp : float or Quantity, optional Twophio * sin(2phib). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - Specify either (phib,twophio) or (cp,sp). - 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.0 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.0 def _evaluate(self, R, phi=0.0, t=0.0): # Calculate relevant time if not self._tform is None: if t < self._tform: smooth = 0.0 elif t < self._tsteady: deltat = t - self._tform xi = 2.0 * deltat / (self._tsteady - self._tform) - 1.0 smooth = ( 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 ) else: # fully on smooth = 1.0 else: smooth = 1.0 return ( smooth * self._twophio / 2.0 * R**self._p * numpy.cos(2.0 * (phi - self._phib)) ) def _Rforce(self, R, phi=0.0, t=0.0): # Calculate relevant time if not self._tform is None: if t < self._tform: smooth = 0.0 elif t < self._tsteady: deltat = t - self._tform xi = 2.0 * deltat / (self._tsteady - self._tform) - 1.0 smooth = ( 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 ) else: # fully on smooth = 1.0 else: smooth = 1.0 return ( -smooth * self._p * self._twophio / 2.0 * R ** (self._p - 1.0) * numpy.cos(2.0 * (phi - self._phib)) ) def _phitorque(self, R, phi=0.0, t=0.0): # Calculate relevant time if not self._tform is None: if t < self._tform: smooth = 0.0 elif t < self._tsteady: deltat = t - self._tform xi = 2.0 * deltat / (self._tsteady - self._tform) - 1.0 smooth = ( 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 ) else: # fully on smooth = 1.0 else: smooth = 1.0 return smooth * self._twophio * R**self._p * numpy.sin(2.0 * (phi - self._phib)) def _R2deriv(self, R, phi=0.0, t=0.0): # Calculate relevant time if not self._tform is None: if t < self._tform: smooth = 0.0 elif t < self._tsteady: deltat = t - self._tform xi = 2.0 * deltat / (self._tsteady - self._tform) - 1.0 smooth = ( 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 ) else: # fully on smooth = 1.0 else: smooth = 1.0 return ( smooth * self._p * (self._p - 1.0) / 2.0 * self._twophio * R ** (self._p - 2.0) * numpy.cos(2.0 * (phi - self._phib)) ) def _phi2deriv(self, R, phi=0.0, t=0.0): # Calculate relevant time if not self._tform is None: if t < self._tform: smooth = 0.0 elif t < self._tsteady: deltat = t - self._tform xi = 2.0 * deltat / (self._tsteady - self._tform) - 1.0 smooth = ( 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 ) else: # perturbation is fully on smooth = 1.0 else: smooth = 1.0 return ( -2.0 * smooth * self._twophio * R**self._p * numpy.cos(2.0 * (phi - self._phib)) ) def _Rphideriv(self, R, phi=0.0, t=0.0): # Calculate relevant time if not self._tform is None: if t < self._tform: smooth = 0.0 elif t < self._tsteady: deltat = t - self._tform xi = 2.0 * deltat / (self._tsteady - self._tform) - 1.0 smooth = ( 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 ) else: # perturbation is fully on smooth = 1.0 else: smooth = 1.0 return ( -smooth * self._p * self._twophio * R ** (self._p - 1.0) * numpy.sin(2.0 * (phi - self._phib)) ) def tform(self): # pragma: no cover """ Return formation time of the perturbation. Returns ------- float Formation time of the perturbation in normalized units. Notes ----- - 2011-10-19 - Written - Bovy (IAS) """ return self._tform ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/FerrersPotential.py0000644000175100001660000003735314761352023021305 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.0, a=1.0, n=2, b=0.35, c=0.2375, omegab=0.0, pa=0.0, normalize=False, ro=None, vo=None, ): """ Initialize a Ferrers potential. Parameters ---------- amp : float or Quantity, optional Total mass of the ellipsoid determines the amplitude of the potential. a : float or Quantity, optional Scale radius. n : int, optional Power of Ferrers density (n > 0). b : float, optional y-to-x axis ratio of the density. c : float, optional z-to-x axis ratio of the density. omegab : float or Quantity, optional Rotation speed of the ellipsoid. pa : float or Quantity, optional If set, the position angle of the x axis (rad or Quantity). normalize : bool or float, optional 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 : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 2011-02-23: Written - Bovy (NYU) """ 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.0 self._c2 = self._c**2.0 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.0) > 10.0**-10.0: self.isNonAxi = True return None def _evaluate(self, R, z, phi=0.0, t=0.0): if not self.isNonAxi: phi = 0.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.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.0) * 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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0 * 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.0 * 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.0 * 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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0 * c * s * phixya + s**2 * phiyya phixy = (c**2 - s**2) * phixya + c * s * (phiyya - phixxa) phiyy = s**2 * phixxa - 2.0 * c * s * phixya + c**2 * phiyya return ( numpy.cos(phi) ** 2.0 * phixx + numpy.sin(phi) ** 2.0 * phiyy + 2.0 * numpy.cos(phi) * numpy.sin(phi) * phixy ) def _Rzderiv(self, R, z, phi=0.0, t=0.0): if not self.isNonAxi: phi = 0.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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0, t=0.0): if not self.isNonAxi: phi = 0.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.0 * c * s * phixya + s**2 * phiyya phixy = (c**2 - s**2) * phixya + c * s * (phiyya - phixxa) phiyy = s**2 * phixxa - 2.0 * c * s * phixya + c**2 * phiyya return R**2.0 * ( numpy.sin(phi) ** 2.0 * phixx + numpy.cos(phi) ** 2.0 * phiyy - 2.0 * numpy.cos(phi) * numpy.sin(phi) * phixy ) + R * (numpy.cos(phi) * Fx + numpy.sin(phi) * Fy) def _Rphideriv(self, R, z, phi=0.0, t=0.0): if not self.isNonAxi: phi = 0.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.0 * c * s * phixya + s**2 * phiyya phixy = (c**2 - s**2) * phixya + c * s * (phiyya - phixxa) phiyy = s**2 * phixxa - 2.0 * c * s * phixya + c**2 * phiyya return ( R * numpy.cos(phi) * numpy.sin(phi) * (phiyy - phixx) + R * numpy.cos(2.0 * (phi)) * phixy + numpy.sin(phi) * Fx - numpy.cos(phi) * Fy ) def _phizderiv(self, R, z, phi=0.0, t=0.0): if not self.isNonAxi: phi = 0.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.0, t=0.0): 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.0 - m2 / self.a**2) ** self.n else: return 0.0 def OmegaP(self): return self._omegab def rot(self, t=0.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 involved 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.0 + (-1.0 - 2.0 * x / (tau + a2)) * (i == 0 or j == 0)) * (1.0 + (-1.0 - 2.0 * y / (tau + b2)) * (i == 1 or j == 1)) * (1.0 + (-1.0 - 2.0 * 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.0 * var2) / ( tau + coef2 ) ** 2 + _FracInt(x, y, z, a2, b2, c2, tau, n) * (-2.0 / (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.0 - 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.0)] return ll[0].real else: return 0.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/FlattenedPowerPotential.py0000644000175100001660000001275214761352023022614 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.0, alpha=0.5, q=0.9, core=_CORE, normalize=False, r1=1.0, ro=None, vo=None, ): """ Initialize a flattened power-law potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential. Can be a Quantity with units of velocity squared. alpha : float, optional Power-law exponent. q : float, optional Flattening parameter. core : float or Quantity, optional Core radius. normalize : bool or float, optional 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. r1 : float or Quantity, optional Reference radius for amplitude. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 self.core2 = core**2.0 # 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.0, t=0.0): if self.alpha == 0.0: return 1.0 / 2.0 * numpy.log(R**2.0 + z**2.0 / self.q2 + self.core2) else: m2 = self.core2 + R**2.0 + z**2.0 / self.q2 return -(m2 ** (-self.alpha / 2.0)) / self.alpha def _Rforce(self, R, z, phi=0.0, t=0.0): if self.alpha == 0.0: return -R / (R**2.0 + z**2.0 / self.q2 + self.core2) else: m2 = self.core2 + R**2.0 + z**2.0 / self.q2 return -(m2 ** (-self.alpha / 2.0 - 1.0)) * R def _zforce(self, R, z, phi=0.0, t=0.0): if self.alpha == 0.0: return -z / self.q2 / (R**2.0 + z**2.0 / self.q2 + self.core2) else: m2 = self.core2 + R**2.0 + z**2.0 / self.q2 return -(m2 ** (-self.alpha / 2.0 - 1.0)) * z / self.q2 def _R2deriv(self, R, z, phi=0.0, t=0.0): if self.alpha == 0.0: denom = 1.0 / (R**2.0 + z**2.0 / self.q2 + self.core2) return denom - 2.0 * R**2.0 * denom**2.0 else: m2 = self.core2 + R**2.0 + z**2.0 / self.q2 return -(m2 ** (-self.alpha / 2.0 - 1.0)) * ( (self.alpha + 2) * R**2.0 / m2 - 1.0 ) def _z2deriv(self, R, z, phi=0.0, t=0.0): if self.alpha == 0.0: denom = 1.0 / (R**2.0 + z**2.0 / self.q2 + self.core2) return denom / self.q2 - 2.0 * z**2.0 * denom**2.0 / self.q2**2.0 else: m2 = self.core2 + R**2.0 + z**2.0 / self.q2 return ( -1.0 / self.q2 * m2 ** (-self.alpha / 2.0 - 1.0) * ((self.alpha + 2) * z**2.0 / m2 / self.q2 - 1.0) ) def _dens(self, R, z, phi=0.0, t=0.0): if self.alpha == 0.0: return ( 1.0 / 4.0 / numpy.pi / self.q2 * ( (2.0 * self.q2 + 1.0) * self.core2 + R**2.0 + (2.0 - 1.0 / self.q2) * z**2.0 ) / (R**2.0 + z**2.0 / self.q2 + self.core2) ** 2.0 ) else: m2 = self.core2 + R**2.0 + z**2.0 / self.q2 return ( 1.0 / self.q2 * ( self.core2 * (1.0 + 2.0 * self.q2) + R**2.0 * (1.0 - self.alpha * self.q2) + z**2.0 * (2.0 - (1.0 + self.alpha) / self.q2) ) * m2 ** (-self.alpha / 2.0 - 2.0) / 4.0 / numpy.pi ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/Force.py0000644000175100001660000002404514761352023017045 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.0, ro=None, vo=None, amp_units=None): """ Initialize Force. Parameters ---------- amp : float, optional Amplitude to be applied when evaluating the potential and its forces. ro : float or Quantity, optional Physical distance scale (in kpc or as Quantity). Default is from the configuration file. vo : float or Quantity, optional Physical velocity scale (in km/s or as Quantity). Default is from the configuration file. amp_units : str, optional Type of units that `amp` should have if it has units. Must be one of 'mass', 'velocity2', or 'density'. Notes ----- - 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): """ Multiply a Force or Potential's amplitude by a number Parameters ---------- b : int or float Number to multiply the amplitude with. Returns ------- Force or Potential instance New instance with amplitude = (old amplitude) x b. Notes ----- - 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.0 / b) __truediv__ = __div__ def __add__(self, b): """ Add Force or Potential instances together to create a multi-component potential (e.g., pot= pot1+pot2+pot3) Parameters ---------- b : Force or Potential instance or a list thereof Returns ------- list of Force or Potential instances Represents the combined potential Notes ----- - 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): """ Turn off automatic returning of outputs in physical units. Returns ------- None Notes ----- - 2016-01-30 - Written - Bovy (UofT) """ self._roSet = False self._voSet = False return None def turn_physical_on(self, ro=None, vo=None): """ Turn on automatic returning of outputs in physical units. Parameters ---------- ro : float or Quantity, optional Reference distance in kpc. Default is None. vo : float or Quantity, optional Reference velocity in km/s. Default is None. Returns ------- None Notes ----- - 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 ro = conversion.parse_length_kpc(ro) if not ro is None: self._ro = ro if not vo is False: self._voSet = True vo = conversion.parse_velocity_kms(vo) if not vo is None: self._vo = vo return None @potential_physical_input @physical_conversion("force", pop=True) def rforce(self, R, z, **kwargs): """ Evaluate the spherical radial force F_r (R,z). Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. z : float or Quantity Vertical height. phi : float or Quantity, optional Azimuth. Default is None. t : float or Quantity, optional Time. Default is 0.0. v : float or Quantity, optional Current velocity in cylindrical coordinates. Required when including dissipative forces. Default is None. Returns ------- F_r : float or Quantity The spherical radial force F_r (R,z). Notes ----- - 2016-06-20 - Written - Bovy (UofT) """ r = numpy.sqrt(R**2.0 + z**2.0) kwargs["use_physical"] = False return self.Rforce(R, z, **kwargs) * R / r + self.zforce(R, z, **kwargs) * z / r def toPlanar(self): """ Convert a 3D potential into a planar potential in the mid-plane. Returns ------- planarPotential, planarAxiPotential, or planarDissipativeForce instance """ from ..potential import toPlanarPotential return toPlanarPotential(self) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/GaussianAmplitudeWrapperPotential.py0000644000175100001660000000437514761352023024653 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.0, pot=None, to=0.0, sigma=1.0, ro=None, vo=None): """ Initialize a GaussianAmplitudeWrapper Potential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential. Default is 1.0. pot : Potential instance or list thereof, optional This potential is made to rotate around the z axis by the wrapper. to : float or Quantity, optional Time at which the Gaussian peaks. Default is 0.0. sigma : float or Quantity, optional Standard deviation of the Gaussian. Default is 1.0. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 self.hasC = True self.hasC_dxdv = True def _smooth(self, t): return numpy.exp(-0.5 * (t - self._to) ** 2.0 / self._sigma2) def _wrap(self, attribute, *args, **kwargs): return self._smooth(kwargs.get("t", 0.0)) * self._wrap_pot_func(attribute)( self._pot, *args, **kwargs ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/HenonHeilesPotential.py0000644000175100001660000000360314761352023022065 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.0, ro=None, vo=None): """ Initialize a Henon-Heiles potential Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1.) ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0, t=0.0): return 0.5 * R * R * (1.0 + 2.0 / 3.0 * R * numpy.sin(3.0 * phi)) def _Rforce(self, R, phi=0.0, t=0.0): return -R * (1.0 + R * numpy.sin(3.0 * phi)) def _phitorque(self, R, phi=0.0, t=0.0): return -(R**3.0) * numpy.cos(3.0 * phi) def _R2deriv(self, R, phi=0.0, t=0.0): return 1.0 + 2.0 * R * numpy.sin(3.0 * phi) def _phi2deriv(self, R, phi=0.0, t=0.0): return -3.0 * R**3.0 * numpy.sin(3.0 * phi) def _Rphideriv(self, R, phi=0.0, t=0.0): return 3.0 * R**2.0 * numpy.cos(3.0 * phi) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/HomogeneousSpherePotential.py0000644000175100001660000000712114761352023023322 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.0, R=1.1, normalize=False, ro=None, vo=None): """ Initialize a homogeneous sphere potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential. Can be a Quantity with units of mass density or Gxmass density. R : float or Quantity, optional Size of the sphere. normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 self._R3 = self.R**3.0 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.0, t=0.0): r2 = R**2.0 + z**2.0 if r2 < self._R2: return r2 - 3.0 * self._R2 else: return -2.0 * self._R3 / numpy.sqrt(r2) def _Rforce(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 if r2 < self._R2: return -2.0 * R else: return -2.0 * self._R3 * R / r2**1.5 def _zforce(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 if r2 < self._R2: return -2.0 * z else: return -2.0 * self._R3 * z / r2**1.5 def _R2deriv(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 if r2 < self._R2: return 2.0 else: return 2.0 * self._R3 / r2**1.5 - 6.0 * self._R3 * R**2.0 / r2**2.5 def _z2deriv(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 if r2 < self._R2: return 2.0 else: return 2.0 * self._R3 / r2**1.5 - 6.0 * self._R3 * z**2.0 / r2**2.5 def _Rzderiv(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 if r2 < self._R2: return 0.0 else: return -6.0 * self._R3 * R * z / r2**2.5 def _dens(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 if r2 < self._R2: return 1.5 / numpy.pi else: return 0.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/Irrgang13.py0000644000175100001660000000674114761352023017547 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.0 Irrgang13I_bulge = PlummerPotential( amp=409.0 * mgal_in_msun / conversion.mass_in_msol(vo, ro), b=0.23 / ro, ro=ro, vo=vo, ) Irrgang13I_disk = MiyamotoNagaiPotential( amp=2856.0 * 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.0, Lambda=200.0 / ro, ): r_over_ah_gamma = (r / ah) ** (gamma - 1.0) return ( amp / 4.0 / numpy.pi / ah * r_over_ah_gamma * (r_over_ah_gamma + gamma) / r**2 / (1.0 + r_over_ah_gamma) ** 2.0 * ((1.0 - numpy.tanh((r - Lambda) / (Lambda / 20.0))) / 2.0) ) a_for_scf = 20.0 # 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.0 * mgal_in_msun / conversion.mass_in_msol(vo, ro), b=0.184 / ro, ro=ro, vo=vo, ) Irrgang13II_disk = MiyamotoNagaiPotential( amp=2829.0 * 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.0 / ro ): return amp / 4.0 / numpy.pi * ah**2.0 / r**2.0 / (r**2.0 + ah**2.0) ** 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.0 * mgal_in_msun / conversion.mass_in_msol(vo, ro), b=0.236 / ro, ro=ro, vo=vo, ) Irrgang13III_disk = MiyamotoNagaiPotential( amp=3096.0 * 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.0 * 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=1741018131.0 galpy-1.10.2/galpy/potential/IsochronePotential.py0000644000175100001660000001043214761352023021613 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.0, b=1.0, normalize=False, ro=None, vo=None): """ Initialize an isochrone potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential, the total mass. Can be a Quantity with units of mass or Gxmass. b : float or Quantity, optional Scale radius of the isochrone potential. normalize : bool or float, optional 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. Default is False. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 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.0, t=0.0): r2 = R**2.0 + z**2.0 rb = numpy.sqrt(r2 + self.b2) return -1.0 / (self.b + rb) def _Rforce(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 rb = numpy.sqrt(r2 + self.b2) dPhidrr = -1.0 / rb / (self.b + rb) ** 2.0 return dPhidrr * R def _zforce(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 rb = numpy.sqrt(r2 + self.b2) dPhidrr = -1.0 / rb / (self.b + rb) ** 2.0 return dPhidrr * z def _R2deriv(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 rb = numpy.sqrt(r2 + self.b2) return ( -( -(self.b**3.0) - self.b * z**2.0 + (2.0 * R**2.0 - z**2.0 - self.b**2.0) * rb ) / rb**3.0 / (self.b + rb) ** 3.0 ) def _z2deriv(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 rb = numpy.sqrt(r2 + self.b2) return ( -( -(self.b**3.0) - self.b * R**2.0 - (R**2.0 - 2.0 * z**2.0 + self.b**2.0) * rb ) / rb**3.0 / (self.b + rb) ** 3.0 ) def _Rzderiv(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 rb = numpy.sqrt(r2 + self.b2) return -R * z * (self.b + 3.0 * rb) / rb**3.0 / (self.b + rb) ** 3.0 def _dens(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 rb = numpy.sqrt(r2 + self.b2) return ( (3.0 * (self.b + rb) * rb**2.0 - r2 * (self.b + 3.0 * rb)) / rb**3.0 / (self.b + rb) ** 3.0 / 4.0 / numpy.pi ) def _surfdens(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 rb = numpy.sqrt(r2 + self.b2) return ( self.b * ( (R * z) / r2 - (self.b * R * z * (self.b**2 + 2.0 * 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.0 / numpy.pi ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/IsothermalDiskPotential.py0000644000175100001660000000375514761352023022616 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.0, sigma=0.1, ro=None, vo=None): """ Initialize an IsothermalDiskPotential. Parameters ---------- amp : float, optional An overall amplitude. sigma : float or Quantity, optional Velocity dispersion. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 self._H = sigma / numpy.sqrt(8.0 * numpy.pi * self._amp) self._amp = 1.0 # 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.0): return 2.0 * self._sigma2 * numpy.log(numpy.cosh(0.5 * x / self._H)) def _force(self, x, t=0.0): return -self._sigma2 * numpy.tanh(0.5 * x / self._H) / self._H ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/KGPotential.py0000644000175100001660000000526314761352023020171 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, amp=1.0, K=1.15, F=0.03, D=1.8, ro=None, vo=None): """ Initialize a KGPotential Parameters ---------- amp : float or Quantity, optional An overall amplitude, by default 1.0 K : float or Quantity, optional K parameter (= 2πΣ_disk; specify either as surface density or directly as force [i.e., including 2πG]), by default 1.15 F : float or Quantity, optional F parameter (= 4πρ_halo; specify either as density or directly as second potential derivative [i.e., including 4πG]), by default 0.03 D : float or Quantity, optional D parameter (natural units or Quantity length units), by default 1.8 ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * 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.0 self.hasC = True def _evaluate(self, x, t=0.0): return self._K * (numpy.sqrt(x**2.0 + self._D2) - self._D) + self._F * x**2.0 def _force(self, x, t=0.0): return -x * (self._K / numpy.sqrt(x**2 + self._D2) + 2.0 * self._F) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/KingPotential.py0000644000175100001660000000545114761352023020557 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.0, M=3.0, rt=1.5, npt=1001, _sfkdf=None, ro=None, vo=None): """ Initialize a King potential Parameters ---------- W0 : float, optional Dimensionless central potential W0 = Psi(0)/sigma^2 (in practice, needs to be <~ 200, where the DF is essentially isothermal). Default: 2. M : float or Quantity, optional Total mass. Default: 1. rt : float or Quantity, optional Tidal radius. Default: 1. npt : int, optional Number of points to use to solve for Psi(r) when solving the King DF. Default: 1001. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * numpy.interp(r / radius_scale, sfkdf._r, sfkdf._dWdr), rgrid=sfkdf._r * radius_scale, Phi0=-W0 * mass_scale / radius_scale - M / rt, ro=ro, vo=vo, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/KuzminDiskPotential.py0000644000175100001660000000624714761352023021763 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.0, a=1.0, normalize=False, ro=None, vo=None): """ Initialize a Kuzmin disk Potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential, the total mass. Can be a Quantity with units of mass or Gxmass. a : float or Quantity, optional Scale length. normalize : bool or float, optional 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 : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0, t=0.0): return -(self._denom(R, z) ** -0.5) def _Rforce(self, R, z, phi=0.0, t=0.0): return -(self._denom(R, z) ** -1.5) * R def _zforce(self, R, z, phi=0.0, t=0.0): return -numpy.sign(z) * self._denom(R, z) ** -1.5 * (self._a + numpy.fabs(z)) def _R2deriv(self, R, z, phi=0.0, t=0.0): return self._denom(R, z) ** -1.5 - 3.0 * R**2 * self._denom(R, z) ** -2.5 def _z2deriv(self, R, z, phi=0.0, t=0.0): a = self._a return ( self._denom(R, z) ** -1.5 - 3.0 * (a + numpy.fabs(z)) ** 2.0 * self._denom(R, z) ** -2.5 ) def _Rzderiv(self, R, z, phi=0.0, t=0.0): return ( -3 * numpy.sign(z) * R * (self._a + numpy.fabs(z)) * self._denom(R, z) ** -2.5 ) def _surfdens(self, R, z, phi=0.0, t=0.0): return self._a * (R**2 + self._a**2) ** -1.5 / 2.0 / numpy.pi def _mass(self, R, z=None, t=0.0): return 1.0 - self._a / numpy.sqrt(R**2.0 + self._a**2.0) def _denom(self, R, z): return R**2.0 + (self._a + numpy.fabs(z)) ** 2.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/KuzminKutuzovStaeckelPotential.py0000644000175100001660000002004314761352023024222 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.0, ac=5.0, Delta=1.0, normalize=False, ro=None, vo=None): """ Initialize a Kuzmin-Kutuzov Staeckel potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential; can be a Quantity with units of mass density or Gxmass density. ac : float, optional Axis ratio of the coordinate surfaces; (a/c) = sqrt(-alpha) / sqrt(-gamma) (default: 5.). Delta : float or Quantity, optional Focal distance that defines the spheroidal coordinate system (default: 1.); Delta=sqrt(gamma-alpha). normalize : bool or float, optional 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 : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 - 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.0, t=0.0): l, n = coords.Rz_to_lambdanu(R, z, ac=self._ac, Delta=self._Delta) return -1.0 / (numpy.sqrt(l) + numpy.sqrt(n)) def _Rforce(self, R, z, phi=0.0, t=0.0): 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.0, t=0.0): 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.0, t=0.0): 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.0 * dldR * dndR * self._lnderiv(l, n) ) def _z2deriv(self, R, z, phi=0.0, t=0.0): 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.0 * dldz * dndz * self._lnderiv(l, n) ) def _Rzderiv(self, R, z, phi=0.0, t=0.0): 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): """ Evaluate the derivative w.r.t. lambda for this potential. Parameters ---------- l : float Prolate spheroidal coordinate lambda. n : float Prolate spheroidal coordinate nu. Returns ------- float Derivative w.r.t. lambda. Notes ----- - 2015-02-15 - Written - Trick (MPIA) """ return 0.5 / numpy.sqrt(l) / (numpy.sqrt(l) + numpy.sqrt(n)) ** 2 def _nderiv(self, l, n): """ Evaluate the derivative w.r.t. nu for this potential. Parameters ---------- l : float Prolate spheroidal coordinate lambda. n : float Prolate spheroidal coordinate nu. Returns ------- float Derivative w.r.t. nu. Notes ----- - 2015-02-15 - Written - Trick (MPIA) """ return 0.5 / numpy.sqrt(n) / (numpy.sqrt(l) + numpy.sqrt(n)) ** 2 def _l2deriv(self, l, n): """ Evaluate the second derivative w.r.t. lambda for this potential. Parameters ---------- l : float Prolate spheroidal coordinate lambda. n : float Prolate spheroidal coordinate nu. Returns ------- float Second derivative w.r.t. lambda. Notes ----- - 2015-02-15 - Written - Trick (MPIA) """ number = -3.0 * numpy.sqrt(l) - numpy.sqrt(n) denom = 4.0 * l**1.5 * (numpy.sqrt(l) + numpy.sqrt(n)) ** 3 return number / denom def _n2deriv(self, l, n): """ Evaluate the second derivative w.r.t. nu for this potential. Parameters ---------- l : float Prolate spheroidal coordinate lambda. n : float Prolate spheroidal coordinate nu. Returns ------- float Second derivative w.r.t. nu. Notes ----- - 2015-02-15 - Written - Trick (MPIA) """ number = -numpy.sqrt(l) - 3.0 * numpy.sqrt(n) denom = 4.0 * n**1.5 * (numpy.sqrt(l) + numpy.sqrt(n)) ** 3 return number / denom def _lnderiv(self, l, n): """ Evaluate the mixed derivative w.r.t. lambda and nu for this potential. Parameters ---------- l : float Prolate spheroidal coordinate lambda. n : float Prolate spheroidal coordinate nu. Returns ------- float Mixed derivative w.r.t. lambda and nu. Notes ----- - 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=1741018131.0 galpy-1.10.2/galpy/potential/KuzminLikeWrapperPotential.py0000644000175100001660000001202214761352023023302 0ustar00runnerdocker############################################################################### # KuzminLikeWrapperPotential.py: Wrapper to convert a potential to a Kuzmin # like potential (phi(r) --> phi(xi) where xi = sqrt(R^2 + (a + sqrt(z^2 + b^2))^2)) ############################################################################### import numpy from ..util import conversion from .Potential import ( _evaluatePotentials, _evaluateRforces, _isNonAxi, evaluateR2derivs, ) from .WrapperPotential import WrapperPotential # Only implement 3D wrapper class KuzminLikeWrapperPotential(WrapperPotential): """Wrapper to convert a spherical potential to a Kuzmin-like potential .. math:: \\Phi(r) \\rightarrow \\Phi(\\xi)\\,, where .. math:: \\xi = \\sqrt{R^2 + \\left(a + \\sqrt{z^2 + b^2}\\right)^2}\\,. Applying this wrapper to a ``KeplerPotential`` results in the ``KuzminDiskPotential`` (for :math:`b=0`) or the ``MiyamotoNagaiPotential`` (for :math:`b \\neq 0`). """ def __init__( self, amp=1.0, a=1.1, b=0.0, pot=None, ro=None, vo=None, ): """ Initialize a KuzminLikeWrapperPotential Parameters ---------- amp : float, optional Overall amplitude to apply to the potential. Default is 1.0. a : float or Quantity, optional Scale radius of the Kuzmin-like potential. Default is 1.1. b : float or Quantity, optional Scale height of the Kuzmin-like potential. Default is 0.0. pot : Potential instance or list thereof The potential to be wrapped. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 2024-01-15 - Written - Bovy (UofT) """ WrapperPotential.__init__(self, amp=amp, pot=pot, ro=ro, vo=vo, _init=True) self._a = conversion.parse_length(a, ro=self._ro) self._scale = self._a self._b = conversion.parse_length(b, ro=self._ro) self._b2 = self._b**2.0 if _isNonAxi(self._pot): raise RuntimeError( "KuzminLikeWrapperPotential only works for spherical or axisymmetric potentials" ) self.hasC = True self.hasC_dxdv = True self.isNonAxi = False def _xi(self, R, z): return numpy.sqrt(R**2.0 + (self._a + numpy.sqrt(z**2.0 + self._b2)) ** 2.0) def _dxidR(self, R, z): return R / self._xi(R, z) def _dxidz(self, R, z): return ( (self._a + numpy.sqrt(z**2.0 + self._b2)) * z / self._xi(R, z) / numpy.sqrt(z**2.0 + self._b2) ) def _d2xidR2(self, R, z): return ((self._a + numpy.sqrt(z**2.0 + self._b2)) ** 2.0) / self._xi( R, z ) ** 3.0 def _d2xidz2(self, R, z): return ( ( self._a**3.0 * self._b2 + 3.0 * self._a**2.0 * self._b2 * numpy.sqrt(self._b2 + z**2.0) + self._a * self._b2 * (3.0 * self._b2 + R**2.0 + 3.0 * z**2.0) + (self._b2 + R**2.0) * (self._b2 + z**2.0) ** (1.5) ) / (self._b2 + z**2.0) ** 1.5 / self._xi(R, z) ** 3.0 ) def _d2xidRdz(self, R, z): return -(R * z * (self._a + numpy.sqrt(self._b2 + z**2.0))) / ( numpy.sqrt(self._b2 + z**2.0) * ((self._a + numpy.sqrt(self._b2 + z**2.0)) ** 2.0 + R**2.0) ** 1.5 ) def _evaluate(self, R, z, phi=0.0, t=0.0): return _evaluatePotentials(self._pot, self._xi(R, z), 0.0, phi=phi, t=t) def _Rforce(self, R, z, phi=0.0, t=0.0): return _evaluateRforces( self._pot, self._xi(R, z), 0.0, phi=phi, t=t ) * self._dxidR(R, z) def _zforce(self, R, z, phi=0.0, t=0.0): return _evaluateRforces( self._pot, self._xi(R, z), 0.0, phi=phi, t=t ) * self._dxidz(R, z) def _phitorque(self, R, z, phi=0.0, t=0.0): return 0.0 def _R2deriv(self, R, z, phi=0.0, t=0.0): return evaluateR2derivs( self._pot, self._xi(R, z), 0.0, phi=phi, t=t ) * self._dxidR(R, z) ** 2.0 - _evaluateRforces( self._pot, self._xi(R, z), 0.0, phi=phi, t=t ) * self._d2xidR2(R, z) def _z2deriv(self, R, z, phi=0.0, t=0.0): return evaluateR2derivs( self._pot, self._xi(R, z), 0.0, phi=phi, t=t ) * self._dxidz(R, z) ** 2.0 - _evaluateRforces( self._pot, self._xi(R, z), 0.0, phi=phi, t=t ) * self._d2xidz2(R, z) def _Rzderiv(self, R, z, phi=0.0, t=0.0): return evaluateR2derivs( self._pot, self._xi(R, z), 0.0, phi=phi, t=t ) * self._dxidR(R, z) * self._dxidz(R, z) - _evaluateRforces( self._pot, self._xi(R, z), 0.0, phi=phi, t=t ) * self._d2xidRdz(R, z) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/LogarithmicHaloPotential.py0000644000175100001660000002163114761352023022733 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.0, core=_CORE, q=1.0, b=None, normalize=False, ro=None, vo=None ): """ Initialize a logarithmic potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential; can be a Quantity with units of velocity-squared. core : float or Quantity, optional Core radius at which the logarithm is cut. q : float Potential flattening (z/q)**2. b : float, optional Shape parameter in y-direction (y --> y/b; see definition). normalize : bool or float, optional 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 : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 self._q = q self._b = b if not self._b is None: self.isNonAxi = True self._1m1overb2 = 1.0 - 1.0 / self._b**2.0 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.0, t=0.0): if self.isNonAxi: return ( 1.0 / 2.0 * numpy.log( R**2.0 * (1.0 - self._1m1overb2 * numpy.sin(phi) ** 2.0) + (z / self._q) ** 2.0 + self._core2 ) ) else: return 1.0 / 2.0 * numpy.log(R**2.0 + (z / self._q) ** 2.0 + self._core2) def _Rforce(self, R, z, phi=0.0, t=0.0): if self.isNonAxi: Rt2 = R**2.0 * (1.0 - self._1m1overb2 * numpy.sin(phi) ** 2.0) return -Rt2 / R / (Rt2 + (z / self._q) ** 2.0 + self._core2) else: return -R / (R**2.0 + (z / self._q) ** 2.0 + self._core2) def _zforce(self, R, z, phi=0.0, t=0.0): if self.isNonAxi: Rt2 = R**2.0 * (1.0 - self._1m1overb2 * numpy.sin(phi) ** 2.0) return -z / self._q**2.0 / (Rt2 + (z / self._q) ** 2.0 + self._core2) else: return -z / self._q**2.0 / (R**2.0 + (z / self._q) ** 2.0 + self._core2) def _phitorque(self, R, z, phi=0.0, t=0.0): if self.isNonAxi: Rt2 = R**2.0 * (1.0 - self._1m1overb2 * numpy.sin(phi) ** 2.0) return ( R**2.0 / (Rt2 + (z / self._q) ** 2.0 + self._core2) * numpy.sin(2.0 * phi) * self._1m1overb2 / 2.0 ) else: return 0 def _dens(self, R, z, phi=0.0, t=0.0): if self.isNonAxi: R2 = R**2.0 Rt2 = R2 * (1.0 - self._1m1overb2 * numpy.sin(phi) ** 2.0) denom = 1.0 / (Rt2 + (z / self._q) ** 2.0 + self._core2) denom2 = denom**2.0 return ( 1.0 / 4.0 / numpy.pi * ( 2.0 * Rt2 / R2 * (denom - Rt2 * denom2) + denom / self._q**2.0 - 2.0 * z**2.0 * denom2 / self._q**4.0 - self._1m1overb2 * ( 2.0 * R2 * numpy.sin(2.0 * phi) ** 2.0 / 4.0 * self._1m1overb2 * denom2 + denom * numpy.cos(2.0 * phi) ) ) ) else: return ( 1.0 / 4.0 / numpy.pi / self._q**2.0 * ( (2.0 * self._q**2.0 + 1.0) * self._core2 + R**2.0 + (2.0 - self._q**-2.0) * z**2.0 ) / (R**2.0 + (z / self._q) ** 2.0 + self._core2) ** 2.0 ) def _R2deriv(self, R, z, phi=0.0, t=0.0): if self.isNonAxi: Rt2 = R**2.0 * (1.0 - self._1m1overb2 * numpy.sin(phi) ** 2.0) denom = 1.0 / (Rt2 + (z / self._q) ** 2.0 + self._core2) return (denom - 2.0 * Rt2 * denom**2.0) * Rt2 / R**2.0 else: denom = 1.0 / (R**2.0 + (z / self._q) ** 2.0 + self._core2) return denom - 2.0 * R**2.0 * denom**2.0 def _z2deriv(self, R, z, phi=0.0, t=0.0): if self.isNonAxi: Rt2 = R**2.0 * (1.0 - self._1m1overb2 * numpy.sin(phi) ** 2.0) denom = 1.0 / (Rt2 + (z / self._q) ** 2.0 + self._core2) return denom / self._q**2.0 - 2.0 * z**2.0 * denom**2.0 / self._q**4.0 else: denom = 1.0 / (R**2.0 + (z / self._q) ** 2.0 + self._core2) return denom / self._q**2.0 - 2.0 * z**2.0 * denom**2.0 / self._q**4.0 def _Rzderiv(self, R, z, phi=0.0, t=0.0): if self.isNonAxi: Rt2 = R**2.0 * (1.0 - self._1m1overb2 * numpy.sin(phi) ** 2.0) return ( -2.0 * Rt2 / R * z / self._q**2.0 / (Rt2 + (z / self._q) ** 2.0 + self._core2) ** 2.0 ) else: return ( -2.0 * R * z / self._q**2.0 / (R**2.0 + (z / self._q) ** 2.0 + self._core2) ** 2.0 ) def _phi2deriv(self, R, z, phi=0.0, t=0.0): if self.isNonAxi: Rt2 = R**2.0 * (1.0 - self._1m1overb2 * numpy.sin(phi) ** 2.0) denom = 1.0 / (Rt2 + (z / self._q) ** 2.0 + self._core2) return -self._1m1overb2 * ( R**4.0 * numpy.sin(2.0 * phi) ** 2.0 / 2.0 * self._1m1overb2 * denom**2.0 + R**2.0 * denom * numpy.cos(2.0 * phi) ) else: return 0.0 def _Rphideriv(self, R, z, phi=0.0, t=0.0): if self.isNonAxi: Rt2 = R**2.0 * (1.0 - self._1m1overb2 * numpy.sin(phi) ** 2.0) denom = 1.0 / (Rt2 + (z / self._q) ** 2.0 + self._core2) return ( -(denom - Rt2 * denom**2.0) * R * numpy.sin(2.0 * phi) * self._1m1overb2 ) else: return 0.0 def _phizderiv(self, R, z, phi=0.0, t=0.0): if self.isNonAxi: Rt2 = R**2.0 * (1.0 - self._1m1overb2 * numpy.sin(phi) ** 2.0) denom = 1.0 / (Rt2 + (z / self._q) ** 2.0 + self._core2) return ( 2 * R**2 * z * numpy.sin(phi) * numpy.cos(phi) * self._1m1overb2 * denom**2 / self._q**2 ) else: return 0.0 @kms_to_kpcGyrDecorator def _nemo_accpars(self, vo, ro): 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.0 return "0,{},{},1.0,{}".format( ampl, self._core2 * ro**2.0 * self._q ** (2.0 / 3.0), # somewhat weird gyrfalcon implementation self._q, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/MN3ExponentialDiskPotential.py0000644000175100001660000002244514761352023023310 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.0, hr=1.0 / 3.0, hz=1.0 / 16.0, sech=False, posdens=False, normalize=False, ro=None, vo=None, ): """ Initialize a 3MN approximation to an exponential disk potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass density or Gxmass density. hr : float or Quantity, optional Disk scale-length. hz : float or Quantity, optional Scale-height. sech : bool, optional If True, hz is the scale height of a sech vertical profile (default is exponential vertical profile). posdens : bool, optional If True, allow only positive density solutions (Table 2 in Smith et al. rather than Table 1). normalize : bool or float, optional 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 : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * numpy.pi * self._hr**2.0 * 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.0: raise OSError( "MN3ExponentialDiskPotential's b/Rd is negative for the given hz" ) # Check range if (not posdens and self._brd > 3.0) 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.0, t=0.0): 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.0, t=0.0): 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.0, t=0.0): 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.0, t=0.0): 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.0, t=0.0): 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.0, t=0.0): 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.0, t=0.0): 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): 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.0 * 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 + 0.0640 * brd**3.0 - 0.1653 * brd**2.0 + 0.1164 * brd + 1.9487 ) def _mass2_tab1(brd): return ( 0.0173 * brd**4.0 - 0.0903 * brd**3.0 + 0.0877 * brd**2.0 + 0.2029 * brd - 1.3077 ) def _mass3_tab1(brd): return ( -0.0051 * brd**4.0 + 0.0287 * brd**3.0 - 0.0361 * brd**2.0 - 0.0544 * brd + 0.2242 ) def _a1_tab1(brd): return ( -0.0358 * brd**4.0 + 0.2610 * brd**3.0 - 0.6987 * brd**2.0 - 0.1193 * brd + 2.0074 ) def _a2_tab1(brd): return ( -0.0830 * brd**4.0 + 0.4992 * brd**3.0 - 0.7967 * brd**2.0 - 1.2966 * brd + 4.4441 ) def _a3_tab1(brd): return ( -0.0247 * brd**4.0 + 0.1718 * brd**3.0 - 0.4124 * brd**2.0 - 0.5944 * brd + 0.7333 ) # Equations from Table 2 def _mass1_tab2(brd): return ( 0.0036 * brd**4.0 - 0.0330 * brd**3.0 + 0.1117 * brd**2.0 - 0.1335 * brd + 0.1749 ) def _mass2_tab2(brd): return ( -0.0131 * brd**4.0 + 0.1090 * brd**3.0 - 0.3035 * brd**2.0 + 0.2921 * brd - 5.7976 ) def _mass3_tab2(brd): return ( -0.0048 * brd**4.0 + 0.0454 * brd**3.0 - 0.1425 * brd**2.0 + 0.1012 * brd + 6.7120 ) def _a1_tab2(brd): return ( -0.0158 * brd**4.0 + 0.0993 * brd**3.0 - 0.2070 * brd**2.0 - 0.7089 * brd + 0.6445 ) def _a2_tab2(brd): return ( -0.0319 * brd**4.0 + 0.1514 * brd**3.0 - 0.1279 * brd**2.0 - 0.9325 * brd + 2.6836 ) def _a3_tab2(brd): return ( -0.0326 * brd**4.0 + 0.1816 * brd**3.0 - 0.2943 * brd**2.0 - 0.6329 * brd + 2.3193 ) # Equations to go from hz to b def _b_exphz(hz): return -0.269 * hz**3.0 + 1.080 * hz**2.0 + 1.092 * hz def _b_sechhz(hz): return -0.033 * hz**3.0 + 0.262 * hz**2.0 + 0.659 * hz ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/McMillan17.py0000644000175100001660000000512614761352023017652 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.0 / ro Rm_HI = 4.0 / ro zd_HI = 0.085 / ro Sigma0_HI = 53.1 / sigo Rd_H2 = 1.5 / ro Rm_H2 = 12.0 / ro zd_H2 = 0.045 / ro Sigma0_H2 = 2180.0 / sigo # parameters of best-fitting model in McMillan (2017) # stellar disks Sigma0_thin = 896.0 / sigo Rd_thin = 2.5 / ro zd_thin = 0.3 / ro Sigma0_thick = 183.0 / 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=1741018131.0 galpy-1.10.2/galpy/potential/MiyamotoNagaiPotential.py0000644000175100001660000001314414761352023022423 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 [1]_ .. 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.0, a=1.0, b=0.1, normalize=False, ro=None, vo=None): """ Initialize a Miyamoto-Nagai potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential, the total mass (default: 1); can be a Quantity with units of mass or Gxmass. a : float or Quantity, optional Scale length. b : float or Quantity, optional Scale height. normalize : bool or float, optional 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 : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 2010-07-09 - Started - Bovy (NYU) References ---------- .. [1] Miyamoto, M., & Nagai, R. (1975). Three-dimensional models for the distribution of mass in galaxies. Publications of the Astronomical Society of Japan, 27(4), 533-543. ADS: https://ui.adsabs.harvard.edu/abs/1975PASJ...27..533M/abstract """ 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.0 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.0, t=0.0): return -1.0 / numpy.sqrt( R**2.0 + (self._a + numpy.sqrt(z**2.0 + self._b2)) ** 2.0 ) def _Rforce(self, R, z, phi=0.0, t=0.0): return -R / (R**2.0 + (self._a + numpy.sqrt(z**2.0 + self._b2)) ** 2.0) ** ( 3.0 / 2.0 ) def _zforce(self, R, z, phi=0.0, t=0.0): sqrtbz = numpy.sqrt(self._b2 + z**2.0) asqrtbz = self._a + sqrtbz if isinstance(R, float) and sqrtbz == asqrtbz: return -z / (R**2.0 + (self._a + numpy.sqrt(z**2.0 + self._b2)) ** 2.0) ** ( 3.0 / 2.0 ) else: return ( -z * asqrtbz / sqrtbz / (R**2.0 + (self._a + numpy.sqrt(z**2.0 + self._b2)) ** 2.0) ** (3.0 / 2.0) ) def _dens(self, R, z, phi=0.0, t=0.0): sqrtbz = numpy.sqrt(self._b2 + z**2.0) asqrtbz = self._a + sqrtbz if isinstance(R, float) and sqrtbz == asqrtbz: return 3.0 / (R**2.0 + sqrtbz**2.0) ** 2.5 / 4.0 / numpy.pi * self._b2 else: return ( (self._a * R**2.0 + (self._a + 3.0 * sqrtbz) * asqrtbz**2.0) / (R**2.0 + asqrtbz**2.0) ** 2.5 / sqrtbz**3.0 / 4.0 / numpy.pi * self._b2 ) def _R2deriv(self, R, z, phi=0.0, t=0.0): return ( 1.0 / (R**2.0 + (self._a + numpy.sqrt(z**2.0 + self._b2)) ** 2.0) ** 1.5 - 3.0 * R**2.0 / (R**2.0 + (self._a + numpy.sqrt(z**2.0 + self._b2)) ** 2.0) ** 2.5 ) def _z2deriv(self, R, z, phi=0.0, t=0.0): sqrtbz = numpy.sqrt(self._b2 + z**2.0) asqrtbz = self._a + sqrtbz if isinstance(R, float) and sqrtbz == asqrtbz: return (self._b2 + R**2.0 - 2.0 * z**2.0) * ( self._b2 + R**2.0 + z**2.0 ) ** -2.5 else: return ( self._a**3.0 * self._b2 + self._a**2.0 * (3.0 * self._b2 - 2.0 * z**2.0) * numpy.sqrt(self._b2 + z**2.0) + (self._b2 + R**2.0 - 2.0 * z**2.0) * (self._b2 + z**2.0) ** 1.5 + self._a * (3.0 * self._b2**2.0 - 4.0 * z**4.0 + self._b2 * (R**2.0 - z**2.0)) ) / ((self._b2 + z**2.0) ** 1.5 * (R**2.0 + asqrtbz**2.0) ** 2.5) def _Rzderiv(self, R, z, phi=0.0, t=0.0): sqrtbz = numpy.sqrt(self._b2 + z**2.0) asqrtbz = self._a + sqrtbz if isinstance(R, float) and sqrtbz == asqrtbz: return -(3.0 * R * z / (R**2.0 + asqrtbz**2.0) ** 2.5) else: return -(3.0 * R * z * asqrtbz / sqrtbz / (R**2.0 + asqrtbz**2.0) ** 2.5) @kms_to_kpcGyrDecorator def _nemo_accpars(self, vo, ro): ampl = self._amp * vo**2.0 * ro return f"0,{ampl},{self._a * ro},{self._b * ro}" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/MovingObjectPotential.py0000644000175100001660000001200614761352023022247 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): """ Initialize a MovingObjectPotential. Parameters ---------- orbit : galpy.orbit.Orbit The orbit of the object. pot : Potential object or list of Potential objects A potential object or list of potential objects representing the potential of the moving object; should be spherical, but this is not checked. Default is `PlummerPotential(amp=0.06,b=0.01)`. amp : float, optional Another amplitude to apply to the potential. Default is 1.0. ro : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0, t=0.0): # 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.0, t=0.0): # 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.0, t=0.0): # 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.0, t=0.0): # 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.0, t=0.0): # 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.0 + R2**2.0 - 2.0 * 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=1741018131.0 galpy-1.10.2/galpy/potential/NonInertialFrameForce.py0000644000175100001660000003456414761352023022172 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.0, Omega=None, Omegadot=None, x0=None, v0=None, a0=None, ro=None, vo=None, ): """ Initialize a NonInertialFrameForce. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1). Omega : float or list of floats or Quantity or list of Quantities or callable or list of callables, optional 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 : float or list of floats or Quantity or list of Quantities or callable or list of callables, optional 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 : list of callables, optional 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 : list of callables, optional 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 : float or list of callables, optional 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]). ro : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 if self._rot_acc and not self._Omega_as_func else 0.0 ) if not self._omegaz_only and not self._Omega_as_func: self._Omega_for_cross = numpy.array( [ [0.0, -self._Omega[2], self._Omega[1]], [self._Omega[2], 0.0, -self._Omega[0]], [-self._Omega[1], self._Omega[0], 0.0], ] ) if not self._const_freq: self._Omegadot_for_cross = numpy.array( [ [0.0, -self._Omegadot[2], self._Omegadot[1]], [self._Omegadot[2], 0.0, -self._Omegadot[0]], [-self._Omegadot[1], self._Omegadot[0], 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.0 else: tOmega = self._Omega + self._Omegadot * t tOmega2 = numpy.linalg.norm(tOmega) ** 2.0 if self._omegaz_only: force += -2.0 * tOmega * numpy.array( [-vy, vx, 0.0] ) + tOmega2 * numpy.array([x, y, 0.0]) if self._lin_acc: force += -2.0 * tOmega * numpy.array( [-self._v0[1](t), self._v0[0](t), 0.0] ) + tOmega2 * numpy.array([self._x0[0](t), self._x0[1](t), 0.0]) if not self._const_freq: if self._Omega_as_func: force -= self._Omegadot_py(t) * numpy.array([-y, x, 0.0]) if self._lin_acc: force -= self._Omegadot_py(t) * numpy.array( [-self._x0[1](t), self._x0[0](t), 0.0] ) else: force -= self._Omegadot * numpy.array([-y, x, 0.0]) if self._lin_acc: force -= self._Omegadot * numpy.array( [-self._x0[1](t), self._x0[0](t), 0.0] ) else: if self._Omega_as_func: self._Omega_for_cross = numpy.array( [ [0.0, -self._Omega[2](t), self._Omega[1](t)], [self._Omega[2](t), 0.0, -self._Omega[0](t)], [-self._Omega[1](t), self._Omega[0](t), 0.0], ] ) if not self._const_freq: self._Omegadot_for_cross = numpy.array( [ [0.0, -self._Omegadot[2](t), self._Omegadot[1](t)], [self._Omegadot[2](t), 0.0, -self._Omegadot[0](t)], [-self._Omegadot[1](t), self._Omegadot[0](t), 0.0], ] ) force += ( -2.0 * 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.0 * 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.0 * 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.0 * 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.0, t=0.0, v=None): 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.0, t=0.0, v=None): 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.0, t=0.0, v=None): return self._force(R, z, phi, t, v)[2] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/NullPotential.py0000644000175100001660000000354314761352023020601 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.0, ro=None, vo=None): """ Initialize a null potential: a constant potential with, thus, zero forces Parameters ---------- amp : float or Quantity, optional Constant value of the potential (default: 1); can be a Quantity with units of velocity-squared ro : float, optional Distance scale for translation into internal units (default from configuration file) vo : float, optional Velocity scale for translation into internal units (default from configuration file) Notes ----- - 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.0, t=0.0): return 1.0 def _Rforce(self, R, z, phi=0.0, t=0.0): return 0.0 def _zforce(self, R, z, phi=0.0, t=0.0): return 0.0 def _dens(self, R, z, phi=0.0, t=0.0): return 0.0 def _R2deriv(self, R, z, phi=0.0, t=0.0): return 0.0 def _z2deriv(self, R, z, phi=0.0, t=0.0): return 0.0 def _Rzderiv(self, R, z, phi=0.0, t=0.0): return 0.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/NumericalPotentialDerivativesMixin.py0000644000175100001660000001537314761352023025025 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.0, t=0.0): # Do forward difference because R cannot be negative RplusdR = R + self._dR Rplus2dR = R + 2.0 * self._dR dR = (Rplus2dR - R) / 2.0 return ( 1.5 * self._evaluate(R, z, phi=phi, t=t) - 2.0 * 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.0, t=0.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.0, t=0.0): if not self.isNonAxi: return 0.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.0, t=0.0): # Do forward difference because R cannot be negative RplusdR = R + self._dR2 Rplus2dR = R + 2.0 * self._dR2 Rplus3dR = R + 3.0 * self._dR2 dR = (Rplus3dR - R) / 3.0 return ( 2.0 * self._evaluate(R, z, phi=phi, t=t) - 5.0 * self._evaluate(RplusdR, z, phi=phi, t=t) + 4.0 * self._evaluate(Rplus2dR, z, phi=phi, t=t) - 1.0 * self._evaluate(Rplus3dR, z, phi=phi, t=t) ) / dR**2.0 def _z2deriv(self, R, z, phi=0.0, t=0.0): # Central derivative zplusdz = z + self._dz2 zminusdz = z - self._dz2 dz = (zplusdz - zminusdz) / 2.0 return ( self._evaluate(R, zplusdz, phi=phi, t=t) + self._evaluate(R, zminusdz, phi=phi, t=t) - 2.0 * self._evaluate(R, z, phi=phi, t=t) ) / dz**2.0 def _phi2deriv(self, R, z, phi=0.0, t=0.0): if not self.isNonAxi: return 0.0 # Central derivative phiplusdphi = phi + self._dphi2 phiminusdphi = phi - self._dphi2 dphi = (phiplusdphi - phiminusdphi) / 2.0 return ( self._evaluate(R, z, phi=phiplusdphi, t=t) + self._evaluate(R, z, phi=phiminusdphi, t=t) - 2.0 * self._evaluate(R, z, phi=phi, t=t) ) / dphi**2.0 def _Rzderiv(self, R, z, phi=0.0, t=0.0): # Do forward difference in R because R cannot be negative RplusdR = R + self._dR2 Rplus2dR = R + 2.0 * self._dR2 dR = (Rplus2dR - R) / 2.0 zplusdz = z + self._dz2 zminusdz = z - self._dz2 dz = zplusdz - zminusdz return ( ( -1.5 * self._evaluate(R, zplusdz, phi=phi, t=t) + 2.0 * 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.0 * 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.0, t=0.0): if not self.isNonAxi: return 0.0 # Do forward difference in R because R cannot be negative RplusdR = R + self._dR2 Rplus2dR = R + 2.0 * self._dR2 dR = (Rplus2dR - R) / 2.0 phiplusdphi = phi + self._dphi2 phiminusdphi = phi - self._dphi2 dphi = phiplusdphi - phiminusdphi return ( ( -1.5 * self._evaluate(R, z, phi=phiplusdphi, t=t) + 2.0 * 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.0 * 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.0, t=0.0): if not self.isNonAxi: return 0.0 # Central derivative phiplusdphi = phi + self._dphi2 phiminusdphi = phi - self._dphi2 dphi = (phiplusdphi - phiminusdphi) / 2.0 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.0 ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/PerfectEllipsoidPotential.py0000644000175100001660000000725614761352023023131 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.0, a=5.0, b=1.0, c=1.0, zvec=None, pa=None, glorder=50, normalize=False, ro=None, vo=None, ): """ Initialize a perfect ellipsoid potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or G x mass. a : float or Quantity, optional Scale radius. b : float, optional Y-to-x axis ratio of the density. c : float, optional Z-to-x axis ratio of the density. zvec : numpy.ndarray, optional If set, a unit vector that corresponds to the z axis. pa : float or Quantity, optional If set, the position angle of the x axis (rad or Quantity). glorder : int, optional If set, compute the relevant force and potential integrals with Gaussian quadrature of this order. ro : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 / (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.0 * m * (self.a2 + m**2) ** -3 def _mass(self, R, z=None, t=0.0): if not z is None: raise AttributeError # Hack to fall back to general return ( 2.0 * numpy.pi * self._b * self._c / self.a * (numpy.arctan(R / self.a) - R * self.a / (1.0 + R**2.0)) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/PlummerPotential.py0000644000175100001660000001215314761352023021305 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.0, b=0.8, normalize=False, ro=None, vo=None): """ Initialize a Plummer potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential, the total mass. Default is 1. Can be a Quantity with units of mass or Gxmass. b : float or Quantity, optional Scale parameter. Can be a Quantity. normalize : bool or float, optional 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. Default is False. ro : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 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.0, t=0.0): return -1.0 / numpy.sqrt(R**2.0 + z**2.0 + self._b2) def _Rforce(self, R, z, phi=0.0, t=0.0): dPhidrr = -((R**2.0 + z**2.0 + self._b2) ** -1.5) return dPhidrr * R def _zforce(self, R, z, phi=0.0, t=0.0): dPhidrr = -((R**2.0 + z**2.0 + self._b2) ** -1.5) return dPhidrr * z def _rforce_jax(self, r): # No need for actual JAX! return -self._amp * r * (r**2.0 + self._b2) ** -1.5 def _dens(self, R, z, phi=0.0, t=0.0): return 3.0 / 4.0 / numpy.pi * self._b2 * (R**2.0 + z**2.0 + self._b2) ** -2.5 def _surfdens(self, R, z, phi=0.0, t=0.0): Rb = R**2.0 + self._b2 return ( self._b2 * z * (3.0 * Rb + 2.0 * z**2.0) / Rb**2.0 * (Rb + z**2.0) ** -1.5 / 2.0 / numpy.pi ) def _R2deriv(self, R, z, phi=0.0, t=0.0): return (self._b2 - 2.0 * R**2.0 + z**2.0) * (R**2.0 + z**2.0 + self._b2) ** -2.5 def _z2deriv(self, R, z, phi=0.0, t=0.0): return (self._b2 + R**2.0 - 2.0 * z**2.0) * (R**2.0 + z**2.0 + self._b2) ** -2.5 def _Rzderiv(self, R, z, phi=0.0, t=0.0): return -3.0 * R * z * (R**2.0 + z**2.0 + self._b2) ** -2.5 def _ddensdr(self, r, t=0.0): return ( self._amp * (-15.0) / 4.0 / numpy.pi * self._b2 * r * (r**2 + self._b2) ** -3.5 ) def _d2densdr2(self, r, t=0.0): return ( self._amp * (-15.0) / 4.0 / numpy.pi * self._b2 * ((r**2.0 + self._b2) ** -3.5 - 7.0 * r**2.0 * (r**2 + self._b2) ** -4.5) ) def _ddenstwobetadr(self, r, beta=0): """ Evaluate the radial density derivative x r^(2beta) for this potential. Parameters ---------- r : float Spherical radius. beta : int, optional Power of r in the density derivative. Default is 0. Returns ------- float The derivative of the density times r^(2beta). Notes ----- - 2021-03-15 - Written - Lane (UofT) """ return ( self._amp * 3.0 / 4.0 / numpy.pi * self._b2 * r ** (2.0 * beta - 1.0) * ( 2.0 * beta * (r**2.0 + self._b2) ** -2.5 - 5.0 * r**2.0 * (r**2.0 + self._b2) ** -3.5 ) ) def _mass(self, R, z=None, t=0.0): if z is not None: raise AttributeError # use general implementation r2 = R**2.0 return (1.0 + self._b2 / r2) ** -1.5 # written so it works for r=numpy.inf @kms_to_kpcGyrDecorator def _nemo_accpars(self, vo, ro): ampl = self._amp * vo**2.0 * ro return f"0,{ampl},{self._b * ro}" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/Potential.py0000644000175100001660000037064214761352023017755 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 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): """ Decorator to check inputs and throw TypeError if any of the inputs are arrays for Potentials that do not support array evaluation. Parameters ---------- func : function Function to be decorated. Returns ------- function Decorated function. Notes ----- - 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 def potential_positional_arg(func): @wraps(func) def wrapper(Pot, /, *args, **kwargs): return func(Pot, *args, **kwargs) return wrapper class Potential(Force): """Top-level class for a potential""" def __init__(self, amp=1.0, ro=None, vo=None, amp_units=None): """ Initialize a Potential object. Parameters ---------- amp : float, optional Amplitude to be applied when evaluating the potential and its forces. amp_units : str, optional Type of units that `amp` should have if it has units. Possible values are 'mass', 'velocity2', and 'density'. ro : float or Quantity, optional Physical distance scale (in kpc or as Quantity). Default is from the configuration file. vo : float or Quantity, optional Physical velocity scale (in km/s or as Quantity). Default is from the configuration file. """ Force.__init__(self, amp=amp, ro=ro, vo=vo, amp_units=amp_units) self.dim = 3 self.isRZ = True self.isNonAxi = False self.isDissipative = 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.0, t=0.0, dR=0, dphi=0): """ Evaluate the potential at the specified position and time. Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. z : float or Quantity Vertical height. phi : float or Quantity, optional Azimuth (default: 0.0). t : float or Quantity, optional Time (default: 0.0). dR : int, optional Order of radial derivative (default: 0). dphi : int, optional Order of azimuthal derivative (default: 0). Returns ------- float or Quantity The potential at the specified position and time. Notes ----- - 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.0, t=0.0, dR=0.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" ) return self._amp * rawOut if not rawOut is None else 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.0, t=0.0): """ Evaluate the cylindrical radial force F_R. Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. z : float or Quantity Vertical height. phi : float or Quantity, optional Azimuth (default: 0.0). t : float or Quantity, optional Time (default: 0.0). Returns ------- float or Quantity F_R (R,z,phi,t). Notes ----- - 2010-04-16 - Written - Bovy (NYU) """ return self._Rforce_nodecorator(R, z, phi=phi, t=t) def _Rforce_nodecorator(self, R, z, phi=0.0, t=0.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.0, t=0.0): """ Evaluate the vertical force F_z. Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. z : float or Quantity Vertical height. phi : float or Quantity, optional Azimuth (default: 0.0). t : float or Quantity, optional Time (default: 0.0). Returns ------- float or Quantity F_z (R,z,phi,t). Notes ----- - 2010-04-16 - Written - Bovy (NYU) """ return self._zforce_nodecorator(R, z, phi=phi, t=t) def _zforce_nodecorator(self, R, z, phi=0.0, t=0.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.0, t=0.0): """ Evaluate the second spherical radial derivative. Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. z : float or Quantity Vertical height. phi : float or Quantity, optional Azimuth (default: 0.0). t : float or Quantity, optional Time (default: 0.0). Returns ------- float or Quantity d2phi/dr2. Notes ----- - 2018-03-21 - Written - Webb (UofT) """ r = numpy.sqrt(R**2.0 + z**2.0) 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.0, t=0.0, forcepoisson=False): """ Evaluate the density rho(R,z,t). Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. z : float or Quantity Vertical height. phi : float or Quantity, optional Azimuth (default: 0.0). t : float or Quantity, optional Time (default: 0.0). forcepoisson : bool, optional If True, calculate the density through the Poisson equation, even if an explicit expression for the density exists (default: False). Returns ------- float or Quantity rho (R,z,phi,t). Notes ----- - 2010-08-08 - Written - Bovy (NYU) - 2018-03-21 - Modified - Webb (UofT) """ 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.0 + self.z2deriv(R, z, phi=phi, t=t, use_physical=False) ) / 4.0 / numpy.pi ) @potential_physical_input @physical_conversion("surfacedensity", pop=True) def surfdens(self, R, z, phi=0.0, t=0.0, forcepoisson=False): """ Evaluate the surface density Sigma(R,z,phi,t) = int_{-z}^{+z} dz' rho(R,z',phi,t). Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. z : float or Quantity Vertical height. phi : float or Quantity, optional Azimuth (default: 0.0). t : float or Quantity, optional Time (default: 0.0). forcepoisson : bool, optional If True, calculate the surface density through the Poisson equation, even if an explicit expression for the surface density exists (default: False). Returns ------- float or Quantity Sigma(R,z,phi,t). Notes ----- - 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.0, -numpy.fabs(z), numpy.fabs(z), )[0] ) / 4.0 / numpy.pi ) def _surfdens(self, R, z, phi=0.0, t=0.0): """ Evaluate the surface density for this potential. Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. z : float or Quantity Vertical height. phi : float or Quantity, optional Azimuth (default: 0.0). t : float or Quantity, optional Time (default: 0.0). Returns ------- float or Quantity The surface density. Notes ----- - 2018-08-19 - Written - Bovy (UofT). - 2021-04-19 - Adjusted for non-z-symmetric densities by 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.0, forceint=False): """ Evaluate the mass enclosed. Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. z : float or Quantity, optional Vertical height up to which to integrate (default: None). t : float or Quantity, optional Time (default: 0.0). forceint : bool, optional If True, calculate the mass through integration of the density, even if an explicit expression for the mass exists (default: False). Returns ------- float or Quantity 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.0 - integrate.quad( lambda x: x * self.zforce(x, z, t=t, use_physical=False), 0.0, R )[0] ) @physical_conversion("position", pop=True) def rhalf(self, t=0.0, INF=numpy.inf): """ Calculate the half-mass radius, the radius of the spherical shell that contains half the total mass. Parameters ---------- t : float or Quantity, optional Time (default: 0.0). INF : float or Quantity, optional Radius at which the total mass is calculated (default: numpy.inf). Returns ------- float or Quantity Half-mass radius. Notes ----- - 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.0): """ Calculate the dynamical time from tdyn^2 = 3pi/[G] Parameters ---------- R : float or Quantity Galactocentric radius. t : float or Quantity, optional Time (default: 0.0). Returns ------- float or Quantity Dynamical time. Notes ----- - 2021-03-18 - Written - Bovy (UofT) """ return 2.0 * numpy.pi * R * numpy.sqrt(R / self.mass(R, use_physical=False)) @physical_conversion("mass", pop=False) def mvir( self, H=70.0, Om=0.3, t=0.0, overdens=200.0, wrtcrit=False, forceint=False, ro=None, vo=None, use_physical=False, ): # use_physical necessary bc of pop=False, does nothing inside """ Calculate the virial mass. Parameters ---------- H : float, optional Hubble constant in km/s/Mpc (default: 70). Om : float, optional Omega matter (default: 0.3). overdens : float, optional Overdensity which defines the virial radius (default: 200). wrtcrit : bool, optional If True, the overdensity is wrt the critical density rather than the mean matter density (default: False). ro : float or Quantity, optional Distance scale in kpc (default: object-wide, which if not set is 8 kpc). vo : float or Quantity, optional Velocity scale in km/s (default: object-wide, which if not set is 220 km/s). forceint : bool, optional If True, calculate the mass through integration of the density, even if an explicit expression for the mass exists. Returns ------- float or Quantity M( 0.0: if lower: rtry /= 2.0 else: rtry *= 2.0 return rtry @potential_positional_arg @physical_conversion("position", pop=True) def rE(Pot, E, t=0.0): """ Calculate the radius of a circular orbit with energy E. Parameters ---------- Pot : Potential instance or list thereof Potential instance or list thereof. E : float or Quantity Energy. t : float, optional Time (default is 0.0). Returns ------- radius : float Radius. Notes ----- - 2022-04-06 - Written - Bovy (UofT) - An efficient way to call this function on many objects is provided as the Orbit method rE. See Also -------- Orbit.rE """ Pot = flatten(Pot) E = conversion.parse_energy(E, **conversion.get_physical(Pot)) # Find interval rstart = _rEFindStart(1.0, E, Pot, t=t) try: return optimize.brentq( _rEfunc, 10.0**-5.0, rstart, args=(E, Pot, t), maxiter=200, disp=False ) except ValueError: # Probably E small and starting rE to great rlower = _rEFindStart(10.0**-5.0, E, Pot, t=t, lower=True) return optimize.brentq(_rEfunc, rlower, rstart, args=(E, Pot, t)) def _rEfunc(rE, E, pot, t=0.0): """Function that gives vc^2/2+Pot(rc)-E""" thisvcirc = vcirc(pot, rE, t=t, use_physical=False) return thisvcirc**2.0 / 2.0 + _evaluatePotentials(pot, rE, 0.0, t=t) - E def _rEFindStart(rE, E, pot, t=0.0, lower=False): """find a starting interval for rE""" rtry = 2.0 * rE while (2.0 * lower - 1.0) * _rEfunc(rtry, E, pot, t=t) > 0.0: if lower: rtry /= 2.0 else: rtry *= 2.0 return rtry @potential_positional_arg @physical_conversion("action", pop=True) def LcE(Pot, E, t=0.0): """ Calculate the angular momentum of a circular orbit with energy E. Parameters ---------- Pot : Potential instance or list thereof Potential instance or list thereof. E : float or Quantity Energy. t : float or Quantity, optional Time (default: 0.0). Returns ------- float or Quantity Angular momentum of circular orbit with energy E Notes ----- - 2022-04-06 - Written - Bovy (UofT) """ thisrE = rE(Pot, E, t=t, use_physical=False) return thisrE * vcirc(Pot, thisrE, use_physical=False) @potential_positional_arg @physical_conversion("position", pop=True) def lindbladR(Pot, OmegaP, m=2, t=0.0, **kwargs): """ Calculate the radius of a Lindblad resonance. Parameters ---------- Pot : Potential instance or list of such instances Potential instance or list of such instances. OmegaP : float or Quantity Pattern speed. m : int or str, optional Order of the resonance (as in m(O-Op)=kappa (negative m for outer)). Use m='corotation' for corotation (default: 2). t : float or Quantity, optional Time (default: 0.0). **kwargs Additional arguments to be passed to scipy.optimize.brentq. Returns ------- float or Quantity or None Radius of Lindblad resonance, None if there is no resonance. Notes ----- - 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.0, 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.0, 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.0, 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.0): return omegac(Pot, R, t=t, use_physical=False) - OmegaP def _lindbladR_eq(R, Pot, OmegaP, m, t=0.0): return m * (omegac(Pot, R, t=t, use_physical=False) - OmegaP) - epifreq( Pot, R, t=t, use_physical=False ) @potential_positional_arg @potential_physical_input @physical_conversion("frequency", pop=True) def omegac(Pot, R, t=0.0): """ Calculate the circular angular speed velocity at R in potential Pot. Parameters ---------- Pot : Potential instance or list of such instances Potential instance or list of such instances. R : float or Quantity Galactocentric radius. t : float or Quantity, optional Time (default: 0.0). Returns ------- float or Quantity Circular angular speed. Notes ----- - 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): """ Return the accname potential name for use of this potential or list of potentials with NEMO. Parameters ---------- Pot : Potential instance or list of such instances Returns ------- str Acceleration name in the correct format to give to accname= Notes ----- - 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): """ Return the accpars potential parameters for use of this potential or list of potentials with NEMO. Parameters ---------- Pot : Potential instance or list of such instances vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- str Accpars string in the correct format to give to accpars. Notes ----- - 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.0, tgalpy=0.0, reverse=False, ro=None, vo=None ): # pragma: no cover """ Return an AMUSE representation of a galpy Potential or list of Potentials Parameters ---------- Pot : Potential instance or list of such instances Potential(s) to convert to an AMUSE representation. t : float, optional Initial time in AMUSE (can be in internal galpy units or AMUSE units), by default 0.0. tgalpy : float, optional 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, by default 0.0. reverse : bool, optional 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, by default False. ro : float, optional Length unit in kpc, by default None. vo : float, optional Velocity unit in km/s, by default None. Returns ------- AMUSE representation of Pot. Notes ----- - 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): """ Turn off automatic returning of outputs in physical units. Parameters ---------- Pot : Potential instance or list of such instances Potential(s) to turn off automatic returning of outputs in physical units. Returns ------- None Notes ----- - 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): """ Turn on automatic returning of outputs in physical units. Parameters ---------- Pot : Potential instance or list of such instances Potential(s) to turn on automatic returning of outputs in physical units. ro : float or Quantity, optional Reference distance (kpc). Default is None. vo : float or Quantity, optional Reference velocity (km/s). Default is None. Returns ------- None Notes ----- - 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): """ Flatten a possibly nested list of Potential instances into a flat list. Parameters ---------- Pot : list or Potential instance List (possibly nested) of Potential instances. Returns ------- list Flattened list of Potential instances. Notes ----- - 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): """ Check whether a potential or list thereof has a C implementation. Parameters ---------- Pot : Potential instance or list of such instances Potential instance or list of such instances to check. dxdv : bool, optional If True, check whether the potential has dxdv implementation. dens : bool, optional If True, check whether the potential has its density implemented in C. Returns ------- bool True if a C implementation exists, False otherwise. Notes ----- - 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, planarForce 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, planarForce) or isinstance(Pot, linearPotential) ): return Pot.__dict__[hasC_attr] def _dim(Pot): """ Determine the dimensionality of this potential Parameters ---------- Pot : Potential instance or list of such instances Returns ------- int Minimum of the dimensionality of all potentials if list; otherwise Pot.dim Notes ----- - 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): """ Determine whether this potential is non-axisymmetric Parameters ---------- Pot : Potential instance or list of such instances Returns ------- bool 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) Notes ----- - 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: try: nonAxi = Pot.isNonAxi except AttributeError: raise PotentialError( "'isNonAxi' attribute has not been set for this potential" ) 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.0), args[2], **kwargs) return kms_to_kpcGyr_wrapper @potential_positional_arg @potential_physical_input @physical_conversion("position", pop=True) def rtide(Pot, R, z, phi=0.0, t=0.0, M=None): """ 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. Parameters ---------- R : float or Quantity Galactocentric radius z : float or Quantity height phi : float or Quantity, optional azimuth (default: 0.0) t : float or Quantity, optional time (default: 0.0) M : float or Quantity, optional Mass of object (default: None) Returns ------- float or Quantity Tidal Radius Notes ----- - 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.0 + z**2.0) 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.0 / 3.0) @potential_positional_arg @potential_physical_input @physical_conversion("forcederivative", pop=True) def ttensor(Pot, R, z, phi=0.0, t=0.0, eigenval=False): """ Calculate the tidal tensor Tij=-d(Psi)(dxidxj) Parameters ---------- Pot : Potential instance or list of such instances Potential instance or list of such instances R : float or Quantity Galactocentric radius z : float or Quantity height phi : float or Quantity, optional azimuth (default: 0.0) t : float or Quantity, optional time (default: 0.0) eigenval : bool, optional return eigenvalues if true (default: False) Returns ------- array, shape (3,3) or (3,) Tidal tensor or eigenvalues of the tidal tensor Notes ----- - 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.0 * cosphi * sinphi / R + Rderiv * sin2phi / R + phi2deriv * sin2phi / R2 + phideriv * 2.0 * 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.0 * cosphi * sinphi / R + Rderiv * cos2phi / R + phi2deriv * cos2phi / R2 - phideriv * 2.0 * 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 @potential_positional_arg @physical_conversion("position", pop=True) def zvc(Pot, R, E, Lz, phi=0.0, t=0.0): """ 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) Parameters ---------- Pot : Potential instance or list of such instances Potential instance or list of such instances. R : float or Quantity Galactocentric radius. E : float or Quantity Energy. Lz : float or Quantity Angular momentum. phi : float or Quantity, optional Azimuth (default: 0.0). t : float or Quantity, optional Time (default: 0.0). Returns ------- float z such that Phi(R,z) + Lz/[2R^2] = E. Notes ----- - 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.0 / 2.0 / R**2.0 # Check z=0 and whether a solution exists if ( numpy.fabs(_evaluatePotentials(Pot, R, 0.0, phi=phi, t=t) + Lz2over2R2 - E) < 1e-8 ): return 0.0 elif _evaluatePotentials(Pot, R, 0.0, phi=phi, t=t) + Lz2over2R2 > E: return numpy.nan # s.t. this does not get plotted # Find starting value zstart = 1.0 zmax = 1000.0 while ( E - _evaluatePotentials(Pot, R, zstart, phi=phi, t=t) - Lz2over2R2 > 0.0 and zstart < zmax ): zstart *= 2.0 try: out = optimize.brentq( lambda z: _evaluatePotentials(Pot, R, z, phi=phi, t=t) + Lz2over2R2 - E, 0.0, zstart, ) except ValueError: raise ValueError( "No solution for the zero-velocity curve found for this combination of parameters" ) return out @potential_positional_arg @physical_conversion("position", pop=True) def zvc_range(Pot, E, Lz, phi=0.0, t=0.0): """ 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) Parameters ---------- Pot : Potential instance or list of such instances Potential instance or list of such instances. E : float or Quantity Energy. Lz : float or Quantity Angular momentum. phi : float or Quantity, optional Azimuth (default: 0.0). t : float or Quantity, optional Time (default: 0.0). Returns ------- numpy.ndarray Solutions R such that Phi(R,0) + Lz/[2R^2] = E. Notes ----- - 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.0 / 2.0 # Check whether a solution exists RLz = rl(Pot, Lz, t=t, use_physical=False) Rstart = RLz if _evaluatePotentials(Pot, Rstart, 0.0, phi=phi, t=t) + Lz2over2 / Rstart**2.0 > 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.0 < E and Rstart > Rstartmin ): Rstart /= 2.0 Rmin = optimize.brentq( lambda R: _evaluatePotentials(Pot, R, 0, phi=phi, t=t) + Lz2over2 / R**2.0 - E, Rstart, RLz, ) # Find starting value for Rmax Rstart = RLz Rstartmax = 1000.0 while ( _evaluatePotentials(Pot, Rstart, 0, phi=phi, t=t) + Lz2over2 / Rstart**2.0 < E and Rstart < Rstartmax ): Rstart *= 2.0 Rmax = optimize.brentq( lambda R: _evaluatePotentials(Pot, R, 0, phi=phi, t=t) + Lz2over2 / R**2.0 - E, RLz, Rstart, ) return numpy.array([Rmin, Rmax]) @potential_positional_arg @physical_conversion("position", pop=True) def rhalf(Pot, t=0.0, INF=numpy.inf): """ Calculate the half-mass radius, the radius of the spherical shell that contains half the total mass. Parameters ---------- Pot : Potential instance or list thereof Potential instance or list of instances. t : float or Quantity, optional Time (default: 0.0). INF : numeric, optional Radius at which the total mass is calculated (internal units, just set this to something very large) (default: numpy.inf). Returns ------- float Half-mass radius. Notes ----- - 2021-03-18 - Written - Bovy (UofT) """ Pot = flatten(Pot) tot_mass = mass(Pot, INF, t=t) # Find interval rhi = _rhalfFindStart(1.0, Pot, tot_mass, t=t) rlo = _rhalfFindStart(1.0, 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.0): return mass(pot, rh, t=t) / tot_mass - 0.5 def _rhalfFindStart(rh, pot, tot_mass, t=0.0, lower=False): """find a starting interval for rhalf""" rtry = 2.0 * rh while (2.0 * lower - 1.0) * _rhalffunc(rtry, pot, tot_mass, t=t) > 0.0: if lower: rtry /= 2.0 else: rtry *= 2.0 return rtry @potential_positional_arg @potential_physical_input @physical_conversion("time", pop=True) def tdyn(Pot, R, t=0.0): """ Calculate the dynamical time from tdyn^2 = 3pi/[G]. Parameters ---------- Pot : Potential instance or list thereof Potential instance or list of instances. R : float or Quantity Galactocentric radius. t : float or Quantity, optional Time (default: 0.0). Returns ------- float Dynamical time. Notes ----- - 2021-03-18 - Written - Bovy (UofT) """ return 2.0 * numpy.pi * R * numpy.sqrt(R / mass(Pot, R, use_physical=False)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/PowerSphericalPotential.py0000644000175100001660000002775414761352023022630 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.0, alpha=1.0, normalize=False, r1=1.0, ro=None, vo=None): """ Initialize a power-law-density potential Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass alpha : float, optional Power-law exponent r1 : float, optional Reference radius for amplitude (can be Quantity) normalize : bool or float, optional 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 : float, optional Distance scale for translation into internal units (default from configuration file) vo : float, optional Velocity scale for translation into internal units (default from configuration file) Notes ----- - 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.0: self._amp *= r1 ** (self.alpha - 3.0) * 4.0 * numpy.pi / (3.0 - 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.0, t=0.0): """ Evaluate the potential at R,z Parameters ---------- R : float Galactocentric cylindrical radius z : float Vertical height phi : float, optional Azimuth (default: 0.0) t : float, optional Time (default: 0.0) Returns ------- float Potential at (R, z) Notes ----- - Started: 2010-07-10 by Bovy (NYU) """ r2 = R**2.0 + z**2.0 if self.alpha == 2.0: return numpy.log(r2) / 2.0 elif isinstance(r2, (float, int)) and r2 == 0 and self.alpha > 2: return -numpy.inf else: out = -(r2 ** (1.0 - self.alpha / 2.0)) / (self.alpha - 2.0) if isinstance(r2, numpy.ndarray) and self.alpha > 2: out[r2 == 0] = -numpy.inf return out def _Rforce(self, R, z, phi=0.0, t=0.0): """ Evaluate the radial force for this potential. Parameters ---------- R : float Galactocentric cylindrical radius z : float Vertical height phi : float, optional Azimuth (default: 0.0) t : float, optional Time (default: 0.0) Returns ------- float The radial force. Notes ----- - 2010-07-10 - Written - Bovy (NYU) """ return -R / (R**2.0 + z**2.0) ** (self.alpha / 2.0) def _zforce(self, R, z, phi=0.0, t=0.0): """ Evaluate the vertical force for this potential. Parameters ---------- R : float Galactocentric cylindrical radius z : float Vertical height phi : float, optional Azimuth (default: 0.0) t : float, optional Time (default: 0.0) Returns ------- float The vertical force. Notes ----- - 2010-07-10 - Written - Bovy (NYU) """ return -z / (R**2.0 + z**2.0) ** (self.alpha / 2.0) def _rforce_jax(self, r): """ Evaluate the spherical radial force for this potential using JAX. Parameters ---------- r : float Galactocentric spherical radius. Returns ------- float The radial force. Notes ----- - 2021-02-14 - Written - Bovy (UofT) """ # No need for actual JAX! return -self._amp / r ** (self.alpha - 1.0) def _R2deriv(self, R, z, phi=0.0, t=0.0): """ Evaluate the second radial derivative for this potential. Parameters ---------- R : float Galactocentric cylindrical radius. z : float Vertical height. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float The second radial derivative. Notes ----- - 2011-10-09 - Written - Bovy (NYU) """ return 1.0 / (R**2.0 + z**2.0) ** (self.alpha / 2.0) - self.alpha * R**2.0 / ( R**2.0 + z**2.0 ) ** (self.alpha / 2.0 + 1.0) def _z2deriv(self, R, z, phi=0.0, t=0.0): """ Evaluate the second vertical derivative for this potential. Parameters ---------- R : float Galactocentric cylindrical radius. z : float Vertical height. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float The second vertical derivative. Notes ----- - 2012-07-26 - Written - Bovy (IAS@MPIA) """ return self._R2deriv(z, R) # Spherical potential def _Rzderiv(self, R, z, phi=0.0, t=0.0): """ Evaluate the mixed R,z derivative for this potential. Parameters ---------- R : float Galactocentric cylindrical radius. z : float Vertical height. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float The mixed R,z derivative. Notes ----- - 2013-08-28 - Written - Bovy (IAs) """ return -self.alpha * R * z * (R**2.0 + z**2.0) ** (-1.0 - self.alpha / 2.0) def _dens(self, R, z, phi=0.0, t=0.0): """ Evaluate the density for this potential. Parameters ---------- R : float Galactocentric cylindrical radius. z : float Vertical height. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float The density. Notes ----- - 2013-01-09 - Written - Bovy (IAS) """ r = numpy.sqrt(R**2.0 + z**2.0) return (3.0 - self.alpha) / 4.0 / numpy.pi / r**self.alpha def _ddensdr(self, r, t=0.0): """ Evaluate the radial density derivative for this potential. Parameters ---------- r : float Spherical radius. t : float, optional Time (default: 0.0). Returns ------- float The density derivative. Notes ----- - 2021-02-25 - Written - Bovy (UofT) """ return ( -self._amp * self.alpha * (3.0 - self.alpha) / 4.0 / numpy.pi / r ** (self.alpha + 1.0) ) def _d2densdr2(self, r, t=0.0): """ Evaluate the second radial density derivative for this potential. Parameters ---------- r : float Spherical radius. t : float, optional Time (default: 0.0). Returns ------- float The second density derivative. Notes ----- - 2021-02-25: Written by Bovy (UofT) """ return ( self._amp * (self.alpha + 1.0) * self.alpha * (3.0 - self.alpha) / 4.0 / numpy.pi / r ** (self.alpha + 2.0) ) def _ddenstwobetadr(self, r, beta=0): """ Evaluate the radial density derivative x r^(2beta) for this potential. Parameters ---------- r : float Spherical radius. beta : int, optional Power of r (default: 0). Returns ------- float The d (rho x r^{2beta} ) / d r derivative. Notes ----- - 2021-02-14: Written by Bovy (UofT) """ return ( -self._amp * (self.alpha - 2.0 * beta) * (3.0 - self.alpha) / 4.0 / numpy.pi / r ** (self.alpha + 1.0 - 2.0 * beta) ) def _surfdens(self, R, z, phi=0.0, t=0.0): """ Evaluate the surface density for this potential. Parameters ---------- R : float Galactocentric cylindrical radius. z : float Vertical height. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float The surface density. Notes ----- - 2018-08-19: Written by Bovy (UofT) """ return ( (3.0 - self.alpha) / 2.0 / numpy.pi * z * R**-self.alpha * special.hyp2f1(0.5, self.alpha / 2.0, 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.0, normalize=False, ro=None, vo=None): """ Initialize a Kepler, point-mass potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential, the mass of the point mass (default: 1); can be a Quantity with units of mass or Gxmass. normalize : bool or float, optional 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 : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Returns ------- None Notes ----- - 2010-07-10: Written by Bovy (NYU) """ PowerSphericalPotential.__init__( self, amp=amp, normalize=normalize, alpha=3.0, ro=ro, vo=vo ) def _mass(self, R, z=None, t=0.0): """ Evaluate the mass within R for this potential. Parameters ---------- R : float Galactocentric cylindrical radius. z : float, optional Vertical height (default: None). t : float, optional Time (default: 0.0). Returns ------- float The mass enclosed. Notes ----- - Written on 2014-07-02 by Bovy (IAS). """ return 1.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/PowerSphericalPotentialwCutoff.py0000644000175100001660000001711714761352023024156 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.0, alpha=1.0, rc=1.0, normalize=False, r1=1.0, ro=None, vo=None ): """ Initialize a power-law-density potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential. Can be a Quantity with units of mass density or Gxmass density. alpha : float, optional Inner power. rc : float or Quantity, optional Cut-off radius. r1 : float or Quantity, optional Reference radius for amplitude. Default is 1.0. Can be Quantity. normalize : bool or float, optional 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 : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) out = ( 2.0 * numpy.pi * self.rc ** (3.0 - self.alpha) * ( 1 / self.rc * special.gamma(1.0 - self.alpha / 2.0) * special.gammainc(1.0 - self.alpha / 2.0, (r / self.rc) ** 2.0) - special.gamma(1.5 - self.alpha / 2.0) * special.gammainc(1.5 - self.alpha / 2.0, (r / self.rc) ** 2.0) / r ) ) if isinstance(r, (float, int)): if r == 0: return 0.0 else: return out else: out[r == 0] = 0.0 return out def _Rforce(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R * R + z * z) return -self._mass(r) * R / r**3.0 def _zforce(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R * R + z * z) return -self._mass(r) * z / r**3.0 def _R2deriv(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R * R + z * z) return 4.0 * numpy.pi * r ** (-2.0 - self.alpha) * numpy.exp( -((r / self.rc) ** 2.0) ) * R**2.0 + self._mass(r) / r**5.0 * (z**2.0 - 2.0 * R**2.0) def _z2deriv(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R * R + z * z) return 4.0 * numpy.pi * r ** (-2.0 - self.alpha) * numpy.exp( -((r / self.rc) ** 2.0) ) * z**2.0 + self._mass(r) / r**5.0 * (R**2.0 - 2.0 * z**2.0) def _Rzderiv(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R * R + z * z) return ( R * z * ( 4.0 * numpy.pi * r ** (-2.0 - self.alpha) * numpy.exp(-((r / self.rc) ** 2.0)) - 3.0 * self._mass(r) / r**5.0 ) ) def _rforce_jax(self, r): 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.0 * numpy.pi * self.rc ** (3.0 - self.alpha) * jspecial.gammainc(1.5 - 0.5 * self.alpha, (r / self.rc) ** 2.0) * numpy.exp(jspecial.gammaln(1.5 - 0.5 * self.alpha)) / r**2 ) def _ddensdr(self, r, t=0.0): return ( -self._amp * r ** (-1.0 - self.alpha) * numpy.exp(-((r / self.rc) ** 2.0)) * (2.0 * r**2.0 / self.rc**2.0 + self.alpha) ) def _d2densdr2(self, r, t=0.0): return ( self._amp * r ** (-2.0 - self.alpha) * numpy.exp(-((r / self.rc) ** 2)) * ( self.alpha**2.0 + self.alpha + 4 * self.alpha * r**2.0 / self.rc**2.0 - 2.0 * r**2.0 / self.rc**2.0 + 4.0 * r**4.0 / self.rc**4.0 ) ) def _ddenstwobetadr(self, r, beta=0): """ Evaluate the radial density derivative x r^(2beta) for this potential. Parameters ---------- r : float Spherical radius. beta : int, optional Power of r in the density derivative. Default is 0. Returns ------- float The derivative of rho x r^{2beta} / d r. Notes ----- - 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.0)) / r ** (self.alpha - 2.0 * beta) * ((self.alpha - 2.0 * beta) / r + 2.0 * r / self.rc**2.0) ) def _dens(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return 1.0 / r**self.alpha * numpy.exp(-((r / self.rc) ** 2.0)) def _mass(self, R, z=None, t=0.0): if z is not None: raise AttributeError # use general implementation R = numpy.array(R) out = numpy.ones_like(R) out[~numpy.isinf(R)] = ( 2.0 * numpy.pi * R[~numpy.isinf(R)] ** (3.0 - self.alpha) / (1.5 - self.alpha / 2.0) * special.hyp1f1( 1.5 - self.alpha / 2.0, 2.5 - self.alpha / 2.0, -((R[~numpy.isinf(R)] / self.rc) ** 2.0), ) ) out[numpy.isinf(R)] = ( 2.0 * numpy.pi * self.rc ** (3.0 - self.alpha) * special.gamma(1.5 - self.alpha / 2.0) ) return out @kms_to_kpcGyrDecorator def _nemo_accpars(self, vo, ro): ampl = self._amp * vo**2.0 * ro ** (self.alpha - 2.0) return f"0,{ampl},{self.alpha},{self.rc * ro}" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/PowerTriaxialPotential.py0000644000175100001660000000713614761352023022463 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.0, alpha=1.0, r1=1.0, b=1.0, c=1.0, zvec=None, pa=None, glorder=50, normalize=False, ro=None, vo=None, ): """ Initialize a triaxial power-law potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass. alpha : float Power-law exponent. r1 : float or Quantity, optional Reference radius for amplitude. b : float Y-to-x axis ratio of the density. c : float Z-to-x axis ratio of the density. zvec : numpy.ndarray, optional If set, a unit vector that corresponds to the z axis. pa : float or Quantity, optional If set, the position angle of the x axis (rad or Quantity). glorder : int, optional If set, compute the relevant force and potential integrals with Gaussian quadrature of this order. ro : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0: self._amp *= r1 ** (self.alpha - 3.0) * 4.0 * numpy.pi / (3.0 - self.alpha) # Multiply in constants self._amp *= (3.0 - self.alpha) / 4.0 / 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.0 / (2.0 - self.alpha) * m ** (2.0 - 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.0 + self.alpha) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/PseudoIsothermalPotential.py0000644000175100001660000000761514761352023023162 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.0, a=1.0, normalize=False, ro=None, vo=None): """ Initialize a pseudo-isothermal potential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential. a : float or Quantity, optional Core radius. normalize : bool, int, or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 self._a3 = a**3.0 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.0, t=0.0): r2 = R**2.0 + z**2.0 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.0 / self._a else: return out else: out[r == 0] = 1.0 / self._a return out def _Rforce(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 r = numpy.sqrt(r2) return -(1.0 / r - self._a / r2 * numpy.arctan(r / self._a)) / self._a * R / r def _zforce(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 r = numpy.sqrt(r2) return -(1.0 / r - self._a / r2 * numpy.arctan(r / self._a)) / self._a * z / r def _dens(self, R, z, phi=0.0, t=0.0): return 1.0 / (1.0 + (R**2.0 + z**2.0) / self._a2) / 4.0 / numpy.pi / self._a3 def _R2deriv(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 r = numpy.sqrt(r2) return ( 1.0 / r2 * (1.0 - R**2.0 / r2 * (3.0 * self._a2 + 2.0 * r2) / (self._a2 + r2)) + self._a / r2 / r * (3.0 * R**2.0 / r2 - 1.0) * numpy.arctan(r / self._a) ) / self._a def _z2deriv(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 r = numpy.sqrt(r2) return ( 1.0 / r2 * (1.0 - z**2.0 / r2 * (3.0 * self._a2 + 2.0 * r2) / (self._a2 + r2)) + self._a / r2 / r * (3.0 * z**2.0 / r2 - 1.0) * numpy.arctan(r / self._a) ) / self._a def _Rzderiv(self, R, z, phi=0.0, t=0.0): r2 = R**2.0 + z**2.0 r = numpy.sqrt(r2) return ( ( 3.0 * self._a / r2 / r2 * numpy.arctan(r / self._a) - 1.0 / r2 / r * ((3.0 * self._a2 + 2.0 * r2) / (r2 + self._a2)) ) * R * z / r / self._a ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/RazorThinExponentialDiskPotential.py0000644000175100001660000002074414761352023024633 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.0, hr=1.0 / 3.0, normalize=False, ro=None, vo=None, new=True, glorder=100, ): """ Class that implements a razor-thin exponential disk potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of surface-mass or Gxsurface-mass. hr : float or Quantity, optional Disk scale-length. normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). new : bool, optional If True, use a new implementation of the potential that is more accurate for small scale lengths (default: True). glorder : int, optional Gaussian quadrature order to use for numerical integration (default: 100). Notes ----- - 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.0 / 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.0, t=0.0): if self._new: if numpy.fabs(z) < 10.0**-6.0: y = 0.5 * self._alpha * R return ( -numpy.pi * R * (special.i0(y) * special.k1(y) - special.i1(y) * special.k0(y)) ) kalphamax = 10.0 ks = kalphamax * 0.5 * (self._glx + 1.0) weights = kalphamax * self._glw sqrtp = numpy.sqrt(z**2.0 + (ks + R) ** 2.0) sqrtm = numpy.sqrt(z**2.0 + (ks - R) ** 2.0) evalInt = ( numpy.arcsin(2.0 * ks / (sqrtp + sqrtm)) * ks * special.k0(self._alpha * ks) ) return -2.0 * self._alpha * numpy.sum(weights * evalInt) raise NotImplementedError( "Not new=True not implemented for RazorThinExponentialDiskPotential" ) def _Rforce(self, R, z, phi=0.0, t=0.0): if self._new: # if R > 6.: return self._kp(R,z) if numpy.fabs(z) < 10.0**-6.0: y = 0.5 * self._alpha * R return ( -2.0 * numpy.pi * y * (special.i0(y) * special.k0(y) - special.i1(y) * special.k1(y)) ) kalphamax1 = R ks1 = kalphamax1 * 0.5 * (self._glx + 1.0) weights1 = kalphamax1 * self._glw sqrtp = numpy.sqrt(z**2.0 + (ks1 + R) ** 2.0) sqrtm = numpy.sqrt(z**2.0 + (ks1 - R) ** 2.0) evalInt1 = ( ks1**2.0 * special.k0(ks1 * self._alpha) * ((ks1 + R) / sqrtp - (ks1 - R) / sqrtm) / numpy.sqrt(R**2.0 + z**2.0 - ks1**2.0 + sqrtp * sqrtm) / (sqrtp + sqrtm) ) if R < 10.0: kalphamax2 = 10.0 ks2 = (kalphamax2 - kalphamax1) * 0.5 * (self._glx + 1.0) + kalphamax1 weights2 = (kalphamax2 - kalphamax1) * self._glw sqrtp = numpy.sqrt(z**2.0 + (ks2 + R) ** 2.0) sqrtm = numpy.sqrt(z**2.0 + (ks2 - R) ** 2.0) evalInt2 = ( ks2**2.0 * special.k0(ks2 * self._alpha) * ((ks2 + R) / sqrtp - (ks2 - R) / sqrtm) / numpy.sqrt(R**2.0 + z**2.0 - ks2**2.0 + sqrtp * sqrtm) / (sqrtp + sqrtm) ) return ( -2.0 * numpy.sqrt(2.0) * self._alpha * numpy.sum(weights1 * evalInt1 + weights2 * evalInt2) ) else: return ( -2.0 * numpy.sqrt(2.0) * self._alpha * numpy.sum(weights1 * evalInt1) ) raise NotImplementedError( "Not new=True not implemented for RazorThinExponentialDiskPotential" ) def _zforce(self, R, z, phi=0.0, t=0.0): if self._new: # if R > 6.: return self._kp(R,z) if numpy.fabs(z) < 10.0**-6.0: return 0.0 kalphamax1 = R ks1 = kalphamax1 * 0.5 * (self._glx + 1.0) weights1 = kalphamax1 * self._glw sqrtp = numpy.sqrt(z**2.0 + (ks1 + R) ** 2.0) sqrtm = numpy.sqrt(z**2.0 + (ks1 - R) ** 2.0) evalInt1 = ( ks1**2.0 * special.k0(ks1 * self._alpha) * (1.0 / sqrtp + 1.0 / sqrtm) / numpy.sqrt(R**2.0 + z**2.0 - ks1**2.0 + sqrtp * sqrtm) / (sqrtp + sqrtm) ) if R < 10.0: kalphamax2 = 10.0 ks2 = (kalphamax2 - kalphamax1) * 0.5 * (self._glx + 1.0) + kalphamax1 weights2 = (kalphamax2 - kalphamax1) * self._glw sqrtp = numpy.sqrt(z**2.0 + (ks2 + R) ** 2.0) sqrtm = numpy.sqrt(z**2.0 + (ks2 - R) ** 2.0) evalInt2 = ( ks2**2.0 * special.k0(ks2 * self._alpha) * (1.0 / sqrtp + 1.0 / sqrtm) / numpy.sqrt(R**2.0 + z**2.0 - ks2**2.0 + sqrtp * sqrtm) / (sqrtp + sqrtm) ) return ( -z * 2.0 * numpy.sqrt(2.0) * self._alpha * numpy.sum(weights1 * evalInt1 + weights2 * evalInt2) ) else: return ( -z * 2.0 * numpy.sqrt(2.0) * self._alpha * numpy.sum(weights1 * evalInt1) ) raise NotImplementedError( "Not new=True not implemented for RazorThinExponentialDiskPotential" ) def _R2deriv(self, R, z, phi=0.0, t=0.0): if self._new: if numpy.fabs(z) < 10.0**-6.0: 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.0 * self._alpha**2.0 * R * ( special.i1(y) * (3.0 * special.k0(y) + special.kn(2, y)) - special.k1(y) * (3.0 * special.i0(y) + special.iv(2, y)) ) raise AttributeError( "'R2deriv' for RazorThinExponentialDisk not implemented for z =/= 0" ) def _z2deriv(self, R, z, phi=0.0, t=0.0): # pragma: no cover return numpy.infty def _surfdens(self, R, z, phi=0.0, t=0.0): return numpy.exp(-self._alpha * R) def _mass(self, R, z=None, t=0.0): return ( 2.0 * numpy.pi * (1.0 - numpy.exp(-self._alpha * R) * (1.0 + self._alpha * R)) / self._alpha**2.0 ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/RingPotential.py0000644000175100001660000001240314761352023020561 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.0, a=0.75, normalize=False, ro=None, vo=None): """ Class that implements a circular ring potential. Parameters ---------- amp : float or Quantity, optional Mass of the ring (default: 1); can be a Quantity with units of mass or Gxmass. a : float or Quantity, optional Radius of the ring (default: 0.75). normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * numpy.pi * self.a if normalize or ( isinstance(normalize, (int, float)) and not isinstance(normalize, bool) ): if self.a > 1.0: 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.0, t=0.0): # Stable as r -> infty m = 4.0 * self.a / ((numpy.sqrt(R) + self.a / numpy.sqrt(R)) ** 2 + z**2 / R) return -4.0 * self.a / numpy.sqrt((R + self.a) ** 2 + z**2) * special.ellipk(m) def _Rforce(self, R, z, phi=0.0, t=0.0): m = 4.0 * R * self.a / ((R + self.a) ** 2 + z**2) return ( -2.0 * self.a / R / numpy.sqrt((R + self.a) ** 2 + z**2) * ( m * (R**2 - self.a2 - z**2) / 4.0 / (1.0 - m) / self.a / R * special.ellipe(m) + special.ellipk(m) ) ) def _zforce(self, R, z, phi=0.0, t=0.0): m = 4.0 * R * self.a / ((R + self.a) ** 2 + z**2) return ( -4.0 * z * self.a / (1.0 - m) * ((R + self.a) ** 2 + z**2) ** -1.5 * special.ellipe(m) ) def _R2deriv(self, R, z, phi=0.0, t=0.0): Raz2 = (R + self.a) ** 2 + z**2 Raz = numpy.sqrt(Raz2) m = 4.0 * R * self.a / Raz2 R2ma2mz2o4aR1m = (R**2 - self.a2 - z**2) / 4.0 / self.a / R / (1.0 - m) return (2 * R**2 + self.a2 + 3 * R * self.a + z**2) / R / Raz2 * self._Rforce( R, z ) + 2.0 * self.a / R / Raz * ( m * (R**2 + self.a2 + z**2) / 4.0 / (1.0 - m) / self.a / R**2 * special.ellipe(m) + ( R2ma2mz2o4aR1m / (1.0 - m) * special.ellipe(m) + 0.5 * R2ma2mz2o4aR1m * (special.ellipe(m) - special.ellipk(m)) + 0.5 * (special.ellipe(m) / (1.0 - m) - special.ellipk(m)) / m ) * 4 * self.a * (self.a2 + z**2 - R**2) / Raz2**2 ) def _z2deriv(self, R, z, phi=0.0, t=0.0): Raz2 = (R + self.a) ** 2 + z**2 m = 4.0 * R * self.a / Raz2 # Explicitly swapped in zforce here, so the z/z can be cancelled # and z=0 is handled properly return ( -4.0 * ( 3.0 * z**2 / Raz2 - 1.0 + 4.0 * ((1.0 + m) / (1.0 - m) - special.ellipk(m) / special.ellipe(m)) * self.a * R * z**2 / Raz2**2 / m ) * self.a / (1.0 - m) * ((R + self.a) ** 2 + z**2) ** -1.5 * special.ellipe(m) ) def _Rzderiv(self, R, z, phi=0.0, t=0.0): Raz2 = (R + self.a) ** 2 + z**2 m = 4.0 * R * self.a / Raz2 return ( 3.0 * (R + self.a) / Raz2 - 2.0 * ((1.0 + m) / (1.0 - 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=1741018131.0 galpy-1.10.2/galpy/potential/RotateAndTiltWrapperPotential.py0000644000175100001660000003227714761352023023754 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, that is, the relation between the observed coordinates :math:`(x',y',z')` and the intrinsic coordinates :math:`(x,y,z)` is given by .. math:: \\begin{pmatrix} x \\\\ y \\\\ z \\end{pmatrix} = \\begin{pmatrix} \\phantom{-}\\cos(\\text{PA}_\\text{gal}) & \\sin(\\text{PA}_\\text{gal}) & 0 \\\\ -\\sin(\\text{PA}_\\text{gal}) & \\cos(\\text{PA}_\\text{gal}) & 0 \\\\ 0 & 0 & 1 \\end{pmatrix} \\begin{pmatrix} 1 & 0 & 0 \\\\ 0 & \\phantom{-}\\cos i & \\sin i \\\\ 0 & -\\sin i & \\cos i \\end{pmatrix} \\begin{pmatrix} \\sin(\\text{PA}_\\text{sky}) & -\\cos(\\text{PA}_\\text{sky}) & 0 \\\\ \\cos(\\text{PA}_\\text{sky}) & \\phantom{-}\\sin(\\text{PA}_\\text{sky}) & 0 \\\\ 0 & 0 & 1 \\end{pmatrix} \\begin{pmatrix} x' \\\\ y' \\\\ z' \\end{pmatrix}\\,, 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.0, inclination=None, galaxy_pa=None, sky_pa=None, zvec=None, offset=None, pot=None, ro=None, vo=None, ): """ A potential that rotates and tilts another potential. Parameters ---------- amp : float, optional Overall amplitude to apply to the potential. Default is 1.0. inclination : float or Quantity, optional Usual inclination angle (with the line-of-sight being the z axis). galaxy_pa : float or Quantity, optional Rotation angle of the original potential around the original z axis. sky_pa : float or Quantity, optional Rotation angle around the inclined z axis (usual sky position angle measured from North). zvec : numpy.ndarray, optional 3D vector specifying the direction of the rotated z axis. offset : numpy.ndarray or Quantity, optional Static offset in Cartesian coordinates. pot : Potential instance or list thereof The Potential instance or list thereof to rotate and tilt. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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( numpy.array(offset) if isinstance(offset, list) else 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.0 zvec_rot = numpy.dot( numpy.array( [ [numpy.sin(sky_pa), numpy.cos(sky_pa), 0.0], [-numpy.cos(sky_pa), numpy.sin(sky_pa), 0.0], [0.0, 0.0, 1], ] ), numpy.array( [ [1.0, 0.0, 0.0], [0.0, -numpy.cos(inclination), -numpy.sin(inclination)], [0.0, -numpy.sin(inclination), numpy.cos(inclination)], ] ), ) zvec = numpy.dot(zvec_rot, numpy.array([0.0, 0.0, 1.0])) int_rot = _rotate_to_arbitrary_vector( numpy.array([[0.0, 0.0, 1.0]]), zvec, inv=False )[0] pa = numpy.dot(int_rot, numpy.dot(zvec_rot, [1.0, 0.0, 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.0], [-numpy.sin(pa), numpy.cos(pa), 0.0], [0.0, 0.0, 1.0], ] ) 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.0)) zvec_rot = _rotate_to_arbitrary_vector( numpy.array([[0.0, 0.0, 1.0]]), 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.0, t=0.0): x, y, z = coords.cyl_to_rect(R, phi, z) if not numpy.isinf(R) else (R, 0.0, z) if self._norot: xyzp = numpy.array([x, y, z]) else: xyzp = numpy.dot(self._rot, numpy.array([x, y, z])) if self._offset is not None: 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.0, t=0.0): 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.0, t=0.0): 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.0, t=0.0): return self._force_xyz(R, z, phi=phi, t=t)[2] def _force_xyz(self, R, z, phi=0.0, t=0.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 is not None: 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.0, t=0.0): phi2 = self._2ndderiv_xyz(R, z, phi=phi, t=t) return ( numpy.cos(phi) ** 2.0 * phi2[0, 0] + numpy.sin(phi) ** 2.0 * phi2[1, 1] + 2.0 * numpy.cos(phi) * numpy.sin(phi) * phi2[0, 1] ) @check_potential_inputs_not_arrays def _Rzderiv(self, R, z, phi=0.0, t=0.0): 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.0, t=0.0): return self._2ndderiv_xyz(R, z, phi=phi, t=t)[2, 2] @check_potential_inputs_not_arrays def _phi2deriv(self, R, z, phi=0.0, t=0.0): Fxyz = self._force_xyz(R, z, phi=phi, t=t) phi2 = self._2ndderiv_xyz(R, z, phi=phi, t=t) return R**2.0 * ( numpy.sin(phi) ** 2.0 * phi2[0, 0] + numpy.cos(phi) ** 2.0 * phi2[1, 1] - 2.0 * 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.0, t=0.0): 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.0 * 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.0, t=0.0): 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.0, t=0.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 is not None: 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.0, sp**2.0, cp * sp Rp2 = Rp * Rp x2derivp = ( R2derivp * cp2 - 2.0 * Rphiderivp * cpsp / Rp + phi2derivp * sp2 / Rp2 - Rforcep * sp2 / Rp - 2.0 * phitorquep * cpsp / Rp2 ) y2derivp = ( R2derivp * sp2 + 2.0 * Rphiderivp * cpsp / Rp + phi2derivp * cp2 / Rp2 - Rforcep * cp2 / Rp + 2.0 * 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.0, t=0.0): x, y, z = coords.cyl_to_rect(R, phi, z) if not numpy.isinf(R) else (R, 0.0, z) if self._norot: xyzp = numpy.array([x, y, z]) else: xyzp = numpy.dot(self._rot, numpy.array([x, y, z])) if self._offset is not None: 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=1741018131.0 galpy-1.10.2/galpy/potential/SCFPotential.py0000644000175100001660000012373614761352023020311 0ustar00runnerdockerimport hashlib import numpy import scipy from numpy.polynomial.legendre import leggauss from packaging.version import parse as parse_version from scipy import integrate from scipy.special import gamma, gammaln _SCIPY_VERSION = parse_version(scipy.__version__) if _SCIPY_VERSION < parse_version("1.15"): # pragma: no cover from scipy.special import lpmn else: from scipy.special import assoc_legendre_p_all 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.0, Acos=numpy.array([[[1]]]), Asin=None, a=1.0, normalize=False, ro=None, vo=None, ): """ Initialize a SCF Potential from a set of expansion coefficients (use SCFPotential.from_density to directly initialize from a density) Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass. Acos : numpy.ndarray, optional The real part of the expansion coefficient (NxLxL matrix, or optionally NxLx1 if Asin=None). Asin : numpy.ndarray, optional The imaginary part of the expansion coefficient (NxLxL matrix or None). a : float or Quantity, optional Scale length. normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 2016-05-13 - Written - Aladdin Seaifan (UofT) """ NumericalPotentialDerivativesMixin.__init__( self, {} ) # just use default dR etc. Potential.__init__(self, amp=amp / 2.0, 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.0, symmetry=None, radial_order=None, costheta_order=None, phi_order=None, ro=None, vo=None, ): """ Initialize an SCF Potential from a given density. Parameters ---------- dens : function 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 : int Number of radial basis functions. L : int, optional Number of costheta basis functions; for non-axisymmetric profiles also sets the number of azimuthal (phi) basis functions to M = 2L+1). a : float or Quantity, optional Expansion scale length. symmetry : {'spherical','axisymmetry',None}, optional Symmetry of the profile to assume. None is the general, non-axisymmetric case. radial_order : int, optional Number of sample points for the radial integral. If None, radial_order=max(20, N + 3/2L + 1). costheta_order : int, optional Number of sample points of the costheta integral. If None, If costheta_order=max(20, L + 1). phi_order : int, optional Number of sample points of the phi integral. If None, If costheta_order=max(20, L + 1). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Returns ------- SCFPotential object Notes ----- - Written - Jo Bovy (UofT) - 2022-06-20 """ # 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): """ Evaluate the square root of equation (3.15) with the (2 - del_m,0) term outside the square root. Parameters ---------- L : int Evaluate Nroot for 0 <= l <= L. M : int Evaluate Nroot for 0 <= m <= M. Returns ------- numpy.ndarray The square root of equation (3.15) with the (2 - del_m,0) outside. Notes ----- - Written on 2016-05-16 by Aladdin Seaifan (UofT). """ 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.0) / (4.0 * numpy.pi) * numpy.e**nLn) ** 0.5 * 2 NN[:, 0] /= 2.0 NN = numpy.tril(NN) return NN def _calculateXi(self, r): """ Calculate xi given r. Parameters ---------- r : float Evaluate at radius r. Returns ------- xi : float The calculated xi. Notes ----- - 2016-05-18 - Written - Aladdin Seaifan (UofT) """ a = self._a if r == 0: return -1 else: return (1.0 - a / r) / (1.0 + a / r) def _rhoTilde(self, r, N, L): """ Evaluate rho_tilde as defined in equation 3.9 and 2.24 for 0 <= n < N and 0 <= l < L Parameters ---------- r : float Evaluate at radius r N : int size of the N dimension L : int size of the L dimension Returns ------- numpy.ndarray The value of rho tilde Notes ----- - Written on 2016-05-17 by 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.0) * (2 * l + 1) rho[:, :] = ( K * ((a * r) ** l) / ((r / a) * (a + r) ** (2 * l + 3.0)) * CC[:, :] * (numpy.pi) ** -0.5 ) return rho def _phiTilde(self, r, N, L): """ Evaluate phi_tilde as defined in equation 3.10 and 2.25 for 0 <= n < N and 0 <= l < L Parameters ---------- r : float Evaluate at radius r N : int size of the N dimension L : int size of the L dimension Returns ------- numpy.ndarray phi tilde Notes ----- - Written on 2016-05-17 by 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.0 / a * CC[:, :] * (4 * numpy.pi) ** 0.5 else: phi[:, :] = ( -(a**l) * r ** (-l - 1.0) / ((1.0 + a / r) ** (2 * l + 1.0)) * CC[:, :] * (4 * numpy.pi) ** 0.5 ) return phi def _compute(self, funcTilde, R, z, phi): """ Evaluate the NxLxM density or potential Parameters ---------- funcTilde : function Must be _rhoTilde or _phiTilde R : float Cylindrical Galactocentric radius z : float vertical height phi : float azimuth Returns ------- numpy.ndarray An NxLxM density or potential at (R,z, phi) Notes ----- - Written on 2016-05-18 by Aladdin Seaifan (UofT) """ Acos, Asin = self._Acos, self._Asin N, L, M = Acos.shape r, theta, phi = coords.cyl_to_spher(R, z, phi) ## Get the Legendre polynomials if _SCIPY_VERSION < parse_version("1.15"): # pragma: no cover PP = lpmn(M - 1, L - 1, numpy.cos(theta))[0].T else: PP = numpy.swapaxes( assoc_legendre_p_all(L - 1, M - 1, numpy.cos(theta), branch_cut=2)[ 0, :, :M ], 0, 1, ).T 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): """ Evaluate the density or potential for a given array of coordinates. Parameters ---------- funcTilde : function Must be _rhoTilde or _phiTilde. R : numpy.ndarray Cylindrical Galactocentric radius. z : numpy.ndarray Vertical height. phi : numpy.ndarray Azimuth. Returns ------- numpy.ndarray Density or potential evaluated at (R,z, phi). Notes ----- - 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.0, t=0.0): if not self.isNonAxi and phi is None: phi = 0.0 return self._computeArray(self._rhoTilde, R, z, phi) def _mass(self, R, z=None, t=0.0): 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.0 * numpy.sum(self._Acos[:, 0, 0] * self._dphiTilde(R, N, 1)[:, 0]) def _evaluate(self, R, z, phi=0.0, t=0.0): if not self.isNonAxi and phi is None: phi = 0.0 return self._computeArray(self._phiTilde, R, z, phi) def _dphiTilde(self, r, N, L): 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) ** 0.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.0 ) def _computeforce(self, R, z, phi=0, t=0): """Computes dPhi/dr, dPhi/dtheta, dPhi/dphi""" 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: ## Get the Legendre polynomials if _SCIPY_VERSION < parse_version("1.15"): # pragma: no cover PP, dPP = lpmn(M - 1, L - 1, numpy.cos(theta)) else: PP, dPP = assoc_legendre_p_all( L - 1, M - 1, numpy.cos(theta), branch_cut=2, diff_n=1 ) PP = numpy.swapaxes(PP[:, :M], 0, 1) dPP = numpy.swapaxes(dPP[:, :M], 0, 1) 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): """ Evaluate the forces in the x direction for a given array of coordinates. Parameters ---------- dr_dx : numpy.ndarray The derivative of r with respect to the chosen variable x. dtheta_dx : numpy.ndarray The derivative of theta with respect to the chosen variable x. dphi_dx : numpy.ndarray The derivative of phi with respect to the chosen variable x. R : numpy.ndarray Cylindrical Galactocentric radius. z : numpy.ndarray Vertical height. phi : numpy.ndarray Azimuth. Returns ------- numpy.ndarray The forces in the x direction. Notes ----- - 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): if not self.isNonAxi and phi is None: phi = 0.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.0, t=0.0): if not self.isNonAxi and phi is None: phi = 0.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): if not self.isNonAxi and phi is None: phi = 0.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.0 + xi), (1.0 - xi)) def _RToxi(r, a=1): out = numpy.divide((r / a - 1.0), (r / a + 1.0), where=True ^ numpy.isinf(r)) if numpy.any(numpy.isinf(r)): if hasattr(r, "__len__"): out[numpy.isinf(r)] = 1.0 else: return 1.0 return out def _C(xi, N, L, alpha=lambda x: 2 * x + 3.0 / 2, singleL=False): """ Evaluate the Gegenbauer polynomial for 0 <= l < L and 0<= n < N Parameters ---------- xi : float Radial transformed variable N : int Size of the N dimension L : int Size of the L dimension alpha : function, optional A lambda function of l. Default alpha = 2l + 3/2 singleL : bool, optional If True only compute the L-th polynomial (default: False) Returns ------- numpy.ndarray An LxN Gegenbauer Polynomial Notes ----- - 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.0 continue elif n == 1: CC[n, l] = 2.0 * 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.0) 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.0 / 2) CC = numpy.roll(CC, 1, axis=0)[:-1, :] CC[0, :] = 0 CC *= 2 * (2 * l + 3.0 / 2) return CC def scf_compute_coeffs_spherical_nbody(pos, N, mass=1.0, a=1.0): """ Numerically compute the expansion coefficients for a spherical expansion for a given $N$-body set of points Parameters ---------- pos : numpy.ndarray Positions of particles in rectangular coordinates with shape [3,n] N : int Size of the Nth dimension of the expansion coefficients mass : float or numpy.ndarray, optional Mass of particles (scalar or array with size n), by default 1.0 a : float, optional Parameter used to scale the radius, by default 1.0 Returns ------- tuple Expansion coefficients for density dens that can be given to SCFPotential.__init__ Notes ----- - 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.0 + r / a), _C(_RToxi(r, a=a), N, 1)[:, 0]) n = numpy.arange(0, N) K = 4 * (n + 3.0 / 2) / ((n + 2) * (n + 1) * (1 + n * (n + 3.0) / 2.0)) Acos[n, 0, 0] = 2 * K * RhoSum return Acos, Asin def _scf_compute_determine_dens_kwargs(dens, param): try: param[0] = 1.0 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.0, radial_order=None): """ Numerically compute the expansion coefficients for a given spherical density Parameters ---------- dens : function A density function that takes a parameter R N : int Size of expansion coefficients a : float, optional Parameter used to scale the radius (default is 1.0) radial_order : int, optional Number of sample points of the radial integral. If None, radial_order=max(20, N + 1) (default is None) Returns ------- tuple (Acos,Asin) - Expansion coefficients for density dens that can be given to SCFPotential.__init__ Notes ----- - 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.0 * dens(*param, **dens_kw) * (1 + xi) ** 2.0 * (1 - xi) ** -3.0 * _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.0, 1.0]], Ksample=Ksample) n = numpy.arange(0, N) K = 16 * numpy.pi * (n + 3.0 / 2) / ((n + 2) * (n + 1) * (1 + n * (n + 3.0) / 2.0)) Acos[n, 0, 0] = 2 * K * integrated return Acos, Asin def scf_compute_coeffs_axi_nbody(pos, N, L, mass=1.0, a=1.0): """ Numerically compute the expansion coefficients for a given $N$-body set of points assuming that the density is axisymmetric Parameters ---------- pos : numpy.ndarray Positions of particles in rectangular coordinates with shape [3,n] N : int Size of the Nth dimension of the expansion coefficients L : int Size of the Lth dimension of the expansion coefficients mass : float or numpy.ndarray, optional Mass of particles (scalar or array with size n), by default 1.0 a : float, optional Parameter used to scale the radius, by default 1.0 Returns ------- tuple Expansion coefficients for density dens that can be given to SCFPotential.__init__ Notes ----- - 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.0 * l + 3.0) + (l + 1) * (2.0 * l + 1.0) Inl = ( -Knl * 2.0 * numpy.pi / 2.0 ** (8.0 * l + 6.0) * gamma(n + 4.0 * l + 3.0) / gamma(n + 1) / (n + 2.0 * l + 1.5) / gamma(2.0 * l + 1.5) ** 2 / numpy.sqrt(2.0 * l + 1) ) # Set up Assoc. Legendre recursion Plm = Pll Plmm1 = 0.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.0 + r / a) ** (2.0 * 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.0) * costheta * Plm - ll * Plmm1) / (ll + 1) Plmm1 = tmp return Acos, Asin def scf_compute_coeffs_axi(dens, N, L, a=1.0, radial_order=None, costheta_order=None): """ Numerically compute the expansion coefficients for a given axi-symmetric density Parameters ---------- dens : function A density function that takes parameters R and z N : int Size of the Nth dimension of the expansion coefficients L : int Size of the Lth dimension of the expansion coefficients a : float, optional Parameter used to shift the basis functions (default is 1.0) radial_order : int, optional Number of sample points of the radial integral. If None, radial_order=max(20, N + 3/2L + 1) (default is None) costheta_order : int, optional Number of sample points of the costheta integral. If None, If costheta_order=max(20, L + 1) (default is None) Returns ------- tuple (Acos,Asin) - Expansion coefficients for density dens that can be given to SCFPotential.__init__ Notes ----- - 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.0) z = r * costheta if _SCIPY_VERSION < parse_version("1.15"): # pragma: no cover PP = lpmn(0, L - 1, costheta)[0].T[numpy.newaxis, :, 0] else: PP = assoc_legendre_p_all(L - 1, 0, costheta, branch_cut=2)[0].T dV = (1.0 + xi) ** 2.0 * numpy.power(1.0 - xi, -4.0) phi_nl = ( a**3 * (1.0 + xi) ** l * (1.0 - xi) ** (l + 1.0) * _C(xi, N, L)[:, :] * PP ) 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 = 0.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.0 / 2) - 2 * gammaln(2 * l + 3.0 / 2) ) I = -K * (4 * numpy.pi) * numpy.e ** (lnI) constants = -(2.0 ** (-2 * l)) * (2 * l + 1.0) ** 0.5 Acos[:, :, 0] = 2 * I**-1 * integrated * constants return Acos, Asin def scf_compute_coeffs_nbody(pos, N, L, mass=1.0, a=1.0): """ Numerically compute the expansion coefficients for a given $N$-body set of points Parameters ---------- pos : numpy.ndarray Positions of particles in rectangular coordinates with shape [3,n] N : int Size of the Nth dimension of the expansion coefficients L : int Size of the Lth and Mth dimension of the expansion coefficients mass : float or numpy.ndarray, optional Mass of particles (scalar or array with size n), by default 1.0 a : float, optional Parameter used to scale the radius, by default 1.0 Returns ------- tuple Expansion coefficients for density dens that can be given to SCFPotential.__init__ Notes ----- - 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.0 - costheta**2.0) 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.0 * l + 3.0) + (l + 1) * (2.0 * l + 1.0) Inl = ( -Knl * 2.0 * numpy.pi / 2.0 ** (8.0 * l + 6.0) * gamma(n + 4.0 * l + 3.0) / gamma(n + 1) / (n + 2.0 * l + 1.5) / gamma(2.0 * 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.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.0 + r / a) ** (2.0 * ll + 1) * Cn[:, 0] * Plm # Acos Sum = numpy.sqrt( (2.0 * 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.0 * 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.0) * costheta * Plm - (ll + mm) * Plmm1) / ( ll - mm + 1 ) Plmm1 = tmp # Recurse Assoc. Legendre Pll *= -(2 * mm + 1.0) * sintheta return Acos, Asin def scf_compute_coeffs( dens, N, L, a=1.0, radial_order=None, costheta_order=None, phi_order=None ): """ Numerically compute the expansion coefficients for a given triaxial density Parameters ---------- dens : function A density function that takes parameters R, z and phi N : int Size of the Nth dimension of the expansion coefficients L : int Size of the Lth and Mth dimension of the expansion coefficients a : float, optional Parameter used to shift the basis functions (default is 1.0) radial_order : int, optional Number of sample points of the radial integral. If None, radial_order=max(20, N + 3/2L + 1) (default is None) costheta_order : int, optional Number of sample points of the costheta integral. If None, If costheta_order=max(20, L + 1) (default is None) phi_order : int, optional Number of sample points of the phi integral. If None, If costheta_order=max(20, L + 1) (default is None) Returns ------- tuple (Acos,Asin) - Expansion coefficients for density dens that can be given to SCFPotential.__init__ Notes ----- - 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.0) z = r * costheta if _SCIPY_VERSION < parse_version("1.15"): # pragma: no cover PP = lpmn(L - 1, L - 1, costheta)[0].T[numpy.newaxis, :, :] else: PP = numpy.swapaxes( assoc_legendre_p_all(L - 1, L - 1, costheta, branch_cut=2)[0][:, :L], 0, 1, ).T[numpy.newaxis, :, :] dV = (1.0 + xi) ** 2.0 * numpy.power(1.0 - xi, -4.0) phi_nl = ( -(a**3) * (1.0 + xi) ** l * (1.0 - xi) ** (l + 1.0) * _C(xi, N, L)[:, :, numpy.newaxis] * PP ) 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.0, 1.0], [-1.0, 1.0], [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 = 0.5 * n * (n + 4 * l + 3) + (l + 1) * (2 * l + 1) Nln = 0.5 * gammaln(l - m + 1) - 0.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.0) ** 0.5 lnI = ( -(8 * l + 6) * numpy.log(2) + gammaln(n + 4 * l + 3) - gammaln(n + 1) - numpy.log(n + 2 * l + 3.0 / 2) - 2 * gammaln(2 * l + 3.0 / 2) ) I = -K * (4 * numpy.pi) * numpy.e ** (lnI) Acos[:, :, :], Asin[:, :, :] = ( 2 * (I**-1.0)[numpy.newaxis, :, :, :] * integrated * constants[numpy.newaxis, :, :, :] ) return Acos, Asin def _cartesian(arraySizes, out=None): """ Generate a cartesian product of input arrays. Parameters ---------- arraySizes : list list of size of arrays out : numpy.ndarray, optional Array to place the cartesian product in. Returns ------- numpy.ndarray 2-D array of shape (product(arraySizes), len(arraySizes)) containing cartesian products Notes ----- - 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): """ Numerically take n integrals over a function that returns a float or an array Parameters ---------- integrand : function The function you're integrating over. bounds : list 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 : list, optional 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. (default is [20]) roundoff : float, optional if the integral is less than this value, round it to 0. (default is 0) Returns ------- numpy.ndarray The integral of the function integrand Notes ----- - 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]] = 0.5 * (b - a) * x + 0.5 * (b + a) wp[i, : Ksample[i]] = 0.5 * (b - a) * w ##Determines the shape of the integrand s = 0.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 s[numpy.where(numpy.fabs(s) < roundoff)] = 0 return s ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/SnapshotRZPotential.py0000644000175100001660000006213514761352023021744 0ustar00runnerdockerimport copy import hashlib from os import system import numpy from scipy import interpolate from ..util._optional_deps import _PYNBODY_GE_20, _PYNBODY_LOADED from .interpRZPotential import ( calc_2dsplinecoeffs_c, interpRZPotential, scalarVectorDecorator, zsymDecorator, ) from .Potential import Potential if _PYNBODY_LOADED: import pynbody if _PYNBODY_GE_20: # pragma: no cover from pynbody import gravity as pynbody_gravity_calc else: from pynbody.gravity import calc as pynbody_gravity_calc 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): """ Initialize a SnapshotRZ potential object Parameters ---------- s : pynbody.snapshot A simulation snapshot loaded with pynbody. num_threads : int, optional Number of threads to use for calculation. Default is None. nazimuths : int, optional Number of azimuths to average over. Default is 4. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * numpy.pi ) self._sinaz = numpy.sin( numpy.arange(self._naz, dtype="float") / self._naz * 2.0 * 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 = pynbody_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.0), 101), zgrid=(0.0, 1.0, 101), interpepifreq=False, interpverticalfreq=False, interpPot=True, enable_c=True, logR=True, zsym=True, numcores=None, nazimuths=4, use_pkdgrav=False, ): """ Initialize an InterpSnapshotRZPotential instance Parameters ---------- s : pynbody.snapshot A simulation snapshot loaded with pynbody. rgrid : tuple, optional R grid to be given to linspace as in rs= linspace(*rgrid). zgrid : tuple, optional z grid to be given to linspace as in zs= linspace(*zgrid). interpepifreq : bool, optional If True, interpolate the epicycle frequencies (default: False). interpverticalfreq : bool, optional If True, interpolate the vertical frequencies (default: False). interpPot : bool, optional If True, interpolate the potential (default: True). enable_c : bool, optional If True, use C for the interpolation (default: True). logR : bool, optional If True, rgrid is in the log of R so logrs= linspace(*rgrid) (default: True). zsym : bool, optional If True (default), the potential is assumed to be symmetric around z=0 (so you can use, e.g., zgrid=(0.,1.,101)). numcores : int, optional Number of cores to use for the interpolation (default: from pynbody configuration). nazimuths : int, optional Number of azimuths to average over (default: 4). use_pkdgrav : bool, optional If True, use PKDGRAV to calculate the snapshot's potential and forces (default: False). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * numpy.pi ) self._sinaz = numpy.sin( numpy.arange(self._naz, dtype="float") / self._naz * 2.0 * 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.0 ) self._rforceInterp = interpolate.RectBivariateSpline( rs, self._zgrid, self._rforceGrid, kx=3, ky=3, s=0.0 ) self._zforceInterp = interpolate.RectBivariateSpline( rs, self._zgrid, self._zforceGrid, kx=3, ky=3, s=0.0 ) if interpepifreq: self._R2interp = interpolate.RectBivariateSpline( rs, self._zgrid, self._R2derivGrid, kx=3, ky=3, s=0.0 ) if interpverticalfreq: self._z2interp = interpolate.RectBivariateSpline( rs, self._zgrid, self._z2derivGrid, kx=3, ky=3, s=0.0 ) if interpepifreq and interpverticalfreq: self._Rzinterp = interpolate.RectBivariateSpline( rs, self._zgrid, self._RzderivGrid, kx=3, ky=3, s=0.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.0 / 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 = pynbody_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 = pynbody_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 = pynbody_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: # reuse 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.0, t=0.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.0 ) self._rforceInterp = interpolate.RectBivariateSpline( rs, self._zgrid, self._rforceGrid, kx=3, ky=3, s=0.0 ) self._zforceInterp = interpolate.RectBivariateSpline( rs, self._zgrid, self._zforceGrid, kx=3, ky=3, s=0.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.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.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): 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=1741018131.0 galpy-1.10.2/galpy/potential/SoftenedNeedleBarPotential.py0000644000175100001660000001463514761352023023204 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.0, a=4.0, b=0.0, c=1.0, normalize=False, pa=0.4, omegab=1.8, ro=None, vo=None, ): """ Initialize a softened-needle bar potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass. a : float or Quantity, optional Bar half-length. b : float , optional Triaxial softening length (can be Quantity). c : float, optional Prolate softening length (can be Quantity). pa : float or Quantity, optional The position angle of the x axis. omegab : float or Quantity, optional Pattern speed. normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 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.0, t=0.0): 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.0 / self._a def _Rforce(self, R, z, phi=0.0, t=0.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.0, t=0.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.0, t=0.0): self._compute_xyzforces(R, z, phi, t) return self._cached_Fz def OmegaP(self): 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.0 + (self._b + numpy.sqrt(self._c2 + z**2.0)) ** 2.0 return ( numpy.sqrt((self._a + x) ** 2.0 + secondpart), numpy.sqrt((self._a - x) ** 2.0 + 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.0 * x / Tp / Tm / (Tp + Tm) def _yforce_xyz(self, x, y, z, Tp, Tm): return ( -y / 2.0 / Tp / Tm * (Tp + Tm - 4.0 * x**2.0 / (Tp + Tm)) / (y**2.0 + (self._b + numpy.sqrt(z**2.0 + self._c2)) ** 2.0) ) def _zforce_xyz(self, x, y, z, Tp, Tm): zc = numpy.sqrt(z**2.0 + self._c2) return ( -z / 2.0 / Tp / Tm * (Tp + Tm - 4.0 * x**2.0 / (Tp + Tm)) / (y**2.0 + (self._b + zc) ** 2.0) * (self._b + zc) / zc ) def _dens(self, R, z, phi=0.0, t=0.0): x, y, z = self._compute_xyz(R, phi, z, t) zc = numpy.sqrt(z**2.0 + self._c2) bzc2 = (self._b + zc) ** 2.0 bigA = self._b * y**2.0 + (self._b + 3.0 * zc) * bzc2 bigC = y**2.0 + bzc2 return ( self._c2 / 24.0 / numpy.pi / self._a / bigC**2.0 / zc**3.0 * ( (x + self._a) * ( 3.0 * bigA * bigC + (2.0 * bigA + self._b * bigC) * (x + self._a) ** 2.0 ) / (bigC + (x + self._a) ** 2.0) ** 1.5 - (x - self._a) * ( 3.0 * bigA * bigC + (2.0 * bigA + self._b * bigC) * (x - self._a) ** 2.0 ) / (bigC + (x - self._a) ** 2.0) ** 1.5 ) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/SolidBodyRotationWrapperPotential.py0000644000175100001660000000450714761352023024641 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.0, pot=None, omega=1.0, pa=0.0, ro=None, vo=None): """ Initialize a SolidBodyRotationWrapper Potential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential. Default is 1.0. pot : Potential instance or list thereof This potential is made to rotate around the z axis by the wrapper. omega : float or Quantity, optional The pattern speed. Default is 1.0. pa : float or Quantity, optional The position angle. Default is 0.0. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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): return self._omega def _wrap(self, attribute, *args, **kwargs): kwargs["phi"] = ( kwargs.get("phi", 0.0) - self._omega * kwargs.get("t", 0.0) - self._pa ) return self._wrap_pot_func(attribute)(self._pot, *args, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/SphericalPotential.py0000644000175100001660000000656114761352023021604 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.0, ro=None, vo=None, amp_units=None): """ Initialize a spherical potential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). amp_units : str, optional Type of units that amp should have if it has units ('mass', 'velocity2', 'density'). Notes ----- - 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.0): """Implement using the Poisson equation in case this isn't implemented""" return (self._r2deriv(r, t=t) - 2.0 * self._rforce(r, t=t) / r) / 4.0 / numpy.pi def _evaluate(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return self._revaluate(r, t=t) def _Rforce(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return self._rforce(r, t=t) * R / r def _zforce(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return self._rforce(r, t=t) * z / r def _R2deriv(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return ( self._r2deriv(r, t=t) * R**2.0 / r**2.0 - self._rforce(r, t=t) * z**2.0 / r**3.0 ) def _z2deriv(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return ( self._r2deriv(r, t=t) * z**2.0 / r**2.0 - self._rforce(r, t=t) * R**2.0 / r**3.0 ) def _Rzderiv(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return ( self._r2deriv(r, t=t) * R * z / r**2.0 + self._rforce(r, t=t) * R * z / r**3.0 ) def _dens(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return self._rdens(r, t=t) def _mass(self, R, z=None, t=0.0): if z is not None: raise AttributeError # use general implementation R = numpy.float64(R) # Avoid indexing issues return -(R**2.0) * self._rforce(R, t=t) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/SphericalShellPotential.py0000644000175100001660000000645714761352023022600 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.0, a=0.75, normalize=False, ro=None, vo=None): """ Initialize a spherical shell potential. Parameters ---------- amp : float or Quantity, optional Mass of the shell (default: 1); can be a Quantity with units of mass or Gxmass. a : float or Quantity, optional Radius of the shell (default: 0.75). normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0: 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.0): """The potential as a function of r""" if r <= self.a: return -1.0 / self.a else: return -1.0 / r def _rforce(self, r, t=0.0): """The force as a function of r""" if r <= self.a: return 0.0 else: return -1 / r**2.0 def _r2deriv(self, r, t=0.0): """The second radial derivative as a function of r""" if r <= self.a: return 0.0 else: return -2.0 / r**3.0 def _rdens(self, r, t=0.0): """The density as a function of r""" if r != self.a: return 0.0 else: # pragma: no cover return numpy.infty def _surfdens(self, R, z, phi=0.0, t=0.0): if R > self.a: return 0.0 h = numpy.sqrt(self.a2 - R**2) if z < h: return 0.0 else: return 1.0 / (2.0 * numpy.pi * self.a * h) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/SpiralArmsPotential.py0000644000175100001660000010012614761352023021737 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], ): """ Initialize a spiral arms potential Parameters ---------- amp : float or Quantity, optional 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 : float or Quantity, optional distance scales for translation into internal units (default from configuration file) vo : float or Quantity, optional velocity scales for translation into internal units (default from configuration file) N : int, optional number of spiral arms. alpha : float or Quantity, optional pitch angle of the logarithmic spiral arms. r_ref : float or Quantity, optional fiducial radius where :math:`\\rho = \\rho_0` (:math:`r_0` in the paper by Cox and Gomez). phi_ref : float or Quantity, optional reference angle (:math:`\\phi_p(r_0)` in the paper by Cox and Gomez). Rs : float or Quantity, optional radial scale length of the drop-off in density amplitude of the arms. H : float or Quantity, optional scale height of the stellar arm perturbation. Cs : list of floats, optional constants multiplying the :math:`\\cos(n \\gamma)` terms. omega : float or Quantity, optional rotational pattern speed of the spiral arms. Notes ----- - 2017-05-12 - Started - Jack Hong (UBC) - 2020-03-30 - Re-implemented using Potential - Bovy (UofT) """ 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): 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): 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): 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): 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): 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): 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): 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.0 * self._ns**2.0 / Ds / Ks / numpy.cosh(z * Ks / Bs) ** Bs * numpy.cos(self._ns * g), axis=0, ) ) def _Rzderiv(self, R, z, phi=0.0, t=0.0): 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): 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): 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): 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): 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.0 + 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=1741018131.0 galpy-1.10.2/galpy/potential/SteadyLogSpiralPotential.py0000644000175100001660000001551614761352023022740 0ustar00runnerdocker############################################################################### # SteadyLogSpiralPotential: a steady-state spiral potential ############################################################################### import numpy from ..util import conversion from .planarPotential import planarPotential _degtorad = numpy.pi / 180.0 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}}`. If the pattern speed is zero, :math:`t_\\mathrm{form}` and :math:`t_\\mathrm{steady}` are straight times, not times divided by the spiral period. """ def __init__( self, amp=1.0, omegas=0.65, A=-0.035, alpha=-7.0, m=2, gamma=numpy.pi / 4.0, p=None, tform=None, tsteady=None, ro=None, vo=None, ): """ Initialize a steady-state logarithmic spiral potential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1., A below). omegas : float or Quantity, optional Pattern speed (default: 0.65). A : float or Quantity, optional Amplitude (alpha*potential-amplitude; default=0.035). alpha : float, optional Parameter that sets the strength of the spiral potential. m : int, optional Number of spiral arms. gamma : float or Quantity, optional Angle between sun-GC line and the line connecting the peak of the spiral pattern at the Solar radius (in rad; default=45 degree). p : float or Quantity, optional Pitch angle. tform : float, optional Start of spiral growth / spiral period (default: -Infinity). tsteady : float, optional Time from tform at which the spiral is fully grown / spiral period (default: 2 periods). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * numpy.pi / self._omegas if self._omegas != 0.0 else 1.0 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.0 * self._ts self.hasC = True def _evaluate(self, R, phi=0.0, t=0.0): if not self._tform is None: if t < self._tform: smooth = 0.0 elif t < self._tsteady: deltat = t - self._tform xi = 2.0 * deltat / (self._tsteady - self._tform) - 1.0 smooth = ( 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 ) else: # spiral is fully on smooth = 1.0 else: smooth = 1.0 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.0, t=0.0): if not self._tform is None: if t < self._tform: smooth = 0.0 elif t < self._tsteady: deltat = t - self._tform xi = 2.0 * deltat / (self._tsteady - self._tform) - 1.0 smooth = ( 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 ) else: # spiral is fully on smooth = 1.0 else: smooth = 1.0 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.0, t=0.0): if not self._tform is None: if t < self._tform: smooth = 0.0 elif t < self._tsteady: deltat = t - self._tform xi = 2.0 * deltat / (self._tsteady - self._tform) - 1.0 smooth = ( 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 ) else: # spiral is fully on smooth = 1.0 else: smooth = 1.0 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): """ 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) Parameters ---------- R : float Cylindrical radius Returns ------- float wavenumber at R Notes ----- - 2014-08-23 - Written - Bovy (IAS) """ return self._alpha / R def OmegaP(self): return self._omegas def m(self): """ Return the number of arms. Returns ------- int Number of arms. Notes ----- - 2014-08-23 - Written - Bovy (IAS) """ return self._m def tform(self): # pragma: no cover """ Return formation time of the bar. Returns ------- tform : float Formation time of the bar in normalized units. Notes ----- - 2011-03-08 - Written - Bovy (NYU) """ return self._tform ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/TimeDependentAmplitudeWrapperPotential.py0000644000175100001660000000630514761352023025621 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.0, A=None, pot=None, ro=None, vo=None): """ Initialize a TimeDependentAmplitudeWrapperPotential. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1.). A : function, optional 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. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - Started - 2022-03-29 - 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.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.0)) * self._wrap_pot_func(attribute)( self._pot, *args, **kwargs ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/TransientLogSpiralPotential.py0000644000175100001660000000757714761352023023466 0ustar00runnerdocker############################################################################### # TransientLogSpiralPotential: a transient spiral potential ############################################################################### import numpy from ..util import conversion from .planarPotential import planarPotential _degtorad = numpy.pi / 180.0 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.0, omegas=0.65, A=-0.035, alpha=-7.0, m=2, gamma=numpy.pi / 4.0, p=None, sigma=1.0, to=0.0, ro=None, vo=None, ): """ Initialize a transient logarithmic spiral potential localized around to Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1). omegas : float or Quantity, optional Pattern speed (default: 0.65). A : float or Quantity, optional Amplitude (alpha*potential-amplitude; default: -0.035). alpha : float, optional Alpha parameter (default: -7.). m : int, optional Number of arms (default: 2). gamma : float or Quantity, optional Angle between sun-GC line and the line connecting the peak of the spiral pattern at the Solar radius (in rad; default: 45 degree). p : float or Quantity, optional Pitch angle. sigma : float or Quantity, optional "Spiral duration" (sigma in Gaussian amplitude; default: 1.). to : float or Quantity, optional Time at which the spiral peaks (default: 0.). Notes ----- - Either provide: * alpha * p - 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.0 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.0, t=0.0): return ( self._A * numpy.exp(-((t - self._to) ** 2.0) / 2.0 / 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.0, t=0.0): return ( self._A * numpy.exp(-((t - self._to) ** 2.0) / 2.0 / self._sigma2) / R * numpy.sin( self._alpha * numpy.log(R) - self._m * (phi - self._omegas * t - self._gamma) ) ) def _phitorque(self, R, phi=0.0, t=0.0): return ( -self._A * numpy.exp(-((t - self._to) ** 2.0) / 2.0 / self._sigma2) / self._alpha * self._m * numpy.sin( self._alpha * numpy.log(R) - self._m * (phi - self._omegas * t - self._gamma) ) ) def OmegaP(self): return self._omegas ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/TriaxialGaussianPotential.py0000644000175100001660000001045314761352023023135 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.0, sigma=5.0, b=1.0, c=1.0, zvec=None, pa=None, glorder=50, normalize=False, ro=None, vo=None, ): """ Initialize a triaxial Gaussian potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass. sigma : float or Quantity, optional Gaussian dispersion scale. b : float, optional y-to-x axis ratio of the density. c : float, optional z-to-x axis ratio of the density. zvec : numpy.ndarray, optional If set, a unit vector that corresponds to the z axis. pa : float or Quantity, optional If set, the position angle of the x axis. glorder : int, optional If set, compute the relevant force and potential integrals with Gaussian quadrature of this order. normalize : bool or float, optional If True, normalize the potential (default: False). If a float, normalize the potential to this value. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * self._sigma**2 self._scale = self._sigma # Adjust amp self._amp /= (2.0 * numpy.pi) ** 1.5 * self._sigma**3.0 * 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.0) / 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.0 * m * numpy.exp(-(m**2) / self._twosigma2) / self._twosigma2 def _mass(self, R, z=None, t=0.0): 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.0 * numpy.pi) * special.erf(R / self._sigma / numpy.sqrt(2.0)) - 2.0 * R / self._sigma * numpy.exp(-(R**2.0) / self._twosigma2) ) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/TwoPowerSphericalPotential.py0000644000175100001660000011221114761352023023301 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.0, a=5.0, alpha=1.5, beta=3.5, normalize=False, ro=None, vo=None ): """ Initialize a two-power-density potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass. a : float or Quantity, optional Scale radius. alpha : float, optional Inner power. beta : float, optional Outer power. normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - Started - 2010-07-09 - 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.0, a=a, normalize=False ) elif int(alpha) == 1 and int(beta) == 4: self._specialSelf = HernquistPotential(amp=1.0, a=a, normalize=False) elif int(alpha) == 2 and int(beta) == 4: self._specialSelf = JaffePotential(amp=1.0, a=a, normalize=False) elif int(alpha) == 1 and int(beta) == 3: self._specialSelf = NFWPotential(amp=1.0, 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.0, t=0.0): if self._specialSelf is not None: return self._specialSelf._evaluate(R, z, phi=phi, t=t) elif self.beta == 3.0: r = numpy.sqrt(R**2.0 + z**2.0) return ( (1.0 / self.a) * ( r - self.a * (r / self.a) ** (3.0 - self.alpha) / (3.0 - self.alpha) * special.hyp2f1( 3.0 - self.alpha, 2.0 - self.alpha, 4.0 - self.alpha, -r / self.a, ) ) / (self.alpha - 2.0) / r ) else: r = numpy.sqrt(R**2.0 + z**2.0) return ( special.gamma(self.beta - 3.0) * ( (r / self.a) ** (3.0 - self.beta) / special.gamma(self.beta - 1.0) * special.hyp2f1( self.beta - 3.0, self.beta - self.alpha, self.beta - 1.0, -self.a / r, ) - special.gamma(3.0 - self.alpha) / special.gamma(self.beta - self.alpha) ) / r ) def _Rforce(self, R, z, phi=0.0, t=0.0): if self._specialSelf is not None: return self._specialSelf._Rforce(R, z, phi=phi, t=t) else: r = numpy.sqrt(R**2.0 + z**2.0) return ( -R / r**self.alpha * self.a ** (self.alpha - 3.0) / (3.0 - self.alpha) * special.hyp2f1( 3.0 - self.alpha, self.beta - self.alpha, 4.0 - self.alpha, -r / self.a, ) ) def _zforce(self, R, z, phi=0.0, t=0.0): if self._specialSelf is not None: return self._specialSelf._zforce(R, z, phi=phi, t=t) else: r = numpy.sqrt(R**2.0 + z**2.0) return ( -z / r**self.alpha * self.a ** (self.alpha - 3.0) / (3.0 - self.alpha) * special.hyp2f1( 3.0 - self.alpha, self.beta - self.alpha, 4.0 - self.alpha, -r / self.a, ) ) def _dens(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return ( (self.a / r) ** self.alpha / (1.0 + r / self.a) ** (self.beta - self.alpha) / 4.0 / numpy.pi / self.a**3.0 ) def _ddensdr(self, r, t=0.0): return ( -self._amp * (self.a / r) ** (self.alpha - 1.0) * (1.0 + r / self.a) ** (self.alpha - self.beta - 1.0) * (self.a * self.alpha + r * self.beta) / r**2 / 4.0 / numpy.pi / self.a**3.0 ) def _d2densdr2(self, r, t=0.0): return ( self._amp * (self.a / r) ** (self.alpha - 2.0) * (1.0 + r / self.a) ** (self.alpha - self.beta - 2.0) * ( self.alpha * (self.alpha + 1.0) * self.a**2 + 2.0 * self.alpha * self.a * (self.beta + 1.0) * r + self.beta * (self.beta + 1.0) * r**2 ) / r**4 / 4.0 / numpy.pi / self.a**3.0 ) def _ddenstwobetadr(self, r, beta=0): """ Evaluate the radial density derivative x r^(2beta) for this potential. Parameters ---------- r : float Spherical radius. beta : float, optional Power of r in the density derivative. Default is 0. Returns ------- float The derivative of the density times r^(2beta). Notes ----- - 2021-02-14 - Written - Bovy (UofT) """ return ( self._amp / 4.0 / numpy.pi / self.a**3.0 * r ** (2.0 * beta - 2.0) * (self.a / r) ** (self.alpha - 1.0) * (1.0 + r / self.a) ** (self.alpha - self.beta - 1.0) * (self.a * (2.0 * beta - self.alpha) + r * (2.0 * beta - self.beta)) ) def _R2deriv(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) A = self.a ** (self.alpha - 3.0) / (3.0 - self.alpha) hyper = special.hyp2f1( 3.0 - self.alpha, self.beta - self.alpha, 4.0 - self.alpha, -r / self.a ) hyper_deriv = ( (3.0 - self.alpha) * (self.beta - self.alpha) / (4.0 - self.alpha) * special.hyp2f1( 4.0 - self.alpha, 1.0 + self.beta - self.alpha, 5.0 - self.alpha, -r / self.a, ) ) term1 = A * r ** (-self.alpha) * hyper term2 = -self.alpha * A * R**2.0 * r ** (-self.alpha - 2.0) * hyper term3 = -A * R**2 * r ** (-self.alpha - 1.0) / self.a * hyper_deriv return term1 + term2 + term3 def _Rzderiv(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) A = self.a ** (self.alpha - 3.0) / (3.0 - self.alpha) hyper = special.hyp2f1( 3.0 - self.alpha, self.beta - self.alpha, 4.0 - self.alpha, -r / self.a ) hyper_deriv = ( (3.0 - self.alpha) * (self.beta - self.alpha) / (4.0 - self.alpha) * special.hyp2f1( 4.0 - self.alpha, 1.0 + self.beta - self.alpha, 5.0 - self.alpha, -r / self.a, ) ) term1 = -self.alpha * A * R * r ** (-self.alpha - 2.0) * z * hyper term2 = -A * R * r ** (-self.alpha - 1.0) * z / self.a * hyper_deriv return term1 + term2 def _z2deriv(self, R, z, phi=0.0, t=0.0): return self._R2deriv(numpy.fabs(z), R) # Spherical potential def _mass(self, R, z=None, t=0.0): if z is not None: raise AttributeError # use general implementation return ( (R / self.a) ** (3.0 - self.alpha) / (3.0 - self.alpha) * special.hyp2f1( 3.0 - self.alpha, -self.alpha + self.beta, 4.0 - 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.0, a=1.0, alpha=1.5, normalize=False, ro=None, vo=None): """ Initialize a Dehnen Spherical Potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass. a : float or Quantity, optional Scale radius. alpha : float, optional Inner power, restricted to [0, 3). normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - Started - Starkman (UofT) - 2019-10-07 """ if (alpha < 0.0) or (alpha >= 3.0): 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.0, a=a, normalize=False ) elif round(alpha) == 1: self._specialSelf = HernquistPotential(amp=1.0, a=a, normalize=False) elif round(alpha) == 2: self._specialSelf = JaffePotential(amp=1.0, 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.0, t=0.0): 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.0 + z**2.0) return -(1.0 - 1.0 / (1.0 + self.a / r) ** (2.0 - self.alpha)) / ( self.a * (2.0 - self.alpha) * (3.0 - self.alpha) ) def _Rforce(self, R, z, phi=0.0, t=0.0): if self._specialSelf is not None: return self._specialSelf._Rforce(R, z, phi=phi, t=t) else: r = numpy.sqrt(R**2.0 + z**2.0) return ( -R / r**self.alpha * (self.a + r) ** (self.alpha - 3.0) / (3.0 - self.alpha) ) def _R2deriv(self, R, z, phi=0.0, t=0.0): 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.0 + z**2.0) # formula not valid for alpha=2,3, (integers?) return ( numpy.power(r, -2.0 - alpha) * numpy.power(r + a, alpha - 4.0) * (-a * r**2.0 + (2.0 * R**2.0 - z**2.0) * r + a * alpha * R**2.0) / (alpha - 3.0) ) def _zforce(self, R, z, phi=0.0, t=0.0): if self._specialSelf is not None: return self._specialSelf._zforce(R, z, phi=phi, t=t) else: r = numpy.sqrt(R**2.0 + z**2.0) return ( -z / r**self.alpha * (self.a + r) ** (self.alpha - 3.0) / (3.0 - self.alpha) ) def _z2deriv(self, R, z, phi=0.0, t=0.0): return self._R2deriv(z, R, phi=phi, t=t) def _Rzderiv(self, R, z, phi=0.0, t=0.0): 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.0 + z**2.0) return ( R * z * numpy.power(r, -2.0 - alpha) * numpy.power(a + r, alpha - 4.0) * (3 * r + a * alpha) ) / (alpha - 3) def _dens(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return ( (self.a / r) ** self.alpha / (1.0 + r / self.a) ** (4.0 - self.alpha) / 4.0 / numpy.pi / self.a**3.0 ) def _mass(self, R, z=None, t=0.0): if z is not None: raise AttributeError # use general implementation return ( 1.0 / (1.0 + self.a / R) ** (3.0 - self.alpha) / (3.0 - 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.0, a=1.0, normalize=False, ro=None, vo=None): """ Initialize a cored Dehnen Spherical Potential; note that the amplitude definition used here does NOT match that of Dehnen (1993) Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a : float or Quantity, optional Scale radius. normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return -(1.0 - 1.0 / (1.0 + self.a / r) ** 2.0) / (6.0 * self.a) def _Rforce(self, R, z, phi=0.0, t=0.0): return -R / numpy.power(numpy.sqrt(R**2.0 + z**2.0) + self.a, 3.0) / 3.0 def _rforce_jax(self, r): # No need for actual JAX! return -self._amp * r / (r + self.a) ** 3.0 / 3.0 def _R2deriv(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return -( ((2.0 * R**2.0 - z**2.0) - self.a * r) / (3.0 * r * numpy.power(r + self.a, 4.0)) ) def _zforce(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return -z / numpy.power(self.a + r, 3.0) / 3.0 def _z2deriv(self, R, z, phi=0.0, t=0.0): return self._R2deriv(z, R, phi=phi, t=t) def _Rzderiv(self, R, z, phi=0.0, t=0.0): a = self.a r = numpy.sqrt(R**2.0 + z**2.0) return -(R * z / r / numpy.power(a + r, 4.0)) def _dens(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) return 1.0 / (1.0 + r / self.a) ** 4.0 / 4.0 / numpy.pi / self.a**3.0 def _mass(self, R, z=None, t=0.0): if z is not None: raise AttributeError # use general implementation return ( 1.0 / (1.0 + self.a / R) ** 3.0 / 3.0 ) # 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.0, a=1.0, normalize=False, ro=None, vo=None): """ Initialize a Two Power Spherical Potential. Parameters ---------- amp : float or Quantity, optional 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 Two Power Spherical potential). a : float or Quantity, optional Scale radius. normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0, t=0.0): return -1.0 / (1.0 + numpy.sqrt(R**2.0 + z**2.0) / self.a) / 2.0 / self.a def _Rforce(self, R, z, phi=0.0, t=0.0): sqrtRz = numpy.sqrt(R**2.0 + z**2.0) return -R / self.a / sqrtRz / (1.0 + sqrtRz / self.a) ** 2.0 / 2.0 / self.a def _zforce(self, R, z, phi=0.0, t=0.0): sqrtRz = numpy.sqrt(R**2.0 + z**2.0) return -z / self.a / sqrtRz / (1.0 + sqrtRz / self.a) ** 2.0 / 2.0 / self.a def _rforce_jax(self, r): # No need for actual JAX! return -self._amp / 2.0 / (r + self.a) ** 2.0 def _R2deriv(self, R, z, phi=0.0, t=0.0): sqrtRz = numpy.sqrt(R**2.0 + z**2.0) return ( (self.a * z**2.0 + (z**2.0 - 2.0 * R**2.0) * sqrtRz) / sqrtRz**3.0 / (self.a + sqrtRz) ** 3.0 / 2.0 ) def _Rzderiv(self, R, z, phi=0.0, t=0.0): sqrtRz = numpy.sqrt(R**2.0 + z**2.0) return ( -R * z * (self.a + 3.0 * sqrtRz) * (sqrtRz * (self.a + sqrtRz)) ** -3.0 / 2.0 ) def _surfdens(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) Rma = numpy.sqrt(R**2.0 - self.a**2.0 + 0j) if Rma == 0.0: return ( ( -12.0 * self.a**3 - 5.0 * self.a * z**2 + numpy.sqrt(1.0 + z**2 / self.a**2) * (12.0 * self.a**3 - self.a * z**2 + 2 / self.a * z**4) ) / 30.0 / numpy.pi * z**-5.0 ) else: return ( self.a * ( (2.0 * self.a**2.0 + R**2.0) * Rma**-5 * (numpy.arctan(z / Rma) - numpy.arctan(self.a * z / r / Rma)) + z * ( 5.0 * self.a**3.0 * r - 4.0 * self.a**4 + self.a**2 * (2.0 * r**2.0 + R**2) - self.a * r * (5.0 * R**2.0 + 3.0 * z**2.0) + R**2.0 * r**2.0 ) / (self.a**2.0 - R**2.0) ** 2.0 / (r**2 - self.a**2.0) ** 2.0 ).real / 4.0 / numpy.pi ) def _mass(self, R, z=None, t=0.0): if z is not None: raise AttributeError # use general implementation return ( 1.0 / (1.0 + self.a / R) ** 2.0 / 2.0 ) # written so it works for r=numpy.inf @kms_to_kpcGyrDecorator def _nemo_accpars(self, vo, ro): """ Return the accpars potential parameters for use of this potential with NEMO. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- str accpars string. Notes ----- - 2018-09-14 - Written - Bovy (UofT) """ GM = self._amp * vo**2.0 * ro / 2.0 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.0, a=1.0, normalize=False, ro=None, vo=None): """ Initialize a Jaffe Potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass. a : float or Quantity, optional Scale radius (can be Quantity). normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0, t=0.0): return -numpy.log(1.0 + self.a / numpy.sqrt(R**2.0 + z**2.0)) / self.a def _Rforce(self, R, z, phi=0.0, t=0.0): sqrtRz = numpy.sqrt(R**2.0 + z**2.0) return -R / sqrtRz**3.0 / (1.0 + self.a / sqrtRz) def _zforce(self, R, z, phi=0.0, t=0.0): sqrtRz = numpy.sqrt(R**2.0 + z**2.0) return -z / sqrtRz**3.0 / (1.0 + self.a / sqrtRz) def _R2deriv(self, R, z, phi=0.0, t=0.0): sqrtRz = numpy.sqrt(R**2.0 + z**2.0) return ( (self.a * (z**2.0 - R**2.0) + (z**2.0 - 2.0 * R**2.0) * sqrtRz) / sqrtRz**4.0 / (self.a + sqrtRz) ** 2.0 ) def _Rzderiv(self, R, z, phi=0.0, t=0.0): sqrtRz = numpy.sqrt(R**2.0 + z**2.0) return ( -R * z * (2.0 * self.a + 3.0 * sqrtRz) * sqrtRz**-4.0 * (self.a + sqrtRz) ** -2.0 ) def _surfdens(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) Rma = numpy.sqrt(R**2.0 - self.a**2.0 + 0j) if Rma == 0.0: return ( ( 3.0 * z**2.0 - 2.0 * self.a**2.0 + 2.0 * numpy.sqrt(1.0 + (z / self.a) ** 2.0) * (self.a**2.0 - 2.0 * z**2.0) + 3.0 * z**3.0 / self.a * numpy.arctan(z / self.a) ) / self.a / z**3.0 / 6.0 / numpy.pi ) else: return ( ( (2.0 * self.a**2.0 - R**2.0) * 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.0 / numpy.pi ) def _mass(self, R, z=None, t=0.0): if z is not None: raise AttributeError # use general implementation return 1.0 / (1.0 + 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.0, a=1.0, normalize=False, rmax=None, vmax=None, conc=None, mvir=None, vo=None, ro=None, H=70.0, Om=0.3, overdens=200.0, wrtcrit=False, ): """ Initialize a NFW Potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass. a : float or Quantity, optional Scale radius (can be Quantity). normalize : bool or float, optional 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. rmax : float or Quantity, optional Radius where the rotation curve peak. vmax : float or Quantity, optional Maximum circular velocity. conc : float, optional Concentration. mvir : float, optional virial mass in 10^12 Msolar H : float, optional Hubble constant in km/s/Mpc. Om : float, optional Omega matter. overdens : float, optional Overdensity which defines the virial radius. wrtcrit : bool, optional If True, the overdensity is wrt the critical density rather than the mean matter density. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - Initialize with one of: * a and amp or normalize * rmax and vmax * conc, mvir, H, Om, overdens, wrtcrit - 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.0 * 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.0 / conversion.mass_in_1010msol(self._vo, self._ro) rvir = (3.0 * mvirNatural / od / 4.0 / numpy.pi) ** (1.0 / 3.0) self.a = rvir / conc self._amp = mvirNatural / (numpy.log(1.0 + conc) - conc / (1.0 + 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.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) if isinstance(r, (float, int)) and r == 0: return -1.0 / self.a elif isinstance(r, (float, int)): return -special.xlogy(1.0 / r, 1.0 + r / self.a) # stable as r -> infty else: out = -special.xlogy(1.0 / r, 1.0 + r / self.a) # stable as r -> infty out[r == 0] = -1.0 / self.a return out def _Rforce(self, R, z, phi=0.0, t=0.0): Rz = R**2.0 + z**2.0 sqrtRz = numpy.sqrt(Rz) return R * ( 1.0 / Rz / (self.a + sqrtRz) - numpy.log(1.0 + sqrtRz / self.a) / sqrtRz / Rz ) def _zforce(self, R, z, phi=0.0, t=0.0): Rz = R**2.0 + z**2.0 sqrtRz = numpy.sqrt(Rz) return z * ( 1.0 / Rz / (self.a + sqrtRz) - numpy.log(1.0 + sqrtRz / self.a) / sqrtRz / Rz ) def _rforce_jax(self, r): if not _JAX_LOADED: # pragma: no cover raise ImportError( "Making use of _rforce_jax function requires the google/jax library" ) return self._amp * (1.0 / r / (self.a + r) - jnp.log(1.0 + r / self.a) / r**2.0) def _R2deriv(self, R, z, phi=0.0, t=0.0): Rz = R**2.0 + z**2.0 sqrtRz = numpy.sqrt(Rz) return ( ( 3.0 * R**4.0 + 2.0 * R**2.0 * (z**2.0 + self.a * sqrtRz) - z**2.0 * (z**2.0 + self.a * sqrtRz) - (2.0 * R**2.0 - z**2.0) * (self.a**2.0 + R**2.0 + z**2.0 + 2.0 * self.a * sqrtRz) * numpy.log(1.0 + sqrtRz / self.a) ) / Rz**2.5 / (self.a + sqrtRz) ** 2.0 ) def _Rzderiv(self, R, z, phi=0.0, t=0.0): Rz = R**2.0 + z**2.0 sqrtRz = numpy.sqrt(Rz) return ( -R * z * ( -4.0 * Rz - 3.0 * self.a * sqrtRz + 3.0 * (self.a**2.0 + Rz + 2.0 * self.a * sqrtRz) * numpy.log(1.0 + sqrtRz / self.a) ) * Rz**-2.5 * (self.a + sqrtRz) ** -2.0 ) def _surfdens(self, R, z, phi=0.0, t=0.0): r = numpy.sqrt(R**2.0 + z**2.0) Rma = numpy.sqrt(R**2.0 - self.a**2.0 + 0j) if Rma == 0.0: za2 = (z / self.a) ** 2 return ( self.a * (2.0 + numpy.sqrt(za2 + 1.0) * (za2 - 2.0)) / 6.0 / 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.0 - self.a**2.0) ).real / 2.0 / numpy.pi ) def _mass(self, R, z=None, t=0.0): if z is not None: raise AttributeError # use general implementation return numpy.log(1 + R / self.a) - R / self.a / (1.0 + R / self.a) @conversion.physical_conversion("position", pop=False) def rvir( self, H=70.0, Om=0.3, t=0.0, overdens=200.0, wrtcrit=False, ro=None, vo=None, use_physical=False, ): # use_physical necessary bc of pop=False, does nothing inside """ Calculate the virial radius for this density distribution. Parameters ---------- H : float, optional Hubble constant in km/s/Mpc. Default is 70.0. Om : float, optional Omega matter. Default is 0.3. t : float, optional Time. Default is 0.0. overdens : float, optional Overdensity which defines the virial radius. Default is 200.0. wrtcrit : bool, optional If True, the overdensity is wrt the critical density rather than the mean matter density. Default is False. ro : float or Quantity, optional Distance scale for translation into internal units (default is the object-wide value). vo : float or Quantity, optional Velocity scale for translation into internal units (default is the object-wide value). Returns ------- float Virial radius. Notes ----- - 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.0 * self.dens(self.a, 0.0, t=t, use_physical=False) / od x = optimize.brentq( lambda y: (numpy.log(1.0 + y) - y / (1.0 + y)) / y**3.0 - 1.0 / dc, 0.01, 100.0, ) return x * self.a @conversion.physical_conversion("position", pop=True) def rmax(self): """ Calculate the radius at which the rotation curve peaks. Returns ------- float Radius at which the rotation curve peaks. Notes ----- - 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): """ Calculate the maximum rotation curve velocity. Returns ------- float Peak velocity in the rotation curve. Notes ----- - 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): """ Return the accpars potential parameters for use of this potential with NEMO Parameters ---------- vo : float Velocity unit in km/s ro : float Length unit in kpc Returns ------- str accpars string Notes ----- - 2014-12-18 - Written - Bovy (IAS) """ ampl = self._amp * vo**2.0 * 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=1741018131.0 galpy-1.10.2/galpy/potential/TwoPowerTriaxialPotential.py0000644000175100001660000004615314761352023023157 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.0, a=5.0, alpha=1.5, beta=3.5, b=1.0, c=1.0, zvec=None, pa=None, glorder=50, normalize=False, ro=None, vo=None, ): """ Initialize a triaxial two-power-density potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass. a : float or Quantity, optional Scale radius. alpha : float, optional Inner power (0 <= alpha < 3). beta : float, optional Outer power ( beta > 2). b : float, optional y-to-x axis ratio of the density. c : float, optional z-to-x axis ratio of the density. zvec : numpy.ndarray, optional If set, a unit vector that corresponds to the z axis. pa : float or Quantity, optional If set, the position angle of the x axis. glorder : int, optional If set, compute the relevant force and potential integrals with Gaussian quadrature of this order. normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 or alpha >= 3.0: 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.0 - self.alpha self.threeminusalpha = 3.0 - self.alpha if self.twominusalpha != 0.0: self.psi_inf = ( special.gamma(self.beta - 2.0) * special.gamma(3.0 - self.alpha) / special.gamma(self.betaminusalpha) ) # Adjust amp self._amp /= 4.0 * 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): r"""\psi(m) = -\int_{m^2}^\infty d m'^2 \rho(m'^2)""" if self.twominusalpha == 0.0: return ( -2.0 * 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.0 * 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.0 + 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.0): if not z is None: raise AttributeError # Hack to fall back to general return ( 4.0 * numpy.pi * self.a**self.alpha * R ** (3.0 - self.alpha) / (3.0 - self.alpha) * self._b * self._c * special.hyp2f1( 3.0 - self.alpha, self.betaminusalpha, 4.0 - 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.0, a=2.0, normalize=False, b=1.0, c=1.0, zvec=None, pa=None, glorder=50, ro=None, vo=None, ): """ Initialize a triaxial two-power-density potential. Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass. a : float or Quantity, optional Scale radius. normalize : bool or float, optional 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. b : float, optional y-to-x axis ratio of the density. c : float, optional z-to-x axis ratio of the density. zvec : numpy.ndarray, optional If set, a unit vector that corresponds to the z axis. pa : float or Quantity, optional If set, the position angle of the x axis. glorder : int, optional If set, compute the relevant force and potential integrals with Gaussian quadrature of this order. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * 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.0 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.0 * m) / m**2 / (self.a + m) ** 4 def _mass(self, R, z=None, t=0.0): if not z is None: raise AttributeError # Hack to fall back to general return ( 4.0 * numpy.pi * self.a4 / self.a / (1.0 + self.a / R) ** 2.0 / 2.0 * 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.0, a=2.0, b=1.0, c=1.0, zvec=None, pa=None, normalize=False, glorder=50, ro=None, vo=None, ): """ Two-power-law triaxial potential Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a : float or Quantity, optional Scale radius. b : float, optional y-to-x axis ratio of the density c : float, optional z-to-x axis ratio of the density zvec : numpy.ndarray, optional If set, a unit vector that corresponds to the z axis pa : float or Quantity, optional If set, the position angle of the x axis glorder : int, optional If set, compute the relevant force and potential integrals with Gaussian quadrature of this order normalize : bool or float, optional 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 : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - 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.0 * 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.0 * self.a2 * (1.0 / (1.0 + m / self.a) + numpy.log(1.0 / (1.0 + self.a / m))) ) def _mdens(self, m): """Density as a function of m""" return self.a2 / m**2 / (1.0 + m / self.a) ** 2 def _mdens_deriv(self, m): """Derivative of the density as a function of m""" return -2.0 * self.a2**2 * (self.a + 2.0 * m) / m**3 / (self.a + m) ** 3 def _mass(self, R, z=None, t=0.0): if not z is None: raise AttributeError # Hack to fall back to general return ( 4.0 * numpy.pi * self.a * self.a2 / (1.0 + 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.0, a=2.0, b=1.0, c=1.0, zvec=None, pa=None, normalize=False, conc=None, mvir=None, glorder=50, vo=None, ro=None, H=70.0, Om=0.3, overdens=200.0, wrtcrit=False, ): """ Initialize a triaxial NFW potential Parameters ---------- amp : float or Quantity, optional Amplitude to be applied to the potential (default: 1); can be a Quantity with units of mass or Gxmass a : float or Quantity, optional Scale radius. b : float, optional y-to-x axis ratio of the density c : float, optional z-to-x axis ratio of the density zvec : numpy.ndarray, optional If set, a unit vector that corresponds to the z axis pa : float or Quantity, optional If set, the position angle of the x axis glorder : int, optional If set, compute the relevant force and potential integrals with Gaussian quadrature of this order normalize : bool or float, optional 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. conc : float, optional Concentration. mvir : float, optional Virial mass in 10^12 Msolar. H : float, optional Hubble constant in km/s/Mpc. Om : float, optional Omega matter. overdens : float, optional Overdensity which defines the virial radius. wrtcrit : bool, optional If True, the overdensity is wrt the critical density rather than the mean matter density. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). Notes ----- - Initialize with one of: * a and amp or normalize * mvir, conc, H, Om, wrtcrit, overdens. - 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) 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.0 * 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.0 * self.a3 / (self.a + m) def _mdens(self, m): """Density as a function of m""" return self.a / m / (1.0 + m / self.a) ** 2 def _mdens_deriv(self, m): """Derivative of the density as a function of m""" return -self.a3 * (self.a + 3.0 * m) / m**2 / (self.a + m) ** 3 def _mass(self, R, z=None, t=0.0): if not z is None: raise AttributeError # Hack to fall back to general return ( 4.0 * numpy.pi * self.a3 * self._b * self._c * (numpy.log(1 + R / self.a) - R / self.a / (1.0 + R / self.a)) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/WrapperPotential.py0000644000175100001660000002623314761352023021310 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 ( Force, 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.0, pot=None, ro=None, vo=None, _init=None, **kwargs): """ Initialize a WrapperPotential, a super-class for wrapper potentials. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1.). pot : Potential instance or list thereof Potential instance or list thereof. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). _init : bool, optional If True, run __init__ at the end of setup (default: True). **kwargs Any other keyword arguments are passed to the Potential superclass. Notes ----- - 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 # Check that we are not wrapping a non-potential Force object if ( isinstance(self._pot, list) and any( [ isinstance(p, Force) and not isinstance(p, Potential) for p in self._pot ] ) ) or (isinstance(self._pot, Force) and not isinstance(self._pot, Potential)): raise RuntimeError( "WrapperPotential cannot currently wrap non-Potential Force objects" ) 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.0, t=0.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.0, t=0.0: _evaluatePotentials( p, R, Z, phi=phi, t=t ) elif attribute == "_dens": return lambda p, R, Z, phi=0.0, t=0.0: evaluateDensities( p, R, Z, phi=phi, t=t, use_physical=False ) elif attribute == "_Rforce": return lambda p, R, Z, phi=0.0, t=0.0: _evaluateRforces( p, R, Z, phi=phi, t=t ) elif attribute == "_zforce": return lambda p, R, Z, phi=0.0, t=0.0: _evaluatezforces( p, R, Z, phi=phi, t=t ) elif attribute == "_phitorque": return lambda p, R, Z, phi=0.0, t=0.0: _evaluatephitorques( p, R, Z, phi=phi, t=t ) elif attribute == "_R2deriv": return lambda p, R, Z, phi=0.0, t=0.0: evaluateR2derivs( p, R, Z, phi=phi, t=t, use_physical=False ) elif attribute == "_z2deriv": return lambda p, R, Z, phi=0.0, t=0.0: evaluatez2derivs( p, R, Z, phi=phi, t=t, use_physical=False ) elif attribute == "_Rzderiv": return lambda p, R, Z, phi=0.0, t=0.0: evaluateRzderivs( p, R, Z, phi=phi, t=t, use_physical=False ) elif attribute == "_phi2deriv": return lambda p, R, Z, phi=0.0, t=0.0: _evaluatePotentials( p, R, Z, phi=phi, t=t, dphi=2 ) elif attribute == "_Rphideriv": return lambda p, R, Z, phi=0.0, t=0.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.0, pot=None, ro=None, vo=None, _init=None, **kwargs): """ Initialize a WrapperPotential, a super-class for wrapper potentials. Parameters ---------- amp : float, optional Amplitude to be applied to the potential (default: 1.). pot : Potential instance or list thereof, optional The potential instance or list thereof; the amplitude of this will be grown by this wrapper. ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). _init : bool, optional If True, run __init__ at the end of setup. Default is None. **kwargs Any other keyword arguments are passed to the Potential class. Notes ----- - 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.0, t=0.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.0, t=0.0: _evaluateplanarPotentials( p, R, phi=phi, t=t ) elif attribute == "_Rforce": return lambda p, R, phi=0.0, t=0.0: _evaluateplanarRforces( p, R, phi=phi, t=t ) elif attribute == "_phitorque": return lambda p, R, phi=0.0, t=0.0: _evaluateplanarphitorques( p, R, phi=phi, t=t ) elif attribute == "_R2deriv": return lambda p, R, phi=0.0, t=0.0: evaluateplanarR2derivs( p, R, phi=phi, t=t, use_physical=False ) elif attribute == "_phi2deriv": return lambda p, R, phi=0.0, t=0.0: _evaluateplanarPotentials( p, R, phi=phi, t=t, dphi=2 ) elif attribute == "_Rphideriv": return lambda p, R, phi=0.0, t=0.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=1741018131.0 galpy-1.10.2/galpy/potential/__init__.py0000644000175100001660000002416614761352023017552 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, KuzminLikeWrapperPotential, 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, planarForce, planarPotential, plotEscapecurve, plotRotcurve, verticalPotential, ) # # Functions # evaluatePotentials = Potential.evaluatePotentials evaluateDensities = Potential.evaluateDensities evaluateSurfaceDensities = Potential.evaluateSurfaceDensities mass = Potential.mass evaluateRforces = Potential.evaluateRforces evaluatephitorques = Potential.evaluatephitorques 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 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 planarForce = planarForce.planarForce 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 ) KuzminLikeWrapperPotential = KuzminLikeWrapperPotential.KuzminLikeWrapperPotential # 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=1741018131.0 galpy-1.10.2/galpy/potential/amuse.py0000644000175100001660000002154114761352023017117 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.0, tgalpy=0.0, ro=8, vo=220.0, reverse=False): """ Initialize a galpy potential for use with AMUSE. Parameters ---------- pot : Potential instance or list thereof, optional Potential object(s) to be used with AMUSE. t : float or Quantity, optional Start time for AMUSE simulation (can be an AMUSE Quantity). tgalpy : float or Quantity, optional Start time for galpy potential, can be less than zero (can be Quantity). ro : float or Quantity, optional Distance scale for translation into internal units (default from configuration file). vo : float or Quantity, optional Velocity scale for translation into internal units (default from configuration file). reverse : bool, optional Set whether galpy potential evolves forwards or backwards in time (default: False). Notes ----- - 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): """ Evolve time parameters to t_end. Parameters ---------- time : float End time for the potential evolution. Returns ------- None Notes ----- - 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): """ Get potential at a given location in the potential. Parameters ---------- eps : AMUSE Quantity Softening length (necessary for AMUSE, but not used by galpy potential). x,y,z : AMUSE Quantity Position in the potential. Returns ------- AMUSE Quantity Phi(x,y,z). Notes ----- - 2019-08-12 - Written - Webb (UofT) - 2019-11-06 - Added physical compatibility - Starkman (UofT). """ R = numpy.sqrt(x.value_in(units.kpc) ** 2.0 + y.value_in(units.kpc) ** 2.0) 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): """ Get acceleration due to potential at a given location in the potential. Parameters ---------- eps : AMUSE Quantity Softening length (necessary for AMUSE, but not used by galpy potential). x,y,z : AMUSE Quantity Position in the potential. Returns ------- ax : AMUSE Quantity Acceleration in the x-direction. ay : AMUSE Quantity Acceleration in the y-direction. az : AMUSE Quantity Acceleration in the z-direction. Notes ----- - 2019-08-12 - Written - Webb (UofT) - 2019-11-06 - Added physical compatibility - Starkman (UofT). """ R = numpy.sqrt(x.value_in(units.kpc) ** 2.0 + y.value_in(units.kpc) ** 2.0) 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): """ Get mass density at a given location in the potential Parameters ---------- eps : AMUSE Quantity Softening length (necessary for AMUSE, but not used by galpy potential) x,y,z : AMUSE Quantity Position in the potential Returns ------- AMUSE Quantity The density Notes ----- - 2019-08-12 - Written - Webb (UofT) - 2019-11-06 - added physical compatibility - Starkman (UofT) """ R = numpy.sqrt(x.value_in(units.kpc) ** 2.0 + y.value_in(units.kpc) ** 2.0) 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): """ Get circular velocity at a given radius in the potential Parameters ---------- r : AMUSE Quantity Radius in the potential Returns ------- AMUSE Quantity The circular velocity Notes ----- - 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): """ Get mass enclosed within a given radius in the potential Parameters ---------- r : AMUSE Quantity Radius in the potential Returns ------- AMUSE Quantity The mass enclosed Notes ----- - 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.0) * r.value_in(units.parsec) / conversion._G | units.MSun def stop(self): """ Stop the potential model (necessary function for AMUSE) Notes ----- - 2019-08-12 - Written - Webb (UofT) """ pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/interpRZPotential.py0000644000175100001660000007516714761352023021457 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.0), 101), zgrid=(0.0, 1.0, 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, ): """ Initialize an interpRZPotential instance. Parameters ---------- RZPot : RZPotential or list of such instances RZPotential to be interpolated. rgrid : tuple, optional R grid to be given to linspace as in rs= linspace(*rgrid). zgrid : tuple, optional z grid to be given to linspace as in zs= linspace(*zgrid). logR : bool, optional If True, rgrid is in the log of R so logrs= linspace(*rgrid). interpPot : bool, optional If True, interpolate the potential. interpRforce : bool, optional If True, interpolate the radial force. interpzforce : bool, optional If True, interpolate the vertical force. interpDens : bool, optional If True, interpolate the density. interpvcirc : bool, optional If True, interpolate the circular velocity. interpdvcircdr : bool, optional If True, interpolate the derivative of the circular velocity with respect to R. interpepifreq : bool, optional If True, interpolate the epicyclic frequency. interpverticalfreq : bool, optional If True, interpolate the vertical frequency. ro : float, optional Distance scale for translation into internal units (default from configuration file). vo : float, optional Velocity scale for translation into internal units (default from configuration file). use_c : bool, optional Use C to speed up the calculation of the grid. enable_c : bool, optional Enable use of C for interpolations. zsym : bool, optional If True (default), the potential is assumed to be symmetric around z=0 (so you can use, e.g., zgrid=(0.,1.,101)). numcores : int, optional If set to an integer, use this many cores (only used for vcirc, dvcircdR, epifreq, and verticalfreq; NOT NECESSARILY FASTER, TIME TO MAKE SURE). Notes ----- - 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.0, 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.0 ) else: self._potInterp = interpolate.RectBivariateSpline( self._rgrid, self._zgrid, self._potGrid, kx=3, ky=3, s=0.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.0 ) else: self._rforceInterp = interpolate.RectBivariateSpline( self._rgrid, self._zgrid, self._rforceGrid, kx=3, ky=3, s=0.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.0 ) else: self._zforceInterp = interpolate.RectBivariateSpline( self._rgrid, self._zgrid, self._zforceGrid, kx=3, ky=3, s=0.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.0**-10.0), kx=3, ky=3, s=0.0, ) else: self._densInterp = interpolate.RectBivariateSpline( self._rgrid, self._zgrid, numpy.log(self._densGrid + 10.0**-10.0), kx=3, ky=3, s=0.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.0, t=0.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.0, t=0.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.0, t=0.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.0, t=0.0): from ..potential import evaluateRzderivs return evaluateRzderivs(self._origPot, R, z) @scalarVectorDecorator @zsymDecorator(False) def _dens(self, R, z, phi=0.0, t=0.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.0**-10.0 ) else: out[indx] = ( numpy.exp(self._densInterp.ev(R[indx], z[indx])) - 10.0**-10.0 ) 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): """ Calculate the potential on a grid. Parameters ---------- pot : Potential or list of such instances Potential object(s) to calculate the potential from. R : numpy.ndarray Grid in R. z : numpy.ndarray Grid in z. rforce : bool, optional If True, calculate the radial force instead. Default is False. zforce : bool, optional If True, calculate the vertical force instead. Default is False. Returns ------- numpy.ndarray Potential on the grid (2D array). Notes ----- - 2013-01-24 - Written - Bovy (IAS) - 2013-01-29 - Added forces - Bovy (IAS) """ from ..orbit.integrateFullOrbit import ( # here bc otherwise there is an infinite loop _parse_pot, ) 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): """ Calculate spline coefficients for a 2D array. Parameters ---------- array2d : numpy.ndarray 2D array to calculate spline coefficients for. Returns ------- ndarray New array with spline coefficients. Notes ----- - 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): """ Use C to evaluate the interpolated potential. Parameters ---------- pot : Potential or list of such instances The potential R : numpy.ndarray Galactocentric cylindrical radius. z : numpy.ndarray Galactocentric height. Returns ------- numpy.ndarray Potential evaluated at R and z. Notes ----- - 2013-01-24: Written - Bovy (IAS) """ from ..orbit.integrateFullOrbit import ( # here bc otherwise there is an infinite loop _parse_pot, ) 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): """ Use C to evaluate the interpolated potential's forces Parameters ---------- pot : Potential or list of such instances The potential R : numpy.ndarray Galactocentric cylindrical radius. z : numpy.ndarray Galactocentric height. zforce : bool, optional If True, return the vertical force, otherwise return the radial force. Default is False. Returns ------- numpy.ndarray Force evaluated at R and z. Notes ----- - 2013-01-29: Written - Bovy (IAS) """ from ..orbit.integrateFullOrbit import ( # here bc otherwise there is an infinite loop _parse_pot, ) 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.0)] = -1.0 return out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/interpSphericalPotential.py0000644000175100001660000001315414761352023023022 0ustar00runnerdocker###################3###################3###################3################## # interpSphericalPotential.py: build spherical potential through interpolation ###################3###################3###################3################## import numpy from scipy import interpolate from ..util._optional_deps import _JAX_LOADED from ..util.conversion import get_physical, physical_compatible from .Potential import _evaluatePotentials, _evaluateRforces from .SphericalPotential import SphericalPotential if _JAX_LOADED: import jax.numpy as jnp 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, ): """ Initialize an interpolated, spherical potential. Parameters ---------- rforce : function or galpy Potential instance or list thereof, optional Either a function that gives the radial force (in internal units) as a function of r (in internal units) or a galpy Potential instance or list thereof. The default is None. rgrid : numpy.ndarray, optional Radial grid in internal units on which to evaluate the potential for interpolation (note that beyond rgrid[-1], the potential is extrapolated as -GM( 10000 else numpy.geomspace( 1e-3 if rgrid[0] == 0.0 else rgrid[0], rgrid[-1], 10001 ) ) # Determine whether rforce is a galpy Potential or list thereof try: _evaluateRforces(rforce, 1.0, 0.0) except: _rforce = rforce Phi0 = 0.0 if Phi0 is None else Phi0 else: _rforce = lambda r: _evaluateRforces(rforce, r, 0.0) # Determine Phi0 Phi0 = _evaluatePotentials(rforce, rgrid[0], 0.0) # Also check that unit systems are compatible if not physical_compatible(self, rforce): raise RuntimeError( "Unit conversion factors ro and vo incompatible between Potential to be interpolated and the factors given to interpSphericalPotential" ) # If set for the parent, set for the interpolated phys = get_physical(rforce, include_set=True) if phys["roSet"]: self.turn_physical_on(ro=phys["ro"]) if phys["voSet"]: self.turn_physical_on(vo=phys["vo"]) self._rforce_grid = numpy.array([_rforce(r) for r in rgrid]) self._force_spline = interpolate.InterpolatedUnivariateSpline( self._rgrid, self._rforce_grid, k=3, ext=0 ) self._rforce_jax_grid = numpy.array( [self._force_spline(r) for r in self._rforce_jax_rgrid] ) # Get potential and r2deriv as splines for the integral and derivative self._pot_spline = self._force_spline.antiderivative() self._Phi0 = Phi0 + self._pot_spline(self._rgrid[0]) self._r2deriv_spline = self._force_spline.derivative() # Extrapolate as mass within rgrid[-1] self._rmin = rgrid[0] self._rmax = rgrid[-1] self._total_mass = -(self._rmax**2.0) * self._force_spline(self._rmax) self._Phimax = ( -self._pot_spline(self._rmax) + self._Phi0 + self._total_mass / self._rmax ) self.hasC = True self.hasC_dxdv = True self.hasC_dens = True return None def _revaluate(self, r, t=0.0): out = numpy.empty_like(r) out[r >= 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.0): out = numpy.empty_like(r) out[r >= self._rmax] = -self._total_mass / r[r >= self._rmax] ** 2.0 out[r < self._rmax] = self._force_spline(r[r < self._rmax]) return out def _rforce_jax(self, r): if not _JAX_LOADED: # pragma: no cover raise ImportError( "Making use of _rforce_jax function requires the google/jax library" ) return jnp.interp(r, self._rforce_jax_rgrid, self._rforce_jax_grid) def _r2deriv(self, r, t=0.0): out = numpy.empty_like(r) out[r >= self._rmax] = -2.0 * self._total_mass / r[r >= self._rmax] ** 3.0 out[r < self._rmax] = -self._r2deriv_spline(r[r < self._rmax]) return out def _rdens(self, r, t=0.0): out = numpy.empty_like(r) out[r >= self._rmax] = 0.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=1741018143.6638844 galpy-1.10.2/galpy/potential/interppotential_c_ext/0000755000175100001660000000000014761352040022032 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/interppotential_c_ext/interppotential_calc_potential.c0000644000175100001660000001334514761352023030467 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=1741018131.0 galpy-1.10.2/galpy/potential/linearPotential.py0000644000175100001660000002771014761352023021143 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, potential_positional_arg class linearPotential: """Class representing 1D potentials""" def __init__(self, amp=1.0, ro=None, vo=None): self._amp = amp self.dim = 1 self.isDissipative = False 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): """ Multiply a linearPotential's amplitude by a number Parameters ---------- b : int or float Number to multiply the amplitude of the linearPotential instance with. Returns ------- linearPotential instance New instance with amplitude = (old amplitude) x b. Notes ----- - 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.0 / b) __truediv__ = __div__ def __add__(self, b): """ Add linearPotential instances together to create a multi-component potential (e.g., pot= pot1+pot2+pot3) Parameters ---------- b : linearPotential instance or a list thereof Returns ------- list of linearPotential instances Represents the combined potential Notes ----- - 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): """ Turn off automatic returning of outputs in physical units. Returns ------- None Notes ----- - 2016-01-30 - Written - Bovy (UofT) """ self._roSet = False self._voSet = False return None def turn_physical_on(self, ro=None, vo=None): """ Turn on automatic returning of outputs in physical units. Parameters ---------- ro : float or Quantity, optional Reference distance (kpc). vo : float or Quantity, optional Reference velocity (km/s). Returns ------- None Notes ----- - 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 ro = conversion.parse_length_kpc(ro) if not ro is None: self._ro = ro if not vo is False: self._voSet = True vo = conversion.parse_velocity_kms(vo) if not vo is None: self._vo = vo return None @potential_physical_input @physical_conversion("energy", pop=True) def __call__(self, x, t=0.0): """ Evaluate the potential. Parameters ---------- x : float or Quantity Position. t : float or Quantity, optional Time (default: 0.0). Returns ------- float or Quantity Potential at position x and time t. Notes ----- - 2010-07-12 - Written - Bovy (NYU) """ return self._call_nodecorator(x, t=t) def _call_nodecorator(self, x, t=0.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.0): """ Evaluate the force. Parameters ---------- x : float or Quantity Position. t : float or Quantity, optional Time (default: 0.0). Returns ------- float or Quantity Force at position x and time t. Notes ----- - 2010-07-12 - Written - Bovy (NYU) """ return self._force_nodecorator(x, t=t) def _force_nodecorator(self, x, t=0.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.0, min=-15.0, max=15, ns=21, savefilename=None): """ Plot the potential Parameters ---------- t : float or Quantity, optional The time at which to evaluate the forces. Default is 0.0. min : float, optional Minimum x. max : float, optional Maximum x. ns : int, optional Grid in x. savefilename : str, optional Save to or restore from this savefile (pickle). Returns ------- plot to output device Notes ----- - 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_positional_arg @potential_physical_input @physical_conversion("energy", pop=True) def evaluatelinearPotentials(Pot, x, t=0.0): """ Evaluate the sum of a list of potentials. Parameters ---------- Pot : list of linearPotential instance(s) The list of potentials to evaluate. x : float or Quantity The position at which to evaluate the potentials. t : float or Quantity, optional The time at which to evaluate the potentials. Default is 0.0. Returns ------- float or Quantity The value of the potential at the given position and time. Notes ----- - 2010-07-13 - Written - Bovy (NYU) """ return _evaluatelinearPotentials(Pot, x, t=t) def _evaluatelinearPotentials(Pot, x, t=0.0): """Raw, undecorated function for internal use""" if isinstance(Pot, list): sum = 0.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_positional_arg @potential_physical_input @physical_conversion("force", pop=True) def evaluatelinearForces(Pot, x, t=0.0): """ Evaluate the forces due to a list of potentials. Parameters ---------- Pot : list of linearPotential instance(s) The list of potentials to evaluate. x : float or Quantity The position at which to evaluate the forces. t : float or Quantity, optional The time at which to evaluate the forces. Default is 0.0. Returns ------- float or Quantity The value of the forces at the given position and time. Notes ----- - 2010-07-13 - Written - Bovy (NYU) """ return _evaluatelinearForces(Pot, x, t=t) def _evaluatelinearForces(Pot, x, t=0.0): """Raw, undecorated function for internal use""" if isinstance(Pot, list): sum = 0.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.0, min=-15.0, max=15, ns=21, savefilename=None): """ Plot a combination of potentials Parameters ---------- Pot : list of linearPotential instance(s) The list of potentials to evaluate. t : float or Quantity, optional The time at which to evaluate the forces. Default is 0.0. min : float, optional Minimum x. max : float, optional Maximum x. ns : int, optional Grid in x. savefilename : str, optional Save to or restore from this savefile (pickle). Returns ------- plot to output device Notes ----- - 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=1741018131.0 galpy-1.10.2/galpy/potential/mwpot_helpers.py0000644000175100001660000000263614761352023020701 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.0: return 0.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.0: return 0.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=1741018131.0 galpy-1.10.2/galpy/potential/mwpotentials.py0000644000175100001660000001342514761352023020535 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=0.6), NFWPotential(a=4.5, normalize=0.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.0), MiyamotoNagaiPotential(a=3.0 / 8.0, b=0.28 / 8.0, normalize=0.6), NFWPotential(a=2.0, 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=1741018131.0 galpy-1.10.2/galpy/potential/planarDissipativeForce.py0000644000175100001660000001421514761352023022446 0ustar00runnerdocker############################################################################### # planarDissipativeForce.py: top-level class for non-conservative 2D forces ############################################################################### import numpy from ..util.conversion import physical_conversion, potential_physical_input from .planarForce import planarForce class planarDissipativeForce(planarForce): """Top-level class for non-conservative forces (cannot be derived from a potential function)""" def __init__(self, amp, ro=None, vo=None, amp_units=None): """ Initialize a planarDissipativeForce object. Parameters ---------- amp : float Amplitude to be applied when evaluating the potential and its forces. ro : float or Quantity, optional Distance from the Galactic center to the observer, in kpc. Default from the configuration file. vo : float or Quantity, optional Circular velocity at the Solar radius, in km/s. Default is from the configuration file. amp_units : str, optional Units of the amplitude. Default is None. Notes ----- - 2023-05-29 - Started - Bovy (UofT) """ planarForce.__init__(self, amp=amp, ro=ro, vo=vo) self.isDissipative = True @potential_physical_input @physical_conversion("force", pop=True) def Rforce(self, R, phi=0.0, t=0.0, v=None): """ Evaluate cylindrical radial force F_R (R,phi). Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. phi : float or Quantity, optional Azimuth. Default is 0.0. t : float or Quantity, optional Time. Default is 0.0. v : numpy.ndarray, optional 2D cylindrical velocity. Default is None. Returns ------- float or Quantity Cylindrical radial force. Notes ----- - 2023-05-29: Written by Bovy (UofT). """ return self._Rforce_nodecorator(R, phi=phi, t=t, v=v) def _Rforce_nodecorator(self, R, phi=0.0, t=0.0, v=None): # Separate, so it can be used during orbit integration try: return self._amp * self._Rforce(R, phi=phi, t=t, v=v) except AttributeError: # pragma: no cover from .Potential import PotentialError raise PotentialError( "'_Rforce' function not implemented for this planarDissipativeForce" ) @potential_physical_input @physical_conversion("force", pop=True) def phitorque(self, R, phi=0.0, t=0.0, v=None): """ Evaluate the azimuthal torque F_phi (R, phi, t, v). Parameters ---------- R : float or Quantity Cylindrical Galactocentric radius. phi : float or Quantity, optional Azimuth. Default is 0.0. t : float or Quantity, optional Time. Default is 0.0. v : numpy.ndarray, optional 2D cylindrical velocity. Default is None. Returns ------- float or Quantity Azimuthal torque. Notes ----- - 2023-05-29: Written - Bovy (UofT) """ return self._phitorque_nodecorator(R, phi=phi, t=t, v=v) def _phitorque_nodecorator(self, R, phi=0.0, t=0.0, v=None): # Separate, so it can be used during orbit integration try: return self._amp * self._phitorque(R, 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.0 class planarDissipativeForceFromFullDissipativeForce(planarDissipativeForce): """Class that represents a planar dissipative force derived from a 3D dissipative force""" def __init__(self, Pot): """ Initialize the planarDissipativeForce instance. Parameters ---------- Pot : DissipativeForce instance The instance of the DissipativeForce class. Returns ------- None Notes ----- - 2023-05-29 - Written - Bovy (UofT) """ planarDissipativeForce.__init__(self, amp=1.0, 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 _Rforce(self, R, phi=0.0, t=0.0, v=None): r""" Evaluate the radial force. Parameters ---------- R : float Galactocentric radius. phi : float, optional Azimuth (in radians). Default is 0.0. t : float, optional Time. Default is 0.0. v : numpy.ndarray, optional Velocity in cylindrical coordinates (vR, vT, vz). Default is None. Returns ------- float or Quantity Radial force F_R(R(,\phi,t,v)) Notes ----- - 2023-09-29 - Written - Bovy (UofT) """ return self._Pot.Rforce( R, 0.0, phi=phi, t=t, v=[v[0], v[1], 0.0], use_physical=False ) def _phitorque(self, R, phi=0.0, t=0.0, v=None): r""" Evaluate the azimuthal torque. Parameters ---------- R : float Galactocentric radius. phi : float, optional Azimuth (in radians). Default is 0.0. t : float, optional Time. Default is 0.0. v : numpy.ndarray, optional Velocity in cylindrical coordinates (vR, vT, vz). Default is None. Returns ------- float or Quantity Azimuthal torque tau_phi(R(,\phi,t,v)) Notes ----- - 2029-05-29 - Written - Bovy (UofT) """ return self._Pot.phitorque( R, 0.0, phi=phi, t=t, v=[v[0], v[1], 0.0], use_physical=False ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/planarForce.py0000644000175100001660000001330414761352023020237 0ustar00runnerdocker############################################################################### # plabarForce.py: top-level class for a 2D force, conservative # (planarPotential) or not (planarDissipativeForce) # ############################################################################### 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, ) from .Force import Force if _APY_LOADED: from astropy import units class planarForce: """Top-level class for any 2D force, conservative or dissipative""" def __init__(self, amp=1.0, ro=None, vo=None): """ Initialize 2D Force. Parameters ---------- amp : float Amplitude to be applied when evaluating the potential and its forces. ro : float or Quantity, optional Physical distance scale (in kpc or as Quantity). Default is from the configuration file. vo : float or Quantity, optional Physical velocity scale (in km/s or as Quantity). Default is from the configuration file. Notes ----- - 2023-05-29 - Written to generalize planarPotential to force that may or may not be conservative - Bovy (UofT) """ self._amp = amp self.dim = 2 self.isNonAxi = True 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): """ Multiply a planarPotential's amplitude by a number. Parameters ---------- b : number The number to multiply the amplitude by. Returns ------- planarPotential instance A new instance with amplitude = (old amplitude) x b. Notes ----- - 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.0 / b) __truediv__ = __div__ def __add__(self, b): """ Add planarPotential instances together to create a multi-component potential (e.g., pot= pot1+pot2+pot3) Parameters ---------- b : planarPotential instance or a list thereof Returns ------- list of planarPotential instances Represents the combined potential Notes ----- - 2019-01-27 - Written - Bovy (UofT) """ from ..potential import flatten as flatten_pot if not isinstance(flatten_pot([b])[0], (Force, planarForce)): 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], (Force, planarForce)): 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): """ Turn off automatic returning of outputs in physical units. Returns ------- None Notes ----- - 2016-01-30 - Written - Bovy (UofT) """ self._roSet = False self._voSet = False return None def turn_physical_on(self, ro=None, vo=None): """ Turn on automatic returning of outputs in physical units. Parameters ---------- ro : float or Quantity, optional Reference distance in kpc. Default is None. vo : float or Quantity, optional Reference velocity in km/s. Default is None. Returns ------- None Notes ----- - 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 ro = conversion.parse_length_kpc(ro) if not ro is None: self._ro = ro if not vo is False: self._voSet = True vo = conversion.parse_velocity_kms(vo) if not vo is None: self._vo = vo return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/planarPotential.py0000644000175100001660000011757114761352023021153 0ustar00runnerdockerimport os import pickle 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 DissipativeForce, _isDissipative from .planarDissipativeForce import planarDissipativeForceFromFullDissipativeForce from .planarForce import planarForce from .plotEscapecurve import _INF, plotEscapecurve from .plotRotcurve import plotRotcurve from .Potential import ( Potential, PotentialError, flatten, lindbladR, potential_positional_arg, ) class planarPotential(planarForce): r"""Class representing 2D (R,\phi) potentials""" def __init__(self, amp=1.0, ro=None, vo=None): planarForce.__init__(self, amp=amp, ro=ro, vo=vo) self.isDissipative = False @potential_physical_input @physical_conversion("energy", pop=True) def __call__(self, R, phi=0.0, t=0.0, dR=0, dphi=0): """ Evaluate the potential. Parameters ---------- R : float or Quantity Cylindrical radius. phi : float or Quantity, optional Azimuth (default: 0). t : float or Quantity, optional Time (default: 0). dR : int, optional Order of radial derivative (default: 0). dphi : int, optional Order of azimuthal derivative (default: 0). Returns ------- float or Quantity Potential at (R, phi, t) or its derivative. Notes ----- - 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.0, t=0.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.0, t=0.0): r""" Evaluate the radial force. Parameters ---------- R : float or Quantity Cylindrical radius. phi : float or Quantity, optional Azimuth (default 0.0). t : float or Quantity, optional Time (default 0.0). Returns ------- float or Quantity Cylindrical radial force F_R(R, (\phi, t)). Notes ----- - 2010-07-13 - Written - Bovy (NYU) """ return self._Rforce_nodecorator(R, phi=phi, t=t) def _Rforce_nodecorator(self, R, phi=0.0, t=0.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" ) @potential_physical_input @physical_conversion("energy", pop=True) def phitorque(self, R, phi=0.0, t=0.0): """ Evaluate the azimuthal torque = - d Phi / d phi. Parameters ---------- R : float or Quantity Cylindrical radius. phi : float or Quantity, optional Azimuth (default 0.0). t : float or Quantity, optional Time (default 0.0). Returns ------- float or Quantity Azimuthal torque tau_phi(R, (phi, t)). Notes ----- - 2010-07-13 - Written - Bovy (NYU) """ return self._phitorque_nodecorator(R, phi=phi, t=t) def _phitorque_nodecorator(self, R, phi=0.0, t=0.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.0, t=0.0): """ Evaluate the second radial derivative. Parameters ---------- R : float or Quantity Cylindrical radius. phi : float or Quantity, optional Azimuth (default 0.0). t : float or Quantity, optional Time (default 0.0). Returns ------- float or Quantity Second radial derivative d2phi/dR2 of the potential. Notes ----- - 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.0, t=0.0): """ Evaluate the second azimuthal derivative. Parameters ---------- R : float or Quantity Cylindrical radius. phi : float or Quantity, optional Azimuth (default 0.0). t : float or Quantity, optional Time (default 0.0). Returns ------- float or Quantity Second azimuthal derivative d2phi/dazi2 of the potential. Notes ----- - 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.0, t=0.0): """ Evaluate the mixed radial and azimuthal derivative. Parameters ---------- R : float or Quantity Cylindrical radius. phi : float or Quantity, optional Azimuth (default 0.0). t : float or Quantity, optional Time (default 0.0). Returns ------- float or Quantity Mixed radial and azimuthal derivative d2phi/dR dazi of the potential. Notes ----- - 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): """ Plot the potential. Parameters ---------- Rrange : float or Quantity, optional Range (can be Quantity). grid : int, optional Number of points to plot. savefilename : str, optional Save to or restore from this savefile (pickle). *args : list Arguments to be passed to `galpy.util.plot.plot`. **kwargs : dict Keyword arguments to be passed to `galpy.util.plot.plot`. Returns ------- plot Notes ----- - 2010-07-13 - Written - Bovy (NYU) """ return plotplanarPotentials(self, *args, **kwargs) class planarAxiPotential(planarPotential): """Class representing axisymmetric planar potentials""" def __init__(self, amp=1.0, ro=None, vo=None): planarPotential.__init__(self, amp=amp, ro=ro, vo=vo) self.isNonAxi = False def _phitorque(self, R, phi=0.0, t=0.0): return 0.0 def _phi2deriv(self, R, phi=0.0, t=0.0): # pragma: no cover """ Evaluate the second azimuthal derivative for this potential. Parameters ---------- R : float Galactocentric cylindrical radius. phi : float, optional Azimuth. t : float, optional Time. Returns ------- float The second azimuthal derivative. Notes ----- - 2011-10-17 - Written - Bovy (IAS) """ return 0.0 def _Rphideriv(self, R, phi=0.0, t=0.0): # pragma: no cover """ Evaluate the radial+azimuthal derivative for this potential. Parameters ---------- R : float Galactocentric cylindrical radius. phi : float, optional Azimuth. t : float, optional Time. Returns ------- float The radial+azimuthal derivative. Notes ----- - 2011-10-17 - Written - Bovy (IAS) """ return 0.0 @potential_physical_input @physical_conversion("velocity", pop=True) def vcirc(self, R, phi=None, t=0.0): """ Calculate the circular velocity at R in potential Pot. Parameters ---------- R : float or Quantity Galactocentric radius. phi : float or Quantity, optional Azimuth to use for non-axisymmetric potentials. t : float or Quantity, optional Time (default: 0.0) Returns ------- float or Quantity Circular rotation velocity. Notes ----- - 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.0): """ Calculate the circular angular speed at R in potential Pot. Parameters ---------- R : float or Quantity Galactocentric radius. t : float or Quantity, optional Time (default: 0.0) Returns ------- float or Quantity Circular angular speed. Notes ----- - Written on 2011-10-09 by 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.0): """ Calculate the epicycle frequency at R in this potential. Parameters ---------- R : float or Quantity Galactocentric radius. t : float or Quantity, optional Time (default: 0.0). Returns ------- float or Quantity Epicycle frequency. Notes ----- - Written on 2011-10-09 by Bovy (IAS). """ return numpy.sqrt( self.R2deriv(R, t=t, use_physical=False) - 3.0 / R * self.Rforce(R, t=t, use_physical=False) ) @physical_conversion("position", pop=True) def lindbladR(self, OmegaP, m=2, t=0.0, **kwargs): """ Calculate the radius of a Lindblad resonance. Parameters ---------- OmegaP : float or Quantity Pattern speed. m : int or str, optional Order of the resonance (as in m(O-Op)=kappa (negative m for outer)). Use m='corotation' for corotation. t : float or Quantity, optional Time (default: 0.0). **kwargs Additional arguments passed to `scipy.optimize.brentq`. Returns ------- float or Quantity or None Radius of Lindblad resonance. None if there is no resonance. Notes ----- - Written on 2011-10-09 by 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.0): """ Calculate the escape velocity at R for the potential. Parameters ---------- R : float or Quantity Galactocentric radius. t : float or Quantity, optional Time (default: 0.0). Returns ------- float or Quantity Escape velocity. Notes ----- - Written on 2011-10-09 by Bovy (IAS). """ return numpy.sqrt( 2.0 * (self(_INF, t=t, use_physical=False) - self(R, t=t, use_physical=False)) ) def plotRotcurve(self, *args, **kwargs): """ Plot the rotation curve for this potential. Parameters ---------- Rrange : list or Quantity, optional Range to plot. grid : int, optional Number of points to plot. savefilename : str, optional Save to or restore from this savefile (pickle). *args, **kwargs : Arguments and keyword arguments for `galpy.util.plot.plot`. Returns ------- matplotlib plot Plot to output device. Notes ----- - 2010-07-13 - Written - Bovy (NYU) """ return plotRotcurve(self, *args, **kwargs) def plotEscapecurve(self, *args, **kwargs): """ Plot the escape velocity curve for this potential. Parameters ---------- Rrange : list or Quantity, optional Range to plot. grid : int, optional Number of points to plot. savefilename : str, optional Save to or restore from this savefile (pickle). *args, **kwargs : Arguments and keyword arguments for `galpy.util.plot.plot`. Returns ------- matplotlib plot Plot to output device. Notes ----- - 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): """ Initialize. Parameters ---------- RZPot : RZPotential instance RZPotential instance. Returns ------- planarAxiPotential instance Notes ----- - 2010-07-13 - Written - Bovy (NYU) """ planarAxiPotential.__init__(self, amp=1.0, 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.0, t=0.0): """ Evaluate the potential. Parameters ---------- R : float Galactocentric radius. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float Potential at (R, phi, t). Notes ----- - 2010-07-13 - Written - Bovy (NYU) """ return self._Pot(R, 0.0, t=t, use_physical=False) def _Rforce(self, R, phi=0.0, t=0.0): """ Evaluate the radial force. Parameters ---------- R : float Galactocentric radius. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float Radial force at (R, phi, t). Notes ----- - Written on 2010-07-13 by Bovy (NYU). """ return self._Pot.Rforce(R, 0.0, t=t, use_physical=False) def _R2deriv(self, R, phi=0.0, t=0.0): """ Evaluate the second radial derivative. Parameters ---------- R : float Galactocentric radius. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float Second radial derivative at (R, phi, t). Notes ----- - 2011-10-09: Written by Bovy (IAS). """ return self._Pot.R2deriv(R, 0.0, t=t, use_physical=False) def RZToplanarPotential(RZPot): """ Convert an RZPotential to a planarPotential in the mid-plane (z=0). Parameters ---------- RZPot : RZPotential instance or list of such instances Existing planarPotential instances are just copied to the output. Returns ------- planarPotential instance(s) Notes ----- - 2010-07-13 - Written - Bovy (NYU) """ RZPot = flatten(RZPot) if _isDissipative(RZPot): raise NotImplementedError( "Converting dissipative forces to 2D axisymmetric 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): """ Initialize. Parameters ---------- Pot : Potential instance Potential instance. Returns ------- planarPotential instance Notes ----- - 2016-06-02 - Written - Bovy (UofT) """ planarPotential.__init__(self, amp=1.0, 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.0, t=0.0): """ Evaluate the potential. Parameters ---------- R : float Cylindrical radius. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float Potential at (R, phi, t). Notes ----- - 2016-06-02: Written - Bovy (UofT) """ return self._Pot(R, 0.0, phi=phi, t=t, use_physical=False) def _Rforce(self, R, phi=0.0, t=0.0): """ Evaluate the radial force. Parameters ---------- R : float Cylindrical radius. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float Radial force at (R, phi, t). Notes ----- - Written on 2016-06-02 by Bovy (UofT) """ return self._Pot.Rforce(R, 0.0, phi=phi, t=t, use_physical=False) def _phitorque(self, R, phi=0.0, t=0.0): """ Evaluate the azimuthal torque. Parameters ---------- R : float Cylindrical radius. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float Azimuthal torque at (R, phi, t). Notes ----- - 2016-06-02: Written - Bovy (UofT) """ return self._Pot.phitorque(R, 0.0, phi=phi, t=t, use_physical=False) def _R2deriv(self, R, phi=0.0, t=0.0): """ Evaluate the second radial derivative. Parameters ---------- R : float Cylindrical radius. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float Second radial derivative at (R, phi, t). Notes ----- - 2016-06-02: Written - Bovy (UofT) """ return self._Pot.R2deriv(R, 0.0, phi=phi, t=t, use_physical=False) def _phi2deriv(self, R, phi=0.0, t=0.0): """ Evaluate the second azimuthal derivative. Parameters ---------- R : float Cylindrical radius. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float Second azimuthal derivative at (R, phi, t). Notes ----- - 2016-06-02: Written - Bovy (UofT) """ return self._Pot.phi2deriv(R, 0.0, phi=phi, t=t, use_physical=False) def _Rphideriv(self, R, phi=0.0, t=0.0): """ Evaluate the mixed radial-azimuthal derivative. Parameters ---------- R : float Cylindrical radius. phi : float, optional Azimuth (default: 0.0). t : float, optional Time (default: 0.0). Returns ------- float Mixed radial-azimuthal derivative at (R, phi, t). Notes ----- - 2016-06-02: Written - Bovy (UofT) """ return self._Pot.Rphideriv(R, 0.0, phi=phi, t=t, use_physical=False) def OmegaP(self): """ Return the pattern speed. Returns ------- float Pattern speed. Notes ----- - 2016-05-31: Written - Bovy (UofT) """ return self._Pot.OmegaP() def toPlanarPotential(Pot): """ Convert an Potential to a planarPotential in the mid-plane (z=0). Parameters ---------- Pot : Potential instance or list of such instances Existing planarPotential instances are just copied to the output. Returns ------- planarPotential, planarAxiPotential, or planarDissipativeForce instance(s) Notes ----- - 2016-06-11: Written - Bovy (UofT) """ Pot = flatten(Pot) if isinstance(Pot, list): out = [] for pot in Pot: if isinstance(pot, planarForce): out.append(pot) elif isinstance(pot, Potential) and pot.isNonAxi: out.append(planarPotentialFromFullPotential(pot)) elif isinstance(pot, Potential): out.append(planarPotentialFromRZPotential(pot)) elif isinstance(pot, DissipativeForce): out.append(planarDissipativeForceFromFullDissipativeForce(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 elif isinstance(Pot, DissipativeForce): return planarDissipativeForceFromFullDissipativeForce(Pot) else: raise PotentialError( "Input to 'toPlanarPotential' is neither an Potential-instance or a list of such instances" ) @potential_positional_arg @potential_physical_input @physical_conversion("energy", pop=True) def evaluateplanarPotentials(Pot, R, phi=None, t=0.0, dR=0, dphi=0): """ Evaluate a (list of) planarPotential instance(s). Parameters ---------- Pot : planarPotential or list of planarPotential A (list of) planarPotential instance(s). R : float or Quantity Cylindrical radius. phi : float or Quantity, optional Azimuth (default None). t : float or Quantity, optional Time (default 0.0). dR : int, optional If set to a non-zero integer, return the dR derivative instead. Default is 0. dphi : int, optional If set to a non-zero integer, return the dphi derivative instead. Default is 0. Returns ------- float or Quantity Potential Phi(R(,phi,t)). Notes ----- - 2010-07-13 - Written - Bovy (NYU) """ from .Potential import _isNonAxi 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" ) return _evaluateplanarPotentials(Pot, R, phi=phi, t=t, dR=dR, dphi=dphi) def _evaluateplanarPotentials(Pot, R, phi=None, t=0.0, dR=0, dphi=0): isList = isinstance(Pot, list) if isList and numpy.all([isinstance(p, planarPotential) for p in Pot]): sum = 0.0 for pot in Pot: if pot.isNonAxi: 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 Pot.isNonAxi: 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_positional_arg @potential_physical_input @physical_conversion("force", pop=True) def evaluateplanarRforces(Pot, R, phi=None, t=0.0, v=None): """ Evaluate the cylindrical radial force of a (list of) planarPotential instance(s). Parameters ---------- Pot : (list of) planarPotential instance(s) The potential(s) to evaluate. R : float or Quantity Cylindrical radius. phi : float or Quantity, optional Azimuth (default: None). t : float or Quantity, optional Time (default: 0.0). v : numpy.ndarray or Quantity, optional Current velocity in cylindrical coordinates (default: None). Required when including dissipative forces. Returns ------- float or Quantity The cylindrical radial force F_R(R, phi, t). Notes ----- - 2010-07-13 - Written - Bovy (NYU) - 2023-05-29 - Added velocity input for dissipative forces - Bovy (UofT) """ from .Potential import _isNonAxi nonAxi = _isNonAxi(Pot) if nonAxi and phi is None: raise PotentialError( "The (list of) planarForce instances is non-axisymmetric, but you did not provide phi" ) dissipative = _isDissipative(Pot) if dissipative and v is None: raise PotentialError( "The (list of) planarForce instances includes dissipative components, but you did not provide the 2D velocity (required for dissipative forces)" ) return _evaluateplanarRforces(Pot, R, phi=phi, t=t, v=v) def _evaluateplanarRforces(Pot, R, phi=None, t=0.0, v=None): """Raw, undecorated function for internal use""" isList = isinstance(Pot, list) if isinstance(Pot, list) and numpy.all([isinstance(p, planarForce) for p in Pot]): sum = 0.0 for pot in Pot: if pot.isDissipative: sum += pot._Rforce_nodecorator(R, phi=phi, t=t, v=v) elif pot.isNonAxi: sum += pot._Rforce_nodecorator(R, phi=phi, t=t) else: sum += pot._Rforce_nodecorator(R, t=t) return sum elif not isList and Pot.isDissipative: return Pot._Rforce_nodecorator(R, phi=phi, t=t, v=v) elif isinstance(Pot, planarPotential): if Pot.isNonAxi: 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" ) @potential_positional_arg @potential_physical_input @physical_conversion("energy", pop=True) def evaluateplanarphitorques(Pot, R, phi=None, t=0.0, v=None): """ Evaluate the phi torque of a (list of) planarPotential instance(s). Parameters ---------- Pot : (list of) planarPotential instance(s) The potential(s) to evaluate. R : float or Quantity Cylindrical radius phi : float or Quantity, optional Azimuth (default: None) t : float or Quantity, optional Time (default: 0.0) v : numpy.ndarray or Quantity, optional Current velocity in cylindrical coordinates (default: None) Required when including dissipative forces. Returns ------- float or Quantity The phitorque tau_phi(R, phi, t). Notes ----- - 2010-07-13 - Written - Bovy (NYU) - 2023-05-29 - Added velocity input for dissipative forces - Bovy (UofT) """ from .Potential import _isNonAxi 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" ) dissipative = _isDissipative(Pot) if dissipative and v is None: raise PotentialError( "The (list of) planarForce instances includes dissipative components, but you did not provide the 2D velocity (required for dissipative forces)" ) return _evaluateplanarphitorques(Pot, R, phi=phi, t=t, v=v) def _evaluateplanarphitorques(Pot, R, phi=None, t=0.0, v=None): isList = isinstance(Pot, list) if isinstance(Pot, list) and numpy.all([isinstance(p, planarForce) for p in Pot]): sum = 0.0 for pot in Pot: if pot.isDissipative: sum += pot._phitorque_nodecorator(R, phi=phi, t=t, v=v) elif pot.isNonAxi: sum += pot._phitorque_nodecorator(R, phi=phi, t=t) else: sum += pot._phitorque_nodecorator(R, t=t) return sum elif not isList and Pot.isDissipative: return Pot._phitorque_nodecorator(R, phi=phi, t=t, v=v) elif isinstance(Pot, planarPotential): if Pot.isNonAxi: 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_positional_arg @potential_physical_input @physical_conversion("forcederivative", pop=True) def evaluateplanarR2derivs(Pot, R, phi=None, t=0.0): """ Evaluate the second radial derivative of planarPotential instance(s). Parameters ---------- Pot : (list of) planarPotential instance(s) The potential(s) to evaluate. R : float or Quantity Cylindrical radius phi : float or Quantity, optional Azimuth (default: None) t : float or Quantity, optional Time (default: 0.0) Returns ------- float or Quantity The second potential derivative d2Phi/dR2(R, phi, t). Notes ----- - 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.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 ): r""" Calculate the Lin & Shu (1966) reduction factor: the reduced linear response of a kinematically-warm stellar disk to a perturbation Parameters ---------- axiPot : Potential or list of Potential instances The background, axisymmetric potential R : float or Quantity Cylindrical radius sigmar : float or Quantity Radial velocity dispersion of the population nonaxiPot : Potential object, optional A non-axisymmetric Potential instance (such as SteadyLogSpiralPotential) that has functions that return OmegaP, m, and wavenumber. Either provide nonaxiPot or m, k, OmegaP. k : float or Quantity, optional Wavenumber (see Binney & Tremaine 2008). Either provide nonaxiPot or m, k, OmegaP. m : int, optional m in the perturbation's m x phi (number of arms for a spiral). Either provide nonaxiPot or m, k, OmegaP. OmegaP : float or Quantity, optional Pattern speed. Note that in the usual Lin-Shu formula \omega = m x OmegaP. Either provide nonaxiPot or m, k, OmegaP. Returns ------- float The reduction factor Notes ----- - 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) # We define omega = m x OmegaP in the usual Lin-Shu formula s = m * (OmegaP - omegac(axiPot, R)) / tepif chi = sigmar**2.0 * k**2.0 / tepif**2.0 return ( (1.0 - s**2.0) / numpy.sin(numpy.pi * s) * integrate.quad( lambda t: numpy.exp(-chi * (1.0 + numpy.cos(t))) * numpy.sin(s * t) * numpy.sin(t), 0.0, numpy.pi, )[0] ) def plotplanarPotentials(Pot, *args, **kwargs): """ Plot a planar potential. Parameters ---------- Pot : Potential or list of Potential instances Potential or list of potentials to plot Rrange : list or Quantity, optional Range in R to plot (default is [0.01, 5.0]) xrange, yrange : list, optional Range in x and y to plot (can be Quantity) (default is [-5.0, 5.0]) grid, gridx, gridy : int, optional Number of points to plot (default is 100). grid for 1D plots, gridx and gridy for 2D plots savefilename : str, optional Save to or restore from this savefile (pickle) ncontours : int, optional Number of contours to plot (if applicable) *args, **kwargs : Arguments and keyword arguments for `galpy.util.plot.plot` or `galpy.util.plot.dens2d` Returns ------- plot to output device Notes ----- - 2010-07-13 - Written - Bovy (NYU) """ Pot = flatten(Pot) Rrange = kwargs.pop("Rrange", [0.01, 5.0]) xrange = kwargs.pop("xrange", [-5.0, 5.0]) yrange = kwargs.pop("yrange", [-5.0, 5.0]) 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.0 + ys[jj] ** 2.0) if xs[ii] >= 0.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 "origin" 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.0 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=1741018131.0 galpy-1.10.2/galpy/potential/plotEscapecurve.py0000644000175100001660000001400214761352023021143 0ustar00runnerdockerimport os import pickle import numpy from ..util import conversion, plot from ..util.conversion import ( parse_length, parse_velocity, physical_conversion, potential_physical_input, ) _INF = 10**12.0 def plotEscapecurve(Pot, *args, **kwargs): """ Plot the escape velocity curve for this potential (in the z=0 plane for non-spherical potentials). Parameters ---------- Pot : Potential or list of Potential instances Potential(s) for which to plot the escape velocity curve. Rrange : numpy.ndarray or Quantity, optional Range in R to consider (can be Quantity). grid : int, optional Grid in R. savefilename : str, optional Save to or restore from this savefile (pickle). *args, **kwargs : dict Arguments and keyword arguments for `galpy.util.plot.plot`. Returns ------- matplotlib.AxesSubplot Plot to output device. Notes ----- - 2010-08-08 - Written by 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 # Following just to deal with Quantity ro/vo and check whether they are set _ro = parse_length(kwargs.get("ro", None), ro=potro) _vo = parse_velocity(kwargs.get("vo", None), vo=potvo) if ( kwargs.get("use_physical", True) and (not _ro is None or roSet) and (not _vo is None or 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.0 * 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.0]) # 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.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.0): """ Calculate the escape velocity curve for this potential (in the z=0 plane for non-spherical potentials). Parameters ---------- Pot : Potential or list of Potential instances Potential or list of Potential instances. Rs : numpy.ndarray or Quantity Radius(i). t : float, optional Instantaneous time (default is 0.0). Returns ------- numpy.ndarray or Quantity Array of v_esc. Raises ------ AttributeError Escape velocity curve plotting for non-axisymmetric potentials is not currently supported. Notes ----- - 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.0): """ Calculate the escape velocity at R for potential Pot. Parameters ---------- Pot : Potential or list of Potential instances Potential or list of Potential instances. R : numpy.ndarray or Quantity Galactocentric radius. t : float, optional Time (default is 0.0). Returns ------- numpy.ndarray or Quantity Escape velocity. Notes ----- - 2011-10-09 - Written - Bovy (IAS) """ from ..potential import PotentialError, evaluateplanarPotentials try: return numpy.sqrt( 2.0 * ( 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.0 * ( evaluateplanarPotentials(Pot, _INF, t=t, use_physical=False) - evaluateplanarPotentials(Pot, R, t=t, use_physical=False) ) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/plotRotcurve.py0000644000175100001660000001707514761352023020524 0ustar00runnerdockerimport os import pickle import numpy from ..util import conversion, plot from ..util.conversion import ( parse_length, parse_velocity, physical_conversion, potential_physical_input, ) def plotRotcurve(Pot, *args, **kwargs): """ Plot the rotation curve for this potential (in the z=0 plane for non-spherical potentials). Parameters ---------- Pot : Potential or list of Potential instances Potential instance or list of such instances. Rrange : numpy.ndarray or Quantity Range in R to consider (needs to be in the units that you are plotting). grid : int, optional Number of grid points in R. phi : float or Quantity, optional Azimuth to use for non-axisymmetric potentials. savefilename : str, optional Save to or restore from this savefile (pickle). *args Arguments passed to `galpy.util.plot.plot`. **kwargs Keyword arguments passed to `galpy.util.plot.plot`. Returns ------- matplotlib.Axes Axes on which the plot was drawn. Notes ----- - 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 # Following just to deal with Quantity ro/vo and check whether they are set _ro = parse_length(kwargs.get("ro", None), ro=potro) _vo = parse_velocity(kwargs.get("vo", None), vo=potvo) if ( kwargs.get("use_physical", True) and (not _ro is None or roSet) and (not _vo is None or 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.0 * 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.0]) # 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.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.0): """ Calculate the rotation curve for this potential (in the z=0 plane for non-spherical potentials). Parameters ---------- Pot : Potential or list of Potential instances Potential instance or list of such instances. Rs : numpy.ndarray or float Radius(i). phi : float or Quantity, optional Azimuth to use for non-axisymmetric potentials. t : float or Quantity, optional Instantaneous time (default: 0.0) Returns ------- numpy.ndarray Array of circular rotation velocities. Notes ----- - 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.0): """ Calculate the circular velocity at R in potential Pot. Parameters ---------- Pot : Potential or list of Potential instances Potential instance or list of such instances. R : float or Quantity Galactocentric radius. phi : float or Quantity, optional Azimuth to use for non-axisymmetric potentials. t : float or Quantity, optional Instantaneous time (default: 0.0) Returns ------- float or Quantity Circular rotation velocity. Notes ----- - 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.0): """ Calculate the derivative of the circular velocity wrt R at R in potential Pot. Parameters ---------- Pot : Potential or list of Potential instances Potential instance or list of such instances. R : float or Quantity Galactocentric radius. phi : float or Quantity, optional Azimuth to use for non-axisymmetric potentials. t : float, optional Instantaneous time (default: 0) Returns ------- float or Quantity Derivative of the circular rotation velocity wrt R. Notes ----- - 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 ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6728845 galpy-1.10.2/galpy/potential/potential_c_ext/0000755000175100001660000000000014761352040020610 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/BurkertPotential.c0000644000175100001660000000475214761352023024263 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/ChandrasekharDynamicalFrictionForce.c0000644000175100001660000001112414761352023030011 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/CorotatingRotationWrapperPotential.c0000644000175100001660000001055714761352023030037 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/CosmphiDiskPotential.c0000644000175100001660000000516514761352023025061 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/DehnenBarPotential.c0000644000175100001660000001217314761352023024467 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/DehnenCorePotential.c0000644000175100001660000000373414761352023024656 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/DehnenPotential.c0000644000175100001660000000462314761352023024043 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/DehnenSmoothWrapperPotential.c0000644000175100001660000000735414761352023026602 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/DiskSCFPotential.c0000644000175100001660000001354414761352023024072 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/DoubleExponentialDiskPotential.c0000644000175100001660000000730514761352023027076 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/EllipsoidalPotential.c0000644000175100001660000001774614761352023025115 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/EllipticalDiskPotential.c0000644000175100001660000000566214761352023025543 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/FlattenedPowerPotential.c0000644000175100001660000000615014761352023025562 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/GaussianAmplitudeWrapperPotential.c0000644000175100001660000000657314761352023027630 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/HenonHeilesPotential.c0000644000175100001660000000212014761352023025031 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/HernquistPotential.c0000644000175100001660000000373414761352023024626 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/HomogeneousSpherePotential.c0000644000175100001660000000461614761352023026303 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/IsochronePotential.c0000644000175100001660000000431614761352023024572 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/IsothermalDiskPotential.c0000644000175100001660000000047114761352023025561 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/JaffePotential.c0000644000175100001660000000367214761352023023660 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/KGPotential.c0000644000175100001660000000105514761352023023137 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/KuzminDiskPotential.c0000644000175100001660000000320414761352023024724 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/KuzminKutuzovStaeckelPotential.c0000644000175100001660000000750114761352023027201 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/KuzminLikeWrapperPotential.c0000644000175100001660000000672414761352023026271 0ustar00runnerdocker#include #include #include // Helper functions double KuzminLikeWrapperPotential_xi(double R,double z,double a,double b2){ double asqrtbz= a + sqrt ( z * z + b2 ); return sqrt ( R * R + asqrtbz * asqrtbz ); } double KuzminLikeWrapperPotential_dxidR(double R,double z,double a,double b2){ double asqrtbz= a + sqrt ( z * z + b2 ); return R / sqrt ( R * R + asqrtbz * asqrtbz ); } double KuzminLikeWrapperPotential_dxidz(double R,double z,double a,double b2){ double sqrtbz= sqrt ( z * z + b2 ); double asqrtbz= a + sqrtbz; return asqrtbz * z / sqrt ( R * R + asqrtbz * asqrtbz ) / sqrtbz; } double KuzminLikeWrapperPotential_d2xidR2(double R,double z,double a,double b2){ double asqrtbz= a + sqrt ( z * z + b2 ); return asqrtbz * asqrtbz / pow ( R * R + asqrtbz * asqrtbz , 3.0 ); } //KuzminLikeWrapperPotential: 3 arguments: amp, a, b**2 double KuzminLikeWrapperPotentialEval(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args; double a= *(args+1); double b2= *(args+2); //Calculate potential, only used in actionAngle, so phi=0, t=0 return amp * evaluatePotentials( KuzminLikeWrapperPotential_xi(R,z,a,b2), 0.0, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg ); } double KuzminLikeWrapperPotentialRforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args; double a= *(args+1); double b2= *(args+2); //Calculate Rforce return amp * calcRforce( KuzminLikeWrapperPotential_xi(R,z,a,b2), 0.0, 0.0, t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg ) * KuzminLikeWrapperPotential_dxidR(R,z,a,b2); } double KuzminLikeWrapperPotentialzforce(double R,double z,double phi, double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args; double a= *(args+1); double b2= *(args+2); //Calculate zforce return amp * calcRforce( KuzminLikeWrapperPotential_xi(R,z,a,b2), 0.0, 0.0, t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg ) * KuzminLikeWrapperPotential_dxidz(R,z,a,b2); } double KuzminLikeWrapperPotentialPlanarRforce(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args; double a= *(args+1); double b2= *(args+2); //Calculate planarRforce return amp * calcPlanarRforce( KuzminLikeWrapperPotential_xi(R,0.0,a,b2), 0.0, t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg ) * KuzminLikeWrapperPotential_dxidR(R,0.0,a,b2); } double KuzminLikeWrapperPotentialPlanarR2deriv(double R,double phi,double t, struct potentialArg * potentialArgs){ double * args= potentialArgs->args; double amp= *args; double a= *(args+1); double b2= *(args+2); //Calculate planarRforce return amp * ( calcPlanarR2deriv( KuzminLikeWrapperPotential_xi(R,0.0,a,b2), 0.0, t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg ) * KuzminLikeWrapperPotential_dxidR(R,0.0,a,b2) * KuzminLikeWrapperPotential_dxidR(R,0.0,a,b2) - calcPlanarRforce( KuzminLikeWrapperPotential_xi(R,0.0,a,b2), 0.0, t, potentialArgs->nwrapped, potentialArgs->wrappedPotentialArg ) * KuzminLikeWrapperPotential_d2xidR2(R,0.0,a,b2) ); } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/LogarithmicHaloPotential.c0000644000175100001660000001273314761352023025711 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/LopsidedDiskPotential.c0000644000175100001660000000333314761352023025215 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/MiyamotoNagaiPotential.c0000644000175100001660000000474514761352023025405 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/MovingObjectPotential.c0000644000175100001660000001076414761352023025233 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/NFWPotential.c0000644000175100001660000000405114761352023023267 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/NonInertialFrameForce.c0000644000175100001660000002103414761352023025131 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); // Reuse 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 NonInertialFrameForcePlanarRforce(double R,double phi,double t, struct potentialArg * potentialArgs, double vR,double vT){ return NonInertialFrameForceRforce(R,0.,phi,t,potentialArgs,vR,vT,0.); } 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 NonInertialFrameForcePlanarphitorque(double R,double phi,double t, struct potentialArg * potentialArgs, double vR,double vT){ return NonInertialFrameForcephitorque(R,0.,phi,t,potentialArgs,vR,vT,0.); } 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/PerfectEllipsoidPotential.c0000644000175100001660000000110714761352023026071 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/PlummerPotential.c0000644000175100001660000000360014761352023024255 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/PowerSphericalPotential.c0000644000175100001660000000370314761352023025567 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/PowerSphericalPotentialwCutoff.c0000644000175100001660000000542314761352023027126 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_P ( 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/PowerTriaxialPotential.c0000644000175100001660000000116114761352023025426 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/PseudoIsothermalPotential.c0000644000175100001660000000431014761352023026122 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/RotateAndTiltWrapperPotential.c0000644000175100001660000000736514761352023026727 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/SCFPotential.c0000644000175100001660000005633714761352023023266 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/SoftenedNeedleBarPotential.c0000644000175100001660000001072214761352023026150 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); zc= sqrt ( z * z + c2 ); Fx= -2. * x / Tp / Tm / (Tp+Tm); Fy= -y / 2. / Tp /Tm * ( Tp + Tm -4. * x * x / (Tp+Tm) ) \ / ( y * y + pow( b + zc, 2)); Fz= -z / 2. / Tp /Tm * ( Tp + Tm -4. * x * x / (Tp+Tm) ) \ / ( y * y + pow( b + zc, 2)) * ( 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/SolidBodyRotationWrapperPotential.c0000644000175100001660000000541314761352023027611 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/SphericalPotential.c0000644000175100001660000000426014761352023024551 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/SpiralArmsPotential.c0000644000175100001660000006653514761352023024731 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/SteadyLogSpiralPotential.c0000644000175100001660000000306614761352023025710 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/TimeDependentAmplitudeWrapperPotential.c0000644000175100001660000000663314761352023030600 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/TransientLogSpiralPotential.c0000644000175100001660000000203614761352023026422 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/TriaxialGaussianPotential.c0000644000175100001660000000115614761352023026110 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/TwoPowerTriaxialPotential.c0000644000175100001660000000371514761352023026127 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/ZeroPotential.c0000644000175100001660000000043014761352023023551 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/galpy_potentials.c0000644000175100001660000002035314761352023024336 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, double vR, double vT){ int ii; double Rforce= 0.; for (ii=0; ii < nargs; ii++){ if ( potentialArgs->requiresVelocity ) Rforce+= potentialArgs->planarRforceVelocity(R,phi,t,potentialArgs,vR,vT); else 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, double vR, double vT){ int ii; double phitorque= 0.; for (ii=0; ii < nargs; ii++){ if ( potentialArgs->requiresVelocity ) phitorque+= potentialArgs->planarphitorqueVelocity(R,phi,t,potentialArgs,vR,vT); else 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/galpy_potentials.h0000644000175100001660000011065014761352023024343 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); double (*planarRforceVelocity)(double R,double phi, double t, struct potentialArg *,double,double); double (*planarphitorqueVelocity)(double R,double phi, double t, struct potentialArg *,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 // (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 *); // Same hack as for Rforce etc. above to allow optional velocity for dissipative forces #ifdef _MSC_VER #define calcPlanarRforce(...) EXPAND(CALCPLANARRFORCE(__VA_ARGS__,0.,0.)) #define calcPlanarphitorque(...) EXPAND(CALCPLANARPHITORQUE(__VA_ARGS__,0.,0.)) #else #define calcPlanarRforce(R,phi,t,nargs,potentialArgs,...) CALCPLANARRFORCE(R,phi,t,nargs,potentialArgs,##__VA_ARGS__,0.,0.) #define calcPlanarphitorque(R,phi,t,nargs,potentialArgs,...) CALCPLANARPHITORQUE(R,phi,t,nargs,potentialArgs,##__VA_ARGS__,0.,0.) #endif #define CALCPLANARRFORCE(R,phi,t,nargs,potentialArgs,vR,vT,...) calcPlanarRforce(R,phi,t,nargs,potentialArgs,vR,vT) #define CALCPLANARPHITORQUE(R,phi,t,nargs,potentialArgs,vR,vT,...) calcPlanarphitorque(R,phi,t,nargs,potentialArgs,vR,vT) double (calcPlanarRforce)(double, double, double, int, struct potentialArg *,double,double); double (calcPlanarphitorque)(double, double, double, int, struct potentialArg *,double,double); // end hack 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 NonInertialFrameForcePlanarRforce(double,double,double, struct potentialArg *, double,double); double NonInertialFrameForcephitorque(double,double,double,double, struct potentialArg *, double,double,double); double NonInertialFrameForcePlanarphitorque(double,double,double, struct potentialArg *, 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 *); //KuzminLikeWrapperPotential double KuzminLikeWrapperPotentialEval(double,double,double,double, struct potentialArg *); double KuzminLikeWrapperPotentialRforce(double,double,double,double, struct potentialArg *); double KuzminLikeWrapperPotentialzforce(double,double,double,double, struct potentialArg *); double KuzminLikeWrapperPotentialPlanarRforce(double,double,double, struct potentialArg *); double KuzminLikeWrapperPotentialPlanarR2deriv(double,double,double, struct potentialArg *); #ifdef __cplusplus } #endif #endif /* galpy_potentials.h */ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/interpRZPotential.c0000644000175100001660000000351114761352023024412 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/interpSphericalPotential.c0000644000175100001660000000354214761352023025775 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=1741018131.0 galpy-1.10.2/galpy/potential/potential_c_ext/verticalPotential.c0000644000175100001660000000071614761352023024452 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=1741018131.0 galpy-1.10.2/galpy/potential/verticalPotential.py0000644000175100001660000002110214761352023021467 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.0, phi=None, t0=0.0): """ Initialize a verticalPotential instance. Parameters ---------- Pot : Potential instance The potential to convert to a vertical potential. R : float, optional Galactocentric radius at which to create the vertical potential. Default is 1.0. phi : float, optional Galactocentric azimuth at which to create the vertical potential (rad); necessary for non-axisymmetric potentials. Default is None. t0 : float, optional Time at which to create the vertical potential. Default is 0.0. Returns ------- verticalPotential instance Notes ----- - 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.0, 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.0 else: self._phi = phi self._midplanePot = self._Pot( self._R, 0.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.0): """ Evaluate the potential. Parameters ---------- z : float Height above the midplane. t : float, optional Time at which to evaluate the potential. Default is 0.0. Returns ------- float The potential at (R, z, phi, t) - phi(R, 0., phi, t0). Notes ----- - 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) """ 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.0): """ Evaluate the force. Parameters ---------- z : float Height above the midplane. t : float, optional Time at which to evaluate the force. Default is 0.0. Returns ------- float The vertical force at (R, z, phi, t). Notes ----- - 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) """ 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): """ Convert a 3D azisymmetric potential to a vertical potential at a given R. Parameters ---------- Pot : Potential instance The 3D potential to convert. R : float or Quantity Galactocentric radius at which to evaluate the vertical potential. Returns ------- verticalPotential instance The vertical potential at (R, z, phi, t). Notes ----- - 2010-07-21 - Written - Bovy (NYU) """ RZPot = flatten(RZPot) try: conversion.get_physical(RZPot) except: raise PotentialError( "Input to 'RZToverticalPotential' is neither an RZPotential-instance or a list of such instances" ) if _isDissipative(RZPot): raise NotImplementedError( "Converting dissipative forces to 1D vertical potentials is currently not supported" ) 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: # pragma: no cover 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.0): """ Convert a 3D azisymmetric potential to a vertical potential at a given R. Parameters ---------- Pot : Potential instance The 3D potential to convert. R : float or Quantity Galactocentric radius at which to evaluate the vertical potential. phi : float, optional Azimuth at which to evaluate the vertical potential. Default is None. t0 : float, optional Time at which to evaluate the vertical potential. Default is 0.0. Returns ------- verticalPotential instance The vertical potential at (R, z, phi, t). Notes ----- - 2010-07-21 - Written - Bovy (NYU) """ Pot = flatten(Pot) try: conversion.get_physical(Pot) except: raise PotentialError( "Input to 'toVerticalPotential' is neither an Potential-instance or a list of such instances" ) if _isDissipative(Pot): raise NotImplementedError( "Converting dissipative forces to 1D vertical potentials is currently not supported" ) 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" ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6738844 galpy-1.10.2/galpy/snapshot/0000755000175100001660000000000014761352040015267 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/snapshot/GadgetSnapshot.py0000644000175100001660000000427414761352023020564 0ustar00runnerdockertry: import pynbody _PYNBODYENABLED = True except ImportError: _PYNBODYENABLED = False class GadgetSnapshot: """Snapshot coming out of gadget""" def __init__(self, *args, **kwargs): """ Initialize a Gadget snapshot object. Parameters ---------- *args : str Filename of the snapshot. Notes ----- - 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): """ Plot the snapshot. Notes ----- - 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=1741018131.0 galpy-1.10.2/galpy/snapshot/Snapshot.py0000644000175100001660000003427714761352023017456 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): """ Initialize a snapshot object. Parameters ---------- *args : list List of orbits, list of masses (masses=) **kwargs : dict Coming soon: 1) observations 2) DFs to draw from Notes ----- - 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): """ Integrate the snapshot in time. Parameters ---------- t : numpy.ndarray Times to save the snapshots at (must start at 0). pot : object or list of objects, optional Potential object(s) (default=None). method : str, optional Method to use ('test-particle' or 'direct-python' for now). **kwargs Additional keyword arguments to pass to the integration method. Returns ------- list List of snapshots at times t. Notes ----- - 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.0 + nbody_out[ii][0][jj][1] ** 2.0 ) phi = nu.arccos(nbody_out[ii][0][jj][0] / R) if nbody_out[ii][0][jj][1] < 0.0: phi = 2.0 * 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.0 + nbody_out[ii][0][jj][1] ** 2.0 ) phi = nu.arccos(nbody_out[ii][0][jj][0] / R) if nbody_out[ii][0][jj][1] < 0.0: phi = 2.0 * 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): """ Plot the snapshot (with reasonable defaults) Parameters ---------- d1 : str, optional First dimension to plot ('x', 'y', 'R', 'vR', 'vT', 'z', 'vz', ...). d2 : str, optional Second dimension to plot. *args : tuple Matplotlib.plot inputs + galpy.util.plot.plot inputs. **kwargs : dict Matplotlib.plot inputs + galpy.util.plot.plot inputs. Returns ------- None Sends plot to output device. Notes ----- - 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): """ Plot the snapshot in 3D (with reasonable defaults) Parameters ---------- d1 : str, optional First dimension to plot ('x', 'y', 'R', 'vR', 'vT', 'z', 'vz', ...). d2 : str, optional Second dimension to plot. d3 : str, optional Third dimension to plot. *args Matplotlib.plot inputs+galpy.util.plot.plot3d inputs. **kwargs Matplotlib.plot inputs+galpy.util.plot.plot3d inputs. Returns ------- None Sends plot to output device. Notes ----- - 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=1741018131.0 galpy-1.10.2/galpy/snapshot/__init__.py0000644000175100001660000000022514761352023017400 0ustar00runnerdockerfrom . import Snapshot, snapshotMovies # # Functions # snapshotToMovie = snapshotMovies.snapshotToMovie # # Classes # Snapshot = Snapshot.Snapshot ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/snapshot/directnbody.py0000644000175100001660000001177014761352023020156 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, ): """ N-body code using direct summation for force evaluation Parameters ---------- q : list list of initial positions (numpy.ndarrays) p : list list of initial momenta (numpy.ndarrays) m : list list of masses t : list times at which output is desired pot : galpy.potential or list of galpy.potentials, optional external potential softening_model : str, optional type of softening to use ('plummer') softening_length : float, optional softening length Parameters ---------- list list of [q,p] at times t Notes ----- - 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.0) for ii in range(len(q))] # kick force = _direct_nbody_force(q12, m, t + dt / 2.0, 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.0) 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.0 + x[1] ** 2.0) phi = nu.arccos(x[0] / R) sinphi = x[1] / R cosphi = x[0] / R if x[1] < 0.0: phi = 2.0 * 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.0 / R * sinphi * phitorque, sinphi * Rforce + 1.0 / 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.0 + x[1] ** 2.0) phi = nu.arccos(x[0] / R) sinphi = x[1] / R cosphi = x[0] / R if x[1] < 0.0: phi = 2.0 * 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.0 / R * sinphi * phitorque, sinphi * Rforce + 1.0 / R * cosphi * phitorque, ] ) elif dim == 1: return evaluatelinearForces(x, pot, t=t) def _plummer_soft(d, eps): return d / (d**2.0 + eps**2.0) ** 1.5 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/snapshot/nemo_util.py0000644000175100001660000000406014761352023017635 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): """ Read a NEMO snapshot file consisting of mass, position, velocity. Parameters ---------- filename : str Name of the file. ext : str, optional If set, 'nemo' for NEMO binary format, otherwise assumed ASCII; if not set, gleaned from extension. swapyz : bool, optional If True, swap the y and z axes in the output (only for position and velocity). Returns ------- ndarray Array of shape (nbody, ndim, nt). Notes ----- - 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=1741018131.0 galpy-1.10.2/galpy/snapshot/snapshotMovies.py0000644000175100001660000000773214761352023020675 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): """ Turn a list of snapshots into a movie Parameters ---------- snap : list The snapshots filename : str Name of the file to save the movie to framerate : int, optional Frames per second (default is 25) bitrate : int, optional Bitrate (default is 1000) thumbnail : bool, optional Create thumbnail image (filename-extension+.jpg) (default is False) thumbsize : int, optional Size of thumbnail (default is 300) *args Arguments for Snapshot.plot **kwargs Keyword arguments for Snapshot.plot Returns ------- None Notes ----- - 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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6778846 galpy-1.10.2/galpy/util/0000755000175100001660000000000014761352040014405 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/util/__init__.py0000644000175100001660000001335414761352023016525 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): """ Save things to a pickle file. Parameters ---------- savefilename : str 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. *args : tuple Things to pickle (as many as you want!). **kwargs : dict testKeyboardInterrupt : bool, optional If True, raises KeyboardInterrupt to test if the function can handle it. Returns ------- None Notes ----- - 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): """ Stable version of the cholesky decomposition Parameters ---------- x : numpy.ndarray Positive definite matrix tiny : float, optional Tiny number to add to the covariance matrix to make the decomposition stable (has a default) Returns ------- tuple (L, lowerFlag) - output from scipy.linalg.cho_factor for lower=True Notes ----- - 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): """ Invert a positive definite matrix by using its Cholesky decomposition. Parameters ---------- A : numpy.ndarray Matrix to be inverted. logdet : bool, optional If True, return the logarithm of the determinant as well. tiny : float, optional Tiny number to add to the covariance matrix to make the decomposition stable (has a default). Returns ------- numpy.ndarray or tuple If logdet is False, the inverse of the input matrix. If logdet is True, the inverse and the logarithm of the determinant of the input matrix. Notes ----- - 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.0 * 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.0, axis=1)), (3, 1)).T rotaxis = numpy.cross(normv, a) rotaxis /= numpy.tile(numpy.sqrt(numpy.sum(rotaxis**2.0, 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.0 - costheta**2.0) if inv: sgn = 1.0 else: sgn = -1.0 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.0 - costheta, (3, 3, 1)).T * (rotaxis[:, :, numpy.newaxis] * rotaxis[:, numpy.newaxis, :]) ) if not _dontcutsmall: out[numpy.fabs(costheta - 1.0) < 10.0**-10.0] = numpy.eye(3) out[numpy.fabs(costheta + 1.0) < 10.0**-10.0] = -numpy.eye(3) return out ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/util/_load_extension_libs.py0000644000175100001660000001540514761352023021150 0ustar00runnerdocker# _load_extension_libs.py: centralized place to load the C extensions import ctypes import re import subprocess import sys import sysconfig import warnings from pathlib import Path 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 _checked_openmp_issue = False def _detect_openmp_issue(): # Check whether we get an error of the type "OMP: Error #15: Initializing libomp.dylib, but found libomp.dylib already initialized.", which occurs, e.g., when using pip-installed galpy with conda-installed numpy and causes segmentation faults and general issues # This is a known issue with OpenMP and is not galpy's fault (GitHub Copilot suggested this comment!) # Check this by running a subprocess that tries to import the C library without this check try: subprocess.run( [ sys.executable, "-c", "from galpy.util._load_extension_libs import load_libgalpy; load_libgalpy(check_openmp_issue=False)", ], check=True, capture_output=True, ) except subprocess.CalledProcessError as e: # pragma: no cover if re.match( r"OMP: Error #15: Initializing libomp[0-9]*.dylib, but found libomp[0-9]*.dylib already initialized", e.stderr.decode("utf-8"), ): warnings.warn( "Encountered OpenMP issue with multiple OpenMP runtimes causing conflicts and segmentation faults (similar to https://github.com/pytorch/pytorch/issues/78490). " "This generally happens when you combine a pip-installed galpy with a conda-installed numpy. " "If you are using conda, your best bet is to install galpy with conda-forge, because there is little advantage to using pip. " "If you insist on using pip, you can try installing galpy with the --no-binary galpy flag, but note that you have to have the GSL and OpenMP installed and available for linking (e.g., using 'brew install gsl libomp' on a Mac, but make sure to add the OpenMP library to your CFLAGS/LDFLAGS/LD_LIBRARY_PATH as it is keg-only; please refer to galpy's installation page for more info). " "Please open an Issue or Discussion on the galpy GitHub repository if you require further assistance.\n" "For now, we will disable the libgalpy C extension module to avoid segmentation faults and other issues, but be aware that this significantly slows down the code.", galpyWarning, ) return True else: return False finally: global _checked_openmp_issue _checked_openmp_issue = True def load_libgalpy(check_openmp_issue=sys.platform != "emscripten"): global _libgalpy global _libgalpy_loaded if _libgalpy_loaded is False or not _libgalpy is None: return (_libgalpy, _libgalpy_loaded) if check_openmp_issue and not _checked_openmp_issue: if _detect_openmp_issue(): # pragma: no cover _libgalpy_loaded = False return (_libgalpy, _libgalpy_loaded) outerr = None # Add top-level galpy repository directory for pip install (-e) ., # just becomes site-packages for regular install paths = sys.path paths.append(str(Path(__file__).parent.parent.parent.absolute())) for path in [Path(p) for p in paths]: if not path.is_dir(): 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(str(path / f"libgalpy{_ext_suffix}"), winmode=0x008) else: _lib = ctypes.CDLL(str(path / f"libgalpy{_ext_suffix}")) except OSError as e: if (path / f"libgalpy{_ext_suffix}").exists(): # pragma: no cover outerr = e _lib = None else: break if _lib is None: # pragma: no cover if not outerr is None: warnings.warn( f"libgalpy C extension module not loaded, because of error '{outerr}'", galpyWarning, ) else: warnings.warn( f"libgalpy C extension module not loaded, because libgalpy{_ext_suffix} image was not found", 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 # Add top-level galpy repository directory for pip install (-e) ., # just becomes site-packages for regular install paths = sys.path paths.append(str(Path(__file__).parent.parent.parent.absolute())) for path in [Path(p) for p in paths]: if not path.is_dir(): 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( str(path / f"libgalpy_actionAngleTorus{_ext_suffix}"), winmode=0x008 ) else: _lib = ctypes.CDLL( str(path / f"libgalpy_actionAngleTorus{_ext_suffix}") ) except OSError as e: if ( path / f"libgalpy_actionAngleTorus{_ext_suffix}" ).exists(): # pragma: no cover outerr = e _lib = None else: break if _lib is None: # pragma: no cover if not outerr is None: warnings.warn( f"libgalpy_actionAngleTorus C extension module not loaded, because of error '{outerr}'", galpyWarningVerbose, ) else: warnings.warn( f"libgalpy_actionAngleTorus C extension module not loaded, because libgalpy{_ext_suffix} image was not found", galpyWarningVerbose, ) _libgalpy_actionAngleTorus_loaded = False else: _libgalpy_actionAngleTorus_loaded = True _libgalpy_actionAngleTorus = _lib return (_libgalpy_actionAngleTorus, _libgalpy_actionAngleTorus_loaded) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/util/_optional_deps.py0000644000175100001660000000327114761352023017762 0ustar00runnerdocker# Central place to process optional dependencies from packaging.version import Version 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 _AQ_GT_47 = None try: from astroquery.simbad import Simbad except ImportError: _ASTROQUERY_LOADED = False else: import astroquery _AQ_GT_47 = parse_version(astroquery.__version__) > parse_version("0.4.7") # numexpr _NUMEXPR_LOADED = True try: import numexpr except ImportError: # pragma: no cover _NUMEXPR_LOADED = False # 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 _PYNBODY_GE_20 = None try: import pynbody except ImportError: # pragma: no cover _PYNBODY_LOADED = False else: _PYNBODY_GE_20 = Version( parse_version(pynbody.__version__).base_version ) >= Version("2.0.0") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/util/ars.py0000644000175100001660000003755714761352023015566 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): """ 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 Parameters ---------- domain : list or numpy.ndarray [.,.] upper and lower limit to the domain isDomainFinite : list or numpy.ndarray [.,.] is there a lower/upper limit to the domain? abcissae : list or numpy.ndarray initial list of abcissae (must lie on either side of the peak in hx if the domain is unbounded) hx : function function that evaluates h(x) = ln g(x) hpx : function function that evaluates hp(x) = d h(x) / d x nsamples : int, optional number of desired samples (default=1) hxparams : tuple, optional a tuple of parameters for h(x) and h'(x) maxn : int, optional maximum number of updates to the hull (default=100) Returns ------- list list with nsamples of samples from exp(h(x)) Notes ----- - 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): """ Set up the upper and lower hull and everything that comes with that Parameters ---------- domain : list or numpy.ndarray [.,.] upper and lower limit to the domain isDomainFinite : list or numpy.ndarray [.,.] is there a lower/upper limit to the domain? abcissae : list or numpy.ndarray initial list of abcissae (must lie on either side of the peak in hx if the domain is unbounded) hx : function function that evaluates h(x) hpx : function function that evaluates hp(x) hxparams : tuple tuple of parameters for h(x) and h'(x) Returns ------- list list with: [0]= c_u [1]= xs [2]= h(xs) [3]= hp(xs) [4]= zs [5]= s_cum [6]= hu(zi) Notes ----- - 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.0 / hpxs[0] * (numpy.exp(hus[0]) - numpy.exp(hpxs[0] * (domain[0] - xs[0]) + hxs[0])) ) else: scum[0] = 1.0 / hpxs[0] * numpy.exp(hus[0]) if nx > 2: for jj in range(nx - 2): if hpxs[jj + 1] == 0.0: scum[jj + 1] = (zs[jj + 1] - zs[jj]) * numpy.exp(hxs[jj + 1]) else: scum[jj + 1] = ( 1.0 / hpxs[jj + 1] * (numpy.exp(hus[jj + 1]) - numpy.exp(hus[jj])) ) if isDomainFinite[1]: cu = ( 1.0 / hpxs[nx - 1] * ( numpy.exp(hpxs[nx - 1] * (domain[1] - xs[nx - 1]) + hxs[nx - 1]) - numpy.exp(hus[nx - 2]) ) ) else: cu = -1.0 / 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): """ Sample one point by ars Parameters ---------- hull : list the hull (see doc of setup_hull for definition) hx : function function that evaluates h(x) hpx : function function that evaluates hp(x) domain : list or numpy.ndarray [.,.] upper and lower limit to the domain isDomainFinite : list or numpy.ndarray [.,.] is there a lower/upper limit to the domain? maxn : int maximum number of updates to the hull nupdates : int number of updates to the hull that have occurred hxparams : tuple tuple of parameters for h(x) and h'(x) Returns ------- list [0]= a sample [1]= a new hull [2]= nupdates Notes ----- - 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 the upper hull Parameters ---------- hull : list the hull (see doc of setup_hull for definition) domain : list or numpy.ndarray [.,.] upper and lower limit to the domain isDomainFinite : list or numpy.ndarray [.,.] is there a lower/upper limit to the domain? Returns ------- float a sample from the hull Notes ----- - 2009-05-21 - Written - Bovy (NYU) """ 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.0 / hull[3][0] * numpy.log( 1.0 - 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.0 - hull[5][indx] ) * (domain[1] - hull[4][indx]) else: thissample = 100000 # Throw some kind of error else: thissample = hull[4][indx] + 1.0 / hull[3][indx + 1] * numpy.log( 1.0 + hull[3][indx + 1] * hull[0] * (u - hull[5][indx]) / numpy.exp(hull[6][indx]) ) return thissample def evaluate_hull(x, hull): """ Evaluate h_u(x) and (optional) h_l(x) Parameters ---------- x : float abcissa hull : list the hull (see doc of setup_hull for definition) Returns ------- float or tuple hu(x) (optional), hl(x) Notes ----- - 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 the hull with a new function evaluation Parameters ---------- hull : list the hull (see doc of setup_hull for definition) newx : float new abcissa newhx : float h(newx) newhpx : float hp(newx) domain : list or numpy.ndarray [.,.] upper and lower limit to the domain isDomainFinite : list or numpy.ndarray [.,.] is there a lower/upper limit to the domain? Returns ------- list new hull Notes ----- - 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.0 / newhpxs[0] * ( numpy.exp(newhus[0]) - numpy.exp(newhpxs[0] * (domain[0] - newxs[0]) + newhxs[0]) ) ) else: newscum[0] = 1.0 / newhpxs[0] * numpy.exp(newhus[0]) if nx > 2: for jj in range(nx - 2): if newhpxs[jj + 1] == 0.0: newscum[jj + 1] = (newzs[jj + 1] - newzs[jj]) * numpy.exp( newhxs[jj + 1] ) else: newscum[jj + 1] = ( 1.0 / newhpxs[jj + 1] * (numpy.exp(newhus[jj + 1]) - numpy.exp(newhus[jj])) ) if isDomainFinite[1]: newcu = ( 1.0 / newhpxs[nx - 1] * ( numpy.exp( newhpxs[nx - 1] * (domain[1] - newxs[nx - 1]) + newhxs[nx - 1] ) - numpy.exp(newhus[nx - 2]) ) ) else: newcu = -1.0 / 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=1741018131.0 galpy-1.10.2/galpy/util/bovy_coords.c0000644000175100001660000001475314761352023017114 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; } /* NAME: cyl_to_sos_galpy PURPOSE: convert (R,vR,vT,z,vz,phi,t) to (x,y,vx,vy,A,t,psi) coordinates for SOS integration INPUT: double * vxvv - (R,vR,vT,z,vz,phi,t) OUTPUT: performed in-place HISTORY: 2023-03-19 - Written - Bovy (UofT) */ void cyl_to_sos_galpy(double *vxvv){ double R,cp,sp,vR,vT; R = *vxvv; cp = cos ( *(vxvv+5) ); sp = sin ( *(vxvv+5) ); vR = *(vxvv+1); vT = *(vxvv+2); *(vxvv+5)= *(vxvv+6); *(vxvv+6)= atan2( *(vxvv+3) , *(vxvv+4) ); *(vxvv+4)= sqrt( *(vxvv+3) * *(vxvv+3) + *(vxvv+4) * *(vxvv+4) ); *vxvv = R * cp; *(vxvv+1)= R * sp; *(vxvv+2)= vR * cp - vT * sp; *(vxvv+3)= vR * sp + vT * cp; } /* NAME: sos_to_cyl_galpy PURPOSE: convert (x,y,vx,vy,A,t,psi) to (R,vR,vT,z,vz,phi,t) INPUT: double * vxvv - (x,y,vx,vy,A,t,psi) OUTPUT (as arguments): performed in-place HISTORY: 2023-03-19 - Written - Bovy (UofT) */ void sos_to_cyl_galpy(double *vxvv){ double x,y,vx,vy,phi,cp,sp; x = *(vxvv ); y = *(vxvv+1); vx= *(vxvv+2); vy= *(vxvv+3); phi= atan2( y , x ); cp = cos ( phi ); sp = sin ( phi ); *(vxvv )= sqrt ( x * x + y * y ); *(vxvv+1)= vx * cp + vy * sp; *(vxvv+2)= -vx * sp + vy * cp; *(vxvv+3)= *(vxvv+4) * sin ( *(vxvv+6) ); *(vxvv+4)= *(vxvv+4) * cos ( *(vxvv+6) ); *(vxvv+6)= *(vxvv+5); *(vxvv+5)= phi; } /* NAME: polar_to_sos_galpy PURPOSE: convert (R,vR,vT,phi,t) to (x,vx,A,t,psi) coordinates for SOS integration [or (y,vy,A,t) if surface == 1] INPUT: double * vxvv - (R,vR,vT,z,vz,phi,t) int surface - if 1, convert to (x,vx,A,t) instead of (y,vy,A,t) OUTPUT: performed in-place HISTORY: 2023-03-24 - Written - Bovy (UofT) */ void polar_to_sos_galpy(double *vxvv,int surface){ double R,cp,sp,vR,vT,x,y,vx,vy; R = *vxvv; cp = cos ( *(vxvv+3) ); sp = sin ( *(vxvv+3) ); vR = *(vxvv+1); vT = *(vxvv+2); if ( surface == 1 ) { // surface: y=0, vy > 0 *(vxvv )= R * cp; *(vxvv+1)= vR * cp - vT * sp; y= R * sp; vy= vR * sp + vT * cp; *(vxvv+2)= sqrt ( y * y + vy * vy ); *(vxvv+3)= *(vxvv+4); *(vxvv+4)= atan2( y , vy ); } else { // surface: x=0, vx > 0 *(vxvv )= R * sp; *(vxvv+1)= vR * sp + vT * cp; x= R * cp; vx= vR * cp - vT * sp; *(vxvv+2)= sqrt ( x * x + vx * vx ); *(vxvv+3)= *(vxvv+4); *(vxvv+4)= atan2( x , vx ); } } /* NAME: sos_to_polar_galpy PURPOSE: convert (y,vy,A,t,psi) or (x,vx,A,t,psi) to (R,vR,vT,phi,t ) INPUT: double * vxvv - (x,y,vx,vy,A,t,psi) surface - if 1, convert from (y,vy,A,t) instead of (x,vx,A,t) OUTPUT (as arguments): performed in-place HISTORY: 2023-03-24 - Written - Bovy (UofT) */ void sos_to_polar_galpy(double *vxvv,int surface){ double x,y,vx,vy,phi,cp,sp; if ( surface == 1) { // surface: y=0, vy > 0 x= *(vxvv ); vx= *(vxvv+1); y= *(vxvv+2) * sin ( *(vxvv+4) ); vy= *(vxvv+2) * cos ( *(vxvv+4) ); } else { // surface: x=0, vx > 0 y= *(vxvv ); vy= *(vxvv+1); x= *(vxvv+2) * sin ( *(vxvv+4) ); vx= *(vxvv+2) * cos ( *(vxvv+4) ); } phi= atan2( y , x ); cp = cos ( phi ); sp = sin ( phi ); *(vxvv )= sqrt ( x * x + y * y ); *(vxvv+1)= vx * cp + vy * sp; *(vxvv+2)= -vx * sp + vy * cp; *(vxvv+4)= *(vxvv+3); *(vxvv+3)= phi; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/util/bovy_coords.h0000644000175100001660000000417514761352023017116 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 *); void cyl_to_sos_galpy(double *); void sos_to_cyl_galpy(double *); void polar_to_sos_galpy(double *,int); void sos_to_polar_galpy(double *,int); #ifdef __cplusplus } #endif #endif /* bovy_coords.h */ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/util/bovy_rk.c0000644000175100001660000006204314761352023016232 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_dump(); // LCOV_EXCL_START __gcov_reset(); #endif 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_dump(); // LCOV_EXCL_START __gcov_reset(); #endif 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); //reuse 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_dump(); // LCOV_EXCL_START __gcov_reset(); #endif 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=1741018131.0 galpy-1.10.2/galpy/util/bovy_rk.h0000644000175100001660000001005614761352023016234 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=1741018131.0 galpy-1.10.2/galpy/util/bovy_symplecticode.c0000644000175100001660000007467614761352023020501 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_dump(); // LCOV_EXCL_START __gcov_reset(); #endif 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_dump(); // LCOV_EXCL_START __gcov_reset(); #endif 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_dump(); // LCOV_EXCL_START __gcov_reset(); #endif 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=1741018131.0 galpy-1.10.2/galpy/util/bovy_symplecticode.h0000644000175100001660000000634114761352023020466 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=1741018131.0 galpy-1.10.2/galpy/util/config.py0000644000175100001660000000753414761352023016236 0ustar00runnerdockerimport configparser import copy import os import os.path import warnings # 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 fix_config(configuration=None): # Reduces to default if configuration is None # otherwise fixes the configuration if it is not standard fixedconfig = configparser.ConfigParser() # Write different sections for sec_key in default_configuration.keys(): fixedconfig.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) ): fixedconfig.set(sec_key, key, default_configuration[sec_key][key]) else: fixedconfig.set(sec_key, key, configuration.get(sec_key, key)) return fixedconfig def write_config(filename, configuration): try: with open(filename, "w") as configfile: configuration.write(configfile) except Exception as e: # pragma: no cover warnings.warn( f"""Could not write new/fixed galpy configuration to {filename},""" f""" because of \"{type(e).__name__}: {e.__str__()}\"""" ) return None # Read the configuration file __config__ = configparser.ConfigParser() cfilename = __config__.read(".galpyrc") if not cfilename: cfilename = __config__.read(default_filename) if not cfilename: __config__ = fix_config() write_config(default_filename, __config__) cfilename = [default_filename] if not check_config(__config__): __config__ = fix_config(__config__) write_config(cfilename[-1], __config__) # 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): """ Set the global configuration value of ro (distance scale) Parameters ---------- ro : float or astropy Quantity Scale in kpc or astropy Quantity Returns ------- None Notes ----- - 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): """ Set the global configuration value of vo (velocity scale) Parameters ---------- vo : float or astropy Quantity Scale in km/s or astropy Quantity Returns ------- None Notes ----- - 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=1741018131.0 galpy-1.10.2/galpy/util/conversion.py0000644000175100001660000012622214761352023017152 0ustar00runnerdocker############################################################################### # # conversion: utilities to convert from galpy 'natural units' to physical # units # ############################################################################### import copy import math as m import numbers import warnings from functools import wraps from typing import Any, Tuple import numpy from ..util._optional_deps import _APY_LOADED, _APY_UNITS from ..util.config import __config__ if not _APY_LOADED: _G = 4.302 * 10.0**-3.0 # 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.0**18.0 # 10^18 cm _CIN10p5KMS = constants.c.to(units.km / units.s).value / 10.0**5.0 # 10^5 km/s _MSOLAR10p30KG = units.Msun.to(units.kg) / 10.0**30.0 # 10^30 kg _EVIN10m19J = units.eV.to(units.J) * 10.0**19.0 # 10^-19 J _MyrIn1013Sec = 3.65242198 * 0.24 * 3.6 # use tropical year, like for pms _TWOPI = 2.0 * m.pi def dens_in_criticaldens(vo, ro, H=70.0): """ Convert density to units of the critical density. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. H : float, optional Hubble constant in km/s/Mpc. Default is 70.0. Returns ------- float Conversion from units where vo=1. at ro=1. to units of the critical density. Notes ----- - 2014-01-28 - Written - Bovy (IAS) """ return vo**2.0 / ro**2.0 * 10.0**6.0 / H**2.0 * 8.0 * numpy.pi / 3.0 def dens_in_meanmatterdens(vo, ro, H=70.0, Om=0.3): """ Convert density to units of the mean matter density. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. H : float, optional Hubble constant in km/s/Mpc. Default is 70.0. Om : float, optional Omega matter. Default is 0.3. Returns ------- float Conversion from units where vo=1. at ro=1. to units of the mean matter density. Notes ----- - 2014-01-28 - Written - Bovy (IAS) """ return dens_in_criticaldens(vo, ro, H=H) / Om def dens_in_gevcc(vo, ro): """ Convert density to GeV / cm^3. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- float Conversion from units where vo=1. at ro=1. to GeV/cm^3. Notes ----- - 2014-06-16 - Written - Bovy (IAS) """ return ( vo**2.0 / ro**2.0 / _G * _MSOLAR10p30KG * _CIN10p5KMS**2.0 / _EVIN10m19J / _PCIN10p18CM**3.0 * 10.0**-4.0 ) def dens_in_msolpc3(vo, ro): """ Convert density to Msolar / pc^3. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- float Conversion from units where vo=1. at ro=1. to Msolar/pc^3. Notes ----- - 2013-09-01 - Written - Bovy (IAS) """ return vo**2.0 / ro**2.0 / _G * 10.0**-6.0 def force_in_2piGmsolpc2(vo, ro): """ Convert a force or acceleration to 2piG x Msolar / pc^2 Parameters ---------- vo : float Velocity unit in km/s ro : float Length unit in kpc Returns ------- float Conversion from units where vo=1. at ro=1. Notes ----- - 2013-09-01 - Written - Bovy (IAS) """ return vo**2.0 / ro / _G * 10.0**-3.0 / _TWOPI def force_in_pcMyr2(vo, ro): """ Convert a force or acceleration to pc/Myr^2. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- float Conversion from units where vo=1. at ro=1. Notes ----- - 2013-09-01 - Written - Bovy (IAS) """ return vo**2.0 / ro * _kmsInPcMyr**2.0 * 10.0**-3.0 def force_in_kmsMyr(vo, ro): """ Convert a force or acceleration to km/s/Myr. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- float Conversion from units where vo=1. at ro=1. Notes ----- - 2013-09-01 - Written - Bovy (IAS) """ return vo**2.0 / ro * _kmsInPcMyr * 10.0**-3.0 def force_in_10m13kms2(vo, ro): """ Convert a force or acceleration to 10^(-13) km/s^2 Parameters ---------- vo : float Velocity unit in km/s ro : float Length unit in kpc Returns ------- float Conversion from units where vo=1. at ro=1. Notes ----- - 2014-01-22 - Written - Bovy (IAS) """ return vo**2.0 / ro * _kmsInPcMyr * 10.0**-3.0 / _MyrIn1013Sec def freq_in_Gyr(vo, ro): """ Convert a frequency to 1/Gyr. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- float Conversion from units where vo=1. at ro=1. Notes ----- - 2013-09-01 - Written - Bovy (IAS) """ return vo / ro * _kmsInPcMyr def freq_in_kmskpc(vo, ro): """ Convert a frequency to km/s/kpc. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- float Conversion from units where vo=1. at ro=1. Notes ----- - 2013-09-01 - Written - Bovy (IAS) """ return vo / ro def surfdens_in_msolpc2(vo, ro): """ Convert a surface density to Msolar / pc^2. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- float Conversion from units where vo=1. at ro=1. Notes ----- - 2013-09-01 - Written - Bovy (IAS) """ return vo**2.0 / ro / _G * 10.0**-3.0 def mass_in_msol(vo, ro): """ Convert a mass to Msolar. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- float Conversion from units where vo=1. at ro=1. Notes ----- - 2013-09-01 - Written - Bovy (IAS) """ return vo**2.0 * ro / _G * 10.0**3.0 def mass_in_1010msol(vo, ro): """ Convert a mass to 10^10 x Msolar. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- float Conversion from units where vo=1. at ro=1. Notes ----- - 2013-09-01 - Written - Bovy (IAS) """ return vo**2.0 * ro / _G * 10.0**-7.0 def time_in_Gyr(vo, ro): """ Convert a time to Gyr. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- float Conversion from units where vo=1. at ro=1. Notes ----- - 2013-09-01 - Written - Bovy (IAS) """ return ro / vo / _kmsInPcMyr def velocity_in_kpcGyr(vo, ro): """ Convert a velocity to kpc/Gyr. Parameters ---------- vo : float Velocity unit in km/s. ro : float Length unit in kpc. Returns ------- float Conversion from units where vo=1. at ro=1. Notes ----- - 2014-12-19 - Written - Bovy (IAS) """ return vo * _kmsInPcMyr def get_physical(obj: Any, include_set: bool = False) -> dict: """ 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. Parameters ---------- obj : Any A galpy object or list of such objects (e.g., a Potential, list of Potentials, Orbit, actionAngle instance, DF instance). include_set : bool, optional If True, also include roSet and voSet, flags of whether the unit is explicitly set in the object. Default is False. Returns ------- dict A 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. Notes ----- - 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 extract_physical_kwargs(kwargs: dict) -> dict: """ Extract the physical kwargs from a kwargs dictionary. Parameters ---------- kwargs : dict A dictionary of kwargs. Returns ------- dict A dictionary with just the physical kwargs. Notes ----- - 2023-04-24 - Written - Bovy (UofT) """ out = {} for key in kwargs.copy(): if key in ["use_physical", "ro", "vo", "quantity"]: out[key] = kwargs.pop(key) return out def physical_compatible(obj: Any, other_obj: Any) -> bool: """ Test whether the velocity and length units for converting between physical and internal units are compatible for two galpy objects. Parameters ---------- obj : galpy object or list of such objects A galpy object or list of such objects (e.g., a Potential, list of Potentials, Orbit, actionAngle instance, DF instance) other_obj : galpy object or list of such objects Another galpy object or list of such objects (e.g., a Potential, list of Potentials, Orbit, actionAngle instance, DF instance) Returns ------- bool True if the units are compatible, False if not (compatible means that the units are the same when they are set for both objects). Notes ----- - 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 check_parser_input_type(func): """ Decorator to check the inputs to a parse_ function; should be either: a) a number b) an array of numbers c) an astropy Quantity (incl. arrays) Also parses ro/vo if they are provided and converts them to the correct internal representation """ @wraps(func) def parse_x_wrapper(x, **kwargs): if ( not x is None and not isinstance(x, numbers.Number) and not ( isinstance(x, numpy.ndarray) and (x.size == 0 or isinstance(x.flatten()[0], numbers.Number)) ) and not (_APY_LOADED and isinstance(x, units.Quantity)) ): raise RuntimeError( f"Input '{x}' not understood; should either be a number or an astropy Quantity" ) # Also parse ro and vo inputs if "ro" in kwargs: if ( not kwargs["ro"] is None and not isinstance(kwargs["ro"], numbers.Number) and not (_APY_LOADED and isinstance(kwargs["ro"], units.Quantity)) ): raise RuntimeError( f"Input 'ro={kwargs['ro']}' not understood; should either be a number or an astropy Quantity" ) else: kwargs["ro"] = ( kwargs["ro"].to(units.kpc).value if _APY_LOADED and isinstance(kwargs["ro"], units.Quantity) else kwargs["ro"] ) if "vo" in kwargs: if ( not kwargs["vo"] is None and not isinstance(kwargs["vo"], numbers.Number) and not (_APY_LOADED and isinstance(kwargs["vo"], units.Quantity)) ): raise RuntimeError( f"Input 'vo={kwargs['vo']}' not understood; should either be a number or an astropy Quantity" ) else: kwargs["vo"] = ( kwargs["vo"].to(units.km / units.s).value if _APY_LOADED and isinstance(kwargs["vo"], units.Quantity) else kwargs["vo"] ) return func(x, **kwargs) return parse_x_wrapper @check_parser_input_type 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 ) @check_parser_input_type def parse_length_kpc(x): return x.to(units.kpc).value if _APY_LOADED and isinstance(x, units.Quantity) else x @check_parser_input_type 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 ) @check_parser_input_type def parse_velocity_kms(x): return ( x.to(units.km / units.s).value if _APY_LOADED and isinstance(x, units.Quantity) else x ) @check_parser_input_type def parse_angle(x): return x.to(units.rad).value if _APY_LOADED and isinstance(x, units.Quantity) else x @check_parser_input_type 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 ) @check_parser_input_type 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 ) @check_parser_input_type def parse_energy(x, ro=None, vo=None): return ( x.to(units.km**2 / units.s**2).value / vo**2.0 if _APY_LOADED and isinstance(x, units.Quantity) else x ) @check_parser_input_type 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 ) @check_parser_input_type 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 ) @check_parser_input_type 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 ) @check_parser_input_type 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 ) @check_parser_input_type 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 ) @check_parser_input_type 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, "massphasespacedensity": True, "massenergydensity": False, "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 _voNecessary["massenergydensity"] = True # Determine whether or not outputs will be physical or not def physical_output(obj: Any, kwargs: dict, quantity: str) -> Tuple[bool, float, float]: """ Determine whether or not outputs will be physical or not Parameters ---------- obj : galpy object (or list in case of potentials) galpy object. kwargs : dict Kwargs passed to the method. quantity : str Quantity to be returned. Returns ------- tuple A tuple containing: - boolean that indicates whether or not to use physical units. - ro. - vo. Notes ----- - 2023-04-24 - Written - Bovy (UofT). """ 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(obj, "_roSet") and obj._roSet)): ro = obj._ro if ( ro is None and isinstance(obj, list) and hasattr(obj[0], "_roSet") and obj[0]._roSet ): # For lists of Potentials ro = obj[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(obj, "_voSet") and obj._voSet)): vo = obj._vo if ( vo is None and isinstance(obj, list) and hasattr(obj[0], "_voSet") and obj[0]._voSet ): # For lists of Potentials vo = obj[0]._vo if _APY_LOADED and isinstance(vo, units.Quantity): vo = vo.to(units.km / units.s).value return ( ( use_physical and not (_voNecessary[quantity.lower()] and vo is None) and not (_roNecessary[quantity.lower()] and ro is None) ), ro, vo, ) 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): # Determine whether or not to return outputs in physical units use_physical_output, ro, vo = physical_output(args[0], kwargs, quantity) # Determine whether physical outputs were explicitly asked for use_physical_explicitly_set = kwargs.get("use_physical", False) # Determine whether or not to return outputs as quantities _apy_units = kwargs.get("quantity", _APY_UNITS) # Remove ro, vo, use_physical, and quantity kwargs if necessary if pop: _ = extract_physical_kwargs(kwargs) if use_physical_output: 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.0 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.0 if _apy_units: u = (units.km / units.s) ** 2 elif quantity.lower() == "velocity_kms": # already in km/s fac = 1.0 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.0 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.0 if _apy_units: u = units.km**2.0 / units.s**2.0 elif quantity.lower() == "angle": # in rad fac = 1.0 if _apy_units: u = units.rad elif quantity.lower() == "angle_deg": # already in deg fac = 1.0 if _apy_units: u = units.deg elif quantity.lower() == "proper-motion_masyr": # already in mas/yr fac = 1.0 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.0 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.0 / ro**2.0 if _apy_units: u = 1 / units.kpc**2 elif quantity.lower() == "surfacedensitydistance": fac = surfdens_in_msolpc2(vo, ro) * ro * 1000.0 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.0 if _apy_units: u = units.Gyr**-2.0 elif quantity.lower() == "phasespacedensity": fac = 1.0 / vo**3.0 / ro**3.0 if _apy_units: u = 1 / (units.km / units.s) ** 3 / units.kpc**3 elif quantity.lower() == "phasespacedensity2d": fac = 1.0 / vo**2.0 / ro**2.0 if _apy_units: u = 1 / (units.km / units.s) ** 2 / units.kpc**2 elif quantity.lower() == "phasespacedensityvelocity": fac = 1.0 / vo**2.0 / ro**3.0 if _apy_units: u = 1 / (units.km / units.s) ** 2 / units.kpc**3 elif quantity.lower() == "phasespacedensityvelocity2": fac = 1.0 / vo / ro**3.0 if _apy_units: u = 1 / (units.km / units.s) / units.kpc**3 elif quantity.lower() == "massphasespacedensity": fac = mass_in_msol(vo, ro) / vo**3.0 / ro**3.0 if _apy_units: u = units.Msun / (units.km / units.s) ** 3 / units.kpc**3 elif quantity.lower() == "massenergydensity": fac = mass_in_msol(vo, ro) / vo**2.0 if _apy_units: u = units.Msun / (units.km / units.s) ** 2 elif quantity.lower() == "dimensionless": fac = 1.0 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: # complicated logic for dealing with ro and vo arrays return out * ( fac[:, numpy.newaxis] if isinstance(fac, numpy.ndarray) and len(out.shape) > 1 else fac ) else: if 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 physical_conversion_tuple(quantities, pop=False): """Decorator to convert to physical coordinates for tuple outputs. So outputs are a tuple of quantities that each need to be converted, with possibly different conversions, e.g., (R,vR)""" def wrapper(method): @wraps(method) def wrapped(*args, **kwargs): rawOut = method(*args, **kwargs) out = () for ii in range(len(rawOut)): # Apply physical conversion by converting a wrapped dummy function that returns the raw output out = out + ( physical_conversion(quantities[ii])(lambda x, **kwargs: rawOut[ii])( args[0], **kwargs ), ) return out 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.0 u.append(Freqsu) else: fac.extend([FreqsFac, FreqsFac, FreqsFac]) if _APY_UNITS: Freqsu = units.Gyr**-1.0 u.extend([Freqsu, Freqsu, Freqsu]) if "Angles" in quantity: if len(out) < 4: # 1D system fac.append(1.0) if _APY_UNITS: Freqsu = units.Gyr**-1.0 u.append(units.rad) else: fac.extend([1.0, 1.0, 1.0]) if _APY_UNITS: Freqsu = units.Gyr**-1.0 u.extend([units.rad, units.rad, units.rad]) if "EccZmaxRperiRap" in quantity: fac = [1.0, ro, ro, ro] if _APY_UNITS: u = [1.0, 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.0]) 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.0 u.append(Freqsu) else: fac.extend([FreqsFac, FreqsFac, FreqsFac]) if _APY_UNITS: Freqsu = units.Gyr**-1.0 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=1741018131.0 galpy-1.10.2/galpy/util/coords.py0000644000175100001660000024500214761352023016254 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.0 if _APY_LOADED: import astropy.coordinates as apycoords from astropy import units _K = ( (1.0 * 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: 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): """ Decorator to transform angles from and to degrees if necessary Parameters ---------- inDegrees : list List specifying indices of angle arguments (e.g., [0,1]) outDegrees : list Same as inDegrees, but for function return Returns ------- function Wrapped function Notes ----- - ____-__-__ - 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.0 / 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): """ Transform from equatorial coordinates to Galactic coordinates Parameters ---------- ra : float or numpy.ndarray Right ascension dec : float or numpy.ndarray Declination degree : bool, optional If True, ra and dec are given in degree and l and b will be as well epoch : float, optional 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) Returns ------- tuple or numpy.ndarray Galactic longitude and latitude Notes ----- - 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.0], [numpy.sin(theta), -numpy.cos(theta), 0.0], [0.0, 0.0, 1.0], ] ), numpy.dot( numpy.array( [ [-numpy.sin(dec_ngp), 0.0, numpy.cos(dec_ngp)], [0.0, 1.0, 0.0], [numpy.cos(dec_ngp), 0.0, numpy.sin(dec_ngp)], ] ), numpy.array( [ [numpy.cos(ra_ngp), numpy.sin(ra_ngp), 0.0], [-numpy.sin(ra_ngp), numpy.cos(ra_ngp), 0.0], [0.0, 0.0, 1.0], ] ), ), ) # 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.0] = 1.0 galXYZ[2][galXYZ[2] < -1.0] = -1.0 b = numpy.arcsin(galXYZ[2]) l = numpy.arctan2(galXYZ[1] / numpy.cos(b), galXYZ[0] / numpy.cos(b)) l[l < 0.0] += 2.0 * 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): """ Transform from Galactic coordinates to equatorial coordinates Parameters ---------- l : float or numpy.ndarray Galactic longitude b : float or numpy.ndarray Galactic latitude degree : bool, optional If True, l and b are given in degree and ra and dec will be as well epoch : float, optional 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) Returns ------- tuple or numpy.ndarray Right ascension and declination Notes ----- - 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.0], [numpy.sin(ra_ngp), numpy.cos(ra_ngp), 0.0], [0.0, 0.0, 1.0], ] ), numpy.dot( numpy.array( [ [-numpy.sin(dec_ngp), 0.0, numpy.cos(dec_ngp)], [0.0, 1.0, 0.0], [numpy.cos(dec_ngp), 0.0, numpy.sin(dec_ngp)], ] ), numpy.array( [ [numpy.cos(theta), numpy.sin(theta), 0.0], [numpy.sin(theta), -numpy.cos(theta), 0.0], [0.0, 0.0, 1.0], ] ), ), ) # 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.0] += 2.0 * numpy.pi return numpy.array([ra, dec]).T @scalarDecorator @degreeDecorator([0, 1], []) def lbd_to_XYZ(l, b, d, degree=False): """ Transform from spherical Galactic coordinates to rectangular Galactic coordinates (works with vector inputs) Parameters ---------- l : float or numpy.ndarray Galactic longitude (rad) b : float or numpy.ndarray Galactic latitude (rad) d : float or numpy.ndarray Distance (arbitrary units) degree : bool, optional If True, l and b are in degrees. Default is False. Returns ------- tuple or numpy.ndarray [X,Y,Z] in whatever units d was in. For vector inputs [:,3] Notes ----- - 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): """ Transform phase-space coordinates in rectangular Galactic coordinates to spherical Galactic coordinates (can take vector inputs) Parameters ---------- X : float or numpy.ndarray Component towards the Galactic Center (kpc) Y : float or numpy.ndarray Component in the direction of Galactic rotation (kpc) Z : float or numpy.ndarray Component towards the North Galactic Pole (kpc) vx : float or numpy.ndarray Velocity towards the Galactic Center (km/s) vy : float or numpy.ndarray Velocity in the direction of Galactic rotation (km/s) vz : float or numpy.ndarray Velocity towards the North Galactic Pole (km/s) degree : bool, optional If True, return l and b in degrees. Default is False. Returns ------- tuple or numpy.ndarray [l,b,d,vr,pmll x cos(b),pmbb] in (rad,rad,kpc,km/s,mas/yr,mas/yr). For vector inputs [:,6] Notes ----- - 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): """ Transform phase-space coordinates in spherical Galactic coordinates to rectangular Galactic coordinates (can take vector inputs) Parameters ---------- l : float or numpy.ndarray Galactic longitude b : float or numpy.ndarray Galactic latitude d : float or numpy.ndarray Distance (kpc) vr : float or numpy.ndarray Line-of-sight velocity (km/s) pmll : float or numpy.ndarray Proper motion in the Galactic longitude direction (mu_l * cos(b)) (mas/yr) pmbb : float or numpy.ndarray Proper motion in the Galactic latitude (mas/yr) degree : bool, optional If True, l and b are in degrees. Default is False. Returns ------- tuple or numpy.ndarray [X,Y,Z,vx,vy,vz] in (kpc,kpc,kpc,km/s,km/s,km/s). For vector inputs [:,6] Notes ----- - 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): """ Transform velocities in the spherical Galactic coordinate frame to the rectangular Galactic coordinate frame (can take vector inputs) Parameters ---------- vr : float or numpy.ndarray Line-of-sight velocity (km/s) pmll : float or numpy.ndarray Proper motion in the Galactic longitude direction (mu_l * cos(b)) (mas/yr) pmbb : float or numpy.ndarray Proper motion in the Galactic latitude (mas/yr) l : float or numpy.ndarray Galactic longitude b : float or numpy.ndarray Galactic latitude d : float or numpy.ndarray Distance (kpc) XYZ : bool, optional If True, then l,b,d is actually X,Y,Z (rectangular Galactic coordinates). Default is False. degree : bool, optional If True, l and b are in degrees. Default is False. Returns ------- tuple or numpy.ndarray [vx,vy,vz] in (km/s,km/s,km/s). For vector inputs [:,3] Notes ----- - 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.0 / numpy.pi b *= 180.0 / 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): """ Transform velocities in the rectangular Galactic coordinate frame to the spherical Galactic coordinate frame (can take vector inputs) Parameters ---------- vx : float or numpy.ndarray Velocity towards the Galactic Center (km/s) vy : float or numpy.ndarray Velocity in the direction of Galactic rotation (km/s) vz : float or numpy.ndarray Velocity towards the North Galactic Pole (km/s) l : float or numpy.ndarray Galactic longitude b : float or numpy.ndarray Galactic latitude d : float or numpy.ndarray Distance (kpc) XYZ : bool, optional If True, then l,b,d is actually X,Y,Z (rectangular Galactic coordinates). Default is False. degree : bool, optional If True, l and b are in degrees. Default is False. Returns ------- tuple or numpy.ndarray [vr,pmll x cos(b),pmbb] in (km/s,mas/yr,mas/yr). For vector inputs [:,3] Notes ----- - 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.0 / numpy.pi b *= 180.0 / 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): """ Transform from rectangular Galactic coordinates to spherical Galactic coordinates (works with vector inputs) Parameters ---------- X : float or numpy.ndarray Component towards the Galactic Center (in kpc; though this obviously does not matter)) Y : float or numpy.ndarray Component in the direction of Galactic rotation (in kpc) Z : float or numpy.ndarray Component towards the North Galactic Pole (kpc) degree : bool, optional If True, return l and b in degrees (default is False) Returns ------- tuple or numpy.ndarray [l,b,d] in (rad or degree,rad or degree,kpc); for vector inputs [:,3] Notes ----- - 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.0 + Y**2.0 + Z**2.0) b = numpy.arcsin(Z / d) l = numpy.arctan2(Y, X) l[l < 0.0] += 2.0 * 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): """ Rotate proper motions in (ra,dec) into proper motions in (l,b) Parameters ---------- pmra : float or numpy.ndarray Proper motion in ra (multiplied with cos(dec)) (mas/yr) pmdec : float or numpy.ndarray Proper motion in dec (mas/yr) ra : float or numpy.ndarray Right ascension dec : float or numpy.ndarray Declination degree : bool, optional If True, ra and dec are given in degrees (default=False) epoch : float, optional 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) Returns ------- tuple or numpy.ndarray [pmll x cos(b),pmbb] in (mas/yr,mas/yr). For vector inputs [:,2] Notes ----- - 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.0**-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.0 + sinphi**2.0) 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): """ Rotate proper motions in (l,b) into proper motions in (ra,dec) Parameters ---------- pmll : float or numpy.ndarray Proper motion in l (multiplied with cos(b)) (mas/yr) pmbb : float or numpy.ndarray Proper motion in b (mas/yr) l : float or numpy.ndarray Galactic longitude b : float or numpy.ndarray Galactic latitude degree : bool, optional If True, l and b are given in degrees (default=False) epoch : float, optional 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) Returns ------- tuple or numpy.ndarray [pmra x cos(dec),pmdec] in (mas/yr,mas/yr). For vector inputs [:,2] Notes ----- - 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.0**-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.0 + sinphi**2.0) 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): """ Propagate the proper motions errors through the rotation from (ra,dec) to (l,b) Parameters ---------- cov_pmradec : numpy.ndarray Uncertainty covariance matrix of the proper motion in ra (multiplied with cos(dec)) and dec [2,2] or [:,2,2] ra : float or numpy.ndarray Right ascension dec : float or numpy.ndarray Declination degree : bool, optional If True, ra and dec are given in degrees (default=False) epoch : float, optional 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) Returns ------- numpy.ndarray covar_pmllbb [2,2] or [:,2,2] [pmll here is pmll x cos(b)] Notes ----- - 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.0 + sinphi**2.0) 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 ): """ Propagate distance, radial velocity, and proper motion uncertainties to Galactic coordinates Parameters ---------- d : float or numpy.ndarray Distance [kpc, as/mas for plx] e_d : float or numpy.ndarray Distance uncertainty [kpc, [as/mas] for plx] e_vr : float or numpy.ndarray Low velocity uncertainty [km/s] pmll : float or numpy.ndarray Proper motion in l (*cos(b)) [ [as/mas]/yr ] pmbb : float or numpy.ndarray Proper motion in b [ [as/mas]/yr ] cov_pmllbb : numpy.ndarray Uncertainty covariance for proper motion [pmll is pmll x cos(b)] [:,2,2] l : float or numpy.ndarray Galactic longitude b : float or numpy.ndarray Galactic latitude plx : bool, optional If True, d is a parallax, and e_d is a parallax uncertainty (default=False) degree : bool, optional If True, l and b are given in degree (default=False) Returns ------- numpy.ndarray cov(vx,vy,vz) [3,3] or [:,3,3] Notes ----- - 2010-04-12 - Written - Bovy (NYU) - 2020-09-21 - Adapted for array input - Mackereth (UofT) """ if plx: d = 1.0 / d e_d *= d**2.0 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.0 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.0 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.0, Zsun=0.0): """ Propagate uncertainties in vxyz to galactocentric cylindrical coordinates Parameters ---------- cov_vxyz : numpy.ndarray Uncertainty covariance in U,V,W [3,3] or [:,3,3] phi : float or numpy.ndarray Angular position of star in Galactocentric cylindrical coords [rad] Xsun : float, optional Cylindrical distance to the GC (can be array of same length as R) [kpc] Zsun : float, optional Sun's height above the midplane (can be array of same length as R) [kpc] Returns ------- numpy.ndarray cov(vR,vT,vz) [3,3] or [:,3,3] Notes ----- - 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.0, Zsun=0.0): """ Propagate uncertainties in vxyz to galactocentric rectangular coordinates Parameters ---------- cov_vxyz : numpy.ndarray Uncertainty covariance in U,V,W [3,3] or [:,3,3] Xsun : float, optional Cylindrical distance to the GC (can be array of same length as R) [kpc] Zsun : float, optional Sun's height above the midplane (can be array of same length as R) [kpc] Returns ------- numpy.ndarray cov(vx,vy,vz) [3,3] or [:,3,3] Notes ----- - 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.0 + Zsun**2.0) costheta, sintheta = Xsun / dgc, Zsun / dgc R = numpy.array( [[costheta, 0.0, -sintheta], [0.0, 1.0, 0.0], [sintheta, 0.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): """ Propagate uncertainties in galactocentric rectangular to galactocentric cylindrical coordinates Parameters ---------- cov_galcenrect : numpy.ndarray Uncertainty covariance in vx,vy,vz [3,3] or [:,3,3] phi : float or numpy.ndarray Angular position of star in Galactocentric cylindrical coords [rad] Returns ------- numpy.ndarray cov(vR,vT,vz) [3,3] or [:,3,3] Notes ----- - 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.0 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.0, Zsun=0.0, _extra_rot=True): """ Transform XYZ coordinates (wrt Sun) to rectangular Galactocentric coordinates. Parameters ---------- X : float or numpy.ndarray X coordinate. Y : float or numpy.ndarray Y coordinate. Z : float or numpy.ndarray Z coordinate. Xsun : float or numpy.ndarray, optional Cylindrical distance to the Galactic center (default is 1.0). Zsun : float or numpy.ndarray, optional Sun's height above the midplane (default is 0.0). _extra_rot : bool, optional If True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition (default is True). Returns ------- numpy.ndarray Containing: * Xg : X coordinate in Galactocentric frame. * Yg : Y coordinate in Galactocentric frame. * Zg : Z coordinate in Galactocentric frame. Notes ----- - 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) - 2023-07-23 - Allowed Xsun/Zsun to be arrays - Bovy (UofT) """ if _extra_rot: X, Y, Z = numpy.dot(galcen_extra_rot, numpy.array([X, Y, Z])) dgc = numpy.sqrt(Xsun**2.0 + Zsun**2.0) costheta, sintheta = Xsun / dgc, Zsun / dgc zero = numpy.zeros_like(costheta) one = numpy.ones_like(costheta) return numpy.einsum( ( "ijk,jk->ik" if isinstance(costheta, numpy.ndarray) and costheta.ndim > 0 else "ij,jk->ik" ), numpy.array( [[costheta, zero, -sintheta], [zero, one, zero], [sintheta, zero, costheta]] ), numpy.array([-X + dgc, Y, numpy.sign(Xsun) * Z]), ).T @scalarDecorator def galcenrect_to_XYZ(X, Y, Z, Xsun=1.0, Zsun=0.0, _extra_rot=True): """ Transform rectangular Galactocentric to XYZ coordinates (wrt Sun) coordinates. Parameters ---------- X : float or numpy.ndarray Galactocentric rectangular coordinate. Y : float or numpy.ndarray Galactocentric rectangular coordinate. Z : float or numpy.ndarray Galactocentric rectangular coordinate. Xsun : float or numpy.ndarray, optional Cylindrical distance to the GC (can be array of same length as X). Zsun : float or numpy.ndarray, optional Sun's height above the midplane (can be array of same length as X). _extra_rot : bool, optional If True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition. Returns ------- numpy.ndarray Containing (X,Y,Z) Notes ----- - 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.0 + Zsun**2.0) costheta, sintheta = Xsun / dgc, Zsun / dgc zero = numpy.zeros_like(costheta) one = numpy.ones_like(costheta) out = ( numpy.einsum( ( "ijk,jk->ik" if isinstance(costheta, numpy.ndarray) and costheta.ndim > 0 else "ij,jk->ik" ), numpy.array( [ [-costheta, zero, -sintheta], [zero, one, zero], [-numpy.sign(Xsun) * sintheta, zero, numpy.sign(Xsun) * costheta], ] ), numpy.array([X, Y, Z]), ).T + numpy.array([dgc, zero, zero]).T ) if _extra_rot: return numpy.dot(galcen_extra_rot.T, out.T).T else: return out def rect_to_cyl(X, Y, Z): """ Convert from rectangular to cylindrical coordinates Parameters ---------- X : float or numpy.ndarray X coordinate. Y : float or numpy.ndarray Y coordinate. Z : float or numpy.ndarray Z coordinate. Returns ------- numpy.ndarray Containing (R,phi,z) Notes ----- - 2010-09-24 - Written - Bovy (NYU) - 2019-06-21 - Changed such that phi in [-pi,pi] - Bovy (UofT) """ return (numpy.sqrt(X**2.0 + Y**2.0), numpy.arctan2(Y, X), Z) def cyl_to_rect(R, phi, Z): """ Convert from cylindrical to rectangular coordinates Parameters ---------- R : float or numpy.ndarray Cylindrical R coordinate. phi : float or numpy.ndarray Cylindrical phi coordinate. Z : float or numpy.ndarray Cylindrical Z coordinate. Returns ------- numpy.ndarray Containing (X,Y,Z) Notes ----- - 2011-02-23 - Written - Bovy (NYU) """ return (R * numpy.cos(phi), R * numpy.sin(phi), Z) def cyl_to_spher(R, Z, phi): """ Convert from cylindrical to spherical coordinates Parameters ---------- R : float or numpy.ndarray Cylindrical R coordinate. Z : float or numpy.ndarray Cylindrical Z coordinate. phi : float or numpy.ndarray Cylindrical phi coordinate. Returns ------- numpy.ndarray Containing (r,theta,phi) Notes ----- - 2016-05-16 - Written - Aladdin """ theta = numpy.arctan2(R, Z) r = (R**2 + Z**2) ** 0.5 return (r, theta, phi) def spher_to_cyl(r, theta, phi): """ Convert from spherical to cylindrical coordinates Parameters ---------- r : float or numpy.ndarray Spherical r coordinate. theta : float or numpy.ndarray Spherical theta coordinate. phi : float or numpy.ndarray Spherical phi coordinate. Returns ------- numpy.ndarray Containing (R,z,phi) Notes ----- - 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.0, Zsun=0.0, _extra_rot=True): """ Transform XYZ coordinates (wrt Sun) to cylindrical Galactocentric coordinates Parameters ---------- X : float or numpy.ndarray X coordinate. Y : float or numpy.ndarray Y coordinate. Z : float or numpy.ndarray Z coordinate. Xsun : float or numpy.ndarray, optional Cylindrical distance to the GC (can be array of same length as R). Zsun : float or numpy.ndarray, optional Sun's height above the midplane (can be array of same length as R). _extra_rot : bool, optional If True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition. Returns ------- numpy.ndarray Containing (R,phi,z) Notes ----- - 2010-09-24 - Written - Bovy (NYU) - 2023-07-19 - Allowed Xsun/Zsun to be arrays - Bovy (UofT) """ 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.0, Zsun=0.0, _extra_rot=True): """ Transform cylindrical Galactocentric coordinates to XYZ coordinates (wrt Sun) Parameters ---------- R : float or numpy.ndarray Cylindrical R coordinate. phi : float or numpy.ndarray Cylindrical phi coordinate. Z : float or numpy.ndarray Cylindrical Z coordinate. Xsun : float or numpy.ndarray, optional Cylindrical distance to the GC (can be array of same length as R). Zsun : float or numpy.ndarray, optional Sun's height above the midplane (can be array of same length as R). _extra_rot : bool, optional If True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition. Returns ------- numpy.ndarray Containing (X,Y,Z) Notes ----- - 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.0, 1.0, 0.0], Xsun=1.0, Zsun=0.0, _extra_rot=True ): """ Transform velocities in XYZ coordinates (wrt Sun) to rectangular Galactocentric coordinates for velocities Parameters ---------- vx : float or numpy.ndarray U velocity. vy : float or numpy.ndarray V velocity. vz : float or numpy.ndarray W velocity. vsun : float or numpy.ndarray, optional Velocity of the Sun in the GC frame (can also be array of same length as vx; shape [3,N]). Xsun : float or numpy.ndarray, optional Cylindrical distance to the GC (can be array of same length as vXg). Zsun : float or numpy.ndarray, optional Sun's height above the midplane (can be array of same length as vXg). _extra_rot : bool, optional If True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition. Returns ------- numpy.ndarray Containing (vx,vy,vz) Notes ----- - 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) - 2023-07-23 - Allowed Xsun/Zsun/vsun to be arrays- Bovy (UofT) """ if _extra_rot: vx, vy, vz = numpy.dot(galcen_extra_rot, numpy.array([vx, vy, vz])) dgc = numpy.sqrt(Xsun**2.0 + Zsun**2.0) costheta, sintheta = Xsun / dgc, Zsun / dgc zero = numpy.zeros_like(costheta) one = numpy.ones_like(costheta) return ( numpy.einsum( ( "ijk,jk->ik" if isinstance(costheta, numpy.ndarray) and costheta.ndim > 0 else "ij,jk->ik" ), numpy.array( [ [costheta, zero, -sintheta], [zero, one, zero], [sintheta, zero, costheta], ] ), numpy.array([-vx, vy, numpy.sign(Xsun) * vz]), ).T + numpy.array(vsun).T ) @scalarDecorator def vxvyvz_to_galcencyl( vx, vy, vz, X, Y, Z, vsun=[0.0, 1.0, 0.0], Xsun=1.0, Zsun=0.0, galcen=False, _extra_rot=True, ): """ Transform velocities in XYZ coordinates (wrt Sun) to cylindrical Galactocentric coordinates for velocities Parameters ---------- vx : float or numpy.ndarray U velocity. vy : float or numpy.ndarray V velocity. vz : float or numpy.ndarray W velocity. X : float or numpy.ndarray X in Galactocentric rectangular coordinates. Y : float or numpy.ndarray Y in Galactocentric rectangular coordinates. Z : float or numpy.ndarray Z in Galactocentric rectangular coordinates. vsun : float or numpy.ndarray, optional Velocity of the Sun in the GC frame (can also be array of same length as vx; shape [3,N]). Xsun : float or numpy.ndarray, optional Cylindrical distance to the GC (can be array of same length as vXg). Zsun : float or numpy.ndarray, optional Sun's height above the midplane (can be array of same length as vXg). galcen : bool, optional If True, then X,Y,Z are in cylindrical Galactocentric coordinates rather than rectangular coordinates. _extra_rot : bool, optional If True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition. Returns ------- numpy.ndarray Containing (vR,vT,vz) Notes ----- - 2010-09-24 - Written - Bovy (NYU) - 2023-07-19 - Allowed Xsun/Zsun/vsun to be arrays- Bovy (UofT) """ 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.0, 1.0, 0.0], Xsun=1.0, Zsun=0.0, _extra_rot=True ): """ Transform rectangular Galactocentric coordinates to XYZ coordinates (wrt Sun) for velocities Parameters ---------- vXg : float or numpy.ndarray Galactocentric x-velocity. vYg : float or numpy.ndarray Galactocentric y-velocity. vZg : float or numpy.ndarray Galactocentric z-velocity. vsun : float or numpy.ndarray, optional Velocity of the Sun in the GC frame (can also be array of same length as vXg; shape [3,N]). Xsun : float or numpy.ndarray, optional Cylindrical distance to the GC (can be array of same length as vXg). Zsun : float or numpy.ndarray, optional Sun's height above the midplane (can be array of same length as vXg). _extra_rot : bool, optional If True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition. Returns ------- numpy.ndarray Containing (vx,vy,vz) Notes ----- - 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 Xsun/Zsun/vsun to be arrays - Bovy (UofT) - 2018-04-18 - Tweaked to be consistent with astropy's Galactocentric frame - Bovy (UofT) """ dgc = numpy.sqrt(Xsun**2.0 + Zsun**2.0) costheta, sintheta = Xsun / dgc, Zsun / dgc zero = numpy.zeros_like(costheta) one = numpy.ones_like(costheta) out = numpy.einsum( ( "ijk,jk->ik" if isinstance(costheta, numpy.ndarray) and costheta.ndim > 0 else "ij,jk->ik" ), numpy.array( [ [-costheta, zero, -sintheta], [zero, one, zero], [-numpy.sign(Xsun) * sintheta, zero, 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.0, 1.0, 0.0], Xsun=1.0, Zsun=0.0, _extra_rot=True ): """ Transform cylindrical Galactocentric coordinates to XYZ coordinates (wrt Sun) for velocities Parameters ---------- vR : float or numpy.ndarray Galactocentric radial velocity. vT : float or numpy.ndarray Galactocentric rotational velocity. vZ : float or numpy.ndarray Galactocentric vertical velocity. phi : float or numpy.ndarray Galactocentric azimuth. vsun : float or numpy.ndarray, optional Velocity of the Sun in the GC frame (can also be array of same length as vR; shape [3,N]). Xsun : float or numpy.ndarray, optional Cylindrical distance to the GC (can be array of same length as vR). Zsun : float or numpy.ndarray, optional Sun's height above the midplane (can be array of same length as vR). _extra_rot : bool, optional If True, perform an extra tiny rotation to align the Galactocentric coordinate frame with astropy's definition. Parameters ---------- numpy.ndarray Containing (vx,vy,vz) Notes ----- - 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): """ Transform vectors from cylindrical to spherical coordinates. vtheta is positive from pole towards equator. Parameters ---------- vR : float or numpy.ndarray Galactocentric cylindrical radial velocity vT : float or numpy.ndarray Galactocentric cylindrical rotational velocity vz : float or numpy.ndarray Galactocentric cylindrical vertical velocity R : float or numpy.ndarray Galactocentric cylindrical radius z : float or numpy.ndarray Galactocentric cylindrical height Returns ------- tuple Containing: * vr : Galactocentric spherical radial velocity * vT : Galactocentric spherical rotational velocity * vtheta : Galactocentric spherical polar velocity Notes ----- - 2020-07-01 - Written - James Lane (UofT) """ r = numpy.sqrt(R**2.0 + z**2.0) 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): """ Transform vectors from spherical polar to cylindrical coordinates. Parameters ---------- vr : float or numpy.ndarray Galactocentric spherical radial velocity vT : float or numpy.ndarray Galactocentric spherical azimuthal velocity vtheta : float or numpy.ndarray Galactocentric spherical polar velocity theta : float or numpy.ndarray Galactocentric spherical polar angle Returns ------- tuple A tuple containing the Galactocentric cylindrical radial velocity, azimuthal velocity, and vertical velocity in that order. Notes ----- - 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): """ Transform vectors from rectangular to cylindrical coordinates vectors. Parameters ---------- vx : float or numpy.ndarray Velocity in the x-direction. vy : float or numpy.ndarray Velocity in the y-direction. vz : float or numpy.ndarray Velocity in the z-direction. X : float or numpy.ndarray X-coordinate. Y : float or numpy.ndarray Y-coordinate. Z : float or numpy.ndarray Z-coordinate. cyl : bool, optional If True, X, Y, Z are already cylindrical (i.e., [X,Y,Z] == [R,phi,Z]), by default False. Returns ------- tuple Tuple containing vR, vT, vz. Notes ----- - 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): """ Transform vectors from cylindrical to rectangular coordinate vectors. Parameters ---------- vr : float or numpy.ndarray Radial velocity. vt : float or numpy.ndarray Rotational velocity. vz : float or numpy.ndarray Vertical velocity. phi : float or numpy.ndarray Azimuth. Returns ------- tuple Rectangular coordinate vectors (vx, vy, vz). Notes ----- - 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): """ Calculate the Jacobian of the cylindrical to rectangular conversion Parameters ---------- R : float or numpy.ndarray Cylindrical R coordinate. phi : float or numpy.ndarray Cylindrical phi coordinate. Z : float or numpy.ndarray Cylindrical Z coordinate. vR : float or numpy.ndarray, optional Cylindrical vR coordinate. vT : float or numpy.ndarray, optional Cylindrical vT coordinate. vz : float or numpy.ndarray, optional Cylindrical vz coordinate. Returns ------- numpy.ndarray Jacobian of the cylindrical to rectangular conversion either just spatial or spatial+velocity. Notes ----- - 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.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.0 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.0 if len(args) == 3: out = out[:3, outIndx] out[:, [1, 2]] = out[:, [2, 1]] return out def galcenrect_to_XYZ_jac(*args, **kwargs): """ Calculate the Jacobian of the Galactocentric rectangular to Galactic coordinates Parameters ---------- X : float or numpy.ndarray Galactocentric rectangular coordinate. Y : float or numpy.ndarray Galactocentric rectangular coordinate. Z : float or numpy.ndarray Galactocentric rectangular coordinate. vX : float or numpy.ndarray, optional Galactocentric rectangular velocity. vY : float or numpy.ndarray, optional Galactocentric rectangular velocity. vZ : float or numpy.ndarray, optional Galactocentric rectangular velocity. Xsun : float or numpy.ndarray, optional Cylindrical distance to the GC (can be array of same length as X). Zsun : float or numpy.ndarray, optional Sun's height above the midplane (can be array of same length as X). Returns ------- numpy.ndarray Jacobian of the Galactocentric rectangular to Galactic coordinates either just spatial or spatial+velocity. Notes ----- - 2013-12-09 - Written - Bovy (IAS) """ Xsun = kwargs.get("Xsun", 1.0) dgc = numpy.sqrt(Xsun**2.0 + kwargs.get("Zsun", 0.0) ** 2.0) costheta, sintheta = Xsun / dgc, kwargs.get("Zsun", 0.0) / dgc out = numpy.zeros((6, 6)) out[0, 0] = -costheta out[0, 2] = -sintheta out[1, 1] = 1.0 out[2, 0] = -numpy.sign(Xsun) * sintheta out[2, 2] = numpy.sign(Xsun) * costheta out[3, 3] = -costheta out[3, 5] = -sintheta out[4, 4] = 1.0 out[5, 3] = -numpy.sign(Xsun) * sintheta out[5, 5] = numpy.sign(Xsun) * costheta return out[: len(args), : len(args)] def lbd_to_XYZ_jac(*args, **kwargs): """ Calculate the Jacobian of the Galactic spherical coordinates to Galactic rectangular coordinates transformation Parameters ---------- l : float or numpy.ndarray Galactic longitude. b : float or numpy.ndarray Galactic latitude. D : float or numpy.ndarray Distance. vlos : float or numpy.ndarray, optional Line-of-sight velocity. pmll : float or numpy.ndarray, optional Proper motion in Galactic longitude. pmbb : float or numpy.ndarray, optional Proper motion in Galactic latitude. degree : bool, optional If True, l and b are in degrees rather than rad. Returns ------- numpy.ndarray Jacobian of the Galactic spherical coordinates to Galactic rectangular coordinates transformation either just spatial or spatial+velocity. Notes ----- - 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.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.0, phio=0.0): """ Convert Galactic longitude and distance to Galactocentric radius and azimuth Parameters ---------- d : float or numpy.ndarray Distance. l : float or numpy.ndarray Galactic longitude. degree : bool, optional If True, l is in degrees rather than rad, by default False. ro : float, optional Galactocentric radius of the observer, by default 1.0. phio : float, optional Galactocentric azimuth of the observer, by default 0.0. Returns ------- tuple (R, phi); phi in degree if degree Notes ----- - 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.0 + d**2.0 - 2.0 * d * ro * numpy.cos(l)) phi = numpy.arcsin(d / R * numpy.sin(l)) indx = (ro / numpy.cos(l) < d) * (numpy.cos(l) > 0.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.0, phio=0.0): """ Convert Galactocentric radius and azimuth to distance and Galactic longitude Parameters ---------- R : float or numpy.ndarray Galactocentric radius. phi : float or numpy.ndarray Galactocentric azimuth. degree : bool, optional If True, phi is in degrees rather than rad, by default False. ro : float, optional Galactocentric radius of the observer, by default 1.0. phio : float, optional Galactocentric azimuth of the observer, by default 0.0. Returns ------- tuple (d, l); phi in degree if degree Notes ----- - 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.0 + ro**2.0 - 2.0 * R * ro * numpy.cos(phi)) l = numpy.arcsin(R / d * numpy.sin(phi)) indx = (ro / numpy.cos(phi) < R) * (numpy.cos(phi) > 0.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.0, oblate=False): """ Calculate prolate confocal cosh(u) and cos(v) coordinates from R,z, and delta. Parameters ---------- R : float or numpy.ndarray Radius. z : float or numpy.ndarray Height. delta : float or numpy.ndarray, optional Focus. Default is 1.0. oblate : bool, optional If True, compute oblate confocal coordinates instead of prolate. Default is False. Returns ------- tuple Tuple containing cosh(u) and cos(v). Notes ----- - 2012-11-27 - Written - Bovy (IAS) - 2017-10-11 - Added oblate coordinates - Bovy (UofT) """ if oblate: d12 = (R + delta) ** 2.0 + z**2.0 d22 = (R - delta) ** 2.0 + z**2.0 else: d12 = (z + delta) ** 2.0 + R**2.0 d22 = (z - delta) ** 2.0 + R**2.0 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.0 - cosv**2.0) return (coshu, cosv) def Rz_to_uv(R, z, delta=1.0, oblate=False): """ Calculate prolate or oblate confocal u and v coordinates from R, z, and delta. Parameters ---------- R : float or numpy.ndarray Radius. z : float or numpy.ndarray Height. delta : float or numpy.ndarray, optional Focus. Default is 1.0. oblate : bool, optional If True, compute oblate confocal coordinates instead of prolate. Default is False. Returns ------- tuple (u, v) Notes ----- - 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.0, oblate=False): """ Calculate R and z from prolate confocal u and v coordinates. Parameters ---------- u : float or numpy.ndarray Confocal u. v : float or numpy.ndarray Confocal v. delta : float or numpy.ndarray, optional Focus. Default is 1.0. oblate : bool, optional If True, compute oblate confocal coordinates instead of prolate. Default is False. Returns ------- tuple (R, z) Notes ----- - 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.0, oblate=False, uv=False): """ Calculate momenta in prolate or oblate confocal u and v coordinates from cylindrical velocities vR,vz for a given focal length delta. Parameters ---------- vR : float or numpy.ndarray Radial velocity in cylindrical coordinates. vz : float or numpy.ndarray Vertical velocity in cylindrical coordinates. R : float or numpy.ndarray Radius. z : float or numpy.ndarray Height. delta : float or numpy.ndarray, optional Focus. Default is 1.0. oblate : bool, optional If True, compute oblate confocal coordinates instead of prolate. Default is False. uv : bool, optional If True, the given R,z are actually u,v. Default is False. Returns ------- tuple (pu, pv) Notes ----- - 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.0, oblate=False): """ Calculate cylindrical vR and vz from momenta in prolate or oblate confocal u and v coordinates for a given focal length delta. Parameters ---------- pu : float or numpy.ndarray Momentum in u. pv : float or numpy.ndarray Momentum in v. u : float or numpy.ndarray Confocal u. v : float or numpy.ndarray Confocal v. delta : float or numpy.ndarray, optional Focus. Default is 1.0. oblate : bool, optional If True, compute oblate confocal coordinates instead of prolate. Default is False. Returns ------- tuple (vR, vz) Notes ----- - 2017-12-04 - Written - Bovy (UofT) """ if oblate: denom = delta * (numpy.sinh(u) ** 2.0 + numpy.cos(v) ** 2.0) 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.0 + numpy.sin(v) ** 2.0) 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.0, Delta=1.0): """ 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). Parameters ---------- R : float Galactocentric cylindrical radius z : float Vertical height ac : float, optional Axis ratio of the coordinate surfaces (a/c) = sqrt(-a) / sqrt(-g) (default: 5.) Delta : float, optional Focal distance that defines the spheroidal coordinate system (default: 1.) Delta=sqrt(g-a) Returns ------- tuple (lambda,nu) Notes ----- - 2015-02-13 - Written - Trick (MPIA) """ g = Delta**2 / (1.0 - ac**2) a = g - Delta**2 term = R**2 + z**2 - a - g discr = (R**2 + z**2 - Delta**2) ** 2 + (4.0 * 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.0: l = R**2 - a n = -g elif isinstance(z, numpy.ndarray) and numpy.sum(z == 0.0) > 0: R = numpy.atleast_1d(R) l[z == 0.0] = R[z == 0.0] ** 2 - a n[z == 0.0] = -g return (l, n) def Rz_to_lambdanu_jac(R, z, Delta=1.0): """ Calculate the Jacobian of the cylindrical (R,z) to prolate spheroidal (lambda,nu) conversion. Parameters ---------- R : float or numpy.ndarray Galactocentric cylindrical radius. z : float or numpy.ndarray Vertical height. Delta : float, optional Focal distance that defines the spheroidal coordinate system (default: 1.). Delta=sqrt(g-a). Returns ------- ndarray Jacobian d((lambda,nu))/d((R,z)). Notes ----- - 2015-02-13 - Written - Trick (MPIA). """ discr = (R**2 + z**2 - Delta**2) ** 2 + (4.0 * Delta**2 * R**2) dldR = R * (1.0 + (R**2 + z**2 + Delta**2) / numpy.sqrt(discr)) dndR = R * (1.0 - (R**2 + z**2 + Delta**2) / numpy.sqrt(discr)) dldz = z * (1.0 + (R**2 + z**2 - Delta**2) / numpy.sqrt(discr)) dndz = z * (1.0 - (R**2 + z**2 - Delta**2) / numpy.sqrt(discr)) dim = numpy.amax([len(numpy.atleast_1d(R)), len(numpy.atleast_1d(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.0): """ Calculate the Hessian of the cylindrical (R,z) to prolate spheroidal (lambda,nu) conversion. Parameters ---------- R : float Galactocentric cylindrical radius. z : float Vertical height. Delta : float, optional Focal distance that defines the spheroidal coordinate system (default: 1.). Delta=sqrt(g-a) Returns ------- ndarray Hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2]. Notes ----- - 2015-02-13 - Written - Trick (MPIA). """ D = Delta R2 = R**2 z2 = z**2 D2 = D**2 discr = (R2 + z2 - D2) ** 2 + (4.0 * D2 * R2) d2ldR2 = ( 1.0 + (3.0 * R2 + z2 + D2) / discr**0.5 - (2.0 * R2 * (R2 + z2 + D2) ** 2) / discr**1.5 ) d2ndR2 = ( 1.0 - (3.0 * R2 + z2 + D2) / discr**0.5 + (2.0 * R2 * (R2 + z2 + D2) ** 2) / discr**1.5 ) d2ldz2 = ( 1.0 + (R2 + 3.0 * z2 - D2) / discr**0.5 - (2.0 * z2 * (R2 + z2 - D2) ** 2) / discr**1.5 ) d2ndz2 = ( 1.0 - (R2 + 3.0 * z2 - D2) / discr**0.5 + (2.0 * z2 * (R2 + z2 - D2) ** 2) / discr**1.5 ) d2ldRdz = 2.0 * R * z / discr**0.5 * (1.0 - ((R2 + z2) ** 2 - D**4) / discr) d2ndRdz = 2.0 * R * z / discr**0.5 * (-1.0 + ((R2 + z2) ** 2 - D**4) / discr) dim = numpy.amax([len(numpy.atleast_1d(R)), len(numpy.atleast_1d(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.0, Delta=1.0): """ Calculate galactocentric cylindrical coordinates (R,z) from prolate spheroidal coordinates (lambda,nu). Parameters ---------- l : float Prolate spheroidal coordinate lambda. n : float Prolate spheroidal coordinate nu. ac : float, optional Axis ratio of the coordinate surfaces (a/c) = sqrt(-a) / sqrt(-g) (default: 5.). Delta : float, optional Focal distance that defines the spheroidal coordinate system (default: 1.). Delta=sqrt(g-a) Returns ------- tuple (R,z) Notes ----- - 2015-02-13 - Written - Trick (MPIA) """ g = Delta**2 / (1.0 - ac**2) a = g - Delta**2 r2 = (l + a) * (n + a) / (a - g) z2 = (l + g) * (n + g) / (g - a) index = (r2 < 0.0) * ((n + a) > 0.0) * ((n + a) < 1e-10) if numpy.any(index): if isinstance(r2, numpy.ndarray): r2[index] = 0.0 else: r2 = 0.0 index = (z2 < 0.0) * ((n + g) < 0.0) * ((n + g) > -1e-10) if numpy.any(index): if isinstance(z2, numpy.ndarray): z2[index] = 0.0 else: z2 = 0.0 return (numpy.sqrt(r2), numpy.sqrt(z2)) @scalarDecorator @degreeDecorator([0, 1], [0, 1]) def radec_to_custom(ra, dec, T=None, degree=False): """ Transform from equatorial coordinates to a custom set of sky coordinates Parameters ---------- ra : float or numpy.ndarray Right ascension. dec : float or numpy.ndarray Declination. T : numpy.ndarray, optional 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, optional If True, ra and dec are given in degree and l and b will be as well. Returns ------- tuple (custom longitude, custom latitude) (with longitude -180 to 180) Notes ----- - 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): """ Rotate proper motions in (ra,dec) to proper motions in a custom set of sky coordinates (phi1,phi2) Parameters ---------- pmra : float or numpy.ndarray Proper motion in ra (multiplied with cos(dec)) [mas/yr]. pmdec : float or numpy.ndarray Proper motion in dec [mas/yr]. ra : float or numpy.ndarray Right ascension. dec : float or numpy.ndarray Declination. T : numpy.ndarray, optional 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, optional If True, ra and dec are given in degrees (default=False). Returns ------- tuple (pmphi1 x cos(phi2),pmph2) for vector inputs [:,2] Notes ----- - 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.0, numpy.pi / 2, T=T) # Whether to use degrees and scalar input is handled by decorators dec[dec == dec_ngp] += 10.0**-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.0 + sinphi**2.0) 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): """ Rotate a custom set of sky coordinates (phi1,phi2) to (ra,dec) given the rotation matrix T for (ra,dec) -> (phi1,phi2) Parameters ---------- phi1 : float or numpy.ndarray Custom sky coord. phi2 : float or numpy.ndarray Custom sky coord. T : numpy.ndarray, optional 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, optional If True, phi1 and phi2 are given in degrees (default=False). Returns ------- tuple (ra,dec) for vector inputs [:,2] Notes ----- - 2018-10-23 - Written - Starkman (UofT) """ if T is None: raise ValueError("Must set T= for custom_to_radec") return radec_to_custom( phi1, phi2, T=numpy.transpose(T), degree=degree, # T.T = inv(T) ) def custom_to_pmrapmdec(pmphi1, pmphi2, phi1, phi2, T=None, degree=False): """ Rotate proper motions in a custom set of sky coordinates (phi1,phi2) to (ra,dec) given the rotation matrix T for (ra,dec) -> (phi1,phi2) Parameters ---------- pmphi1 : float or numpy.ndarray Proper motion in phi1 (multiplied with cos(phi2)) [mas/yr]. pmphi2 : float or numpy.ndarray Proper motion in phi2 [mas/yr]. phi1 : float or numpy.ndarray Custom sky coord. phi2 : float or numpy.ndarray Custom sky coord. T : numpy.ndarray, optional 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, optional If True, phi1 and phi2 are given in degrees (default=False). Returns ------- tuple (pmra x cos(dec), dec) for vector inputs [:,2] Notes ----- - 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), degree=degree, # T.T = inv(T) ) def get_epoch_angles(epoch=2000.0): """ Get the angles relevant for the transformation from ra, dec to l,b for the given epoch. Parameters ---------- epoch : float or str, optional Epoch of ra,dec. Default is 2000.0. When internally using astropy's coordinate transformations, epoch can be None for ICRS, 'JXXXX' for FK5, and 'BXXXX' for FK4. For B1950 FK4 with no E aberration terms is assumed. Returns ------- tuple Set of angles. Notes ----- - 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.0 * numpy.pi dec_ngp = 27.12825118085622 / 180.0 * numpy.pi ra_ngp = 192.8594812065348 / 180.0 * numpy.pi elif epoch == 1950.0: theta = 123.0 / 180.0 * numpy.pi dec_ngp = 27.4 / 180.0 * numpy.pi ra_ngp = 192.25 / 180.0 * 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.0 * units.deg, 90.0 * units.deg, frame=frame, equinox=epoch ) c = c.transform_to(apycoords.Galactic) theta = c.l.to(units.rad).value c = apycoords.SkyCoord(180.0 * units.deg, 90.0 * 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.0 * units.deg, 90.0 * units.deg, frame="icrs") c = c.transform_to(apycoords.Galactic) theta_icrs = c.l.to(units.rad).value c = apycoords.SkyCoord(180.0 * units.deg, 90.0 * 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.0 * numpy.pi, 266.4051 / 180.0 * numpy.pi, ) # from apy def. eta = 58.5986320306 / 180.0 * 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.0, 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, 0.0, 1.0]), inv=True, _dontcutsmall=True, )[0] # Leave x axis alone, because in place by rot1 galcen_extra_rot2[0, 0] = 1.0 galcen_extra_rot2[0, 1:] = 0.0 galcen_extra_rot2[1:, 0] = 0.0 galcen_extra_rot = numpy.dot(galcen_extra_rot2, galcen_extra_rot1) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6788845 galpy-1.10.2/galpy/util/interp_2d/0000755000175100001660000000000014761352040016273 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/util/interp_2d/cubic_bspline_2d_coeffs.c0000644000175100001660000002127514761352023023162 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=1741018131.0 galpy-1.10.2/galpy/util/interp_2d/cubic_bspline_2d_coeffs.h0000644000175100001660000000153314761352023023162 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=1741018131.0 galpy-1.10.2/galpy/util/interp_2d/cubic_bspline_2d_interpol.c0000644000175100001660000002047314761352023023550 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=1741018131.0 galpy-1.10.2/galpy/util/interp_2d/interp_2d.c0000644000175100001660000001141514761352023020330 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=1741018131.0 galpy-1.10.2/galpy/util/interp_2d/interp_2d.h0000644000175100001660000000207614761352023020340 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=1741018131.0 galpy-1.10.2/galpy/util/leung_dop853.c0000644000175100001660000005013714761352023016774 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_dump(); // LCOV_EXCL_START __gcov_reset(); #endif 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) && (pos_neg * t[finished_user_t_ii + 1] < pos_neg * 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=1741018131.0 galpy-1.10.2/galpy/util/leung_dop853.h0000644000175100001660000000463214761352023017000 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=1741018131.0 galpy-1.10.2/galpy/util/leung_dop853.py0000644000175100001660000004757014761352023017211 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.84289382761090128651353491142e1 d46 = 0.56671495351937776962531783590 d47 = -0.30689499459498916912797304727e1 d48 = 0.23846676565120698287728149680e1 d49 = 0.21170345824450282767155149946e1 d410 = -0.87139158377797299206789907490 d411 = 0.22404374302607882758541771650e1 d412 = 0.63157877876946881815570249290 d413 = -0.88990336451333310820698117400e-1 d414 = 0.18148505520854727256656404962e2 d415 = -0.91946323924783554000451984436e1 d416 = -0.44360363875948939664310572000e1 d51 = 0.10427508642579134603413151009e2 d56 = 0.24228349177525818288430175319e3 d57 = 0.16520045171727028198505394887e3 d58 = -0.37454675472269020279518312152e3 d59 = -0.22113666853125306036270938578e2 d510 = 0.77334326684722638389603898808e1 d511 = -0.30674084731089398182061213626e2 d512 = -0.93321305264302278729567221706e1 d513 = 0.15697238121770843886131091075e2 d514 = -0.31139403219565177677282850411e2 d515 = -0.93529243588444783865713862664e1 d516 = 0.35816841486394083752465898540e2 d61 = 0.19985053242002433820987653617e2 d66 = -0.38703730874935176555105901742e3 d67 = -0.18917813819516756882830838328e3 d68 = 0.52780815920542364900561016686e3 d69 = -0.11573902539959630126141871134e2 d610 = 0.68812326946963000169666922661e1 d611 = -0.10006050966910838403183860980e1 d612 = 0.77771377980534432092869265740 d613 = -0.27782057523535084065932004339e1 d614 = -0.60196695231264120758267380846e2 d615 = 0.84320405506677161018159903784e2 d616 = 0.11992291136182789328035130030e2 d71 = -0.25693933462703749003312586129e2 d76 = -0.15418974869023643374053993627e3 d77 = -0.23152937917604549567536039109e3 d78 = 0.35763911791061412378285349910e3 d79 = 0.93405324183624310003907691704e2 d710 = -0.37458323136451633156875139351e2 d711 = 0.10409964950896230045147246184e3 d712 = 0.29840293426660503123344363579e2 d713 = -0.43533456590011143754432175058e2 d714 = 0.96324553959188282948394950600e2 d715 = -0.39177261675615439165231486172e2 d716 = -0.14972683625798562581422125276e3 # Error calculation coefficients er1 = 0.1312004499419488073250102996e-1 er6 = -0.1225156446376204440720569753e1 er7 = -0.4957589496572501915214079952 er8 = 0.1664377182454986536961530415e1 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.float64).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 extrapolating """ # 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.0, h=0.0, rtol=1e-12, atol=1e-12, nmax=int(1e8), safe=0.9, beta=0.0, fac1=0.333, fac2=6.0, args=(), ): """ Solve a system of ordinary differential equations using the DOP853 method, an explicit Runge-Kutta method of order 8(5,3) due to Dormand & Prince with step size control and dense output. Parameters ---------- func : callable function of the differential equation, usually take func([position, velocity], time) and return velocity, acceleration x : float or ndarray initial x, usually is [position, velocity] t : ndarray set of times at which one wants the result hmax : float Maximal step size, default 0. which will be internally as t[-1]-t[0] h : float Initial step size, default 0. which will be computed by the function hinit() rtol : float or ndarray Relative error tolerances, default 1e-8 atol : float or ndarray Absolute error tolerances, default 1e-8 nmax : int Maximum number of steps, default 1e8 safe : float Safety factor in the step size prediction, default 0.9 beta : float 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. fac1, fac2 : float Parameters for step size selection; the new step size is chosen subject to the restriction fac1 <= hnew/hold <= fac2 Returns ------- ndarray integrated result Notes ----- - 2018-11-23 - Written - Henry Leung (University of Toronto) References ---------- - 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). """ # 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.0, 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=1741018131.0 galpy-1.10.2/galpy/util/multi.py0000644000175100001660000002032214761352023016111 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. Parameters ---------- f : callable function that accepts argument from iterable ii : int process ID chunk : ? slice of input iterable out_q : ? thread-safe output queue err_q : ? thread-safe queue to populate on exception lock : ? thread-safe lock to protect a resource ( useful in extending parallel_map() ) progressbar : bool if True, display a progress bar iter_elapsed : int shared-memory Value to track progress tot_iter : int total number of iterations (for progressbar) 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. Parameters ---------- procs : list list of Process objects out_q : ? thread-safe output queue err_q : ? thread-safe queue to populate on exception num : int 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. Parameters ---------- function : callable callable function that accepts argument from iterable sequence : iterable iterable sequence numcores : int, optional number of cores to use progressbar : bool, optional if True, display a progress bar """ 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=1741018131.0 galpy-1.10.2/galpy/util/plot.py0000644000175100001660000012433614761352023015747 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.0]), "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): """ Save the current figure(s) to a file. Parameters ---------- filename : str Filename for the plot (with extension). **kwargs Additional keyword arguments to pass to `pyplot.savefig`. Notes ----- - 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): """ Plot a histogram of the input array using matplotlib's hist function. Parameters ---------- x : numpy.ndarray Array to histogram. xlabel : str, optional x-axis label, LaTeX math mode, no $s needed. ylabel : str, optional y-axis label, LaTeX math mode, no $s needed. overplot : bool, optional If True, plot on top of the current figure. **kwargs All other keyword arguments are passed to ``pyplot.hist``. Returns ------- tuple Output from ``pyplot.hist`` Notes ----- - 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.nanmin(numpy.array(x)), numpy.nanmax(numpy.array(x))) else: pyplot.xlim(numpy.nanmin(x), numpy.nanmax(x)) elif xrangeSet: pyplot.xlim(xlimits) else: pyplot.xlim(kwargs["range"]) if yrangeSet: pyplot.ylim(ylimits) _add_ticks() return out def plot(*args, **kwargs): """ Wrapper around matplotlib's plot function. Parameters ---------- *args: Inputs to ``pyplot.plot``. xlabel : str, optional x-axis label, LaTeX math mode, no $s needed. ylabel : str, optional y-axis label, LaTeX math mode, no $s needed. xrange : tuple, optional x range to plot over. yrange : tuple, optional y range to plot over. overplot : bool, optional If True, plot on top of the current figure. gcf : bool, optional If True, do not start a new figure. onedhists : bool, optional If True, make one-d histograms on the sides. onedhistcolor : str, optional Histogram color. onedhistfc : str, optional Histogram fill color. onedhistec : str, optional Histogram edge color. onedhistxnormed : bool, optional If True, normalize the x-axis histogram. onedhistynormed : bool, optional If True, normalize the y-axis histogram. onedhistxweights : numpy.ndarray, optional Weights for the x-axis histogram. onedhistyweights : numpy.ndarray, optional Weights for the y-axis histogram. bins : int, optional Number of bins for the one-d histograms. semilogx : bool, optional If True, plot the x-axis on a log scale. semilogy : bool, optional If True, plot the y-axis on a log scale. loglog : bool, optional If True, plot both axes on a log scale. scatter : bool, optional If True, use ``pyplot.scatter`` instead of ``pyplot.plot``. colorbar : bool, optional If True, add a colorbar. crange : tuple, optional Range for the colorbar. clabel : str, optional Label for the colorbar. **kwargs All other keyword arguments are passed to ``pyplot.plot`` or ``pyplot.scatter``. Returns ------- tuple Output from ``pyplot.plot``/``pyplot.scatter`` or 3 Axes instances if ``onedhists=True``. Notes ----- - 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.nanmin(numpy.array(args[0])), numpy.nanmax(numpy.array(args[0])), ) else: xlimits = (numpy.nanmin(args[0]), numpy.nanmax(args[0])) ylimits = kwargs.pop("yrange", None) if ylimits is None: if isinstance(args[1], list): ylimits = ( numpy.nanmin(numpy.array(args[1])), numpy.nanmax(numpy.array(args[1])), ) else: ylimits = (numpy.nanmin(args[1]), numpy.nanmax(args[1])) climits = kwargs.pop("crange", None) if climits is None and scatter: if "c" in kwargs and isinstance(kwargs["c"], list): climits = ( numpy.nanmin(numpy.array(kwargs["c"])), numpy.nanmax(numpy.array(kwargs["c"])), ) elif "c" in kwargs: climits = (numpy.nanmin(kwargs["c"]), numpy.nanmax(kwargs["c"].nanmax())) 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): """ Wrapper around ``pyplot.plot`` for 3D plots, much like plot is a wrapper around ``pyplot.plot`` for 2D plots. Parameters ---------- *args: Inputs to ``pyplot.plot3d``. xlabel : str, optional x-axis label, LaTeX math mode, no $s needed. ylabel : str, optional y-axis label, LaTeX math mode, no $s needed. zlabel : str, optional z-axis label, LaTeX math mode, no $s needed. xrange : tuple, optional x range to plot over. yrange : tuple, optional y range to plot over. zrange : tuple, optional z range to plot over. overplot : bool, optional If True, plot on top of the current figure. Returns ------- tuple Output from ``pyplot.plot3d``. Notes ----- - 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.nanmin(numpy.array(args[0])), numpy.nanmax(numpy.array(args[0])), ) else: xlimits = (numpy.nanmin(args[0]), numpy.nanmax(args[0])) if "yrange" in kwargs: ylimits = kwargs.pop("yrange") else: if isinstance(args[1], list): ylimits = ( numpy.nanmin(numpy.array(args[1])), numpy.nanmax(numpy.array(args[1])), ) else: ylimits = (numpy.nanmin(args[1]), numpy.nanmax(args[1])) if "zrange" in kwargs: zlimits = kwargs.pop("zrange") else: if isinstance(args[2], list): zlimits = ( numpy.nanmin(numpy.array(args[2])), numpy.nanmax(numpy.array(args[2])), ) else: zlimits = (numpy.nanmin(args[2]), numpy.nanmax(args[2])) 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): """ Plot a 2d density with optional contours. Parameters ---------- X : numpy.ndarray The density to plot. *args : Arguments for ``pyplot.imshow``. xlabel : str, optional x-axis label, LaTeX math mode, no $s needed. ylabel : str, optional y-axis label, LaTeX math mode, no $s needed. xrange : tuple, optional x range to plot over. yrange : tuple, optional y range to plot over. noaxes : bool, optional If True, don't plot any axes. overplot : bool, optional If True, overplot. gcf : bool, optional If True, do not start a new figure. colorbar : bool, optional If True, add colorbar. shrink : float, optional Colorbar shrink factor. conditional : bool, optional Normalize each column separately (for probability densities, i.e., ``cntrmass=True``). justcontours : bool, optional If True, only draw contours. contours : bool, optional If True, draw contours (10 by default). levels : numpy.ndarray, optional Contour levels. cntrmass : bool, optional If True, the density is a probability and the levels are probability masses contained within the contour. cntrcolors : str or list, optional Colors for contours (single color or array). cntrlabel : bool, optional Label the contours. cntrlw : float, optional Linewidths for contour. cntrls : str, optional Linestyles for contour. cntrlabelsize : float, optional Size of contour labels. cntrlabelcolors : str, optional Color of contour labels. cntrinline : bool, optional If True, put contour labels inline with contour. cntrSmooth : float, optional Use ``ndimage.gaussian_filter`` to smooth before contouring. retAxes : bool, optional Return all Axes instances. retCont : bool, optional Return the contour instance. Returns ------- Axes or tuple Plot to output device, Axes instances depending on input. Notes ----- - 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.0, 1.0, _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)) * 0.87, 1.0]) 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.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, 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.0 histy[numpy.isnan(histy)] = 0.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, ): """ Set up a figure for plotting. Parameters ---------- fig_width : float, optional Width in inches. Default is 5. fig_height : float, optional Height in inches. Default is 5. axes_labelsize : int, optional Size of the axis-labels. Default is 16. text_fontsize : int, optional Font-size of the text (if any). Default is 11. legend_fontsize : int, optional Font-size of the legend (if any). Default is 12. xtick_labelsize : int, optional Size of the x-axis labels. Default is 10. ytick_labelsize : int, optional Size of the y-axis labels. Default is 10. xtick_minor_size : int, optional Size of the minor x-ticks. Default is 2. ytick_minor_size : int, optional Size of the minor y-ticks. Default is 2. xtick_major_size : int, optional Size of the major x-ticks. Default is 4. ytick_major_size : int, optional Size of the major y-ticks. Default is 4. Notes ----- - 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): """ Thin wrapper around matplotlib's text and annotate. Parameters ---------- *args : See matplotlib's text (http://matplotlib.sourceforge.net/api/pyplot_api.html#matplotlib.pyplot.text). **kwargs : '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. Notes ----- - 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): """ Make a 'smart' scatterplot that is a density plot in high-density regions and a regular scatterplot for outliers. Parameters ---------- x : numpy.ndarray x data. y : numpy.ndarray y data. xlabel : str, optional x-axis label, LaTeX math mode, no $s needed. ylabel : str, optional y-axis label, LaTeX math mode, no $s needed. xrange : tuple, optional x range to plot over. yrange : tuple, optional y range to plot over. bins : int, optional Number of bins to use in each dimension. weights : numpy.ndarray, optional Data-weights. aspect : float, optional Aspect ratio. conditional : bool, optional Normalize each column separately (for probability densities, i.e., ``cntrmass=True``). overplot : bool, optional If True, overplot. gcf : bool, optional Do not start a new figure (does change the ranges and labels). contours : bool, optional If False, don't plot contours. justcontours : bool, optional If True, only draw contours, no density. cntrcolors : str or list, optional Color of contours (can be array as for dens2d). cntrlw : float, optional Linewidths for contour. cntrls : str, optional Linestyles for contour. cntrSmooth : float, optional Use ``ndimage.gaussian_filter`` to smooth before contouring. levels : numpy.ndarray, optional 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 : bool, optional If True, make one-d histograms on the sides. onedhistx : bool, optional If True, make one-d histograms on the side of the x distribution. onedhisty : bool, optional If True, make one-d histograms on the side of the y distribution. onedhistcolor : str, optional Color of one-d histograms. onedhistfc : str, optional Facecolor of one-d histograms. onedhistec : str, optional Edgecolor of one-d histograms. onedhistxnormed : bool, optional Normed keyword for one-d histograms. onedhistynormed : bool, optional Normed keyword for one-d histograms. onedhistxweights : numpy.ndarray, optional Weights keyword for one-d histograms. onedhistyweights : numpy.ndarray, optional Weights keyword for one-d histograms. cmap : matplotlib.colors.Colormap, optional Colormap for density plot. hist : numpy.ndarray, optional 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. edges : numpy.ndarray, optional 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 : bool, optional Return all Axes instances. Returns ------- Axes or tuple Plot to output device, Axes instance(s) or not, depending on input. Notes ----- - 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.0))) 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.0) binxs = numpy.array(binxs) binys = [] yedge = edges[1] for ii in range(len(yedge) - 1): binys.append((yedge[ii] + yedge[ii + 1]) / 2.0) 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.0 - 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): """ Add axis labels to the current figure. Parameters ---------- xlabel : str x-axis label, LaTeX math mode, no $s needed. ylabel : str y-axis label, LaTeX math mode, no $s needed. Notes ----- = 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): """ Add minor axis ticks to a plot. Parameters ---------- xticks : bool, optional If True, add minor ticks to the x-axis. Default is True. yticks : bool, optional If True, add minor ticks to the y-axis. Default is True. Notes ----- - 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.0)) if yticks: ystep = ax.yaxis.get_majorticklocs() ystep = ystep[1] - ystep[0] ax.yaxis.set_minor_locator(ticker.MultipleLocator(ystep / 5.0)) 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.float64) 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=1741018131.0 galpy-1.10.2/galpy/util/quadpack.py0000644000175100001660000001721014761352023016552 0ustar00runnerdocker############################################################################### # galpy.util.quadpack: some variations on scipy's quadpack/quad ############################################################################### import warnings import numpy from scipy.integrate import fixed_quad, 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, ) # scipy adaptive Gaussian quadrature that was deprecated in 1.12.0 and associated # functions; same BSD-3 license as galpy class AccuracyWarning(Warning): pass def vectorize1(func, args=(), vec_func=False): """Vectorize the call to a function. This is an internal utility function used by `romberg` and `quadrature` to create a vectorized version of a function. If `vec_func` is True, the function `func` is assumed to take vector arguments. Parameters ---------- func : callable User defined function. args : tuple, optional Extra arguments for the function. vec_func : bool, optional True if the function func takes vector arguments. Returns ------- vfunc : callable A function that will take a vector argument and return the result. """ if vec_func: def vfunc(x): return func(x, *args) else: # pragma: no cover # Don't cover because not used in galpy def vfunc(x): if numpy.isscalar(x): return func(x, *args) x = numpy.asarray(x) # call with first point to get output type y0 = func(x[0], *args) n = len(x) dtype = getattr(y0, "dtype", type(y0)) output = numpy.empty((n,), dtype=dtype) output[0] = y0 for i in range(1, n): output[i] = func(x[i], *args) return output return vfunc def quadrature( func, a, b, args=(), tol=1.49e-8, rtol=1.49e-8, maxiter=50, vec_func=True, miniter=1 ): """ Compute a definite integral using fixed-tolerance Gaussian quadrature. Integrate `func` from `a` to `b` using Gaussian quadrature with absolute tolerance `tol`. Parameters ---------- func : function A Python function or method to integrate. a : float Lower limit of integration. b : float Upper limit of integration. args : tuple, optional Extra arguments to pass to function. tol, rtol : float, optional Iteration stops when error between last two iterates is less than `tol` OR the relative change is less than `rtol`. maxiter : int, optional Maximum order of Gaussian quadrature. vec_func : bool, optional True or False if func handles arrays as arguments (is a "vector" function). Default is True. miniter : int, optional Minimum order of Gaussian quadrature. Returns ------- val : float Gaussian quadrature approximation (within tolerance) to integral. err : float Difference between last two estimates of the integral. """ vfunc = vectorize1(func, args, vec_func=vec_func) val = numpy.inf err = numpy.inf maxiter = max(miniter + 1, maxiter) for n in range(miniter, maxiter + 1): newval = fixed_quad(vfunc, a, b, (), n)[0] err = abs(newval - val) val = newval if err < tol or err < rtol * abs(val): break else: warnings.warn( "maxiter (%d) exceeded. Latest difference = %e" % (maxiter, err), AccuracyWarning, ) return val, err # scipy Romberg quadrature that was deprecated in 1.12.0 and associated # functions; same BSD-3 license as galpy def _difftrap(function, interval, numtraps): """ Perform part of the trapezoidal rule to integrate a function. Assume that we had called difftrap with all lower powers-of-2 starting with 1. Calling difftrap only returns the summation of the new ordinates. It does _not_ multiply by the width of the trapezoids. This must be performed by the caller. 'function' is the function to evaluate (must accept vector arguments). 'interval' is a sequence with lower and upper limits of integration. 'numtraps' is the number of trapezoids to use (must be a power-of-2). """ if numtraps == 1: return 0.5 * (function(interval[0]) + function(interval[1])) else: numtosum = numtraps / 2 h = float(interval[1] - interval[0]) / numtosum lox = interval[0] + 0.5 * h points = lox + h * numpy.arange(numtosum) s = numpy.sum(function(points), axis=0) return s def _romberg_diff(b, c, k): """ Compute the differences for the Romberg quadrature corrections. See Forman Acton's "Real Computing Made Real," p 143. """ tmp = 4.0**k return (tmp * c - b) / (tmp - 1.0) def romberg( function, a, b, args=(), tol=1.48e-8, rtol=1.48e-8, divmax=10, vec_func=False, ): """ Romberg integration of a callable function or method. Returns the integral of `function` (a function of one variable) over the interval (`a`, `b`). If `show` is 1, the triangular array of the intermediate results will be printed. If `vec_func` is True (default is False), then `function` is assumed to support vector arguments. Parameters ---------- function : callable Function to be integrated. a : float Lower limit of integration. b : float Upper limit of integration. Returns ------- results : float Result of the integration. Other Parameters ---------------- args : tuple, optional Extra arguments to pass to function. Each element of `args` will be passed as a single argument to `func`. Default is to pass no extra arguments. tol, rtol : float, optional The desired absolute and relative tolerances. Defaults are 1.48e-8. divmax : int, optional Maximum order of extrapolation. Default is 10. vec_func : bool, optional Whether `func` handles arrays as arguments (i.e., whether it is a "vector" function). Default is False. References ---------- .. [1] 'Romberg's method' https://en.wikipedia.org/wiki/Romberg%27s_method """ if numpy.isinf(a) or numpy.isinf(b): # pragma: no cover raise ValueError("Romberg integration only available for finite limits.") vfunc = vectorize1(function, args, vec_func=vec_func) n = 1 interval = [a, b] intrange = b - a ordsum = _difftrap(vfunc, interval, n) result = intrange * ordsum resmat = [[result]] err = numpy.inf last_row = resmat[0] for i in range(1, divmax + 1): n *= 2 ordsum += _difftrap(vfunc, interval, n) row = [intrange * ordsum / n] for k in range(i): row.append(_romberg_diff(last_row[k], row[k], k + 1)) result = row[i] lastresult = last_row[i - 1] err = abs(result - lastresult) if err < tol or err < rtol * abs(result): break last_row = row else: # pragma: no cover warnings.warn( "divmax (%d) exceeded. Latest difference = %e" % (divmax, err), AccuracyWarning, ) return result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/util/symplecticode.py0000644000175100001660000001202314761352023017622 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.0 def leapfrog(func, yo, t, args=(), rtol=1.49012e-12, atol=1.49012e-12): """ Leapfrog integration of an ODE Parameters ---------- func : function function of (y, *args) yo : numpy.ndarray initial condition [q,p] t : numpy.ndarray set of times at which one wants the result args : tuple, optional any extra arguments for func rtol : float, optional relative tolerance atol : float, optional absolute tolerance Returns ------- numpy.ndarray Array containing the value of y for each desired time in t, with the initial value y0 in the first row. Notes ----- - 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)): # initial half drift q12 = leapfrog_leapq(qo, po, dt / 2.0) for jj in range(ndt - 1): # loop over number of sub-intervals # kick force = func(q12, *args, t=to + dt / 2) po = leapfrog_leapp(po, dt, force) # full drift to next half step q12 = leapfrog_leapq(q12, po, dt) # Get ready for next to += dt # last kick and half drift to arrive at final step force = func(q12, *args, t=to + dt / 2) po = leapfrog_leapp(po, dt, force) qo = leapfrog_leapq(q12, po, dt / 2) 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.0 dt *= 2.0 while err > 1.0 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.0) force = func(q12, *args, t=to + dt / 2) p11 = leapfrog_leapp(po, dt, force) q11 = leapfrog_leapq(q12, p11, dt / 2.0) # dt/2. q12 = leapfrog_leapq(qo, po, dt / 4.0) force = func(q12, *args, t=to + dt / 4) ptmp = leapfrog_leapp(po, dt / 2.0, force) qtmp = leapfrog_leapq(q12, ptmp, dt / 2.0) # Take full step combining two half force = func(qtmp, *args, t=to + 3.0 * dt / 4) p12 = leapfrog_leapp(ptmp, dt / 2.0, force) q12 = leapfrog_leapq(qtmp, p12, dt / 4.0) # Norm delta = numpy.array([numpy.fabs(q11 - q12), numpy.fabs(p11 - p12)]).flatten() err = numpy.sqrt(numpy.mean((delta / scale) ** 2.0)) dt /= 2.0 return dt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/util/wez_ias15.c0000644000175100001660000005172714761352023016375 0ustar00runnerdocker/* C implementation of IAS15 integrator */ /* Copyright (c) 2023, John Weatherall 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" const double integrator_error_threshold = 1e-16; //e_deltab from paper const double precision_parameter = 1e-9; //e_b from paper const int order = 7; const double h[8] = { 0.0, 0.05626256053692215, 0.18024069173689236, 0.35262471711316964, 0.54715362633055538, 0.73421017721541053, 0.88532094683909577, 0.97752061356128750 }; const double c_0_0 = 1; const double c_1_0 = -0.05626256053692215; const double c_1_1 = 1; const double c_2_0 = 0.01014080283006363; const double c_2_1 = -0.23650325227381452; const double c_2_2 = 1; const double c_3_0 = -0.003575897729251617; const double c_3_1 = 0.09353769525946207; const double c_3_2 = -0.5891279693869842; const double c_3_3 = 1; const double c_4_0 = 0.001956565409947221; const double c_4_1 = -0.05475538688906869; const double c_4_2 = 0.4158812000823069; const double c_4_3 = -1.1362815957175396; const double c_4_4 = 1; const double c_5_0 = -0.0014365302363708915; const double c_5_1 = 0.042158527721268706; const double c_5_2 = -0.3600995965020568; const double c_5_3 = 1.250150711840691; const double c_5_4 = -1.87049177293295; const double c_5_5 = 1; const double c_6_0 = 0.0012717903090268678; const double c_6_1 = -0.03876035791590677; const double c_6_2 = 0.360962243452846; const double c_6_3 = -1.466884208400427; const double c_6_4 = 2.906136259308429; const double c_6_5 = -2.7558127197720457; const double c_6_6 = 1; const double c_7_0 = -0.0012432012432012432; const double c_7_1 = 0.03916083916083916; const double c_7_2 = -0.3916083916083916; const double c_7_3 = 1.7948717948717947; const double c_7_4 = -4.3076923076923075; const double c_7_5 = 5.6; const double c_7_6 = -3.7333333333333334; const double c_7_7 = 1; const double c_8_0 = 0.0012432012432012432; const double c_8_1 = -0.0404040404040404; const double c_8_2 = 0.4307692307692308; const double c_8_3 = -2.1864801864801864; const double c_8_4 = 6.102564102564102; const double c_8_5 = -9.907692307692308; const double c_8_6 = 9.333333333333332; const double c_8_7 = -4.733333333333333; const double c_8_8 = 1; const double d_0_0 = 1; const double d_1_0 = 0.05626256053692215; const double d_1_1 = 1; const double d_2_0 = 0.0031654757181708297; const double d_2_1 = 0.23650325227381452; const double d_2_2 = 1; const double d_3_0 = 0.00017809776922174343; const double d_3_1 = 0.04579298550602792; const double d_3_2 = 0.5891279693869842; const double d_3_3 = 1; const double d_4_0 = 1.002023652232913e-05; const double d_4_1 = 0.008431857153525702; const double d_4_2 = 0.25353406905456927; const double d_4_3 = 1.1362815957175396; const double d_4_4 = 1; const double d_5_0 = 5.637641639318209e-07; const double d_5_1 = 0.001529784002500466; const double d_5_2 = 0.09783423653244401; const double d_5_3 = 0.8752546646840911; const double d_5_4 = 1.87049177293295; const double d_5_5 = 1; const double d_6_0 = 3.171881540176138e-08; const double d_6_1 = 0.0002762930909826477; const double d_6_2 = 0.03602855398373646; const double d_6_3 = 0.5767330002770787; const double d_6_4 = 2.2485887607691595; const double d_6_5 = 2.7558127197720457; const double d_6_6 = 1; const double d_7_0 = 1.7845817717010581e-09; const double d_7_1 = 4.9830976656238314e-05; const double d_7_2 = 0.012980851747494276; const double d_7_3 = 0.3515901065098413; const double d_7_4 = 2.2276697528059834; const double d_7_5 = 4.688367487148971; const double d_7_6 = 3.7333333333333334; const double d_7_7 = 1; const double d_8_0 = 1.0040513996341856e-10; const double d_8_1 = 8.98335428421703e-06; const double d_8_2 = 0.004627200152004401; const double d_8_3 = 0.20535465350630017; const double d_8_4 = 1.987167910494932; const double d_8_5 = 6.378379695658343; const double d_8_6 = 8.337777777777777; const double d_8_7 = 4.733333333333333; const double d_8_8 = 1; const double r_1_0 = 17.773808914078; const double r_2_0 = 5.548136718537217; const double r_2_1 = 8.065938648381888; const double r_3_0 = 2.835876078644439; const double r_3_1 = 3.3742499769626355; const double r_3_2 = 5.801001559264062; const double r_4_0 = 1.8276402675175978; const double r_4_1 = 2.0371118353585844; const double r_4_2 = 2.725442211808226; const double r_4_3 = 5.140624105810932; const double r_5_0 = 1.3620078160624696; const double r_5_1 = 1.4750402175604116; const double r_5_2 = 1.8051535801402514; const double r_5_3 = 2.620644926387035; const double r_5_4 = 5.3459768998711095; const double r_6_0 = 1.1295338753367898; const double r_6_1 = 1.2061876660584456; const double r_6_2 = 1.418278263734739; const double r_6_3 = 1.8772424961868102; const double r_6_4 = 2.957116017290456; const double r_6_5 = 6.617662013702422; const double r_7_0 = 1.0229963298234868; const double r_7_1 = 1.0854721939386425; const double r_7_2 = 1.2542646222818779; const double r_7_3 = 1.6002665494908161; const double r_7_4 = 2.3235983002196945; const double r_7_5 = 4.109975778344558; const double r_7_6 = 10.846026190236847; const double r_8_0 = 1.0; const double r_8_1 = 1.0596167516347892; const double r_8_2 = 1.2198702593798945; const double r_8_3 = 1.5446990739910793; const double r_8_4 = 2.2082544062281713; const double r_8_5 = 3.762371295948582; const double r_8_6 = 8.719988284145643; const double r_8_7 = 44.48519992867184; /* IAS15 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 wez_ias15(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 *x= (double *) malloc ( dim * sizeof(double) ); double *v= (double *) malloc ( dim * sizeof(double) ); double *a= (double *) malloc ( dim * sizeof(double) ); double *xs= (double *) malloc ( dim * sizeof(double) ); //x substep double *vs= (double *) malloc ( dim * sizeof(double) ); //v substep double *Bs= (double *) malloc ( (order * dim) * sizeof(double) ); double *Es= (double *) malloc ( (order * dim) * sizeof(double) ); double *BDs= (double *) malloc ( (order * dim) * sizeof(double) ); double *Gs= (double *) malloc ( (order * dim) * sizeof(double) ); double *Fs= (double *) malloc ( ((order + 1) * dim) * sizeof(double) ); double hs; int ii, jj, kk; for (ii=0; ii < dim; ii++) { *x++= *(yo+ii); *v++= *(yo+dim+ii); } x-= dim; v-= dim; for(int i=0; i < (order * dim); i++){ Bs[i] = 0; Es[i] = 0; BDs[i] = 0; Gs[i] = 0; } for(int i=0; i < (order + 1 * dim); i++){ Fs[i] = 0; } double diff_G; save_ias15(dim, x, v, result); result+= 2 * dim; *err= 0; //Estimate necessary stepsize, use the returned time interval if the user does not provide double init_dt= (*(t+1))-(*t); if ( dt == -9999.99 ) { dt = init_dt; //Note in this case of dynamic timestepping, this makes little difference } 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 //WHILE THERE IS TIME REMAINING, INTEGRATE A DYNAMIC TIMESTEP FORWARD AND SUBTRACT FROM THE TIME REMAINING double time_remaining = fabs(nt * dt); int steps = 1; while(time_remaining > 0) { if ( interrupted ) { *err= -10; interrupted= 0; // need to reset, bc library and vars stay in memory #ifdef USING_COVERAGE __gcov_dump(); // LCOV_EXCL_START __gcov_reset(); #endif break; // LCOV_EXCL_STOP } double to_temp; double dt_temp; dt_temp = dt; to_temp = to + dt_temp; func(to_temp,x,a,nargs,potentialArgs); for (int i=0; i < dim; i++){ Fs[i * (order + 1)] = a[i]; } //update G from B update_Gs_from_Bs(dim, Gs, Bs); int iterations = 0; double integrator_error = integrator_error_threshold + 1; //init value above the threshold while(true){ if(iterations == 12){ //See paper for dynamic vs static treatment of predictor–corrector loop. break; } if (integrator_error < integrator_error_threshold){ break; } //at start of each step reset xs //for (int l=0; l < dim; l++) *(xs+l)= *(x+l); double max_delta_B6 = 0.0; //also = max delta G double max_a = 0.0; for (int k=1; k < (order + 1); k++){ //update position, update force, update G, update B update_position(xs, x, v, dim, h[k], dt_temp, Fs, Bs); func(to_temp,xs,a,nargs,potentialArgs); for (int i=0; i < dim; i++){ Fs[i * (order + 1) + k] = a[i]; diff_G = Gs[i * order + (k-1)]; update_Gs_from_Fs(k, i, Gs, Fs); diff_G = Gs[i * order + (k-1)] - diff_G; update_Bs_from_Gs(k, i, Bs, Gs, diff_G); if (k == order){ //on last step, update max delta B6 and a, using "global" strategy in paper. double abs_delta_B6 = fabs(diff_G); double abs_a = fabs(Fs[i * (order + 1)]); //accel at beginning of step if (abs_delta_B6 > max_delta_B6){ max_delta_B6 = abs_delta_B6; } if (abs_a > max_a){ max_a = abs_a; } } } } integrator_error = max_delta_B6/max_a; iterations += 1; } //global error strategy for timestep double max_B6 = 0.0; double max_a = 0.0; for (int i=0; i < dim; i++){ double abs_B6 = fabs(Bs[i * order + 6]); double abs_a = fabs(Fs[i * (order + 1)]); if (abs_B6 > max_B6){ max_B6 = abs_B6; } if (abs_a > max_a){ max_a = abs_a; } }; //fix for inf values issue double correction_factor = seventhroot(precision_parameter / (max_B6/max_a)); double dt_required = dt_temp * correction_factor; if(fabs(dt_temp) > fabs(dt_required)){ //rejected, try again with dt required dt = dt_required; } else { //accepted, update position/velocity and do next timestep with dt required time_remaining -= fabs(dt); //will eventually get negative as we stepped forward the minimum of dt and time_remaining if (init_dt > 0){ //estimate the function over the interval for any points in the t array while(to < t[steps] && t[steps] <= to_temp && steps < nt){ //hs = (fabs(t[steps]) - fabs(to))/(fabs(to_temp) - fabs(to)); hs = (fabs(t[steps] - to))/(fabs(to_temp - to)); update_position(xs, x, v, dim, hs, dt_temp, Fs, Bs); update_velocity(vs, v, dim, hs, dt_temp, Fs, Bs); save_ias15(dim, xs, vs, result); result+= 2 * dim; steps += 1; } } else { //estimate the function over the interval for any points in the t array while(to > t[steps] && t[steps] >= to_temp && steps < nt){ hs = (fabs(t[steps] - to))/(fabs(to_temp - to)); update_position(xs, x, v, dim, hs, dt_temp, Fs, Bs); update_velocity(vs, v, dim, hs, dt_temp, Fs, Bs); save_ias15(dim, xs, vs, result); result+= 2 * dim; steps += 1; } } update_position(x, x, v, dim, 1, dt_temp, Fs, Bs); update_velocity(v, v, dim, 1, dt_temp, Fs , Bs); next_sequence_Bs(1, Bs, Es, BDs, dim); to = to_temp; dt = dt_required; } } // Back to default handler #ifndef _WIN32 action.sa_handler= SIG_DFL; sigaction(SIGINT,&action,NULL); #endif //Free allocated memory free(x); free(v); free(a); free(xs); free(vs); free(Fs); free(Gs); free(Bs); free(Es); free(BDs); } void update_velocity(double *v, double *v1, int dim, double h_n, double T, double * Fs , double * Bs){ for (int ii=0; ii < dim; ii++){ *(v+ii) = *(v1+ii) + (h_n * T * (Fs[ii * (order + 1) + 0] + h_n * (Bs[ii * order + 0]/2 + h_n * (Bs[ii * order + 1]/3 + h_n * (Bs[ii * order + 2]/4 + h_n * (Bs[ii * order + 3]/5 + h_n * (Bs[ii * order + 4]/6 + h_n * (Bs[ii * order + 5]/7 + h_n * (Bs[ii * order + 6]/8 //+ h_n * //(Bs[ii * order + 7]/9 ))))))))); } } void update_position(double *y, double *y1, double *v, int dim, double h_n, double T, double * Fs, double * Bs){ for (int ii=0; ii < dim; ii++){ *(y+ii) = *(y1+ii) + (*(v+ii) * h_n * T) + ((h_n * T)*(h_n * T)) * (Fs[ii * (order + 1) + 0]/2 + h_n * (Bs[ii * order + 0]/6 + h_n * (Bs[ii * order + 1]/12 + h_n * (Bs[ii * order + 2]/20 + h_n * (Bs[ii * order + 3]/30 + h_n * (Bs[ii * order + 4]/42 + h_n * (Bs[ii * order + 5]/56 + h_n * (Bs[ii * order + 6]/72 //+ h_n * //(Bs[ii * order + 7]/90) )))))))); } } void update_Gs_from_Fs(int current_truncation_order, int i, double * Gs, double * Fs){ //int i; int j; int h; //for (i = 0; i < dim; i++){ j = i * order; h = i * (order + 1); if (current_truncation_order == 1){ Gs[j ] = (Fs[h + 1] - Fs[h]) * r_1_0; } if (current_truncation_order == 2){ Gs[j + 1] = ((Fs[h + 2] - Fs[h]) * r_2_0 - Gs[j]) * r_2_1; } if (current_truncation_order == 3){ Gs[j + 2] = (((Fs[h + 3] - Fs[h]) * r_3_0 - Gs[j]) * r_3_1 - Gs[j + 1]) * r_3_2; } if (current_truncation_order == 4){ Gs[j + 3] = ((((Fs[h + 4] - Fs[h]) * r_4_0 - Gs[j]) * r_4_1 - Gs[j + 1]) * r_4_2 - Gs[j + 2]) * r_4_3; } if (current_truncation_order == 5){ Gs[j + 4] = (((((Fs[h + 5] - Fs[h]) * r_5_0 - Gs[j]) * r_5_1 - Gs[j + 1]) * r_5_2 - Gs[j + 2]) * r_5_3 - Gs[j + 3]) * r_5_4; } if (current_truncation_order == 6){ Gs[j + 5] = ((((((Fs[h + 6] - Fs[h]) * r_6_0 - Gs[j]) * r_6_1 - Gs[j + 1]) * r_6_2 - Gs[j + 2]) * r_6_3 - Gs[j + 3]) * r_6_4 - Gs[j + 4]) * r_6_5; } if (current_truncation_order == 7){ Gs[j + 6] = (((((((Fs[h + 7] - Fs[h]) * r_7_0 - Gs[j]) * r_7_1 - Gs[j + 1]) * r_7_2 - Gs[j + 2]) * r_7_3 - Gs[j + 3]) * r_7_4 - Gs[j + 4]) * r_7_5 - Gs[j + 5]) * r_7_6; } //if (current_truncation_order == 7){ Gs[j + 7] = ((((((((Fs[j + 8] - Fs[j + 0]) * r_8_0 - Gs[j + 0]) * r_8_1 - Gs[j + 1]) * r_8_2 - Gs[j + 2]) * r_8_3 - Gs[j + 3]) * r_8_4 - Gs[j + 4]) * r_8_5 - Gs[j + 5]) * r_8_6 - Gs[j + 6]) * r_8_7; } //} } void update_Gs_from_Bs(int dim, double * Gs, double * Bs){ int i; int j; for (i = 0; i < dim; i++){ j = i * order; Gs[j ] = d_0_0 * Bs[j ] + d_1_0 * Bs[j + 1] + d_2_0 * Bs[j + 2] + d_3_0 * Bs[j + 3] + d_4_0 * Bs[j + 4] + d_5_0 * Bs[j + 5] + d_6_0 * Bs[j + 6];// + d_7_0 * Bs[j + 7]; Gs[j + 1] = d_1_1 * Bs[j + 1] + d_2_1 * Bs[j + 2] + d_3_1 * Bs[j + 3] + d_4_1 * Bs[j + 4] + d_5_1 * Bs[j + 5] + d_6_1 * Bs[j + 6];// + d_7_1 * Bs[j + 7]; Gs[j + 2] = d_2_2 * Bs[j + 2] + d_3_2 * Bs[j + 3] + d_4_2 * Bs[j + 4] + d_5_2 * Bs[j + 5] + d_6_2 * Bs[j + 6];// + d_7_2 * Bs[j + 7]; Gs[j + 3] = d_3_3 * Bs[j + 3] + d_4_3 * Bs[j + 4] + d_5_3 * Bs[j + 5] + d_6_3 * Bs[j + 6];// + d_7_3 * Bs[j + 7]; Gs[j + 4] = d_4_4 * Bs[j + 4] + d_5_4 * Bs[j + 5] + d_6_4 * Bs[j + 6];// + d_7_4 * Bs[j + 7]; Gs[j + 5] = d_5_5 * Bs[j + 5] + d_6_5 * Bs[j + 6];// + d_7_5 * Bs[j + 7]; Gs[j + 6] = d_6_6 * Bs[j + 6];// + d_7_6 * Bs[j + 7]; } } void next_sequence_Bs(double Q, double * Bs, double *Es, double * BDs, int dim){ int i; int j; for (i = 0; i < dim; i++){ j = i * order; BDs[j ] = Bs[j ] - Es[j ]; BDs[j + 1] = Bs[j + 1] - Es[j + 1]; BDs[j + 2] = Bs[j + 2] - Es[j + 2]; BDs[j + 3] = Bs[j + 3] - Es[j + 3]; BDs[j + 4] = Bs[j + 4] - Es[j + 4]; BDs[j + 5] = Bs[j + 5] - Es[j + 5]; BDs[j + 6] = Bs[j + 6] - Es[j + 6]; Es[j ] = (Bs[j ] + 2 * Bs[j + 1] + 3 * Bs[j + 2] + 4 * Bs[j + 3] + 5 * Bs[j + 4] + 6 * Bs[j + 5] + 7 * Bs[j + 6]); Es[j + 1] = (Bs[j + 1] + 3 * Bs[j + 2] + 6 * Bs[j + 3] + 10 * Bs[j + 4] + 15 * Bs[j + 5] + 21 * Bs[j + 6]); Es[j + 2] = (Bs[j + 2] + 4 * Bs[j + 3] + 10 * Bs[j + 4] + 20 * Bs[j + 5] + 35 * Bs[j + 6]); Es[j + 3] = (Bs[j + 3] + 5 * Bs[j + 4] + 15 * Bs[j + 5] + 35 * Bs[j + 6]); Es[j + 4] = (Bs[j + 4] + 6 * Bs[j + 5] + 21 * Bs[j + 6]); Es[j + 5] = (Bs[j + 5] + 7 * Bs[j + 6]); Es[j + 6] = (Bs[j + 6]); Bs[j ] = Es[j ] + BDs[j ]; Bs[j + 1] = Es[j + 1] + BDs[j + 1]; Bs[j + 2] = Es[j + 2] + BDs[j + 2]; Bs[j + 3] = Es[j + 3] + BDs[j + 3]; Bs[j + 4] = Es[j + 4] + BDs[j + 4]; Bs[j + 5] = Es[j + 5] + BDs[j + 5]; Bs[j + 6] = Es[j + 6] + BDs[j + 6]; } } void update_Bs_from_Gs(int current_truncation_order, int i, double * Bs, double * Gs, double diff_G){ int j = i * order; if (current_truncation_order == 1){ Bs[j ] += diff_G; } if (current_truncation_order == 2){ Bs[j ] += diff_G * c_1_0; Bs[j + 1] += diff_G; } if (current_truncation_order == 3){ Bs[j ] += diff_G * c_2_0; Bs[j + 1] += diff_G * c_2_1; Bs[j + 2] += diff_G; } if (current_truncation_order == 4){ Bs[j ] += diff_G * c_3_0; Bs[j + 1] += diff_G * c_3_1; Bs[j + 2] += diff_G * c_3_2; Bs[j + 3] += diff_G; } if (current_truncation_order == 5){ Bs[j ] += diff_G * c_4_0; Bs[j + 1] += diff_G * c_4_1; Bs[j + 2] += diff_G * c_4_2; Bs[j + 3] += diff_G * c_4_3; Bs[j + 4] += diff_G; } if (current_truncation_order == 6){ Bs[j ] += diff_G * c_5_0; Bs[j + 1] += diff_G * c_5_1; Bs[j + 2] += diff_G * c_5_2; Bs[j + 3] += diff_G * c_5_3; Bs[j + 4] += diff_G * c_5_4; Bs[j + 5] += diff_G ; } if (current_truncation_order == 7){ Bs[j ] += diff_G * c_6_0; Bs[j + 1] += diff_G * c_6_1; Bs[j + 2] += diff_G * c_6_2; Bs[j + 3] += diff_G * c_6_3; Bs[j + 4] += diff_G * c_6_4; Bs[j + 5] += diff_G * c_6_5; Bs[j + 6] += diff_G; } } static double seventhroot(double a){ // Without scaling, this is only accurate for arguments in [1e-7, 1e2] // With scaling: [1e-14, 1e8] double scale = 1; while(a<1e-7 && isnormal(a)){ scale *= 0.1; a *= 1e7; } while(a>1e2 && isnormal(a)){ scale *= 10; a *= 1e-7; } double x = 1.; for (int k=0; k<20;k++){ // A smaller number should be ok too. double x6 = x*x*x*x*x*x; x += (a/x6-x)/7.; } return x*scale; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/galpy/util/wez_ias15.h0000644000175100001660000000511314761352023016366 0ustar00runnerdocker/* C implementation of IAS15 integrator */ /* Copyright (c) 2023, John Weatherall 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 __WEZ_IAS_H__ #define __WEZ_IAS_H__ #ifdef __cplusplus extern "C" { #endif /* include */ #include #include /* Global variables */ /* Function declarations */ void wez_ias15(void (*func)(double, double *, double *, int, struct potentialArg *), int, double *, int, double, double *, int, struct potentialArg *, double, double, double *,int *); static inline void save_ias15(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++; } void update_velocity(double *, double *, int, double, double, double *, double *); void update_position(double *, double *, double *, int, double, double, double *, double *); void update_Gs_from_Bs(int, double *, double *); void update_Gs_from_Fs(int, int, double *, double * ); void update_Bs_from_Gs(int, int, double *, double *, double); void next_sequence_Bs(double, double *, double *, double *, int); static double seventhroot(double); #ifdef __cplusplus } #endif #endif /* wez_ias15.h */ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6858845 galpy-1.10.2/galpy.egg-info/0000755000175100001660000000000014761352040015122 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018143.0 galpy-1.10.2/galpy.egg-info/PKG-INFO0000644000175100001660000001322714761352037016232 0ustar00runnerdockerMetadata-Version: 2.2 Name: galpy Version: 1.10.2 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.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Environment :: WebAssembly :: Emscripten Classifier: Topic :: Scientific/Engineering :: Astronomy Classifier: Topic :: Scientific/Engineering :: Physics Requires-Python: >=3.8 Description-Content-Type: text/markdown License-File: LICENSE License-File: AUTHORS.txt Requires-Dist: packaging Requires-Dist: numpy>=1.7 Requires-Dist: scipy Requires-Dist: matplotlib Provides-Extra: docs Requires-Dist: sphinxext-opengraph; extra == "docs" Requires-Dist: sphinx-design; extra == "docs" Requires-Dist: markupsafe==2.0.1; extra == "docs" Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: description-content-type Dynamic: home-page Dynamic: license Dynamic: provides-extra Dynamic: requires-dist Dynamic: requires-python Dynamic: summary

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.8, 3.9, 3.10, 3.11, 3.12 and 3.13. GitHub Actions CI builds regularly check support for Python 3.13 (and of 3.8, 3.9, 3.10, 3.11, and 3.12 using a more limited, core set of tests) on Linux and Windows (and 3.13 on Mac OS). 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! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018143.0 galpy-1.10.2/galpy.egg-info/SOURCES.txt0000644000175100001660000002627114761352037017024 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/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/actionAngleVerticalInverse.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/KuzminLikeWrapperPotential.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/planarDissipativeForce.py galpy/potential/planarForce.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/KuzminLikeWrapperPotential.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_coords.c galpy/util/bovy_coords.h 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/wez_ias15.c galpy/util/wez_ias15.h 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 tests/test_SpiralArmsPotential.py tests/test_actionAngle.py tests/test_actionAngleTorus.py tests/test_amuse.py tests/test_conversion.py tests/test_coords.py tests/test_diskdf.py tests/test_dynamfric.py tests/test_evolveddiskdf.py tests/test_galpypaper.py tests/test_import.py tests/test_interp_potential.py tests/test_jeans.py tests/test_kuzminkutuzov.py tests/test_nemo.py tests/test_noninertial.py tests/test_orbit.py tests/test_orbits.py tests/test_potential.py tests/test_pv2qdf.py tests/test_qdf.py tests/test_quantity.py tests/test_scf.py tests/test_snapshotpotential.py tests/test_sphericaldf.py tests/test_streamdf.py tests/test_streamgapdf.py tests/test_streamgapdf_impulse.py tests/test_streamspraydf.py tests/test_util.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018143.0 galpy-1.10.2/galpy.egg-info/dependency_links.txt0000644000175100001660000000000114761352037021176 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018143.0 galpy-1.10.2/galpy.egg-info/requires.txt0000644000175100001660000000014214761352037017525 0ustar00runnerdockerpackaging numpy>=1.7 scipy matplotlib [docs] sphinxext-opengraph sphinx-design markupsafe==2.0.1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018143.0 galpy-1.10.2/galpy.egg-info/top_level.txt0000644000175100001660000000001714761352037017660 0ustar00runnerdockergalpy libgalpy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/gsl-config.bat0000644000175100001660000000073714761352023015044 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=1741018131.0 galpy-1.10.2/pyproject.toml0000644000175100001660000000016114761352023015227 0ustar00runnerdocker[build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.isort] profile = "black" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6868846 galpy-1.10.2/setup.cfg0000644000175100001660000000004614761352040014135 0ustar00runnerdocker[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/setup.py0000644000175100001660000003320714761352023014034 0ustar00runnerdockerimport glob import os import os.path import platform import subprocess import sys import sysconfig from setuptools import Extension, find_namespace_packages, 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 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'] # 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/util/wez_ias15.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.0 and ( float(gsl_version[0]) >= 2.0 or float(gsl_version[1]) >= 14.0 ): 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.0 and (float(gsl_version[0]) >= 2.0 or float(gsl_version[1]) >= 14.0) 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 # Test whether the compiler is clang, allowing for the fact that it's name might be gcc... def compiler_is_clang(compiler): # Test whether the compiler is clang by running the compiler with the --version flag and checking whether the output contains "clang" try: output = subprocess.check_output( [compiler.compiler[0], "--version"], stderr=subprocess.STDOUT ) except (OSError, subprocess.CalledProcessError): return False return b"clang" in output # 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 = [] libraries = ext.libraries for flag in set(ext.extra_compile_args): if compiler_has_flag(self.compiler, flag): extra_compile_args.append(flag) elif compiler_is_clang(self.compiler) and flag == "-fopenmp": # clang does not support -fopenmp, but does support -Xclang -fopenmp extra_compile_args.append("-Xclang") extra_compile_args.append("-fopenmp") # Also adjust libraries as needed if "gomp" in libraries: libraries.remove("gomp") if "omp" not in libraries: libraries.append("omp") elif flag == "-fopenmp" and "gomp" in libraries: libraries.remove("gomp") ext.extra_compile_args = extra_compile_args ext.libraries = libraries build_ext.build_extensions(self) setup( cmdclass=dict(build_ext=BuildExt), # this to allow compiler check above name="galpy", version="1.10.2", 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", packages=find_namespace_packages(where=".", include=["galpy*"]), package_data={ "galpy/orbit": ["named_objects.json"], "galpy/df": ["data/*.sav"], "": ["README.md", "README.dev", "LICENSE", "AUTHORS.rst"], }, include_package_data=True, python_requires=">=3.8", install_requires=["packaging", "numpy>=1.7", "scipy", "matplotlib"], extras_require={ "docs": ["sphinxext-opengraph", "sphinx-design", "markupsafe==2.0.1"] }, 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.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Environment :: WebAssembly :: Emscripten", "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 ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741018143.6858845 galpy-1.10.2/tests/0000755000175100001660000000000014761352040013456 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_SpiralArmsPotential.py0000644000175100001660000022751314761352023021037 0ustar00runnerdockerimport numpy from packaging.version import parse as parse_version _NUMPY_VERSION = parse_version(numpy.__version__) _NUMPY_1_23 = (_NUMPY_VERSION > parse_version("1.22")) * ( _NUMPY_VERSION < parse_version("1.27") ) # For testing 1.23/1.24/1.25/1.26 precision issues import unittest from numpy.testing import assert_allclose from galpy.potential import SpiralArmsPotential as spiral def deriv(func, x0, dx=1.0, args=()): """Like the deprecated scipy.misc.derivative, but using numpy.gradient""" xs = numpy.array([x0 - dx, x0, x0 + dx]) ys = numpy.array([func(x, *args) for x in xs]).flatten() return numpy.gradient(ys, xs)[1] class TestSpiralArmsPotential(unittest.TestCase): def test_constructor(self): """Test that constructor initializes and converts units correctly.""" sp = spiral() # default values assert sp._amp == 1 assert sp._N == -2 # trick to change to left handed coordinate system assert sp._alpha == -0.2 assert sp._r_ref == 1 assert sp._phi_ref == 0 assert sp._Rs == 0.3 assert sp._H == 0.125 assert sp._Cs == [1] assert sp._omega == 0 assert sp._rho0 == 1 / (4 * numpy.pi) assert sp.isNonAxi == True assert sp.hasC == True assert sp.hasC_dxdv == True assert sp._ro == 8 assert sp._vo == 220 def test_Rforce(self): """Tests Rforce against a numerical derivative -d(Potential) / dR.""" dx = 1e-8 rtol = 1e-5 # relative tolerance pot = spiral() assert_allclose( pot.Rforce(1.0, 0.0), -deriv(lambda x: pot(x, 0.0), 1.0, dx=dx), rtol=rtol ) R, z, t = 0.3, 0, 0 assert_allclose( pot.Rforce(R, z, 0, t), -deriv(lambda x: pot(x, z, 0, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi / 2.2, t), -deriv(lambda x: pot(x, z, numpy.pi / 2.2, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi, t), -deriv(lambda x: pot(x, z, numpy.pi, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 3.7 * numpy.pi / 2, t), -deriv(lambda x: pot(x, z, 3.7 * numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) R, z, t = 1, -0.7, 3 assert_allclose( pot.Rforce(R, z, 0, t), -deriv(lambda x: pot(x, z, 0, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(x, z, numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi, t), -deriv(lambda x: pot(x, z, numpy.pi, 0), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 3.3 * numpy.pi / 2, t), -deriv(lambda x: pot(x, z, 3.3 * numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) R, z = 3.14, 0.7 assert_allclose( pot.Rforce(R, z, 0), -deriv(lambda x: pot(x, z, 0), R, dx=dx), rtol=rtol ) assert_allclose( pot.Rforce(R, z, numpy.pi / 2), -deriv(lambda x: pot(x, z, numpy.pi / 2), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi), -deriv(lambda x: pot(x, z, numpy.pi), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(x, z, 3 * numpy.pi / 2), R, dx=dx), rtol=rtol, ) pot = spiral( amp=13, N=7, alpha=-0.3, r_ref=0.5, phi_ref=0.3, Rs=0.7, H=0.7, Cs=[1, 2, 3], omega=3, ) assert_allclose( pot.Rforce(1.0, 0.0), -deriv(lambda x: pot(x, 0.0), 1.0, dx=dx), rtol=rtol ) assert_allclose( pot.Rforce(0.01, 0.0), -deriv(lambda x: pot(x, 0.0), 0.01, dx=dx), rtol=rtol ) R, z, t = 0.3, 0, 1.123 assert_allclose( pot.Rforce(R, z, 0, t), -deriv(lambda x: pot(x, z, 0, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(x, z, numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi, t), -deriv(lambda x: pot(x, z, numpy.pi, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 3 * numpy.pi / 2, t), -deriv(lambda x: pot(x, z, 3 * numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) R, z, t = 1, -0.7, 121 assert_allclose( pot.Rforce(R, z, 0, t), -deriv(lambda x: pot(x, z, 0, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(x, z, numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi, t), -deriv(lambda x: pot(x, z, numpy.pi, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 3 * numpy.pi / 2, t), -deriv(lambda x: pot(x, z, 3 * numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) R, z, t = 3.14, 0.7, 0.123 assert_allclose( pot.Rforce(R, z, 0, t), -deriv(lambda x: pot(x, z, 0, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(x, z, numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi, t), -deriv(lambda x: pot(x, z, numpy.pi, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 3 * numpy.pi / 2, t), -deriv(lambda x: pot(x, z, 3 * numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) pot = spiral( amp=13, N=1, alpha=0.01, r_ref=1.12, phi_ref=0, Cs=[1, 1.5, 8.0], omega=-3 ) assert_allclose( pot.Rforce(1.0, 0.0), -deriv(lambda x: pot(x, 0.0), 1.0, dx=dx), rtol=rtol ) assert_allclose( pot.Rforce(0.1, 0.0), -deriv(lambda x: pot(x, 0.0), 0.1, dx=dx), rtol=rtol ) R, z, t = 0.3, 0, -4.5 assert_allclose( pot.Rforce(R, z, 0, t), -deriv(lambda x: pot(x, z, 0, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(x, z, numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi, t), -deriv(lambda x: pot(x, z, numpy.pi, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 3 * numpy.pi / 2, t), -deriv(lambda x: pot(x, z, 3 * numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) R, z, t = 1, -0.7, -123 assert_allclose( pot.Rforce(R, z, 0, t), -deriv(lambda x: pot(x, z, 0, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(x, z, numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi, t), -deriv(lambda x: pot(x, z, numpy.pi, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 3 * numpy.pi / 2, t), -deriv(lambda x: pot(x, z, 3 * numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) R, z, t = 3.14, 0.7, -123.123 assert_allclose( pot.Rforce(R, z, 0, t), -deriv(lambda x: pot(x, z, 0, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(x, z, numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, numpy.pi, t), -deriv(lambda x: pot(x, z, numpy.pi, t), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 3 * numpy.pi / 2, t), -deriv(lambda x: pot(x, z, 3 * numpy.pi / 2, t), R, dx=dx), rtol=rtol, ) pot = spiral( N=10, r_ref=15, phi_ref=5, Cs=[8.0 / (3.0 * numpy.pi), 0.5, 8.0 / (15.0 * numpy.pi)], ) assert_allclose( pot.Rforce(1.0, 0.0), -deriv(lambda x: pot(x, 0.0), 1.0, dx=dx), rtol=rtol ) assert_allclose( pot.Rforce(0.01, 0.0), -deriv(lambda x: pot(x, 0.0), 0.01, dx=dx), rtol=rtol ) R, z = 0.3, 0 assert_allclose( pot.Rforce(R, z, 0), -deriv(lambda x: pot(x, z, 0), R, dx=dx), rtol=rtol ) assert_allclose( pot.Rforce(R, z, numpy.pi / 2.1), -deriv(lambda x: pot(x, z, numpy.pi / 2.1), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 1.3 * numpy.pi), -deriv(lambda x: pot(x, z, 1.3 * numpy.pi), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(x, z, 3 * numpy.pi / 2), R, dx=dx), rtol=rtol, ) R, z = 1, -0.7 assert_allclose( pot.Rforce(R, z, 0), -deriv(lambda x: pot(x, z, 0), R, dx=dx), rtol=rtol ) assert_allclose( pot.Rforce(R, z, numpy.pi / 2), -deriv(lambda x: pot(x, z, numpy.pi / 2), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 0.9 * numpy.pi), -deriv(lambda x: pot(x, z, 0.9 * numpy.pi), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 3.3 * numpy.pi / 2), -deriv(lambda x: pot(x, z, 3.3 * numpy.pi / 2), R, dx=dx), rtol=rtol, ) R, z = 3.14, 0.7 assert_allclose( pot.Rforce(R, z, 0), -deriv(lambda x: pot(x, z, 0), R, dx=dx), rtol=rtol ) assert_allclose( pot.Rforce(R, z, numpy.pi / 2.3), -deriv(lambda x: pot(x, z, numpy.pi / 2.3), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 1.1 * numpy.pi), -deriv(lambda x: pot(x, z, 1.1 * numpy.pi), R, dx=dx), rtol=rtol, ) assert_allclose( pot.Rforce(R, z, 3.5 * numpy.pi / 2), -deriv(lambda x: pot(x, z, 3.5 * numpy.pi / 2), R, dx=dx), rtol=rtol, ) def test_zforce(self): """Test zforce against a numerical derivative -d(Potential) / dz""" dx = 1e-8 rtol = 1e-6 # relative tolerance pot = spiral() # zforce is zero in the plane of the galaxy assert_allclose(0, pot.zforce(0.3, 0, 0), rtol=rtol) assert_allclose(0, pot.zforce(0.3, 0, numpy.pi / 2), rtol=rtol) assert_allclose(0, pot.zforce(0.3, 0, numpy.pi), rtol=rtol) assert_allclose(0, pot.zforce(0.3, 0, 3 * numpy.pi / 2), rtol=rtol) # test zforce against -dPhi/dz R, z = 1, -0.7 assert_allclose( pot.zforce(R, z, 0), -deriv(lambda x: pot(R, x, 0), z, dx=dx), rtol=rtol ) assert_allclose( pot.zforce(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, numpy.pi), -deriv(lambda x: pot(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) R, z = 3.7, 0.7 assert_allclose( pot.zforce(R, z, 0), -deriv(lambda x: pot(R, x, 0), z, dx=dx), rtol=rtol ) assert_allclose( pot.zforce(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, numpy.pi), -deriv(lambda x: pot(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) pot = spiral( amp=13, N=3, alpha=-0.3, r_ref=0.5, phi_ref=0.3, Rs=0.7, H=0.7, Cs=[1, 2], omega=3, ) # zforce is zero in the plane of the galaxy assert_allclose(0, pot.zforce(0.3, 0, 0, 1), rtol=rtol) assert_allclose(0, pot.zforce(0.6, 0, numpy.pi / 2, 2), rtol=rtol) assert_allclose(0, pot.zforce(0.9, 0, numpy.pi, 3), rtol=rtol) assert_allclose(0, pot.zforce(1.2, 0, 2 * numpy.pi, 4), rtol=rtol) # test zforce against -dPhi/dz R, z, t = 1, -0.7, 123 assert_allclose( pot.zforce(R, z, 0, t), -deriv(lambda x: pot(R, x, 0, t), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(R, x, numpy.pi / 2, t), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, numpy.pi, t), -deriv(lambda x: pot(R, x, numpy.pi, t), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, 3 * numpy.pi / 2, t), -deriv(lambda x: pot(R, x, 3 * numpy.pi / 2, t), z, dx=dx), rtol=rtol, ) R, z = 3.7, 0.7 assert_allclose( pot.zforce(R, z, 0), -deriv(lambda x: pot(R, x, 0), z, dx=dx), rtol=rtol ) assert_allclose( pot.zforce(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, numpy.pi), -deriv(lambda x: pot(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) pot = spiral(N=1, alpha=-0.2, r_ref=0.5, Cs=[1, 1.5], omega=-3) # zforce is zero in the plane of the galaxy assert_allclose(0, pot.zforce(0.3, 0, 0, 123), rtol=rtol) assert_allclose(0, pot.zforce(0.3, 0, numpy.pi / 2, -321), rtol=rtol) assert_allclose(0, pot.zforce(32, 0, numpy.pi, 1.23), rtol=rtol) assert_allclose(0, pot.zforce(0.123, 0, 3.33 * numpy.pi / 2, -3.21), rtol=rtol) # test zforce against -dPhi/dz R, z = 1, -1.5 assert_allclose( pot.zforce(R, z, 0), -deriv(lambda x: pot(R, x, 0), z, dx=dx), rtol=rtol ) assert_allclose( pot.zforce(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, numpy.pi), -deriv(lambda x: pot(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, 3 * numpy.pi / 2.1), -deriv(lambda x: pot(R, x, 3 * numpy.pi / 2.1), z, dx=dx), rtol=rtol, ) R, z, t = 3.7, 0.7, -100 assert_allclose( pot.zforce(R, z, 0, t), -deriv(lambda x: pot(R, x, 0, t), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(R, x, numpy.pi / 2, t), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, numpy.pi, t), -deriv(lambda x: pot(R, x, numpy.pi, t), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, 3.4 * numpy.pi / 2, t), -deriv(lambda x: pot(R, x, 3.4 * numpy.pi / 2, t), z, dx=dx), rtol=rtol, ) pot = spiral( N=5, r_ref=1.5, phi_ref=0.5, Cs=[8.0 / (3.0 * numpy.pi), 0.5, 8.0 / (15.0 * numpy.pi)], ) # zforce is zero in the plane of the galaxy assert_allclose(0, pot.zforce(0.3, 0, 0), rtol=rtol) assert_allclose(0, pot.zforce(0.4, 0, numpy.pi / 2), rtol=rtol) assert_allclose(0, pot.zforce(0.5, 0, numpy.pi * 1.1), rtol=rtol) assert_allclose(0, pot.zforce(0.6, 0, 3 * numpy.pi / 2), rtol=rtol) # test zforce against -dPhi/dz R, z = 1, -0.7 assert_allclose( pot.zforce(R, z, 0), -deriv(lambda x: pot(R, x, 0), z, dx=dx), rtol=rtol ) assert_allclose( pot.zforce(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, numpy.pi), -deriv(lambda x: pot(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) R, z = 37, 1.7 assert_allclose( pot.zforce(R, z, 0), -deriv(lambda x: pot(R, x, 0), z, dx=dx), rtol=rtol ) assert_allclose( pot.zforce(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, numpy.pi), -deriv(lambda x: pot(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.zforce(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) def test_phitorque(self): """Test phitorque against a numerical derivative -d(Potential) / d(phi).""" dx = 1e-8 rtol = 1e-5 # relative tolerance pot = spiral() R, z = 0.3, 0 assert_allclose( pot.phitorque(R, z, 0), -deriv(lambda x: pot(R, z, x), 0, dx=dx), rtol=rtol ) assert_allclose( pot.phitorque(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi), -deriv(lambda x: pot(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z = 0.1, -0.3 assert_allclose( pot.phitorque(R, z, 0), -deriv(lambda x: pot(R, z, x), 0, dx=dx), rtol=rtol ) assert_allclose( pot.phitorque(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi), -deriv(lambda x: pot(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z = 3, 7 assert_allclose( pot.phitorque(R, z, 0), -deriv(lambda x: pot(R, z, x), 0, dx=dx), rtol=rtol ) assert_allclose( pot.phitorque(R, z, numpy.pi / 2.1), -deriv(lambda x: pot(R, z, x), numpy.pi / 2.1, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi), -deriv(lambda x: pot(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) pot = spiral( N=7, alpha=-0.3, r_ref=0.5, phi_ref=0.3, Rs=0.7, H=0.7, Cs=[1, 1, 1], omega=2 * numpy.pi, ) R, z, t = 0.3, 0, 1.2 assert_allclose( pot.phitorque(R, z, 0, 0), -deriv(lambda x: pot(R, z, x, 0), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(R, z, x, t), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi, t), -deriv(lambda x: pot(R, z, x, t), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, 3 * numpy.pi / 2, t), -deriv(lambda x: pot(R, z, x, t), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z = 1, -0.7 assert_allclose( pot.phitorque(R, z, 0), -deriv(lambda x: pot(R, z, x), 0, dx=dx), rtol=rtol ) assert_allclose( pot.phitorque(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi), -deriv(lambda x: pot(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z, t = 3.7, 0.7, -5.1 assert_allclose( pot.phitorque(R, z, 0, t), -deriv(lambda x: pot(R, z, x, t), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(R, z, x, t), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi, t), -deriv(lambda x: pot(R, z, x, t), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, 3.2 * numpy.pi / 2, t), -deriv(lambda x: pot(R, z, x, t), 3.2 * numpy.pi / 2, dx=dx), rtol=rtol, ) pot = spiral(N=1, alpha=0.1, phi_ref=0, Cs=[1, 1.5], omega=-0.333) R, z = 0.3, 0 assert_allclose( pot.phitorque(R, z, 0), -deriv(lambda x: pot(R, z, x), 0, dx=dx), rtol=rtol ) assert_allclose( pot.phitorque(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi), -deriv(lambda x: pot(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, 3.2 * numpy.pi / 2), -deriv(lambda x: pot(R, z, x), 3.2 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z, t = 1, -0.7, 123 assert_allclose( pot.phitorque(R, z, 0, t), -deriv(lambda x: pot(R, z, x, t), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(R, z, x, t), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi, t), -deriv(lambda x: pot(R, z, x, t), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, 3 * numpy.pi / 2, t), -deriv(lambda x: pot(R, z, x, t), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z, t = 3, 4, 5 assert_allclose( pot.phitorque(R, z, 0, t), -deriv(lambda x: pot(R, z, x, t), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi / 2, t), -deriv(lambda x: pot(R, z, x, t), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi, t), -deriv(lambda x: pot(R, z, x, t), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, 3 * numpy.pi / 2, t), -deriv(lambda x: pot(R, z, x, t), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) pot = spiral( N=4, r_ref=1.5, phi_ref=5, Cs=[8.0 / (3.0 * numpy.pi), 0.5, 8.0 / (15.0 * numpy.pi)], ) R, z = 0.3, 0 assert_allclose( pot.phitorque(R, z, 0), -deriv(lambda x: pot(R, z, x), 0, dx=dx), rtol=rtol ) assert_allclose( pot.phitorque(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi), -deriv(lambda x: pot(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z = 1, -0.7 assert_allclose( pot.phitorque(R, z, 0), -deriv(lambda x: pot(R, z, x), 0, dx=dx), rtol=rtol ) assert_allclose( pot.phitorque(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi), -deriv(lambda x: pot(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z = 2.1, 0.12345 assert_allclose( pot.phitorque(R, z, 0), -deriv(lambda x: pot(R, z, x), 0, dx=dx), rtol=rtol ) assert_allclose( pot.phitorque(R, z, numpy.pi / 2), -deriv(lambda x: pot(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, numpy.pi), -deriv(lambda x: pot(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phitorque(R, z, 2 * numpy.pi), -deriv(lambda x: pot(R, z, x), 2 * numpy.pi, dx=dx), rtol=rtol, ) def test_R2deriv(self): """Test R2deriv against a numerical derivative -d(Rforce) / dR.""" dx = 1e-8 rtol = 1e-6 # relative tolerance pot = spiral() assert_allclose( pot.R2deriv(1.0, 0.0), -deriv(lambda x: pot.Rforce(x, 0.0), 1.0, dx=dx), rtol=rtol, ) R, z = 0.3, 0 assert_allclose( pot.R2deriv(R, z, 0), -deriv(lambda x: pot.Rforce(x, z, 0), R, dx=dx), rtol=rtol, ) assert_allclose( pot.R2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, numpy.pi / 2), R, dx=dx), rtol=rtol, ) assert_allclose( pot.R2deriv(R, z, numpy.pi), -deriv(lambda x: pot.Rforce(x, z, numpy.pi), R, dx=dx), rtol=rtol, ) assert_allclose( pot.R2deriv(R, z, 3.1 * numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, 3.1 * numpy.pi / 2), R, dx=dx), rtol=rtol, ) R, z = 1, -0.7 assert_allclose( pot.R2deriv(R, z, 0), -deriv(lambda x: pot.Rforce(x, z, 0), R, dx=dx), rtol=rtol, ) assert_allclose( pot.R2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, numpy.pi / 2), R, dx=dx), rtol=rtol, ) assert_allclose( pot.R2deriv(R, z, numpy.pi), -deriv(lambda x: pot.Rforce(x, z, numpy.pi), R, dx=dx), rtol=rtol, ) assert_allclose( pot.R2deriv(R, z, 2 * numpy.pi), -deriv(lambda x: pot.Rforce(x, z, 2 * numpy.pi), R, dx=dx), rtol=rtol, ) R, z = 5, 0.9 assert_allclose( pot.R2deriv(R, z, 0), -deriv(lambda x: pot.Rforce(x, z, 0), R, dx=dx), rtol=rtol, ) assert_allclose( pot.R2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, numpy.pi / 2), R, dx=dx), rtol=rtol, ) assert_allclose( pot.R2deriv(R, z, numpy.pi), -deriv(lambda x: pot.Rforce(x, z, numpy.pi), R, dx=dx), rtol=rtol, ) assert_allclose( pot.R2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, 3 * numpy.pi / 2), R, dx=dx), rtol=rtol, ) # pot = spiral(N=1, alpha=-.3, r_ref=.1, phi_ref=numpy.pi, Rs=1, H=1, Cs=[1, 2, 3], omega=3) # assert_allclose(pot.R2deriv(1e-3, 0.), -deriv(lambda x: pot.Rforce(x, 0.), 1e-3, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(1., 0.), -deriv(lambda x: pot.Rforce(x, 0.), 1., dx=dx), rtol=rtol) # R, z = 0.3, 0 # assert_allclose(pot.R2deriv(R, z, 0), -deriv(lambda x: pot.Rforce(x, z, 0), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, numpy.pi/2), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi), -deriv(lambda x: pot.Rforce(x, z, numpy.pi), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, 3*numpy.pi/2), R, dx=dx), rtol=rtol) # R, z = 1, -.7 # assert_allclose(pot.R2deriv(R, z, 0), -deriv(lambda x: pot.Rforce(x, z, 0), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, numpy.pi / 2), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi), -deriv(lambda x: pot.Rforce(x, z, numpy.pi), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, 3.1*numpy.pi/2), -deriv(lambda x: pot.Rforce(x, z, 3.1*numpy.pi/2), R, dx=dx), rtol=rtol) # R, z = 5, .9 # assert_allclose(pot.R2deriv(R, z, 0), -deriv(lambda x: pot.Rforce(x, z, 0), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi / 2.4), -deriv(lambda x: pot.Rforce(x, z, numpy.pi / 2.4), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi), -deriv(lambda x: pot.Rforce(x, z, numpy.pi), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, 3*numpy.pi/2), R, dx=dx), rtol=rtol) # # pot = spiral(N=7, alpha=.1, r_ref=1, phi_ref=1, Rs=1.1, H=.1, Cs=[8./(3. *numpy.pi), 0.5, 8./(15. *numpy.pi)], omega=-.3) # assert_allclose(pot.R2deriv(1., 0.), -deriv(lambda x: pot.Rforce(x, 0.), 1., dx=dx), rtol=rtol) # R, z = 0.3, 0 # assert_allclose(pot.R2deriv(R, z, 0), -deriv(lambda x: pot.Rforce(x, z, 0), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi/2), -deriv(lambda x: pot.Rforce(x, z, numpy.pi/2), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi), -deriv(lambda x: pot.Rforce(x, z, numpy.pi), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, 3*numpy.pi/2), R, dx=dx), rtol=rtol) # R, z = 1, -.7 # assert_allclose(pot.R2deriv(R, z, 0), -deriv(lambda x: pot.Rforce(x, z, 0), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, numpy.pi / 2), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi), -deriv(lambda x: pot.Rforce(x, z, numpy.pi), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, 3*numpy.pi/2), R, dx=dx), rtol=rtol) # R, z = 5, .9 # assert_allclose(pot.R2deriv(R, z, 0), -deriv(lambda x: pot.Rforce(x, z, 0), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, numpy.pi / 2), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi), -deriv(lambda x: pot.Rforce(x, z, numpy.pi), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, 3*numpy.pi/2), R, dx=dx), rtol=rtol) # # pot = spiral(N=4, alpha=numpy.pi/2, r_ref=1, phi_ref=1, Rs=.7, H=.77, Cs=[3, 4], omega=-1.3) # assert_allclose(pot.R2deriv(1e-3, 0.), -deriv(lambda x: pot.Rforce(x, 0.), 1e-3, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(1., 0.), -deriv(lambda x: pot.Rforce(x, 0.), 1., dx=dx), rtol=rtol) # R, z = 0.3, 0 # assert_allclose(pot.R2deriv(R, z, 0), -deriv(lambda x: pot.Rforce(x, z, 0), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, numpy.pi/2), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi), -deriv(lambda x: pot.Rforce(x, z, numpy.pi), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, 3*numpy.pi/2), R, dx=dx), rtol=rtol) # R, z = 1, -.7 # assert_allclose(pot.R2deriv(R, z, 0), -deriv(lambda x: pot.Rforce(x, z, 0), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.Rforce(x, z, numpy.pi / 2), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, numpy.pi), -deriv(lambda x: pot.Rforce(x, z, numpy.pi), R, dx=dx), rtol=rtol) # assert_allclose(pot.R2deriv(R, z, .33*numpy.pi/2), -deriv(lambda x: pot.Rforce(x, z, .33*numpy.pi/2), R, dx=dx), rtol=rtol) def test_z2deriv(self): """Test z2deriv against a numerical derivative -d(zforce) / dz""" dx = 1e-8 rtol = 1e-6 # relative tolerance pot = spiral() R, z = 0.3, 0 assert_allclose( pot.z2deriv(R, z, 0), -deriv(lambda x: pot.zforce(R, x, 0), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi), -deriv(lambda x: pot.zforce(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) R, z = 1, -0.3 assert_allclose( pot.z2deriv(R, z, 0), -deriv(lambda x: pot.zforce(R, x, 0), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi), -deriv(lambda x: pot.zforce(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) R, z = 1.2, 0.1 assert_allclose( pot.z2deriv(R, z, 0), -deriv(lambda x: pot.zforce(R, x, 0), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi), -deriv(lambda x: pot.zforce(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) pot = spiral( N=3, alpha=-0.3, r_ref=0.25, Cs=[8.0 / (3.0 * numpy.pi), 0.5, 8.0 / (15.0 * numpy.pi)], ) R, z = 0.3, 0 assert_allclose( pot.z2deriv(R, z, 0), -deriv(lambda x: pot.zforce(R, x, 0), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi), -deriv(lambda x: pot.zforce(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) R, z = 1, -0.3 assert_allclose( pot.z2deriv(R, z, 0), -deriv(lambda x: pot.zforce(R, x, 0), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi), -deriv(lambda x: pot.zforce(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) R, z = 3.3, 0.7 assert_allclose( pot.z2deriv(R, z, 0), -deriv(lambda x: pot.zforce(R, x, 0), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi), -deriv(lambda x: pot.zforce(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) pot = spiral( amp=5, N=1, alpha=0.1, r_ref=0.5, phi_ref=0.3, Rs=0.7, H=0.7, Cs=[1, 2], omega=3, ) R, z = 0.3, 0 assert_allclose( pot.z2deriv(R, z, 0), -deriv(lambda x: pot.zforce(R, x, 0), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi), -deriv(lambda x: pot.zforce(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) R, z = 1, -0.3 assert_allclose( pot.z2deriv(R, z, 0), -deriv(lambda x: pot.zforce(R, x, 0), z, dx=dx), rtol=2 * rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi), -deriv(lambda x: pot.zforce(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=2 * rtol, ) R, z = 3.3, 0.7 assert_allclose( pot.z2deriv(R, z, 0), -deriv(lambda x: pot.zforce(R, x, 0), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi), -deriv(lambda x: pot.zforce(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) pot = spiral(N=1, alpha=1, r_ref=3, phi_ref=numpy.pi, Cs=[1, 2], omega=-3) R, z = 0.7, 0 assert_allclose( pot.z2deriv(R, z, 0), -deriv(lambda x: pot.zforce(R, x, 0), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi), -deriv(lambda x: pot.zforce(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) R, z = 1, -0.3 assert_allclose( pot.z2deriv(R, z, 0), -deriv(lambda x: pot.zforce(R, x, 0), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi), -deriv(lambda x: pot.zforce(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=2 * rtol, ) R, z = 2.1, 0.99 assert_allclose( pot.z2deriv(R, z, 0), -deriv(lambda x: pot.zforce(R, x, 0), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, numpy.pi / 2), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, numpy.pi), -deriv(lambda x: pot.zforce(R, x, numpy.pi), z, dx=dx), rtol=rtol, ) assert_allclose( pot.z2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.zforce(R, x, 3 * numpy.pi / 2), z, dx=dx), rtol=rtol, ) def test_phi2deriv(self): """Test phi2deriv against a numerical derivative -d(phitorque) / d(phi).""" dx = 1e-8 rtol = rtol = _NUMPY_1_23 * 3e-7 + (1 - _NUMPY_1_23) * 1e-7 pot = spiral() R, z = 0.3, 0 assert_allclose( pot.phi2deriv(R, z, 0), -deriv(lambda x: pot.phitorque(R, z, x), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi / 2.1), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi / 2.1, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, 3 * numpy.pi / 2.5), -deriv(lambda x: pot.phitorque(R, z, x), 3 * numpy.pi / 2.5, dx=dx), rtol=rtol, ) R, z = 1, -0.3 assert_allclose( pot.phi2deriv(R, z, 0), -deriv(lambda x: pot.phitorque(R, z, x), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z = 3.3, 0.7 assert_allclose( pot.phi2deriv(R, z, 0), -deriv(lambda x: pot.phitorque(R, z, x), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi / 2.1), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi / 2.1, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) pot = spiral( amp=13, N=1, alpha=-0.3, r_ref=0.5, phi_ref=0.1, Rs=0.7, H=0.7, Cs=[1, 2, 3], omega=3, ) R, z = 0.3, 0 assert_allclose( pot.phi2deriv(R, z, 0), -deriv(lambda x: pot.phitorque(R, z, x), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, 3.3 * numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), 3.3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z = 1, -0.3 assert_allclose( pot.phi2deriv(R, z, 0), -deriv(lambda x: pot.phitorque(R, z, x), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z = 3.3, 0.7 assert_allclose( pot.phi2deriv(R, z, 0), -deriv(lambda x: pot.phitorque(R, z, x), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi / 2.1), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi / 2.1, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) pot = spiral( amp=13, N=5, alpha=0.1, r_ref=0.3, phi_ref=0.1, Rs=0.77, H=0.747, Cs=[3, 2], omega=-3, ) R, z = 0.3, 0 assert_allclose( pot.phi2deriv(R, z, 0), -deriv(lambda x: pot.phitorque(R, z, x), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z = 1, -0.3 assert_allclose( pot.phi2deriv(R, z, 0), -deriv(lambda x: pot.phitorque(R, z, x), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z = 3.3, 0.7 assert_allclose( pot.phi2deriv(R, z, 0), -deriv(lambda x: pot.phitorque(R, z, x), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi / 2.1), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi / 2.1, dx=dx), rtol=rtol * 3, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol * 3, ) pot = spiral( amp=11, N=7, alpha=0.777, r_ref=7, phi_ref=0.7, Cs=[8.0 / (3.0 * numpy.pi), 0.5, 8.0 / (15.0 * numpy.pi)], ) R, z = 0.7, 0 assert_allclose( pot.phi2deriv(R, z, 0), -deriv(lambda x: pot.phitorque(R, z, x), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi / 2, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z = 1, -0.33 assert_allclose( pot.phi2deriv(R, z, 0), -deriv(lambda x: pot.phitorque(R, z, x), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi / 2.2), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi / 2.2, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) R, z = 1.123, 0.123 assert_allclose( pot.phi2deriv(R, z, 0), -deriv(lambda x: pot.phitorque(R, z, x), 0, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi / 2.1), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi / 2.1, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, numpy.pi), -deriv(lambda x: pot.phitorque(R, z, x), numpy.pi, dx=dx), rtol=rtol, ) assert_allclose( pot.phi2deriv(R, z, 3 * numpy.pi / 2), -deriv(lambda x: pot.phitorque(R, z, x), 3 * numpy.pi / 2, dx=dx), rtol=rtol, ) def test_dens(self): """Test dens against density obtained using Poisson's equation.""" rtol = 1e-2 # relative tolerance (this one isn't as precise) pot = spiral() assert_allclose( pot.dens(1, 0, 0, forcepoisson=False), pot.dens(1, 0, 0, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(1, 1, 0.5, forcepoisson=False), pot.dens(1, 1, 0.5, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(1, -1, -1, forcepoisson=False), pot.dens(1, -1, -1, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(0.1, 0.1, 0.1, forcepoisson=False), pot.dens(0.1, 0.1, 0.1, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(33, 0.777, 0.747, forcepoisson=False), pot.dens(33, 0.777, 0.747, forcepoisson=True), rtol=rtol, ) pot = spiral(amp=3, N=5, alpha=0.3, r_ref=0.7, omega=5) assert_allclose( pot.dens(1, 0, 0, forcepoisson=False), pot.dens(1, 0, 0, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(1.2, 1.2, 1.2, forcepoisson=False), pot.dens(1.2, 1.2, 1.2, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(1, -1, -1, forcepoisson=False), pot.dens(1, -1, -1, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(0.1, 0.1, 0.1, forcepoisson=False), pot.dens(0.1, 0.1, 0.1, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(33.3, 0.007, 0.747, forcepoisson=False), pot.dens(33.3, 0.007, 0.747, forcepoisson=True), rtol=rtol, ) pot = spiral( amp=0.6, N=3, alpha=0.24, r_ref=1, phi_ref=numpy.pi, Cs=[8.0 / (3.0 * numpy.pi), 0.5, 8.0 / (15.0 * numpy.pi)], omega=-3, ) assert_allclose( pot.dens(1, 0, 0, forcepoisson=False), pot.dens(1, 0, 0, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(1, 1, 1, forcepoisson=False), pot.dens(1, 1, 1, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(1, -1, -1, forcepoisson=False), pot.dens(1, -1, -1, forcepoisson=True), rtol=rtol, ) # assert_allclose(pot.dens(.1, .1, .1, forcepoisson=False), pot.dens(.1, .1, .1, forcepoisson=True), rtol=rtol) assert_allclose( pot.dens(3.33, -7.77, -0.747, forcepoisson=False), pot.dens(3.33, -7.77, -0.747, forcepoisson=True), rtol=rtol, ) pot = spiral( amp=100, N=4, alpha=numpy.pi / 2, r_ref=1, phi_ref=1, Rs=7, H=77, Cs=[3, 1, 1], omega=-1.3, ) assert_allclose( pot.dens(1, 0, 0, forcepoisson=False), pot.dens(1, 0, 0, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(3, 2, numpy.pi, forcepoisson=False), pot.dens(3, 2, numpy.pi, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(1, -1, -1, forcepoisson=False), pot.dens(1, -1, -1, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(0.1, 0.123, 0.1, forcepoisson=False), pot.dens(0.1, 0.123, 0.1, forcepoisson=True), rtol=rtol, ) assert_allclose( pot.dens(333, -0.777, 0.747, forcepoisson=False), pot.dens(333, -0.777, 0.747, forcepoisson=True), rtol=rtol, ) def test_Rzderiv(self): """Test Rzderiv against a numerical derivative.""" dx = 1e-8 rtol = _NUMPY_1_23 * 3e-6 + (1 - _NUMPY_1_23) * 1e-6 pot = spiral() R, z, phi, t = 1, 0, 0, 0 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 0.7, 0.3, numpy.pi / 3, 0 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 1.1, -0.3, numpy.pi / 4.2, 3 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 0.777, 0.747, 0.343, 2.5 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 12, 1, 2, 3 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 3, 4, 5, 6 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 5, -0.7, 3 * numpy.pi / 2, 5 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 11, 11, 11, 1.123 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 4, 7, 2, 10000 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 0.01, 0, 0, 0 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 1.23, 0, 44, 343 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 7, 7, 7, 7 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) pot = spiral( amp=13, N=7, alpha=0.1, r_ref=1.123, phi_ref=0.3, Rs=0.777, H=0.5, Cs=[4.5], omega=-3.4, ) R, z, phi, t = 1, 0, 0, 0 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 0.777, 0.333, numpy.pi / 3, 0.0 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 1.1, -0.3, numpy.pi / 4.2, 3 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 0.777, 0.747, 0.343, 2.5 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 12, 1, 2, 3 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 3, 4, 5, 6 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 2, -0.7, 3 * numpy.pi / 2, 5 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 11, 11, 11, 1.123 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 4, 7, 2, 10000 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 0.01, 0, 0, 0 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 1.23, 0, 44, 343 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 7, 7, 7, 7 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) pot = spiral(amp=11, N=2, alpha=0.777, r_ref=7, Cs=[8.0], omega=0.1) R, z, phi, t = 1, 0, 0, 0 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 0.7, 0.3, numpy.pi / 12, 0 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 1.1, -0.3, numpy.pi / 4.2, 3 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 0.777, 0.747, 0.343, 2.5 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 2, 1, 2, 3 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 3, 4, 5, 6 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 5, -0.7, 3 * numpy.pi / 2, 5 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 11, 11, 11, 1.123 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 4, 7, 2, 10000 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 0.01, 0, 0, 0 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 1.23, 0, 44, 343 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 7, 7, 7, 7 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) pot = spiral(amp=2, N=1, alpha=-0.1, r_ref=5, Rs=5, H=0.7, Cs=[3.5], omega=3) R, z, phi, t = 1, 0, 0, 0 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 0.77, 0.3, numpy.pi / 3, 0 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 3.1, -0.3, numpy.pi / 5, 2 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 0.777, 0.747, 0.343, 2.5 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 12, 1, 2, 3 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 3, 4, 5, 6 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 5, -0.7, 3 * numpy.pi / 2, 5 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 11, 11, 11, 1.123 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 4, 7, 2, 10000 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 0.01, 0, 0, 0 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 1.23, 0, 44, 343 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) R, z, phi, t = 7, 7, 7, 7 assert_allclose( pot.Rzderiv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, x, phi, t), z, dx=dx), rtol=rtol, ) def test_Rphideriv(self): """Test Rphideriv against a numerical derivative.""" dx = 1e-8 rtol = 5e-5 pot = spiral() R, z, phi, t = 1, 0, 0, 0 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 0.7, 0.3, numpy.pi / 3, 0 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 1.1, -0.3, numpy.pi / 4.2, 3 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 0.777, 0.747, 0.343, 2.5 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 12, 1, 2, 3 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 3, 4, 5, 6 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 5, -0.7, 3 * numpy.pi / 2, 5 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 11, 11, 11, 1.123 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 4, 7, 2, 1000 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 0.01, 0, 0, 0 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 1.23, 0, 44, 343 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 7, 1, 7, 7 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) pot = spiral(N=3, alpha=0.21, r_ref=0.5, phi_ref=numpy.pi, Cs=[2.0], omega=-3) R, z, phi, t = 1, 0, 0, 0 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 0.7, 0.3, numpy.pi / 3, 0 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 1.1, -0.3, numpy.pi / 4.2, 3 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 0.777, 0.747, 0.343, 2.5 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 12, 1, 2, 3 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 3, 4, 5, 6 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 5, -0.7, 3 * numpy.pi / 2, 5 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 11, 11, 11, 1.123 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 3, 2, 1, 100 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 0.01, 0, 0, 0 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 1.12, 0, 2, 343 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) R, z, phi, t = 7, 7, 7, 7 assert_allclose( pot.Rphideriv(R, z, phi, t), -deriv(lambda x: pot.Rforce(R, z, x, t), phi, dx=dx), rtol=rtol, ) def test_OmegaP(self): sp = spiral() assert sp.OmegaP() == 0 sp = spiral( N=1, alpha=2, r_ref=0.1, phi_ref=0.5, Rs=0.2, H=0.7, Cs=[1, 2], omega=-123 ) assert sp.OmegaP() == -123 sp = spiral(omega=123.456) assert sp.OmegaP() == 123.456 def test_K(self): pot = spiral() R = 1 assert_allclose([pot._K(R)], [pot._ns * pot._N / R / numpy.sin(pot._alpha)]) R = 1e-6 assert_allclose([pot._K(R)], [pot._ns * pot._N / R / numpy.sin(pot._alpha)]) R = 0.5 assert_allclose([pot._K(R)], [pot._ns * pot._N / R / numpy.sin(pot._alpha)]) def test_B(self): pot = spiral() R = 1 assert_allclose( [pot._B(R)], [pot._K(R) * pot._H * (1 + 0.4 * pot._K(R) * pot._H)] ) R = 1e-6 assert_allclose( [pot._B(R)], [pot._K(R) * pot._H * (1 + 0.4 * pot._K(R) * pot._H)] ) R = 0.3 assert_allclose( [pot._B(R)], [pot._K(R) * pot._H * (1 + 0.4 * pot._K(R) * pot._H)] ) def test_D(self): pot = spiral() assert_allclose( [pot._D(3)], [ (1.0 + pot._K(3) * pot._H + 0.3 * pot._K(3) ** 2 * pot._H**2.0) / (1.0 + 0.3 * pot._K(3) * pot._H) ], ) assert_allclose( [pot._D(1e-6)], [ (1.0 + pot._K(1e-6) * pot._H + 0.3 * pot._K(1e-6) ** 2 * pot._H**2.0) / (1.0 + 0.3 * pot._K(1e-6) * pot._H) ], ) assert_allclose( [pot._D(0.5)], [ (1.0 + pot._K(0.5) * pot._H + 0.3 * pot._K(0.5) ** 2 * pot._H**2.0) / (1.0 + 0.3 * pot._K(0.5) * pot._H) ], ) def test_dK_dR(self): pot = spiral() dx = 1e-8 assert_allclose(pot._dK_dR(3), deriv(pot._K, 3, dx=dx)) assert_allclose(pot._dK_dR(2.3), deriv(pot._K, 2.3, dx=dx)) assert_allclose(pot._dK_dR(-2.3), deriv(pot._K, -2.3, dx=dx)) def test_dB_dR(self): pot = spiral() dx = 1e-8 assert_allclose(pot._dB_dR(3.3), deriv(pot._B, 3.3, dx=dx)) assert_allclose(pot._dB_dR(1e-3), deriv(pot._B, 1e-3, dx=dx)) assert_allclose(pot._dB_dR(3), deriv(pot._B, 3, dx=dx)) def test_dD_dR(self): pot = spiral() dx = 1e-8 assert_allclose(pot._dD_dR(1e-3), deriv(pot._D, 1e-3, dx=dx)) assert_allclose(pot._dD_dR(2), deriv(pot._D, 2, dx=dx)) def test_gamma(self): pot = spiral() R, phi = 1, 2 assert_allclose( pot._gamma(R, phi), [ pot._N * ( float(phi) - pot._phi_ref - numpy.log(float(R) / pot._r_ref) / numpy.tan(pot._alpha) ) ], ) R, phi = 0.1, -0.2 assert_allclose( pot._gamma(R, phi), [ pot._N * ( float(phi) - pot._phi_ref - numpy.log(float(R) / pot._r_ref) / numpy.tan(pot._alpha) ) ], ) R, phi = 0.01, 0 assert_allclose( pot._gamma(R, phi), [ pot._N * ( float(phi) - pot._phi_ref - numpy.log(float(R) / pot._r_ref) / numpy.tan(pot._alpha) ) ], ) def test_dgamma_dR(self): pot = spiral() dx = 1e-8 assert_allclose( pot._dgamma_dR(3.0), deriv(lambda x: pot._gamma(x, 1), 3.0, dx=dx) ) assert_allclose(pot._dgamma_dR(3), deriv(lambda x: pot._gamma(x, 1), 3, dx=dx)) assert_allclose( pot._dgamma_dR(0.01), deriv(lambda x: pot._gamma(x, 1), 0.01, dx=dx) ) if __name__ == "__main__": suite = unittest.TestLoader().loadTestsFromTestCase(TestSpiralArmsPotential) unittest.TextTestRunner(verbosity=2).run(suite) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_actionAngle.py0000644000175100001660000111444614761352023017327 0ustar00runnerdockerimport sys import warnings import numpy import pytest from galpy.util import galpyWarning PY2 = sys.version < "3" # Print all galpyWarnings always for tests of warnings warnings.simplefilter("always", galpyWarning) # Test the actions of an actionAngleHarmonic def test_actionAngleHarmonic_conserved_actions(): # Create harmonic oscillator potential as isochrone w/ large b --> 1D from galpy.actionAngle import actionAngleHarmonic from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=5.0, b=10000.0) ipz = ip.toVertical(1.2) # Omega = sqrt(4piG density / 3) aAH = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) obs = Orbit([0.1, -0.3]) ntimes = 1001 times = numpy.linspace(0.0, 20.0, ntimes) obs.integrate(times, ipz) js = aAH(obs.x(times), obs.vx(times)) maxdj = numpy.amax( numpy.fabs(js - numpy.tile(numpy.mean(js), (len(times), 1)).T) ) / numpy.mean(js) assert maxdj < 10.0**-4.0, "Action conservation fails at %g%%" % (100.0 * maxdj) return None # Test that the angles of an actionAngleHarmonic increase linearly def test_actionAngleHarmonic_linear_angles(): from galpy.actionAngle import actionAngleHarmonic, dePeriod from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=5.0, b=10000.0) ipz = ip.toVertical(1.2) # Omega = sqrt(4piG density / 3) aAH = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) obs = Orbit([0.1, -0.3]) ntimes = 1001 times = numpy.linspace(0.0, 20.0, ntimes) obs.integrate(times, ipz) acfs_init = aAH.actionsFreqsAngles(obs.x(), obs.vx()) # to check the init. angles acfs = aAH.actionsFreqsAngles(obs.x(times), obs.vx(times)) angle = dePeriod(numpy.reshape(acfs[2], (1, len(times)))).flatten() # Do linear fit to the angle, check that deviations are small, check # that the slope is the frequency linfit = numpy.polyfit(times, angle, 1) assert numpy.fabs((linfit[1] - acfs_init[2]) / acfs_init[2]) < 10.0**-5.0, ( "Angle obtained by fitting linear trend to the orbit does not agree with the initially-calculated angle by %g%%" % (100.0 * numpy.fabs((linfit[1] - acfs_init[2]) / acfs_init[2])) ) assert numpy.fabs(linfit[0] - acfs_init[1]) < 10.0**-5.0, ( "Frequency obtained by fitting linear trend to the orbit does not agree with the initially-calculated frequency by %g%%" % (100.0 * numpy.fabs((linfit[0] - acfs_init[1]) / acfs_init[1])) ) devs = angle - linfit[0] * times - linfit[1] maxdev = numpy.amax(numpy.fabs(devs)) assert maxdev < 10.0**-6.0, ( "Maximum deviation from linear trend in the angles is %g" % maxdev ) # Finally test that the frequency returned by actionsFreqs == that from actionsFreqsAngles assert ( numpy.all( numpy.fabs( aAH.actionsFreqs(obs.x(times), obs.vx(times))[1] - aAH.actionsFreqsAngles(obs.x(times), obs.vx(times))[1] ) ) < 1e-100 ), ( "Frequency returned by actionsFreqs not equal to that returned by actionsFreqsAngles" ) return None # Test physical output for actionAngleHarmonic def test_physical_harmonic(): from galpy.actionAngle import actionAngleHarmonic from galpy.potential import IsochronePotential from galpy.util import conversion ro, vo = 7.0, 230.0 ip = IsochronePotential(normalize=5.0, b=10000.0) # Omega = sqrt(4piG density / 3) aAH = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=ro, vo=vo ) aAHnu = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) # __call__ assert numpy.fabs(aAH(-0.1, 0.1) - aAHnu(-0.1, 0.1) * ro * vo) < 10.0**-8.0, ( "actionAngle function __call__ does not return Quantity with the right value for actionAngleHarmonic" ) # actionsFreqs assert ( numpy.fabs( aAH.actionsFreqs(0.2, 0.1)[0] - aAHnu.actionsFreqs(0.2, 0.1)[0] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value for actionAngleHarmonic" ) assert ( numpy.fabs( aAH.actionsFreqs(0.2, 0.1)[1] - aAHnu.actionsFreqs(0.2, 0.1)[1] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value for actionAngleHarmonic" ) # actionsFreqsAngles assert ( numpy.fabs( aAH.actionsFreqsAngles(0.2, 0.1)[0] - aAHnu.actionsFreqsAngles(0.2, 0.1)[0] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value for actionAngleHarmonic" ) assert ( numpy.fabs( aAH.actionsFreqsAngles(0.2, 0.1)[1] - aAHnu.actionsFreqsAngles(0.2, 0.1)[1] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value for actionAngleHarmonic" ) assert ( numpy.fabs( aAH.actionsFreqsAngles(0.2, 0.1)[2] - aAHnu.actionsFreqsAngles(0.2, 0.1)[2] ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value for actionAngleHarmonic" ) return None # Test the actions of an actionAngleVertical def test_actionAngleVertical_conserved_actions(): # Use an isothermal disk potential from galpy.actionAngle import actionAngleVertical from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAV = actionAngleVertical(pot=isopot) obs = Orbit([0.1, -0.3]) ntimes = 1001 times = numpy.linspace(0.0, 20.0, ntimes) obs.integrate(times, isopot) js = aAV(obs.x(times), obs.vx(times)) maxdj = numpy.amax( numpy.fabs( (js - numpy.tile(numpy.mean(js), (len(times), 1)).T) / numpy.mean(js) ) ) assert maxdj < 10.0**-4.0, "Action conservation fails at %g%%" % (100.0 * maxdj) return None # Test the frequencies of an actionAngleVertical def test_actionAngleVertical_conserved_freqs(): # Use an isothermal disk potential from galpy.actionAngle import actionAngleVertical from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAV = actionAngleVertical(pot=isopot) obs = Orbit([0.1, -0.3]) ntimes = 1001 times = numpy.linspace(0.0, 20.0, ntimes) obs.integrate(times, isopot) js, os = aAV.actionsFreqs(obs.x(times), obs.vx(times)) maxdj = numpy.amax( numpy.fabs( (js - numpy.tile(numpy.mean(js), (len(times), 1)).T) / numpy.mean(js) ) ) assert maxdj < 10.0**-4.0, "Action conservation fails at %g%%" % (100.0 * maxdj) maxdo = numpy.amax( numpy.fabs( (os - numpy.tile(numpy.mean(os), (len(times), 1)).T) / numpy.mean(os) ) ) assert maxdo < 10.0**-4.0, "Frequency conservation fails at %g%%" % (100.0 * maxdo) return None # Test that the angles of an actionAngleVertical increase linearly def test_actionAngleVertical_linear_angles(): from galpy.actionAngle import actionAngleVertical, dePeriod from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAV = actionAngleVertical(pot=isopot) obs = Orbit([0.1, -0.3]) ntimes = 1001 times = numpy.linspace(0.0, 20.0, ntimes) obs.integrate(times, isopot) acfs_init = aAV.actionsFreqsAngles(obs.x(), obs.vx()) # to check the init. angles acfs = aAV.actionsFreqsAngles(obs.x(times), obs.vx(times)) angle = dePeriod(numpy.reshape(acfs[2], (1, len(times)))).flatten() # Do linear fit to the angle, check that deviations are small, check # that the slope is the frequency linfit = numpy.polyfit(times, angle, 1) assert numpy.fabs((linfit[1] - acfs_init[2]) / acfs_init[2]) < 10.0**-5.0, ( "Angle obtained by fitting linear trend to the orbit does not agree with the initially-calculated angle by %g%%" % (100.0 * numpy.fabs((linfit[1] - acfs_init[2]) / acfs_init[2])) ) assert numpy.fabs(linfit[0] - acfs_init[1]) < 10.0**-5.0, ( "Frequency obtained by fitting linear trend to the orbit does not agree with the initially-calculated frequency by %g%%" % (100.0 * numpy.fabs((linfit[0] - acfs_init[1]) / acfs_init[1])) ) devs = angle - linfit[0] * times - linfit[1] maxdev = numpy.amax(numpy.fabs(devs)) assert maxdev < 10.0**-6.0, ( "Maximum deviation from linear trend in the angles is %g" % maxdev ) # Finally test that the frequency returned by actionsFreqs == that from actionsFreqsAngles assert ( numpy.all( numpy.fabs( aAV.actionsFreqs(obs.x(times), obs.vx(times))[1] - aAV.actionsFreqsAngles(obs.x(times), obs.vx(times))[1] ) ) < 1e-100 ), ( "Frequency returned by actionsFreqs not equal to that returned by actionsFreqsAngles" ) return None # Test that unbound orbits are handled properly def test_actionAngleVertical_unbound(): from galpy.actionAngle import actionAngleVertical from galpy.potential import ( MWPotential2014, evaluatelinearPotentials, toVerticalPotential, ) mwp14_v = toVerticalPotential(MWPotential2014, 1.0) aAV = actionAngleVertical(pot=mwp14_v) vesc = numpy.sqrt( 2.0 * ( evaluatelinearPotentials(mwp14_v, numpy.inf) - evaluatelinearPotentials(mwp14_v, 0.0) ) ) assert numpy.fabs(aAV(0.0, vesc + 1e-4) - 9999.99) < 10.0**-8.0, ( "actionAngleVertical does not return J=9999.99 for unbound orbits" ) assert numpy.fabs(aAV.actionsFreqs(0.0, vesc + 1e-4)[0] - 9999.99) < 10.0**-8.0, ( "actionAngleVertical does not return J=9999.99 for unbound orbits" ) assert numpy.fabs(aAV.actionsFreqs(0.0, vesc + 1e-4)[1] - 9999.99) < 10.0**-8.0, ( "actionAngleVertical does not return O=9999.99 for unbound orbits" ) assert ( numpy.fabs(aAV.actionsFreqsAngles(0.0, vesc + 1e-4)[0] - 9999.99) < 10.0**-8.0 ), "actionAngleVertical does not return J=9999.99 for unbound orbits" assert ( numpy.fabs(aAV.actionsFreqsAngles(0.0, vesc + 1e-4)[1] - 9999.99) < 10.0**-8.0 ), "actionAngleVertical does not return O=9999.99 for unbound orbits" assert ( numpy.fabs( aAV.actionsFreqsAngles(0.0, vesc + 1e-4)[2] - ((9999.99 * 9999.99) % (2 * numpy.pi)) ) < 10.0**-8.0 ), "actionAngleVertical does not return O=9999.99 for unbound orbits" return None # Test actionAngleVertical against actionAngleHarmonic for HO def test_actionAngleVertical_Harmonic_actions(): from galpy.actionAngle import actionAngleHarmonic, actionAngleVertical from galpy.orbit import Orbit from galpy.potential import linearPotential # Stop-gap until we implement a proper 1D (or 3D) HO potential, # limit of taking Isochrone leads to 1e-7 fluctuations in the potential # that mess up this test class HO(linearPotential): def __init__(self, omega): linearPotential.__init__(self, amp=1.0) self._omega = omega def _evaluate(self, x, t=0.0): return self._omega**2.0 * x**2.0 / 2.0 def _force(self, x, t=0.0): return -(self._omega**2.0) * x ipz = HO(omega=2.23) aAH = actionAngleHarmonic(omega=ipz._omega) aAV = actionAngleVertical(pot=ipz) obs = Orbit([0.1, -0.3]) ntimes = 101 times = numpy.linspace(0.0, 20.0, ntimes) obs.integrate(times, ipz) js = aAH(obs.x(times), obs.vx(times)) jsv = aAV(obs.x(times), obs.vx(times)) maxdj = numpy.amax(numpy.fabs((js - jsv) / js)) assert maxdj < 10.0**-10.0, ( "Actions of harmonic oscillator computed using actionAngleVertical do not agree with those computed using actionAngleHarmonic at %g%%" % (100.0 * maxdj) ) return None def test_actionAngleVertical_Harmonic_actionsFreqs(): from galpy.actionAngle import actionAngleHarmonic, actionAngleVertical from galpy.orbit import Orbit from galpy.potential import linearPotential # Stop-gap until we implement a proper 1D (or 3D) HO potential, # limit of taking Isochrone leads to 1e-7 fluctuations in the potential # that mess up this test class HO(linearPotential): def __init__(self, omega): linearPotential.__init__(self, amp=1.0) self._omega = omega def _evaluate(self, x, t=0.0): return self._omega**2.0 * x**2.0 / 2.0 def _force(self, x, t=0.0): return -(self._omega**2.0) * x ipz = HO(omega=2.23) aAH = actionAngleHarmonic(omega=ipz._omega) aAV = actionAngleVertical(pot=ipz) obs = Orbit([0.1, -0.3]) ntimes = 101 times = numpy.linspace(0.0, 20.0, ntimes) obs.integrate(times, ipz) js, os = aAH.actionsFreqs(obs.x(times), obs.vx(times)) jsv, osv = aAV.actionsFreqs(obs.x(times), obs.vx(times)) maxdj = numpy.amax(numpy.fabs((js - jsv) / js)) assert maxdj < 10.0**-10.0, ( "Actions of harmonic oscillator computed using actionAngleVertical do not agree with those computed using actionAngleHarmonic at %g%%" % (100.0 * maxdj) ) maxdo = numpy.amax(numpy.fabs((os - osv) / os)) assert maxdo < 10.0**-10.0, ( "Frequencies of harmonic oscillator computed using actionAngleVertical do not agree with those computed using actionAngleHarmonic at %g%%" % (100.0 * maxdo) ) return None def test_actionAngleVertical_Harmonic_actionsFreqsAngles(): from galpy.actionAngle import actionAngleHarmonic, actionAngleVertical from galpy.orbit import Orbit from galpy.potential import linearPotential # Stop-gap until we implement a proper 1D (or 3D) HO potential, # limit of taking Isochrone leads to 1e-7 fluctuations in the potential # that mess up this test class HO(linearPotential): def __init__(self, omega): linearPotential.__init__(self, amp=1.0) self._omega = omega def _evaluate(self, x, t=0.0): return self._omega**2.0 * x**2.0 / 2.0 def _force(self, x, t=0.0): return -(self._omega**2.0) * x ipz = HO(omega=2.236) aAH = actionAngleHarmonic(omega=ipz._omega) aAV = actionAngleVertical(pot=ipz) obs = Orbit([0.1, -0.3]) ntimes = 101 times = numpy.linspace(0.0, 20.0, ntimes) obs.integrate(times, ipz) js, os, anss = aAH.actionsFreqsAngles(obs.x(times), obs.vx(times)) jsv, osv, anssv = aAV.actionsFreqsAngles(obs.x(times), obs.vx(times)) maxdj = numpy.amax(numpy.fabs((js - jsv) / js)) assert maxdj < 10.0**-10.0, ( "Actions of harmonic oscillator computed using actionAngleVertical do not agree with those computed using actionAngleHarmonic at %g%%" % (100.0 * maxdj) ) maxdo = numpy.amax(numpy.fabs((os - osv) / os)) assert maxdo < 10.0**-10.0, ( "Frequencies of harmonic oscillator computed using actionAngleVertical do not agree with those computed using actionAngleHarmonic at %g%%" % (100.0 * maxdo) ) maxda = numpy.amax( numpy.fabs(((anss - anssv) + numpy.pi) % (2.0 * numpy.pi) - numpy.pi) ) assert maxda < 10.0**-10.0, ( "Angles of harmonic oscillator computed using actionAngleVertical do not agree with those computed using actionAngleHarmonic at %g%%" % (100.0 * maxda) ) return None # Test physical output for actionAngleVertical def test_physical_vertical(): from galpy.actionAngle import actionAngleVertical from galpy.potential import IsothermalDiskPotential from galpy.util import conversion ro, vo = 7.0, 230.0 isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) # Omega = sqrt(4piG density / 3) aAV = actionAngleVertical(pot=isopot, ro=ro, vo=vo) aAVnu = actionAngleVertical(pot=isopot) # __call__ assert numpy.fabs(aAV(-0.1, 0.1) - aAVnu(-0.1, 0.1) * ro * vo) < 10.0**-8.0, ( "actionAngle function __call__ does not return Quantity with the right value for actionAngleVertical" ) # actionsFreqs assert ( numpy.fabs( aAV.actionsFreqs(0.2, 0.1)[0] - aAVnu.actionsFreqs(0.2, 0.1)[0] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value for actionAngleVertical" ) assert ( numpy.fabs( aAV.actionsFreqs(0.2, 0.1)[1] - aAVnu.actionsFreqs(0.2, 0.1)[1] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value for actionAngleVertical" ) # actionsFreqsAngles assert ( numpy.fabs( aAV.actionsFreqsAngles(0.2, 0.1)[0] - aAVnu.actionsFreqsAngles(0.2, 0.1)[0] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value for actionAngleVertical" ) assert ( numpy.fabs( aAV.actionsFreqsAngles(0.2, 0.1)[1] - aAVnu.actionsFreqsAngles(0.2, 0.1)[1] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value for actionAngleVertical" ) assert ( numpy.fabs( aAV.actionsFreqsAngles(0.2, 0.1)[2] - aAVnu.actionsFreqsAngles(0.2, 0.1)[2] ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value for actionAngleVertical" ) return None # Basic sanity checking of the actionAngleIsochrone actions def test_actionAngleIsochrone_basic_actions(): from galpy.actionAngle import actionAngleIsochrone from galpy.orbit import Orbit aAI = actionAngleIsochrone(b=1.2) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 js = aAI(R, vR, vT, z, vz) assert numpy.fabs(js[0]) < 10.0**-16.0, ( "Circular orbit in the isochrone potential does not have Jr=0" ) assert numpy.fabs(js[2]) < 10.0**-16.0, ( "Circular orbit in the isochrone potential does not have Jz=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAI(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the isochrone potential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-4.0, ( "Close-to-circular orbit in the isochrone potential does not have small Jz" ) # Close-to-circular orbit, called with time R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAI(Orbit([R, vR, vT, z, vz]), 0.0) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the isochrone potential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-4.0, ( "Close-to-circular orbit in the isochrone potential does not have small Jz" ) return None # Basic sanity checking of the actionAngleIsochrone actions def test_actionAngleIsochrone_basic_freqs(): from galpy.actionAngle import actionAngleIsochrone from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 jos = aAI.actionsFreqs(R, vR, vT, z, vz) assert numpy.fabs((jos[3] - ip.epifreq(1.0)) / ip.epifreq(1.0)) < 10.0**-12.0, ( "Circular orbit in the isochrone potential does not have Or=kappa at %g%%" % (100.0 * numpy.fabs((jos[3] - ip.epifreq(1.0)) / ip.epifreq(1.0))) ) assert numpy.fabs((jos[4] - ip.omegac(1.0)) / ip.omegac(1.0)) < 10.0**-12.0, ( "Circular orbit in the isochrone potential does not have Op=Omega at %g%%" % (100.0 * numpy.fabs((jos[4] - ip.omegac(1.0)) / ip.omegac(1.0))) ) assert ( numpy.fabs((jos[5] - ip.verticalfreq(1.0)) / ip.verticalfreq(1.0)) < 10.0**-12.0 ), "Circular orbit in the isochrone potential does not have Oz=nu at %g%%" % ( 100.0 * numpy.fabs((jos[5] - ip.verticalfreq(1.0)) / ip.verticalfreq(1.0)) ) # close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.01, 1.01, 0.01, 0.01 jos = aAI.actionsFreqs(Orbit([R, vR, vT, z, vz])) assert numpy.fabs((jos[3] - ip.epifreq(1.0)) / ip.epifreq(1.0)) < 10.0**-2.0, ( "Close-to-circular orbit in the isochrone potential does not have Or=kappa at %g%%" % (100.0 * numpy.fabs((jos[3] - ip.epifreq(1.0)) / ip.epifreq(1.0))) ) assert numpy.fabs((jos[4] - ip.omegac(1.0)) / ip.omegac(1.0)) < 10.0**-2.0, ( "Close-to-circular orbit in the isochrone potential does not have Op=Omega at %g%%" % (100.0 * numpy.fabs((jos[4] - ip.omegac(1.0)) / ip.omegac(1.0))) ) assert ( numpy.fabs((jos[5] - ip.verticalfreq(1.0)) / ip.verticalfreq(1.0)) < 10.0**-2.0 ), ( "Close-to-circular orbit in the isochrone potential does not have Oz=nu at %g%%" % (100.0 * numpy.fabs((jos[5] - ip.verticalfreq(1.0)) / ip.verticalfreq(1.0))) ) return None # Test that EccZmaxRperiRap for an IsochronePotential are correctly computed # by comparing to a numerical orbit integration def test_actionAngleIsochrone_EccZmaxRperiRap_againstOrbit(): from galpy.actionAngle import actionAngleIsochrone from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) o = Orbit([1.0, 0.1, 1.1, 0.2, 0.03, 0.0]) ecc, zmax, rperi, rap = aAI.EccZmaxRperiRap(o) ts = numpy.linspace(0.0, 100.0, 100001) o.integrate(ts, ip) assert numpy.fabs(ecc - o.e()) < 1e-10, ( "Analytically calculated eccentricity does not agree with numerically calculated one for an IsochronePotential" ) assert numpy.fabs(zmax - o.zmax()) < 1e-5, ( "Analytically calculated zmax does not agree with numerically calculated one for an IsochronePotential" ) assert numpy.fabs(rperi - o.rperi()) < 1e-10, ( "Analytically calculated rperi does not agree with numerically calculated one for an IsochronePotential" ) assert numpy.fabs(rap - o.rap()) < 1e-10, ( "Analytically calculated rap does not agree with numerically calculated one for an IsochronePotential" ) # Another one o = Orbit([1.0, 0.1, 1.1, 0.2, -0.3, 0.0]) ecc, zmax, rperi, rap = aAI.EccZmaxRperiRap( o.R(), o.vR(), o.vT(), o.z(), o.vz(), o.phi() ) ts = numpy.linspace(0.0, 100.0, 100001) o.integrate(ts, ip) assert numpy.fabs(ecc - o.e()) < 1e-10, ( "Analytically calculated eccentricity does not agree with numerically calculated one for an IsochronePotential" ) assert numpy.fabs(zmax - o.zmax()) < 1e-3, ( "Analytically calculated zmax does not agree with numerically calculated one for an IsochronePotential" ) assert numpy.fabs(rperi - o.rperi()) < 1e-10, ( "Analytically calculated rperi does not agree with numerically calculated one for an IsochronePotential" ) assert numpy.fabs(rap - o.rap()) < 1e-10, ( "Analytically calculated rap does not agree with numerically calculated one for an IsochronePotential" ) return None # Test that EccZmaxRperiRap for an IsochronePotential are correctly computed # by comparing to a numerical orbit integration for a Kepler potential def test_actionAngleIsochrone_EccZmaxRperiRap_againstOrbit_kepler(): from galpy.actionAngle import actionAngleIsochrone from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=0) aAI = actionAngleIsochrone(ip=ip) o = Orbit([1.0, 0.1, 1.1, 0.2, 0.03, 0.0]) ecc, zmax, rperi, rap = aAI.EccZmaxRperiRap(o.R(), o.vR(), o.vT(), o.z(), o.vz()) ts = numpy.linspace(0.0, 100.0, 100001) o.integrate(ts, ip) assert numpy.fabs(ecc - o.e()) < 1e-10, ( "Analytically calculated eccentricity does not agree with numerically calculated one for an IsochronePotential" ) # Don't do zmax, because zmax for Kepler is approximate assert numpy.fabs(rperi - o.rperi()) < 1e-10, ( "Analytically calculated rperi does not agree with numerically calculated one for an IsochronePotential" ) assert numpy.fabs(rap - o.rap()) < 1e-10, ( "Analytically calculated rap does not agree with numerically calculated one for an IsochronePotential" ) return None # Test the actions of an actionAngleIsochrone def test_actionAngleIsochrone_conserved_actions(): from galpy.actionAngle import actionAngleIsochrone from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) obs = Orbit([1.1, 0.3, 1.2, 0.2, 0.5]) from galpy.orbit.Orbits import ext_loaded if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_conserved_actions(aAI, obs, ip, -5.0, -5.0, -5.0) else: check_actionAngle_conserved_actions(aAI, obs, ip, -8.0, -8.0, -8.0) return None # Test that the angles of an actionAngleIsochrone increase linearly def test_actionAngleIsochrone_linear_angles(): from galpy.actionAngle import actionAngleIsochrone from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) obs = Orbit([1.1, 0.3, 1.2, 0.2, 0.5, 2.0]) from galpy.orbit.Orbits import ext_loaded if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_linear_angles( aAI, obs, ip, -5.0, -5.0, -5.0, -6.0, -6.0, -6.0, -5.0, -5.0, -5.0 ) else: check_actionAngle_linear_angles( aAI, obs, ip, -6.0, -6.0, -6.0, -8.0, -8.0, -8.0, -8.0, -8.0, -8.0 ) return None # Test that the angles of an actionAngleIsochrone increase linearly for an # orbit in the mid-plane (non-inclined; has potential issues, because the # the ascending node is not well defined) def test_actionAngleIsochrone_noninclinedorbit_linear_angles(): from galpy.actionAngle import actionAngleIsochrone from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) obs = Orbit([1.1, 0.3, 1.2, 0.0, 0.0, 2.0]) from galpy.orbit.Orbits import ext_loaded if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_linear_angles( aAI, obs, ip, -5.0, -5.0, -5.0, -6.0, -6.0, -6.0, -5.0, -5.0, -5.0 ) else: check_actionAngle_linear_angles( aAI, obs, ip, -6.0, -6.0, -6.0, -8.0, -8.0, -8.0, -8.0, -8.0, -8.0 ) return None def test_actionAngleIsochrone_almostnoninclinedorbit_linear_angles(): from galpy.actionAngle import actionAngleIsochrone from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) eps = 1e-10 obs = Orbit([1.1, 0.3, 1.2, 0.0, eps, 2.0]) from galpy.orbit.Orbits import ext_loaded if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_linear_angles( aAI, obs, ip, -5.0, -5.0, -5.0, -6.0, -6.0, -6.0, -5.0, -5.0, -5.0 ) else: check_actionAngle_linear_angles( aAI, obs, ip, -6.0, -6.0, -6.0, -8.0, -8.0, -8.0, -8.0, -8.0, -8.0 ) return None # Test that the Kelperian limit of the isochrone actions/angles works def test_actionAngleIsochrone_kepler_actions(): from galpy.actionAngle import actionAngleIsochrone from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=0.0) aAI = actionAngleIsochrone(ip=ip) obs = Orbit([1.1, 0.3, 1.2, 0.2, 0.5, 2.0]) times = numpy.linspace(0.0, 100.0, 101) obs.integrate(times, ip, method="dopr54_c") jrs, jps, jzs = aAI( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times), obs.phi(times), ) jc = ip._amp / numpy.sqrt(-2.0 * obs.E()) L = numpy.sqrt(numpy.sum(obs.L() ** 2.0)) # Jr = Jc-L assert numpy.all(numpy.fabs(jrs - (jc - L)) < 10.0**-5.0), ( "Radial action for the Kepler potential not correct" ) assert numpy.all(numpy.fabs(jps - obs.R() * obs.vT()) < 10.0**-10.0), ( "Azimuthal action for the Kepler potential not correct" ) assert numpy.all( numpy.fabs(jzs - (L - numpy.fabs(obs.R() * obs.vT()))) < 10.0**-10.0 ), "Vertical action for the Kepler potential not correct" return None def test_actionAngleIsochrone_kepler_freqs(): from galpy.actionAngle import actionAngleIsochrone from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=0.0) aAI = actionAngleIsochrone(ip=ip) obs = Orbit([1.1, 0.3, 1.2, 0.2, 0.5, 2.0]) times = numpy.linspace(0.0, 100.0, 101) obs.integrate(times, ip, method="dopr54_c") _, _, _, ors, ops, ozs = aAI.actionsFreqs( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times), obs.phi(times), ) jc = ip._amp / numpy.sqrt(-2.0 * obs.E()) oc = ip._amp**2.0 / jc**3.0 # (BT08 eqn. E4) assert numpy.all(numpy.fabs(ors - oc) < 10.0**-10.0), ( "Radial frequency for the Kepler potential not correct" ) assert numpy.all(numpy.fabs(ops - oc) < 10.0**-10.0), ( "Azimuthal frequency for the Kepler potential not correct" ) assert numpy.all( numpy.fabs(ozs - numpy.sign(obs.R() * obs.vT()) * oc) < 10.0**-10.0 ), "Vertical frequency for the Kepler potential not correct" return None def test_actionAngleIsochrone_kepler_angles(): from galpy.actionAngle import actionAngleIsochrone from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=0.0) aAI = actionAngleIsochrone(ip=ip) obs = Orbit([1.1, 0.3, 1.2, 0.2, 0.5, 2.0]) times = numpy.linspace(0.0, 100.0, 101) obs.integrate(times, ip, method="dopr54_c") _, _, _, _, _, _, ars, aps, azs = aAI.actionsFreqsAngles( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times), obs.phi(times), ) jc = ip._amp / numpy.sqrt(-2.0 * obs.E()) oc = ip._amp**2.0 / jc**3.0 # (BT08 eqn. E4) # theta_r = Or x times + theta_r,0 assert numpy.all(numpy.fabs(ars - oc * times - ars[0]) < 10.0**-10.0), ( "Radial angle for the Kepler potential not correct" ) assert numpy.all(numpy.fabs(aps - oc * times - aps[0]) < 10.0**-10.0), ( "Azimuthal angle for the Kepler potential not correct" ) assert numpy.all(numpy.fabs(azs - oc * times - azs[0]) < 10.0**-10.0), ( "Vertical angle for the Kepler potential not correct" ) return None # Basic sanity checking of the actionAngleSpherical actions def test_actionAngleSpherical_basic_actions(): from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) aAS = actionAngleSpherical(pot=lp) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 js = aAS(Orbit([R, vR, vT])) assert numpy.fabs(js[0]) < 10.0**-16.0, ( "Circular orbit in the spherical LogarithmicHaloPotential does not have Jr=0" ) assert numpy.fabs(js[2]) < 10.0**-16.0, ( "Circular orbit in the spherical LogarithmicHaloPotential does not have Jz=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAS(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the spherical LogarithmicHaloPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-4.0, ( "Close-to-circular orbit in the spherical LogarithmicHaloPotential does not have small Jz" ) return None # Basic sanity checking of the actionAngleSpherical actions def test_actionAngleSpherical_basic_freqs(): from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) aAS = actionAngleSpherical(pot=[lp]) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 jos = aAS.actionsFreqs(R, vR, vT, z, vz) assert numpy.fabs((jos[3] - lp.epifreq(1.0)) / lp.epifreq(1.0)) < 10.0**-12.0, ( "Circular orbit in the spherical LogarithmicHaloPotential does not have Or=kappa at %g%%" % (100.0 * numpy.fabs((jos[3] - lp.epifreq(1.0)) / lp.epifreq(1.0))) ) assert numpy.fabs((jos[4] - lp.omegac(1.0)) / lp.omegac(1.0)) < 10.0**-12.0, ( "Circular orbit in the spherical LogarithmicHaloPotential does not have Op=Omega at %g%%" % (100.0 * numpy.fabs((jos[4] - lp.omegac(1.0)) / lp.omegac(1.0))) ) assert ( numpy.fabs((jos[5] - lp.verticalfreq(1.0)) / lp.verticalfreq(1.0)) < 10.0**-12.0 ), ( "Circular orbit in the spherical LogarithmicHaloPotential does not have Oz=nu at %g%%" % (100.0 * numpy.fabs((jos[5] - lp.verticalfreq(1.0)) / lp.verticalfreq(1.0))) ) # close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.01, 1.01, 0.01, 0.01 jos = aAS.actionsFreqs(Orbit([R, vR, vT, z, vz])) assert numpy.fabs((jos[3] - lp.epifreq(1.0)) / lp.epifreq(1.0)) < 10.0**-1.9, ( "Close-to-circular orbit in the spherical LogarithmicHaloPotential does not have Or=kappa at %g%%" % (100.0 * numpy.fabs((jos[3] - lp.epifreq(1.0)) / lp.epifreq(1.0))) ) assert numpy.fabs((jos[4] - lp.omegac(1.0)) / lp.omegac(1.0)) < 10.0**-1.9, ( "Close-to-circular orbit in the spherical LogarithmicHaloPotential does not have Op=Omega at %g%%" % (100.0 * numpy.fabs((jos[4] - lp.omegac(1.0)) / lp.omegac(1.0))) ) assert ( numpy.fabs((jos[5] - lp.verticalfreq(1.0)) / lp.verticalfreq(1.0)) < 10.0**-1.9 ), ( "Close-to-circular orbit in the spherical LogarithmicHaloPotential does not have Oz=nu at %g%%" % (100.0 * numpy.fabs((jos[5] - lp.verticalfreq(1.0)) / lp.verticalfreq(1.0))) ) # Basic sanity checking of the actionAngleSpherical actions def test_actionAngleSpherical_basic_freqsAngles(): from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) aAS = actionAngleSpherical(pot=lp) # v. close-to-circular orbit using actionsFreqsAngles R, vR, vT, z, vz = 1.0, 10.0**-8.0, 1.0, 10.0**-8.0, 0.0 jos = aAS.actionsFreqsAngles(R, vR, vT, z, vz, 0.0) assert numpy.fabs((jos[3] - lp.epifreq(1.0)) / lp.epifreq(1.0)) < 10.0**-1.9, ( "Close-to-circular orbit in the spherical LogarithmicHaloPotential does not have Or=kappa at %g%%" % (100.0 * numpy.fabs((jos[3] - lp.epifreq(1.0)) / lp.epifreq(1.0))) ) assert numpy.fabs((jos[4] - lp.omegac(1.0)) / lp.omegac(1.0)) < 10.0**-1.9, ( "Close-to-circular orbit in the spherical LogarithmicHaloPotential does not have Op=Omega at %g%%" % (100.0 * numpy.fabs((jos[4] - lp.omegac(1.0)) / lp.omegac(1.0))) ) assert ( numpy.fabs((jos[5] - lp.verticalfreq(1.0)) / lp.verticalfreq(1.0)) < 10.0**-1.9 ), ( "Close-to-circular orbit in the spherical LogarithmicHaloPotential does not have Oz=nu at %g%%" % (100.0 * numpy.fabs((jos[5] - lp.verticalfreq(1.0)) / lp.verticalfreq(1.0))) ) return None # Test that EccZmaxRperiRap for a spherical potential are correctly computed # by comparing to a numerical orbit integration def test_actionAngleSpherical_EccZmaxRperiRap_againstOrbit(): from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) aAS = actionAngleSpherical(pot=lp) o = Orbit([1.0, 0.1, 1.1, 0.2, 0.03, 0.0]) ecc, zmax, rperi, rap = aAS.EccZmaxRperiRap(o) ts = numpy.linspace(0.0, 100.0, 100001) o.integrate(ts, lp) assert numpy.fabs(ecc - o.e()) < 1e-9, ( "Analytically calculated eccentricity does not agree with numerically calculated one for a spherical potential" ) assert numpy.fabs(zmax - o.zmax()) < 1e-4, ( "Analytically calculated zmax does not agree with numerically calculated one for a spherical potential" ) assert numpy.fabs(rperi - o.rperi()) < 1e-8, ( "Analytically calculated rperi does not agree with numerically calculated one for a spherical potential" ) assert numpy.fabs(rap - o.rap()) < 1e-8, ( "Analytically calculated rap does not agree with numerically calculated one for a spherical potential" ) # Another one o = Orbit([1.0, 0.1, 1.1, 0.2, -0.3, 0.0]) ecc, zmax, rperi, rap = aAS.EccZmaxRperiRap(o.R(), o.vR(), o.vT(), o.z(), o.vz()) ts = numpy.linspace(0.0, 100.0, 100001) o.integrate(ts, lp) assert numpy.fabs(ecc - o.e()) < 1e-9, ( "Analytically calculated eccentricity does not agree with numerically calculated one for a spherical potential" ) assert numpy.fabs(zmax - o.zmax()) < 1e-3, ( "Analytically calculated zmax does not agree with numerically calculated one for a spherical potential" ) assert numpy.fabs(rperi - o.rperi()) < 1e-8, ( "Analytically calculated rperi does not agree with numerically calculated one for a spherical potential" ) assert numpy.fabs(rap - o.rap()) < 1e-8, ( "Analytically calculated rap does not agree with numerically calculated one for a spherical potential" ) return None # Test the actions of an actionAngleSpherical def test_actionAngleSpherical_conserved_actions(): from galpy import potential from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit lp = potential.LogarithmicHaloPotential(normalize=1.0, q=1.0) aAS = actionAngleSpherical(pot=lp) obs = Orbit([1.1, 0.3, 1.2, 0.2, 0.5]) from galpy.orbit.Orbits import ext_loaded if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_conserved_actions(aAS, obs, lp, -5.0, -5.0, -5.0, ntimes=101) else: check_actionAngle_conserved_actions(aAS, obs, lp, -8.0, -8.0, -8.0, ntimes=101) return None # Test the actions of an actionAngleSpherical def test_actionAngleSpherical_conserved_actions_fixed_quad(): from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) aAS = actionAngleSpherical(pot=lp) obs = Orbit([1.1, 0.3, 1.2, 0.2, 0.5]) from galpy.orbit.Orbits import ext_loaded if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_conserved_actions( aAS, obs, lp, -5.0, -5.0, -5.0, ntimes=101, fixed_quad=True ) else: check_actionAngle_conserved_actions( aAS, obs, lp, -8.0, -8.0, -8.0, ntimes=101, fixed_quad=True ) return None # Test that the angles of an actionAngleIsochrone increase linearly def test_actionAngleSpherical_linear_angles(): from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) aAS = actionAngleSpherical(pot=lp) obs = Orbit([1.1, 0.3, 1.2, 0.2, 0.5, 2.0]) from galpy.orbit.Orbits import ext_loaded if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_linear_angles( aAS, obs, lp, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, ntimes=501, ) # need fine sampling for de-period else: check_actionAngle_linear_angles( aAS, obs, lp, -6.0, -6.0, -6.0, -8.0, -8.0, -8.0, -8.0, -8.0, -8.0, ntimes=501, ) # need fine sampling for de-period return None # Test that the angles of an actionAngleIsochrone increase linearly def test_actionAngleSpherical_linear_angles_fixed_quad(): from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) aAS = actionAngleSpherical(pot=lp) obs = Orbit([1.1, 0.3, 1.2, 0.2, 0.5, 2.0]) from galpy.orbit.Orbits import ext_loaded if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_linear_angles( aAS, obs, lp, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, ntimes=501, # need fine sampling for de-period fixed_quad=True, ) else: check_actionAngle_linear_angles( aAS, obs, lp, -6.0, -6.0, -6.0, -8.0, -8.0, -8.0, -8.0, -8.0, -8.0, ntimes=501, # need fine sampling for de-period fixed_quad=True, ) return None # Test that the angles of an actionAngleSpherical increase linearly for an # orbit in the mid-plane (non-inclined; has potential issues, because the # the ascending node is not well defined) def test_actionAngleSpherical_noninclinedorbit_linear_angles(): from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) aAS = actionAngleSpherical(pot=lp) obs = Orbit([1.1, 0.3, 1.2, 0.0, 0.0, 2.0]) from galpy.orbit.Orbits import ext_loaded if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_linear_angles( aAS, obs, lp, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, ntimes=501, ) # need fine sampling for de-period else: check_actionAngle_linear_angles( aAS, obs, lp, -6.0, -6.0, -6.0, -8.0, -8.0, -8.0, -8.0, -8.0, -8.0, ntimes=501, ) # need fine sampling for de-period return None def test_actionAngleSpherical_almostnoninclinedorbit_linear_angles(): from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) aAS = actionAngleSpherical(pot=lp) eps = 1e-10 obs = Orbit([1.1, 0.3, 1.2, 0.0, eps, 2.0]) from galpy.orbit.Orbits import ext_loaded if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_linear_angles( aAS, obs, lp, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, -4.0, ntimes=501, ) # need fine sampling for de-period else: check_actionAngle_linear_angles( aAS, obs, lp, -6.0, -6.0, -6.0, -8.0, -8.0, -8.0, -8.0, -8.0, -8.0, ntimes=501, ) # need fine sampling for de-period return None # Test the conservation of ecc, zmax, rperi, rap of an actionAngleSpherical def test_actionAngleSpherical_conserved_EccZmaxRperiRap_ecc(): from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import NFWPotential np = NFWPotential(normalize=1.0, a=2.0) aAS = actionAngleSpherical(pot=np) obs = Orbit([1.1, 0.2, 1.3, 0.1, 0.0, 2.0]) check_actionAngle_conserved_EccZmaxRperiRap( aAS, obs, np, -1.1, -0.4, -1.8, -1.8, ntimes=101, inclphi=True ) return None # Test the actionAngleSpherical against an isochrone potential: actions def test_actionAngleSpherical_otherIsochrone_actions(): from galpy.actionAngle import actionAngleIsochrone, actionAngleSpherical from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAS = actionAngleSpherical(pot=ip) R, vR, vT, z, vz, phi = 1.1, 0.3, 1.2, 0.2, 0.5, 2.0 ji = aAI(R, vR, vT, z, vz, phi) jia = aAS(R, vR, vT, z, vz, phi) djr = numpy.fabs((ji[0] - jia[0]) / ji[0]) dlz = numpy.fabs((ji[1] - jia[1]) / ji[1]) djz = numpy.fabs((ji[2] - jia[2]) / ji[2]) assert djr < 10.0**-10.0, ( "actionAngleSpherical applied to isochrone potential fails for Jr at %g%%" % (djr * 100.0) ) # Lz and Jz are easy, because ip is a spherical potential assert dlz < 10.0**-10.0, ( "actionAngleSpherical applied to isochrone potential fails for Lz at %g%%" % (dlz * 100.0) ) assert djz < 10.0**-10.0, ( "actionAngleSpherical applied to isochrone potential fails for Jz at %g%%" % (djz * 100.0) ) return None # Test the actionAngleSpherical against an isochrone potential: frequencies def test_actionAngleSpherical_otherIsochrone_freqs(): from galpy.actionAngle import actionAngleIsochrone, actionAngleSpherical from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAS = actionAngleSpherical(pot=ip) R, vR, vT, z, vz, phi = 1.1, 0.3, 1.2, 0.2, 0.5, 2.0 jiO = aAI.actionsFreqs(R, vR, vT, z, vz, phi) jiaO = aAS.actionsFreqs(R, vR, vT, z, vz, phi) dOr = numpy.fabs((jiO[3] - jiaO[3]) / jiO[3]) dOp = numpy.fabs((jiO[4] - jiaO[4]) / jiO[4]) dOz = numpy.fabs((jiO[5] - jiaO[5]) / jiO[5]) assert dOr < 10.0**-6.0, ( "actionAngleSpherical applied to isochrone potential fails for Or at %g%%" % (dOr * 100.0) ) assert dOp < 10.0**-6.0, ( "actionAngleSpherical applied to isochrone potential fails for Op at %g%%" % (dOp * 100.0) ) assert dOz < 10.0**-6.0, ( "actionAngleSpherical applied to isochrone potential fails for Oz at %g%%" % (dOz * 100.0) ) return None # Test the actionAngleSpherical against an isochrone potential: frequencies def test_actionAngleSpherical_otherIsochrone_freqs_fixed_quad(): from galpy.actionAngle import actionAngleIsochrone, actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAS = actionAngleSpherical(pot=ip) R, vR, vT, z, vz, phi = 1.1, 0.3, 1.2, 0.2, 0.5, 2.0 jiO = aAI.actionsFreqs(R, vR, vT, z, vz, phi) jiaO = aAS.actionsFreqs(Orbit([R, vR, vT, z, vz, phi]), fixed_quad=True) dOr = numpy.fabs((jiO[3] - jiaO[3]) / jiO[3]) dOp = numpy.fabs((jiO[4] - jiaO[4]) / jiO[4]) dOz = numpy.fabs((jiO[5] - jiaO[5]) / jiO[5]) assert dOr < 10.0**-6.0, ( "actionAngleSpherical applied to isochrone potential fails for Or at %g%%" % (dOr * 100.0) ) assert dOp < 10.0**-6.0, ( "actionAngleSpherical applied to isochrone potential fails for Op at %g%%" % (dOp * 100.0) ) assert dOz < 10.0**-6.0, ( "actionAngleSpherical applied to isochrone potential fails for Oz at %g%%" % (dOz * 100.0) ) return None # Test the actionAngleSpherical against an isochrone potential: angles def test_actionAngleSpherical_otherIsochrone_angles(): from galpy.actionAngle import actionAngleIsochrone, actionAngleSpherical from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAS = actionAngleSpherical(pot=ip, b=0.8) R, vR, vT, z, vz, phi = 1.1, 0.3, 1.2, 0.2, 0.5, 2.0 jiO = aAI.actionsFreqsAngles(R, vR, vT, z, vz, phi) jiaO = aAS.actionsFreqsAngles(R, vR, vT, z, vz, phi) dar = numpy.fabs((jiO[6] - jiaO[6]) / jiO[6]) dap = numpy.fabs((jiO[7] - jiaO[7]) / jiO[7]) daz = numpy.fabs((jiO[8] - jiaO[8]) / jiO[8]) assert dar < 10.0**-6.0, ( "actionAngleSpherical applied to isochrone potential fails for ar at %g%%" % (dar * 100.0) ) assert dap < 10.0**-6.0, ( "actionAngleSpherical applied to isochrone potential fails for ap at %g%%" % (dap * 100.0) ) assert daz < 10.0**-6.0, ( "actionAngleSpherical applied to isochrone potential fails for az at %g%%" % (daz * 100.0) ) return None # Test that actionAngleSpherical works at small r def test_actionAngleSpherical_smallr(): from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential() # Orbit at rperi, very small r o = Orbit([0.000000001, 0.0, ip.vcirc(0.000000001), 0.0, 0.0, 0.0]) # Code should have rperi = 0 assert ( numpy.fabs(o.rperi(analytic=True, pot=ip, type="spherical") - 0.0) < 10.0**-10.0 ), "rperi is not 0 for very small r" # Orbit just outside rperi, very small r o = Orbit([0.000000001, 0.0001, ip.vcirc(0.000000001), 0.0, 0.0, 0.0]) assert ( numpy.fabs(o.rperi(analytic=True, pot=ip, type="spherical") - 0.0) < 10.0**-10.0 ), "rperi is not 0 for very small r" return None # Test that actionAngleSpherical's angler works when at pericenter def test_actionAngleSpherical_angler_at_pericenter(): from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential() o = Orbit([1.0, 0.0, ip.vcirc(1.0) * 2.1, 0.0, 0.0, 0.0]) # Radial angle wr should be zero assert numpy.fabs(o.wr(analytic=True, pot=ip, type="spherical")) < 10.0**-10.0, ( "angler is not 0 at pericenter" ) return None # Basic sanity checking of the actionAngleAdiabatic actions def test_actionAngleAdiabatic_basic_actions(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential aAA = actionAngleAdiabatic(pot=MWPotential, gamma=1.0) # circular orbit R, vR, vT, phi = 1.0, 0.0, 1.0, 2.0 js = aAA(Orbit([R, vR, vT, phi])) assert numpy.fabs(js[0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jr=0" ) assert numpy.fabs(js[2]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jz=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAA(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-3.0, ( "Close-to-circular orbit in the MWPotentialspherical LogarithmicHalo does not have small Jz" ) # Another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.0, 0.99, 0.0, 0.0 js = aAA(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-3.0, ( "Close-to-circular orbit in the MWPotentialspherical LogarithmicHalo does not have small Jz" ) # Another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.01, 0.0, 0.0 js = aAA(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-3.0, ( "Close-to-circular orbit in the MWPotentialspherical LogarithmicHalo does not have small Jz" ) return None # Basic sanity checking of the actionAngleAdiabatic actions def test_actionAngleAdiabatic_basic_actions_gamma0(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential aAA = actionAngleAdiabatic(pot=[MWPotential[0], MWPotential[1:]], gamma=0.0) # circular orbit R, vR, vT, phi = 1.0, 0.0, 1.0, 2.0 js = aAA(Orbit([R, vR, vT, phi])) assert numpy.fabs(js[0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jr=0" ) assert numpy.fabs(js[2]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jz=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAA(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-3.0, ( "Close-to-circular orbit in the MWPotentialspherical LogarithmicHalo does not have small Jz" ) # Another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.0, 0.99, 0.0, 0.0 js = aAA(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-3.0, ( "Close-to-circular orbit in the MWPotentialspherical LogarithmicHalo does not have small Jz" ) # Another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.01, 0.0, 0.0 js = aAA(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-3.0, ( "Close-to-circular orbit in the MWPotentialspherical LogarithmicHalo does not have small Jz" ) return None # Basic sanity checking of the actionAngleAdiabatic actions def test_actionAngleAdiabatic_basic_actions_c(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential # test nested list of potentials aAA = actionAngleAdiabatic(pot=[MWPotential[0], MWPotential[1:]], c=True) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 js = aAA(R, vR, vT, z, vz) assert numpy.fabs(js[0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jr=0" ) assert numpy.fabs(js[2]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jz=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAA(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-3.0, ( "Close-to-circular orbit in the MWPotentialspherical LogarithmicHalo does not have small Jz" ) # Basic sanity checking of the actionAngleAdiabatic actions def test_actionAngleAdiabatic_unboundz_actions_c(): from galpy.actionAngle import actionAngleAdiabatic from galpy.potential import MWPotential aAA = actionAngleAdiabatic(pot=MWPotential, c=True, gamma=0.0) # Unbound in z, so jz should be very large R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 10.0 js = aAA(R, vR, vT, z, vz) assert js[2] > 1000.0, ( "Unbound orbit in z in the MWPotential does not have large Jz" ) return None # Basic sanity checking of the actionAngleAdiabatic actions def test_actionAngleAdiabatic_zerolz_actions_c(): from galpy.actionAngle import actionAngleAdiabatic from galpy.potential import MWPotential aAA = actionAngleAdiabatic(pot=MWPotential, c=True, gamma=0.0) # Zero angular momentum, so rperi=0, but should have finite jr R, vR, vT, z, vz = 1.0, 0.0, 0.0, 0.0, 0.0 js = aAA(R, vR, vT, z, vz) R, vR, vT, z, vz = 1.0, 0.0, 0.0000001, 0.0, 0.0 js2 = aAA(R, vR, vT, z, vz) assert numpy.fabs(js[0] - js2[0]) < 10.0**-6.0, ( "Orbit with zero angular momentum does not have the correct Jr" ) # Zero angular momentum, so rperi=0, but should have finite jr R, vR, vT, z, vz = 1.0, -0.5, 0.0, 0.0, 0.0 js = aAA(R, vR, vT, z, vz) R, vR, vT, z, vz = 1.0, -0.5, 0.0000001, 0.0, 0.0 js2 = aAA(R, vR, vT, z, vz) assert numpy.fabs(js[0] - js2[0]) < 10.0**-6.0, ( "Orbit with zero angular momentum does not have the correct Jr" ) return None # Basic sanity checking of the actionAngleAdiabatic ecc, zmax, rperi, rap calc. def test_actionAngleAdiabatic_basic_EccZmaxRperiRap(): from galpy.actionAngle import actionAngleAdiabatic from galpy.potential import MWPotential aAA = actionAngleAdiabatic(pot=MWPotential, gamma=1.0) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 te, tzmax, _, _ = aAA.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have e=0" ) assert numpy.fabs(tzmax) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have zmax=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 te, tzmax, _, _ = aAA.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) # Another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.0, 0.99, 0.0, 0.0 te, tzmax, _, _ = aAA.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) # Another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.01, 0.0 te, tzmax, _, _ = aAA.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) return None # Basic sanity checking of the actionAngleAdiabatic ecc, zmax, rperi, rap calc. def test_actionAngleAdiabatic_basic_EccZmaxRperiRap_gamma0(): from galpy.actionAngle import actionAngleAdiabatic from galpy.potential import MiyamotoNagaiPotential mp = MiyamotoNagaiPotential(normalize=1.0, a=1.5, b=0.3) aAA = actionAngleAdiabatic(pot=mp, gamma=0.0, c=False) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 te, tzmax, _, _ = aAA.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have e=0" ) assert numpy.fabs(tzmax) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have zmax=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 te, tzmax, _, _ = aAA.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) return None # Basic sanity checking of the actionAngleAdiabatic ecc, zmax, rperi, rap calc. def test_actionAngleAdiabatic_basic_EccZmaxRperiRap_gamma_c(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential aAA = actionAngleAdiabatic(pot=MWPotential, gamma=1.0, c=True) # circular orbit R, vR, vT, z, vz, phi = 1.0, 0.0, 1.0, 0.0, 0.0, 2.0 te, tzmax, _, _ = aAA.EccZmaxRperiRap(Orbit([R, vR, vT, z, vz, phi])) assert numpy.fabs(te) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have e=0" ) assert numpy.fabs(tzmax) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have zmax=0" ) # Close-to-circular orbit R, vR, vT, z, vz, phi = 1.01, 0.01, 1.0, 0.01, 0.01, 2.0 te, tzmax, _, _ = aAA.EccZmaxRperiRap(R, vR, vT, z, vz, phi) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) return None # Test the actions of an actionAngleAdiabatic def test_actionAngleAdiabatic_conserved_actions(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential aAA = actionAngleAdiabatic(pot=MWPotential, c=False) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0]) check_actionAngle_conserved_actions( aAA, obs, MWPotential, -1.2, -8.0, -1.7, ntimes=101 ) return None # Test the actions of an actionAngleAdiabatic def test_actionAngleAdiabatic_conserved_actions_c(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0]) aAA = actionAngleAdiabatic(pot=MWPotential, c=True) check_actionAngle_conserved_actions( aAA, obs, MWPotential, -1.4, -8.0, -1.7, ntimes=101 ) return None # Test the actions of an actionAngleAdiabatic, single pot def test_actionAngleAdiabatic_conserved_actions_singlepot(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MiyamotoNagaiPotential mp = MiyamotoNagaiPotential(normalize=1.0) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) aAA = actionAngleAdiabatic(pot=mp, c=False) check_actionAngle_conserved_actions( aAA, obs, mp, -1.5, -8.0, -2.0, ntimes=101, inclphi=True ) return None # Test the actions of an actionAngleAdiabatic, single pot, C def test_actionAngleAdiabatic_conserved_actions_singlepot_c(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MiyamotoNagaiPotential mp = MiyamotoNagaiPotential(normalize=1.0) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) aAA = actionAngleAdiabatic(pot=mp, c=True) check_actionAngle_conserved_actions( aAA, obs, mp, -1.5, -8.0, -2.0, ntimes=101, inclphi=True ) return None # Test the actions of an actionAngleAdiabatic, interpolated pot def test_actionAngleAdiabatic_conserved_actions_interppot_c(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential, interpRZPotential ip = interpRZPotential( RZPot=MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 101), zgrid=(0.0, 1.0, 101), logR=True, use_c=True, enable_c=True, interpPot=True, interpRforce=True, interpzforce=True, ) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) aAA = actionAngleAdiabatic(pot=ip, c=True) check_actionAngle_conserved_actions(aAA, obs, ip, -1.4, -8.0, -1.7, ntimes=101) return None # Test the conservation of ecc, zmax, rperi, rap of an actionAngleAdiabatic def test_actionAngleAdiabatic_conserved_EccZmaxRperiRap(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential aAA = actionAngleAdiabatic(pot=MWPotential, c=False, gamma=1.0) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 0.0]) check_actionAngle_conserved_EccZmaxRperiRap( aAA, obs, MWPotential, -1.7, -1.4, -2.0, -2.0, ntimes=101 ) return None # Test the conservation of ecc, zmax, rperi, rap of an actionAngleAdiabatic def test_actionAngleAdiabatic_conserved_EccZmaxRperiRap_ecc(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential aAA = actionAngleAdiabatic(pot=MWPotential, c=False, gamma=1.0) obs = Orbit([1.1, 0.2, 1.3, 0.1, 0.0, 2.0]) check_actionAngle_conserved_EccZmaxRperiRap( aAA, obs, MWPotential, -1.1, -0.4, -1.8, -1.8, ntimes=101, inclphi=True ) return None # Test the conservation of ecc, zmax, rperi, rap of an actionAngleAdiabatic def test_actionAngleAdiabatic_conserved_EccZmaxRperiRap_singlepot_c(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MiyamotoNagaiPotential mp = MiyamotoNagaiPotential(normalize=1.0) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) aAA = actionAngleAdiabatic(pot=mp, c=True) check_actionAngle_conserved_EccZmaxRperiRap( aAA, obs, mp, -1.7, -1.4, -2.0, -2.0, ntimes=101 ) return None # Test the conservation of ecc, zmax, rperi, rap of an actionAngleAdiabatic def test_actionAngleAdiabatic_conserved_EccZmaxRperiRa_interppot_c(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential, interpRZPotential ip = interpRZPotential( RZPot=MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 101), zgrid=(0.0, 1.0, 101), logR=True, use_c=True, enable_c=True, interpPot=True, interpRforce=True, interpzforce=True, ) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) aAA = actionAngleAdiabatic(pot=ip, c=True) check_actionAngle_conserved_EccZmaxRperiRap( aAA, obs, ip, -1.7, -1.4, -2.0, -2.0, ntimes=101 ) return None # Test the actionAngleAdiabatic against an isochrone potential: actions def test_actionAngleAdiabatic_Isochrone_actions(): from galpy.actionAngle import actionAngleAdiabatic, actionAngleIsochrone from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAA = actionAngleAdiabatic(pot=ip, c=True) R, vR, vT, z, vz, phi = 1.01, 0.05, 1.05, 0.05, 0.0, 2.0 ji = aAI(R, vR, vT, z, vz, phi) jia = aAA(R, vR, vT, z, vz, phi) djr = numpy.fabs((ji[0] - jia[0]) / ji[0]) dlz = numpy.fabs((ji[1] - jia[1]) / ji[1]) djz = numpy.fabs((ji[2] - jia[2]) / ji[2]) assert djr < 10.0**-1.2, ( "actionAngleAdiabatic applied to isochrone potential fails for Jr at %f%%" % (djr * 100.0) ) # Lz and Jz are easy, because ip is a spherical potential assert dlz < 10.0**-10.0, ( "actionAngleAdiabatic applied to isochrone potential fails for Lz at %f%%" % (dlz * 100.0) ) assert djz < 10.0**-1.2, ( "actionAngleAdiabatic applied to isochrone potential fails for Jz at %f%%" % (djz * 100.0) ) return None # Basic sanity checking of the actionAngleAdiabatic actions (incl. conserved, bc takes a lot of time) def test_actionAngleAdiabaticGrid_basicAndConserved_actions(): from galpy.actionAngle import actionAngleAdiabaticGrid from galpy.orbit import Orbit from galpy.potential import MWPotential aAA = actionAngleAdiabaticGrid(pot=MWPotential, gamma=1.0, c=False) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 js = aAA(R, vR, vT, z, vz, 0.0) assert numpy.fabs(js[0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jr=0" ) assert numpy.fabs(aAA.Jz(R, vR, vT, z, vz, 0.0)) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jz=0" ) # setup w/ multi aAA = actionAngleAdiabaticGrid(pot=MWPotential, gamma=1.0, c=False, numcores=2) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAA(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-3.0, ( "Close-to-circular orbit in the MWPotentialspherical LogarithmicHalo does not have small Jz" ) # Check that actions are conserved along the orbit obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0]) check_actionAngle_conserved_actions( aAA, obs, MWPotential, -1.2, -8.0, -1.7, ntimes=101 ) return None # Basic sanity checking of the actionAngleAdiabatic actions def test_actionAngleAdiabaticGrid_basic_actions_c(): from galpy.actionAngle import actionAngleAdiabaticGrid from galpy.orbit import Orbit from galpy.potential import MWPotential aAA = actionAngleAdiabaticGrid(pot=MWPotential, c=True) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 js = aAA(R, vR, vT, z, vz) assert numpy.fabs(js[0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jr=0" ) assert numpy.fabs(js[2]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jz=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAA(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-3.0, ( "Close-to-circular orbit in the MWPotentialspherical LogarithmicHalo does not have small Jz" ) # actionAngleAdiabaticGrid actions outside the grid def test_actionAngleAdiabaticGrid_outsidegrid_c(): from galpy.actionAngle import actionAngleAdiabatic, actionAngleAdiabaticGrid from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) aAA = actionAngleAdiabaticGrid(pot=MWPotential, c=True, Rmax=2.0, zmax=0.2) R, vR, vT, z, vz, phi = 3.0, 0.1, 1.0, 0.1, 0.1, 2.0 js = aA(R, vR, vT, z, vz, phi) jsa = aAA(R, vR, vT, z, vz, phi) assert numpy.fabs(js[0] - jsa[0]) < 10.0**-8.0, ( "actionAngleAdiabaticGrid evaluation outside of the grid fails" ) assert numpy.fabs(js[2] - jsa[2]) < 10.0**-8.0, ( "actionAngleAdiabaticGrid evaluation outside of the grid fails" ) assert numpy.fabs(js[2] - aAA.Jz(R, vR, vT, z, vz, phi)) < 10.0**-8.0, ( "actionAngleAdiabaticGrid evaluation outside of the grid fails" ) # Also for array s = numpy.ones(2) js = aA(R, vR, vT, z, vz, phi) jsa = aAA(R * s, vR * s, vT * s, z * s, vz * s, phi * s) assert numpy.all(numpy.fabs(js[0] - jsa[0]) < 10.0**-8.0), ( "actionAngleAdiabaticGrid evaluation outside of the grid fails" ) assert numpy.all(numpy.fabs(js[2] - jsa[2]) < 10.0**-8.0), ( "actionAngleAdiabaticGrid evaluation outside of the grid fails" ) return None # Test the actions of an actionAngleAdiabatic def test_actionAngleAdiabaticGrid_conserved_actions_c(): from galpy.actionAngle import actionAngleAdiabaticGrid from galpy.orbit import Orbit from galpy.potential import MWPotential obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0]) aAA = actionAngleAdiabaticGrid(pot=MWPotential, c=True) check_actionAngle_conserved_actions( aAA, obs, MWPotential, -1.4, -8.0, -1.7, ntimes=101 ) return None # Test the actionAngleAdiabatic against an isochrone potential: actions def test_actionAngleAdiabaticGrid_Isochrone_actions(): from galpy.actionAngle import actionAngleAdiabaticGrid, actionAngleIsochrone from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAA = actionAngleAdiabaticGrid(pot=ip, c=True) R, vR, vT, z, vz, phi = 1.01, 0.05, 1.05, 0.05, 0.0, 2.0 ji = aAI(R, vR, vT, z, vz, phi) jia = aAA(R, vR, vT, z, vz, phi) djr = numpy.fabs((ji[0] - jia[0]) / ji[0]) dlz = numpy.fabs((ji[1] - jia[1]) / ji[1]) djz = numpy.fabs((ji[2] - jia[2]) / ji[2]) assert djr < 10.0**-1.2, ( "actionAngleAdiabatic applied to isochrone potential fails for Jr at %f%%" % (djr * 100.0) ) # Lz and Jz are easy, because ip is a spherical potential assert dlz < 10.0**-10.0, ( "actionAngleAdiabatic applied to isochrone potential fails for Lz at %f%%" % (dlz * 100.0) ) assert djz < 10.0**-1.2, ( "actionAngleAdiabatic applied to isochrone potential fails for Jz at %f%%" % (djz * 100.0) ) return None # Basic sanity checking of the actionAngleStaeckel actions def test_actionAngleStaeckel_basic_actions(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=False) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 js = aAS(R, vR, vT, z, vz) assert numpy.fabs(js[0][0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jr=0" ) assert numpy.fabs(js[2][0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jz=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAS(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 2.0 * 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jz" ) # Another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.0, 0.99, 0.0, 0.0 js = aAS(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 2.0 * 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jz" ) # Another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.01, 0.0 js = aAS(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 2.0 * 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jz" ) return None # Basic sanity checking of the actionAngleStaeckel actions def test_actionAngleStaeckel_basic_actions_u0(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential # test nested list of potentials aAS = actionAngleStaeckel( pot=[MWPotential[0], MWPotential[1:]], delta=0.71, c=False, useu0=True ) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 js = aAS(R, vR, vT, z, vz) assert numpy.fabs(js[0][0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jr=0" ) assert numpy.fabs(js[2][0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jz=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAS(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 2.0 * 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jz" ) return None # Basic sanity checking of the actionAngleStaeckel actions def test_actionAngleStaeckel_basic_actions_u0_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential # test nested list of potentials aAS = actionAngleStaeckel( pot=[MWPotential[0], MWPotential[1:]], delta=0.71, c=True, useu0=True ) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 js = aAS(R, vR, vT, z, vz) assert numpy.fabs(js[0][0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jr=0" ) assert numpy.fabs(js[2][0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jz=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAS(Orbit([R, vR, vT, z, vz]), u0=1.15) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 2.0 * 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jz" ) return None # Basic sanity checking of the actionAngleStaeckel actions, w/ u0, and interppot def test_actionAngleStaeckel_basic_actions_u0_interppot_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential, interpRZPotential ip = interpRZPotential( RZPot=MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 101), zgrid=(0.0, 1.0, 101), logR=True, use_c=True, enable_c=True, interpPot=True, ) aAS = actionAngleStaeckel(pot=ip, delta=0.71, c=True, useu0=True) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 js = aAS(R, vR, vT, z, vz) assert numpy.fabs(js[0][0]) < 10.0**-12.0, ( "Circular orbit in the MWPotential does not have Jr=0" ) assert numpy.fabs(js[2][0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jz=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAS(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 2.0 * 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jz" ) return None # Basic sanity checking of the actionAngleStaeckel actions def test_actionAngleStaeckel_basic_actions_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=True) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 js = aAS(R, vR, vT, z, vz) assert numpy.fabs(js[0]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jr=0" ) assert numpy.fabs(js[2]) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jz=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAS(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 2.0 * 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jz" ) return None # Basic sanity checking of the actionAngleStaeckel actions, unbound def test_actionAngleStaeckel_unboundr_actions_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=True) # Unbound orbit, shouldn't fail R, vR, vT, z, vz = 1.0, 0.0, 10.0, 0.1, 0.0 js = aAS(R, vR, vT, z, vz) assert js[0] > 1000.0, ( "Unbound in R orbit in the MWPotential does not have large Jr" ) # Another unbound orbit, shouldn't fail R, vR, vT, z, vz = 1.0, 0.1, 10.0, 0.1, 0.0 js = aAS(R, vR, vT, z, vz) assert js[0] > 1000.0, ( "Unbound in R orbit in the MWPotential does not have large Jr" ) return None # Basic sanity checking of the actionAngleStaeckel actions def test_actionAngleStaeckel_zerolz_actions_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, c=True, delta=0.71) # Zero angular momentum, so rperi=0, but should have finite jr R, vR, vT, z, vz = 1.0, 0.0, 0.0, 0.0, 0.0 js = aAS(R, vR, vT, z, vz) R, vR, vT, z, vz = 1.0, 0.0, 0.0000001, 0.0, 0.0 js2 = aAS(R, vR, vT, z, vz) assert numpy.fabs(js[0] - js2[0]) < 10.0**-6.0, ( "Orbit with zero angular momentum does not have the correct Jr" ) # Zero angular momentum, so rperi=0, but should have finite jr R, vR, vT, z, vz = 1.0, -0.5, 0.0, 0.0, 0.0 js = aAS(R, vR, vT, z, vz) R, vR, vT, z, vz = 1.0, -0.5, 0.0000001, 0.0, 0.0 js2 = aAS(R, vR, vT, z, vz) assert numpy.fabs(js[0] - js2[0]) < 10.0**-6.0, ( "Orbit with zero angular momentum does not have the correct Jr" ) return None # Check that precision increases with increasing Gauss-Legendre order def test_actionAngleStaeckel_actions_order(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import KuzminKutuzovStaeckelPotential kksp = KuzminKutuzovStaeckelPotential(normalize=1.0, ac=4.0, Delta=1.4) o = Orbit([1.0, 0.5, 1.1, 0.2, -0.3, 0.4]) aAS = actionAngleStaeckel(pot=kksp, delta=kksp._Delta, c=False) # We'll assume that order=10000 is the truth, so 50 should be better than 5 jrt, jpt, jzt = aAS(o, order=10000, fixed_quad=True) jr1, jp1, jz1 = aAS(o, order=5, fixed_quad=True) jr2, jp2, jz2 = aAS(o, order=50, fixed_quad=True) assert numpy.fabs(jr1 - jrt) > numpy.fabs(jr2 - jrt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(jz1 - jzt) > numpy.fabs(jz2 - jzt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) return None def test_actionAngleStaeckel_actions_order_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import KuzminKutuzovStaeckelPotential kksp = KuzminKutuzovStaeckelPotential(normalize=1.0, ac=4.0, Delta=1.4) o = Orbit([1.0, 0.5, 1.1, 0.2, -0.3, 0.4]) aAS = actionAngleStaeckel(pot=kksp, delta=kksp._Delta, c=True) # We'll assume that order=10000 is the truth, so 50 should be better than 5 jrt, jpt, jzt = aAS(o, order=10000) jr1, jp1, jz1 = aAS(o, order=5) jr2, jp2, jz2 = aAS(o, order=50) assert numpy.fabs(jr1 - jrt) > numpy.fabs(jr2 - jrt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(jz1 - jzt) > numpy.fabs(jz2 - jzt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) return None # Basic sanity checking of the actionAngleStaeckel frequencies def test_actionAngleStaeckel_basic_freqs_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential, epifreq, omegac, verticalfreq aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=True) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 jos = aAS.actionsFreqs(R, vR, vT, z, vz) assert ( numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) < 10.0**-12.0 ), "Circular orbit in the MWPotential does not have Or=kappa at %g%%" % ( 100.0 * numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) ) assert ( numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) < 10.0**-12.0 ), "Circular orbit in the MWPotential does not have Op=Omega at %g%%" % ( 100.0 * numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) ) assert ( numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) < 10.0**-12.0 ), "Circular orbit in the MWPotential does not have Oz=nu at %g%%" % ( 100.0 * numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) ) # close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.01, 1.01, 0.01, 0.01 jos = aAS.actionsFreqs(Orbit([R, vR, vT, z, vz])) assert ( numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) < 10.0**-1.9 ), "Close-to-circular orbit in the MWPotential does not have Or=kappa at %g%%" % ( 100.0 * numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) ) assert ( numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) < 10.0**-1.9 ), "Close-to-circular orbit in the MWPotential does not have Op=Omega at %g%%" % ( 100.0 * numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) ) assert ( numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) < 10.0**-1.5 ), "Close-to-circular orbit in the MWPotential does not have Oz=nu at %g%%" % ( 100.0 * numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) ) # another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.03, 1.02, 0.03, 0.01 jos = aAS.actionsFreqs(Orbit([R, vR, vT, z, vz, 2.0])) assert ( numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) < 10.0**-1.5 ), "Close-to-circular orbit in the MWPotential does not have Or=kappa at %g%%" % ( 100.0 * numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) ) assert ( numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) < 10.0**-1.5 ), "Close-to-circular orbit in the MWPotential does not have Op=Omega at %g%%" % ( 100.0 * numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) ) assert ( numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) < 10.0**-0.9 ), "Close-to-circular orbit in the MWPotential does not have Oz=nu at %g%%" % ( 100.0 * numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) ) return None # Basic sanity checking of the actionAngleStaeckel actions def test_actionAngleStaeckel_basic_freqsAngles(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential, epifreq, omegac, verticalfreq aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=True) # v. close-to-circular orbit R, vR, vT, z, vz = 1.0, 10.0**-4.0, 1.0, 10.0**-4.0, 0.0 jos = aAS.actionsFreqs(Orbit([R, vR, vT, z, vz, 2.0])) assert ( numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) < 10.0**-1.9 ), "Close-to-circular orbit in the MWPotential does not have Or=kappa at %g%%" % ( 100.0 * numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) ) assert ( numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) < 10.0**-1.9 ), "Close-to-circular orbit in the MWPotential does not have Op=Omega at %g%%" % ( 100.0 * numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) ) assert ( numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) < 10.0**-1.9 ), "Close-to-circular orbit in the MWPotential does not have Oz=nu at %g%%" % ( 100.0 * numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) ) return None # Basic sanity checking of the actionAngleStaeckel frequencies def test_actionAngleStaeckel_basic_freqs_c_u0(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential, epifreq, omegac, verticalfreq aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=True, useu0=True) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 jos = aAS.actionsFreqs(R, vR, vT, z, vz) assert ( numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) < 10.0**-12.0 ), "Circular orbit in the MWPotential does not have Or=kappa at %g%%" % ( 100.0 * numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) ) assert ( numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) < 10.0**-12.0 ), "Circular orbit in the MWPotential does not have Op=Omega at %g%%" % ( 100.0 * numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) ) assert ( numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) < 10.0**-12.0 ), "Circular orbit in the MWPotential does not have Oz=nu at %g%%" % ( 100.0 * numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) ) # close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.01, 1.01, 0.01, 0.01 jos = aAS.actionsFreqs(Orbit([R, vR, vT, z, vz]), u0=1.15) assert ( numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) < 10.0**-1.9 ), "Close-to-circular orbit in the MWPotential does not have Or=kappa at %g%%" % ( 100.0 * numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) ) assert ( numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) < 10.0**-1.9 ), "Close-to-circular orbit in the MWPotential does not have Op=Omega at %g%%" % ( 100.0 * numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) ) assert ( numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) < 10.0**-1.5 ), "Close-to-circular orbit in the MWPotential does not have Oz=nu at %g%%" % ( 100.0 * numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) ) return None # Basic sanity checking of the actionAngleStaeckel actions def test_actionAngleStaeckel_basic_freqs_u0(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import ( MWPotential, epifreq, interpRZPotential, omegac, verticalfreq, ) ip = interpRZPotential( RZPot=MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 101), zgrid=(0.0, 1.0, 101), logR=True, use_c=True, enable_c=True, interpPot=True, ) aAS = actionAngleStaeckel(pot=ip, delta=0.71, c=True, useu0=True) # v. close-to-circular orbit R, vR, vT, z, vz = 1.0, 10.0**-4.0, 1.0, 10.0**-4.0, 0.0 jos = aAS.actionsFreqs(Orbit([R, vR, vT, z, vz, 2.0])) assert ( numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) < 10.0**-1.9 ), "Close-to-circular orbit in the MWPotential does not have Or=kappa at %g%%" % ( 100.0 * numpy.fabs((jos[3] - epifreq(MWPotential, 1.0)) / epifreq(MWPotential, 1.0)) ) assert ( numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) < 10.0**-1.9 ), "Close-to-circular orbit in the MWPotential does not have Op=Omega at %g%%" % ( 100.0 * numpy.fabs((jos[4] - omegac(MWPotential, 1.0)) / omegac(MWPotential, 1.0)) ) assert ( numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) < 10.0**-1.9 ), "Close-to-circular orbit in the MWPotential does not have Oz=nu at %g%%" % ( 100.0 * numpy.fabs( (jos[5] - verticalfreq(MWPotential, 1.0)) / verticalfreq(MWPotential, 1.0) ) ) return None # Basic sanity checking of the actionAngleStaeckel actions, unbound def test_actionAngleStaeckel_unboundr_freqs_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=True) # Unbound orbit, shouldn't fail R, vR, vT, z, vz = 1.0, 0.1, 10.0, 0.1, 0.0 js = aAS.actionsFreqs(R, vR, vT, z, vz) assert js[0] > 1000.0, ( "Unbound in R orbit in the MWPotential does not have large Jr" ) assert js[3] > 1000.0, ( "Unbound in R orbit in the MWPotential does not have large Or" ) assert js[4] > 1000.0, ( "Unbound in R orbit in the MWPotential does not have large Op" ) assert js[5] > 1000.0, ( "Unbound in R orbit in the MWPotential does not have large Oz" ) return None # Check that precision increases with increasing Gauss-Legendre order def test_actionAngleStaeckel_freqs_order_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import KuzminKutuzovStaeckelPotential kksp = KuzminKutuzovStaeckelPotential(normalize=1.0, ac=4.0, Delta=1.4) o = Orbit([1.0, 0.5, 1.1, 0.2, -0.3, 0.4]) aAS = actionAngleStaeckel(pot=kksp, delta=kksp._Delta, c=True) # We'll assume that order=10000 is the truth, so 50 should be better than 5 jrt, jpt, jzt, ort, opt, ozt = aAS.actionsFreqs(o, order=10000) jr1, jp1, jz1, or1, op1, oz1 = aAS.actionsFreqs(o, order=5) jr2, jp2, jz2, or2, op2, oz2 = aAS.actionsFreqs(o, order=50) assert numpy.fabs(jr1 - jrt) > numpy.fabs(jr2 - jrt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(jz1 - jzt) > numpy.fabs(jz2 - jzt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(or1 - ort) > numpy.fabs(or2 - ort), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(op1 - opt) > numpy.fabs(op2 - opt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(oz1 - ozt) > numpy.fabs(oz2 - ozt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) return None # Basic sanity checking of the actionAngleStaeckel actions, unbound def test_actionAngleStaeckel_unboundr_angles_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=True) # Unbound orbit, shouldn't fail R, vR, vT, z, vz, phi = 1.0, 0.1, 10.0, 0.1, 0.0, 0.0 js = aAS.actionsFreqsAngles(R, vR, vT, z, vz, phi) assert js[0] > 1000.0, ( "Unbound in R orbit in the MWPotential does not have large Jr" ) assert js[6] > 1000.0, ( "Unbound in R orbit in the MWPotential does not have large ar" ) assert js[7] > 1000.0, ( "Unbound in R orbit in the MWPotential does not have large ap" ) assert js[8] > 1000.0, ( "Unbound in R orbit in the MWPotential does not have large az" ) return None # Basic sanity checking of the actionAngleStaeckel actions, unbound def test_actionAngleStaeckel_circular_angles_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=True) # Circular orbits, have zero r and z angles in our implementation R, vR, vT, z, vz, phi = 1.0, 0.0, 1.0, 0.0, 0.0, 1.0 js = aAS.actionsFreqsAngles(R, vR, vT, z, vz, phi) assert numpy.fabs(js[6]) < 10.0**-8.0, "Circular orbit does not have zero angles" assert numpy.fabs(js[8]) < 10.0**-8.0, "Circular orbit does not have zero angles" return None # Check that precision increases with increasing Gauss-Legendre order def test_actionAngleStaeckel_angles_order_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import KuzminKutuzovStaeckelPotential kksp = KuzminKutuzovStaeckelPotential(normalize=1.0, ac=4.0, Delta=1.4) o = Orbit([1.0, 0.5, 1.1, 0.2, -0.3, 0.4]) aAS = actionAngleStaeckel(pot=kksp, delta=kksp._Delta, c=True) # We'll assume that order=10000 is the truth, so 50 should be better than 5 jrt, jpt, jzt, ort, opt, ozt, art, apt, azt = aAS.actionsFreqsAngles(o, order=10000) jr1, jp1, jz1, or1, op1, oz1, ar1, ap1, az1 = aAS.actionsFreqsAngles(o, order=5) jr2, jp2, jz2, or2, op2, oz2, ar2, ap2, az2 = aAS.actionsFreqsAngles(o, order=50) assert numpy.fabs(jr1 - jrt) > numpy.fabs(jr2 - jrt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(jz1 - jzt) > numpy.fabs(jz2 - jzt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(or1 - ort) > numpy.fabs(or2 - ort), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(op1 - opt) > numpy.fabs(op2 - opt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(oz1 - ozt) > numpy.fabs(oz2 - ozt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(ar1 - art) > numpy.fabs(ar2 - art), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(ap1 - apt) > numpy.fabs(ap2 - apt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) assert numpy.fabs(az1 - azt) > numpy.fabs(az2 - azt), ( "Accuracy of actionAngleStaeckel does not increase with increasing order of integration" ) return None # Basic sanity checking of the actionAngleStaeckel ecc, zmax, rperi, rap calc. def test_actionAngleStaeckel_basic_EccZmaxRperiRap(): from galpy.actionAngle import actionAngleStaeckel from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=False) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 te, tzmax, _, _ = aAS.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have e=0" ) assert numpy.fabs(tzmax) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have zmax=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 te, tzmax, _, _ = aAS.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) # Another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.0, 0.99, 0.0, 0.0 te, tzmax, _, _ = aAS.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) # Another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.01, 0.0 te, tzmax, _, _ = aAS.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) return None # Basic sanity checking of the actionAngleStaeckel ecc, zmax, rperi, rap calc. def test_actionAngleStaeckel_basic_EccZmaxRperiRap_u0(): from galpy.actionAngle import actionAngleStaeckel from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=False, useu0=True) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 te, tzmax, _, _ = aAS.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have e=0" ) assert numpy.fabs(tzmax) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have zmax=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 te, tzmax, _, _ = aAS.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) return None # Basic sanity checking of the actionAngleStaeckel ecc, zmax, rperi, rap calc. def test_actionAngleStaeckel_basic_EccZmaxRperiRap_u0_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=True, useu0=True) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 te, tzmax, _, _ = aAS.EccZmaxRperiRap(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(te) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have e=0" ) assert numpy.fabs(tzmax) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have zmax=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 te, tzmax, _, _ = aAS.EccZmaxRperiRap(R, vR, vT, z, vz, u0=1.15) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) return None # Test that using different delta for different phase-space points works def test_actionAngleStaeckel_indivdelta_actions(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 # Briefly integrate orbit to get multiple points o = Orbit([1.0, 0.1, 1.1, 0.0, 0.25, 1.0]) ts = numpy.linspace(0.0, 1.0, 101) o.integrate(ts, MWPotential2014) deltas = [0.2, 0.4] # actions with one delta aAS = actionAngleStaeckel(pot=MWPotential2014, delta=deltas[0], c=False) jr0, jp0, jz0 = aAS( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]) ) # actions with another delta aAS = actionAngleStaeckel(pot=MWPotential2014, delta=deltas[1], c=False) jr1, jp1, jz1 = aAS( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]) ) # actions with individual delta jri, jpi, jzi = aAS( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]), delta=deltas ) # Check that they agree as expected assert numpy.fabs(jr0[0] - jri[0]) < 1e-10, ( "Radial action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(jr1[1] - jri[1]) < 1e-10, ( "Radial action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(jz0[0] - jzi[0]) < 1e-10, ( "Vertical action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(jz1[1] - jzi[1]) < 1e-10, ( "Vertical action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) return None # Test that no_median option for estimateDeltaStaeckel returns the same results as when # individual values are calculated separately def test_estimateDeltaStaeckel_no_median(): from galpy.actionAngle import estimateDeltaStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 # Briefly integrate orbit to get multiple points o = Orbit([1.0, 0.1, 1.1, 0.001, 0.25, 1.0]) ts = numpy.linspace(0.0, 1.0, 101) o.integrate(ts, MWPotential2014) # generate no_median deltas nomed = estimateDeltaStaeckel( MWPotential2014, o.R(ts[:10]), o.z(ts[:10]), no_median=True ) # and the individual ones indiv = numpy.array( [ estimateDeltaStaeckel(MWPotential2014, o.R(ts[i]), o.z(ts[i])) for i in range(10) ] ) # check that values agree assert (numpy.fabs(nomed - indiv) < 1e-10).all(), ( "no_median option returns different values to individual Delta estimation" ) return None # Test that the replacement of z=0 with a small value works def test_estimateDeltaStaeckel_z_is_0(): from galpy.actionAngle import estimateDeltaStaeckel from galpy.potential import MWPotential2014 # Test that z=0 works for a single value n = 11 rs = numpy.linspace(0.1, 10.0, n) for r in rs: delta0 = estimateDeltaStaeckel(MWPotential2014, r, 0.0) deltasmall = estimateDeltaStaeckel(MWPotential2014, r, 5e-4) assert numpy.fabs(delta0 - deltasmall) < 1e-3, ( "Delta computed with z=0 does not agree with that computed for small z" ) # And an array delta0 = estimateDeltaStaeckel(MWPotential2014, rs, numpy.zeros(n)) deltasmall = estimateDeltaStaeckel(MWPotential2014, rs, 5e-4 * numpy.ones(n)) assert numpy.all(numpy.fabs(delta0 - deltasmall) < 1e-3), ( "Delta computed with array of z=0 does not agree with that computed for array of small z" ) def test_actionAngleStaeckel_indivdelta_actions_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 # Briefly integrate orbit to get multiple points o = Orbit([1.0, 0.1, 1.1, 0.0, 0.25, 1.0]) ts = numpy.linspace(0.0, 1.0, 101) o.integrate(ts, MWPotential2014) deltas = [0.2, 0.4] # actions with one delta aAS = actionAngleStaeckel(pot=MWPotential2014, delta=deltas[0], c=True) jr0, jp0, jz0 = aAS( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]) ) # actions with another delta aAS = actionAngleStaeckel(pot=MWPotential2014, delta=deltas[1], c=True) jr1, jp1, jz1 = aAS( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]) ) # actions with individual delta jri, jpi, jzi = aAS( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]), delta=deltas ) # Check that they agree as expected assert numpy.fabs(jr0[0] - jri[0]) < 1e-10, ( "Radial action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(jr1[1] - jri[1]) < 1e-10, ( "Radial action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(jz0[0] - jzi[0]) < 1e-10, ( "Vertical action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(jz1[1] - jzi[1]) < 1e-10, ( "Vertical action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) return None def test_actionAngleStaeckel_indivdelta_freqs_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 # Briefly integrate orbit to get multiple points o = Orbit([1.0, 0.1, 1.1, 0.0, 0.25, 1.0]) ts = numpy.linspace(0.0, 1.0, 101) o.integrate(ts, MWPotential2014) deltas = [0.2, 0.4] # actions with one delta aAS = actionAngleStaeckel(pot=MWPotential2014, delta=deltas[0], c=True) jr0, jp0, jz0, or0, op0, oz0 = aAS.actionsFreqs( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]), o.phi(ts[:2]), ) # actions with another delta aAS = actionAngleStaeckel(pot=MWPotential2014, delta=deltas[1], c=True) jr1, jp1, jz1, or1, op1, oz1 = aAS.actionsFreqs( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]), o.phi(ts[:2]), ) # actions with individual delta jri, jpi, jzi, ori, opi, ozi = aAS.actionsFreqs( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]), o.phi(ts[:2]), delta=deltas, ) # Check that they agree as expected assert numpy.fabs(jr0[0] - jri[0]) < 1e-10, ( "Radial action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(jr1[1] - jri[1]) < 1e-10, ( "Radial action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(jz0[0] - jzi[0]) < 1e-10, ( "Vertical action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(jz1[1] - jzi[1]) < 1e-10, ( "Vertical action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(or0[0] - ori[0]) < 1e-10, ( "Radial frequencyaction computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(or1[1] - ori[1]) < 1e-10, ( "Radial frequency computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(op0[0] - opi[0]) < 1e-10, ( "Azimuthal computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(op1[1] - opi[1]) < 1e-10, ( "Azimuthal computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(oz0[0] - ozi[0]) < 1e-10, ( "Azimuthal frequency computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(oz1[1] - ozi[1]) < 1e-10, ( "Vertical frequency computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) return None def test_actionAngleStaeckel_indivdelta_angles_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 # Briefly integrate orbit to get multiple points o = Orbit([1.0, 0.1, 1.1, 0.0, 0.25, 1.0]) ts = numpy.linspace(0.0, 1.0, 101) o.integrate(ts, MWPotential2014) deltas = [0.2, 0.4] # actions with one delta aAS = actionAngleStaeckel(pot=MWPotential2014, delta=deltas[0], c=True) jr0, jp0, jz0, or0, op0, oz0, ar0, ap0, az0 = aAS.actionsFreqsAngles( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]), o.phi(ts[:2]), ) # actions with another delta aAS = actionAngleStaeckel(pot=MWPotential2014, delta=deltas[1], c=True) jr1, jp1, jz1, or1, op1, oz1, ar1, ap1, az1 = aAS.actionsFreqsAngles( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]), o.phi(ts[:2]), ) # actions with individual delta jri, jpi, jzi, ori, opi, ozi, ari, api, azi = aAS.actionsFreqsAngles( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]), o.phi(ts[:2]), delta=deltas, ) # Check that they agree as expected assert numpy.fabs(jr0[0] - jri[0]) < 1e-10, ( "Radial action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(jr1[1] - jri[1]) < 1e-10, ( "Radial action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(jz0[0] - jzi[0]) < 1e-10, ( "Vertical action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(jz1[1] - jzi[1]) < 1e-10, ( "Vertical action computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(or0[0] - ori[0]) < 1e-10, ( "Radial frequencyaction computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(or1[1] - ori[1]) < 1e-10, ( "Radial frequency computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(op0[0] - opi[0]) < 1e-10, ( "Azimuthal computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(op1[1] - opi[1]) < 1e-10, ( "Azimuthal computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(oz0[0] - ozi[0]) < 1e-10, ( "Azimuthal frequency computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(oz1[1] - ozi[1]) < 1e-10, ( "Vertical frequency computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(ar0[0] - ari[0]) < 1e-10, ( "Radial frequencyaction computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(ar1[1] - ari[1]) < 1e-10, ( "Radial frequency computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(ap0[0] - api[0]) < 1e-10, ( "Azimuthal computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(ap1[1] - api[1]) < 1e-10, ( "Azimuthal computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(az0[0] - azi[0]) < 1e-10, ( "Azimuthal frequency computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(az1[1] - azi[1]) < 1e-10, ( "Vertical frequency computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) return None def test_actionAngleStaeckel_indivdelta_EccZmaxRperiRap(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 # Briefly integrate orbit to get multiple points o = Orbit([1.0, 0.1, 1.1, 0.0, 0.25, 1.0]) ts = numpy.linspace(0.0, 1.0, 101) o.integrate(ts, MWPotential2014) deltas = [0.2, 0.4] # with one delta aAS = actionAngleStaeckel(pot=MWPotential2014, delta=deltas[0], c=False) e0, z0, rp0, ra0 = aAS.EccZmaxRperiRap( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]) ) # actions with another delta aAS = actionAngleStaeckel(pot=MWPotential2014, delta=deltas[1], c=False) e1, z1, rp1, ra1 = aAS.EccZmaxRperiRap( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]) ) # actions with individual delta ei, zi, rpi, rai = aAS.EccZmaxRperiRap( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]), delta=deltas ) # Check that they agree as expected assert numpy.fabs(e0[0] - ei[0]) < 1e-10, ( "Eccentricity computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(e1[1] - ei[1]) < 1e-10, ( "Eccentricity computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(z0[0] - zi[0]) < 1e-10, ( "Zmax computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(z1[1] - zi[1]) < 1e-10, ( "Zmax computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(rp0[0] - rpi[0]) < 1e-10, ( "Pericenter computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(rp1[1] - rpi[1]) < 1e-10, ( "Pericenter computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(ra0[0] - rai[0]) < 1e-10, ( "Apocenter computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(ra1[1] - rai[1]) < 1e-10, ( "Apocenter computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) return None def test_actionAngleStaeckel_indivdelta_EccZmaxRperiRap_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 # Briefly integrate orbit to get multiple points o = Orbit([1.0, 0.1, 1.1, 0.0, 0.25, 1.0]) ts = numpy.linspace(0.0, 1.0, 101) o.integrate(ts, MWPotential2014) deltas = [0.2, 0.4] # with one delta aAS = actionAngleStaeckel(pot=MWPotential2014, delta=deltas[0], c=True) e0, z0, rp0, ra0 = aAS.EccZmaxRperiRap( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]) ) # actions with another delta aAS = actionAngleStaeckel(pot=MWPotential2014, delta=deltas[1], c=True) e1, z1, rp1, ra1 = aAS.EccZmaxRperiRap( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]) ) # actions with individual delta ei, zi, rpi, rai = aAS.EccZmaxRperiRap( o.R(ts[:2]), o.vR(ts[:2]), o.vT(ts[:2]), o.z(ts[:2]), o.vz(ts[:2]), delta=deltas ) # Check that they agree as expected assert numpy.fabs(e0[0] - ei[0]) < 1e-10, ( "Eccentricity computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(e1[1] - ei[1]) < 1e-10, ( "Eccentricity computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(z0[0] - zi[0]) < 1e-10, ( "Zmax computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(z1[1] - zi[1]) < 1e-10, ( "Zmax computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(rp0[0] - rpi[0]) < 1e-10, ( "Pericenter computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(rp1[1] - rpi[1]) < 1e-10, ( "Pericenter computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(ra0[0] - rai[0]) < 1e-10, ( "Apocenter computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) assert numpy.fabs(ra1[1] - rai[1]) < 1e-10, ( "Apocenter computed with individual delta does not agree with that computed using the fixed orbit-wide default" ) return None # Test the actions of an actionAngleStaeckel def test_actionAngleStaeckel_conserved_actions(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, c=False, delta=0.71) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0]) check_actionAngle_conserved_actions( aAS, obs, MWPotential, -2.0, -8.0, -2.0, ntimes=101 ) return None # Test the actions of an actionAngleStaeckel, more eccentric orbit def test_actionAngleStaeckel_conserved_actions_ecc(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, c=False, delta=0.71) obs = Orbit([1.1, 0.2, 1.3, 0.3, 0.0]) check_actionAngle_conserved_actions( aAS, obs, MWPotential, -1.5, -8.0, -1.4, ntimes=101 ) return None # Test the actions of an actionAngleStaeckel def test_actionAngleStaeckel_conserved_actions_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.orbit.Orbits import ext_loaded from galpy.potential import ( DiskSCFPotential, DoubleExponentialDiskPotential, FlattenedPowerPotential, KeplerPotential, KuzminDiskPotential, KuzminLikeWrapperPotential, MWPotential, PerfectEllipsoidPotential, PowerTriaxialPotential, SCFPotential, TriaxialGaussianPotential, TriaxialHernquistPotential, TriaxialJaffePotential, TriaxialNFWPotential, interpRZPotential, ) ip = interpRZPotential( RZPot=MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 101), zgrid=(0.0, 1.0, 101), logR=True, use_c=True, enable_c=True, interpPot=True, interpRforce=True, interpzforce=True, ) pots = [ MWPotential, DoubleExponentialDiskPotential(normalize=1.0), FlattenedPowerPotential(normalize=1.0), FlattenedPowerPotential(normalize=1.0, alpha=0.0), KuzminDiskPotential(normalize=1.0, a=1.0 / 8.0), TriaxialHernquistPotential( normalize=1.0, c=0.2, pa=1.1 ), # tests rot, but not well TriaxialNFWPotential(normalize=1.0, c=0.3, pa=1.1), TriaxialJaffePotential(normalize=1.0, c=0.4, pa=1.1), SCFPotential(normalize=1.0), DiskSCFPotential(normalize=1.0), ip, PerfectEllipsoidPotential(normalize=1.0, c=0.98), TriaxialGaussianPotential(normalize=1.0, c=0.98), PowerTriaxialPotential(normalize=1.0, c=0.98), KuzminLikeWrapperPotential(pot=KeplerPotential(normalize=1.0), a=0.7, b=0.01), ] for pot in pots: aAS = actionAngleStaeckel(pot=pot, c=True, delta=0.71) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_conserved_actions( aAS, obs, pot, -1.6, -6.0, -1.6, ntimes=101, inclphi=True ) else: check_actionAngle_conserved_actions( aAS, obs, pot, -1.6, -8.0, -1.65, ntimes=101, inclphi=True ) return None # Test the actions of an actionAngleStaeckel, for a dblexp disk far away from the center def test_actionAngleStaeckel_conserved_actions_c_specialdblexp(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import DoubleExponentialDiskPotential pot = DoubleExponentialDiskPotential(normalize=1.0) aAS = actionAngleStaeckel(pot=pot, c=True, delta=0.01) # Close to circular in the Keplerian regime obs = Orbit([7.05, 0.002, pot.vcirc(7.05), 0.003, 0.0, 2.0]) check_actionAngle_conserved_actions( aAS, obs, pot, -2.0, -7.0, -2.0, ntimes=101, inclphi=True ) return None # Test the actions of an actionAngleStaeckel def test_actionAngleStaeckel_wSpherical_conserved_actions_c(): from test_potential import ( mockGaussianAmplitudeSmoothedLogarithmicHaloPotential, mockSCFZeeuwPotential, mockSmoothedLogarithmicHaloPotential, mockSmoothedLogarithmicHaloPotentialwTimeDependentAmplitudeWrapperPotential, mockSphericalSoftenedNeedleBarPotential, ) from galpy import potential from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.orbit.Orbits import ext_loaded lp = potential.LogarithmicHaloPotential(normalize=1.0, q=1.0) lpb = potential.LogarithmicHaloPotential(normalize=1.0, q=1.0, b=1.0) # same |^ hp = potential.HernquistPotential(normalize=1.0) jp = potential.JaffePotential(normalize=1.0) np = potential.NFWPotential(normalize=1.0) ip = potential.IsochronePotential(normalize=1.0, b=1.0) pp = potential.PowerSphericalPotential(normalize=1.0) lp2 = potential.PowerSphericalPotential(normalize=1.0, alpha=2.0) ppc = potential.PowerSphericalPotentialwCutoff(normalize=1.0) plp = potential.PlummerPotential(normalize=1.0) psp = potential.PseudoIsothermalPotential(normalize=1.0) bp = potential.BurkertPotential(normalize=1.0) scfp = potential.SCFPotential(normalize=1.0) scfzp = mockSCFZeeuwPotential() scfzp.normalize(1.0) msoftneedlep = mockSphericalSoftenedNeedleBarPotential() msmlp = mockSmoothedLogarithmicHaloPotential() mgasmlp = mockGaussianAmplitudeSmoothedLogarithmicHaloPotential() dp = potential.DehnenSphericalPotential(normalize=1.0) dcp = potential.DehnenCoreSphericalPotential(normalize=1.0) homp = potential.HomogeneousSpherePotential(normalize=1.0) ihomp = potential.interpSphericalPotential( rforce=potential.HomogeneousSpherePotential(normalize=1.0, R=1.1), rgrid=numpy.linspace(0.0, 1.1, 201), ) msmlpwtdp = ( mockSmoothedLogarithmicHaloPotentialwTimeDependentAmplitudeWrapperPotential() ) pots = [ lp, lpb, hp, jp, np, ip, pp, lp2, ppc, plp, psp, bp, scfp, scfzp, msoftneedlep, msmlp, mgasmlp, dp, dcp, homp, ihomp, msmlpwtdp, ] for pot in pots: aAS = actionAngleStaeckel(pot=pot, c=True, delta=0.01) obs = Orbit([1.1, 0.3, 1.2, 0.2, 0.5, 2.0]) if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_conserved_actions( aAS, obs, pot, -2.0, -5.0, -2.0, ntimes=101, inclphi=True ) else: check_actionAngle_conserved_actions( aAS, obs, pot, -2.0, -8.0, -2.0, ntimes=101, inclphi=True ) return None # Test the actions of an actionAngleStaeckel def test_actionAngleStaeckel_conserved_actions_fixed_quad(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.orbit.Orbits import ext_loaded from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, c=False, delta=0.71) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) if not ext_loaded: # odeint is not as accurate as dopr54_c check_actionAngle_conserved_actions( aAS, obs, MWPotential, -2.0, -5.0, -2.0, ntimes=101, fixed_quad=True, inclphi=True, ) else: check_actionAngle_conserved_actions( aAS, obs, MWPotential, -2.0, -8.0, -2.0, ntimes=101, fixed_quad=True, inclphi=True, ) return None # Test that the angles of an actionAngleStaeckel increase linearly def test_actionAngleStaeckel_linear_angles(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=True) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) check_actionAngle_linear_angles( aAS, obs, MWPotential, -2.0, -4.0, -3.0, -3.0, -3.0, -2.0, -2.0, -3.5, -2.0, ntimes=1001, ) # need fine sampling for de-period return None # Test that the angles of an actionAngleStaeckel increase linearly, interppot def test_actionAngleStaeckel_linear_angles_interppot(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential, interpRZPotential ip = interpRZPotential( RZPot=MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 101), zgrid=(0.0, 1.0, 101), logR=True, use_c=True, enable_c=True, interpPot=True, interpRforce=True, interpzforce=True, ) aAS = actionAngleStaeckel(pot=ip, delta=0.71, c=True) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) check_actionAngle_linear_angles( aAS, obs, MWPotential, -2.0, -4.0, -3.0, -3.0, -3.0, -2.0, -2.0, -3.5, -2.0, ntimes=1001, ) # need fine sampling for de-period return None # Test that the angles of an actionAngleStaeckel increase linearly def test_actionAngleStaeckel_linear_angles_u0(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71, c=True, useu0=True) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) check_actionAngle_linear_angles( aAS, obs, MWPotential, -2.0, -4.0, -3.0, -3.0, -3.0, -2.0, -2.0, -3.5, -2.0, ntimes=1001, ) # need fine sampling for de-period # specifying u0 check_actionAngle_linear_angles( aAS, obs, MWPotential, -2.0, -4.0, -3.0, -3.0, -3.0, -2.0, -2.0, -3.5, -2.0, ntimes=1001, u0=1.23, ) # need fine sampling for de-period return None # Test the conservation of ecc, zmax, rperi, rap of an actionAngleStaeckel def test_actionAngleStaeckel_conserved_EccZmaxRperiRap(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, c=False, delta=0.71) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 0.0]) check_actionAngle_conserved_EccZmaxRperiRap( aAS, obs, MWPotential, -2.0, -2.0, -2.0, -2.0, ntimes=101 ) return None # Test the conservation of ecc, zmax, rperi, rap of an actionAngleStaeckel def test_actionAngleStaeckel_conserved_EccZmaxRperiRap_ecc(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential aAS = actionAngleStaeckel(pot=MWPotential, c=False, delta=0.71) obs = Orbit([1.1, 0.2, 1.3, 0.3, 0.0, 2.0]) check_actionAngle_conserved_EccZmaxRperiRap( aAS, obs, MWPotential, -1.8, -1.4, -1.8, -1.8, ntimes=101, inclphi=True ) return None # Test the conservation of ecc, zmax, rperi, rap of an actionAngleStaeckel def test_actionAngleStaeckel_conserved_EccZmaxRperiRap_c(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.orbit.Orbits import ext_loaded from galpy.potential import ( DiskSCFPotential, DoubleExponentialDiskPotential, FlattenedPowerPotential, KeplerPotential, KuzminDiskPotential, KuzminLikeWrapperPotential, MWPotential, PerfectEllipsoidPotential, SCFPotential, TriaxialHernquistPotential, TriaxialJaffePotential, TriaxialNFWPotential, interpRZPotential, ) ip = interpRZPotential( RZPot=MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 101), zgrid=(0.0, 1.0, 101), logR=True, use_c=True, enable_c=True, interpPot=True, interpRforce=True, interpzforce=True, ) pots = [ MWPotential, DoubleExponentialDiskPotential(normalize=1.0), FlattenedPowerPotential(normalize=1.0), FlattenedPowerPotential(normalize=1.0, alpha=0.0), KuzminDiskPotential(normalize=1.0, a=1.0 / 8.0), TriaxialHernquistPotential( normalize=1.0, c=0.2, pa=1.1 ), # tests rot, but not well TriaxialNFWPotential(normalize=1.0, c=0.3, pa=1.1), TriaxialJaffePotential(normalize=1.0, c=0.4, pa=1.1), SCFPotential(normalize=1.0), DiskSCFPotential(normalize=1.0), ip, PerfectEllipsoidPotential(normalize=1.0, c=0.98), KuzminLikeWrapperPotential(pot=KeplerPotential(normalize=1.0), a=0.7, b=0.01), ] for pot in pots: aAS = actionAngleStaeckel(pot=pot, c=True, delta=0.71) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) check_actionAngle_conserved_EccZmaxRperiRap( aAS, obs, pot, -1.8, -1.3, -1.8, -1.8, ntimes=101 ) return None # Test the actionAngleStaeckel against an isochrone potential: actions def test_actionAngleStaeckel_otherIsochrone_actions(): from galpy.actionAngle import ( actionAngleIsochrone, actionAngleStaeckel, estimateDeltaStaeckel, ) from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAA = actionAngleStaeckel(pot=ip, c=False, delta=0.1) # not ideal R, vR, vT, z, vz, phi = 1.01, 0.05, 1.05, 0.05, 0.0, 2.0 ji = aAI(R, vR, vT, z, vz, phi) jia = aAA(R, vR, vT, z, vz, phi) djr = numpy.fabs((ji[0] - jia[0]) / ji[0]) dlz = numpy.fabs((ji[1] - jia[1]) / ji[1]) djz = numpy.fabs((ji[2] - jia[2]) / ji[2]) assert djr < 10.0**-3.0, ( "actionAngleStaeckel applied to isochrone potential fails for Jr at %f%%" % (djr * 100.0) ) # Lz and Jz are easy, because ip is a spherical potential assert dlz < 10.0**-10.0, ( "actionAngleStaeckel applied to isochrone potential fails for Lz at %f%%" % (dlz * 100.0) ) assert djz < 10.0**-3.0, ( "actionAngleStaeckel applied to isochrone potential fails for Jz at %f%%" % (djz * 100.0) ) return None # Test the actionAngleStaeckel against an isochrone potential: actions def test_actionAngleStaeckel_otherIsochrone_actions_fixed_quad(): from galpy.actionAngle import ( actionAngleIsochrone, actionAngleStaeckel, estimateDeltaStaeckel, ) from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAA = actionAngleStaeckel(pot=ip, c=False, delta=0.1) # not ideal R, vR, vT, z, vz, phi = 1.01, 0.05, 1.05, 0.05, 0.0, 2.0 ji = aAI(R, vR, vT, z, vz, phi) jia = aAA(R, vR, vT, z, vz, phi, fixed_quad=True) djr = numpy.fabs((ji[0] - jia[0]) / ji[0])[0] dlz = numpy.fabs((ji[1] - jia[1]) / ji[1])[0] djz = numpy.fabs((ji[2] - jia[2]) / ji[2])[0] assert djr < 10.0**-3.0, ( "actionAngleStaeckel applied to isochrone potential fails for Jr at %f%%" % (djr * 100.0) ) # Lz and Jz are easy, because ip is a spherical potential assert dlz < 10.0**-10.0, ( "actionAngleStaeckel applied to isochrone potential fails for Lz at %f%%" % (dlz * 100.0) ) assert djz < 10.0**-3.0, ( "actionAngleStaeckel applied to isochrone potential fails for Jz at %f%%" % (djz * 100.0) ) return None # Test the actionAngleStaeckel against an isochrone potential: actions def test_actionAngleStaeckel_otherIsochrone_actions_c(): from galpy.actionAngle import ( actionAngleIsochrone, actionAngleStaeckel, estimateDeltaStaeckel, ) from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAA = actionAngleStaeckel(pot=ip, c=True, delta=0.1) # not ideal R, vR, vT, z, vz, phi = 1.01, 0.05, 1.05, 0.05, 0.0, 2.0 ji = aAI(R, vR, vT, z, vz, phi) jia = aAA(R, vR, vT, z, vz, phi) djr = numpy.fabs((ji[0] - jia[0]) / ji[0]) dlz = numpy.fabs((ji[1] - jia[1]) / ji[1]) djz = numpy.fabs((ji[2] - jia[2]) / ji[2]) assert djr < 10.0**-3.0, ( "actionAngleStaeckel applied to isochrone potential fails for Jr at %f%%" % (djr * 100.0) ) # Lz and Jz are easy, because ip is a spherical potential assert dlz < 10.0**-10.0, ( "actionAngleStaeckel applied to isochrone potential fails for Lz at %f%%" % (dlz * 100.0) ) assert djz < 10.0**-3.0, ( "actionAngleStaeckel applied to isochrone potential fails for Jz at %f%%" % (djz * 100.0) ) return None # Test the actionAngleStaeckel against an isochrone potential: frequencies def test_actionAngleStaeckel_otherIsochrone_freqs(): from galpy.actionAngle import actionAngleIsochrone, actionAngleStaeckel from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAS = actionAngleStaeckel(pot=ip, delta=0.1, c=True) R, vR, vT, z, vz, phi = 1.01, 0.05, 1.05, 0.05, 0.0, 2.0 jiO = aAI.actionsFreqs(R, vR, vT, z, vz, phi) jiaO = aAS.actionsFreqs(R, vR, vT, z, vz, phi) dOr = numpy.fabs((jiO[3] - jiaO[3]) / jiO[3]) dOp = numpy.fabs((jiO[4] - jiaO[4]) / jiO[4]) dOz = numpy.fabs((jiO[5] - jiaO[5]) / jiO[5]) assert dOr < 10.0**-5.0, ( "actionAngleStaeckel applied to isochrone potential fails for Or at %g%%" % (dOr * 100.0) ) assert dOp < 10.0**-5.0, ( "actionAngleStaeckel applied to isochrone potential fails for Op at %g%%" % (dOp * 100.0) ) assert dOz < 1.5 * 10.0**-4.0, ( "actionAngleStaeckel applied to isochrone potential fails for Oz at %g%%" % (dOz * 100.0) ) return None # Test the actionAngleStaeckel against an isochrone potential: angles def test_actionAngleStaeckel_otherIsochrone_angles(): from galpy.actionAngle import actionAngleIsochrone, actionAngleStaeckel from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAS = actionAngleStaeckel(pot=ip, delta=0.1, c=True) R, vR, vT, z, vz, phi = 1.01, 0.05, 1.05, 0.03, -0.01, 2.0 jiO = aAI.actionsFreqsAngles(R, vR, vT, z, vz, phi) jiaO = aAS.actionsFreqsAngles(R, vR, vT, z, vz, phi) dar = numpy.fabs((jiO[6] - jiaO[6]) / jiO[6]) dap = numpy.fabs((jiO[7] - jiaO[7]) / jiO[7]) daz = numpy.fabs((jiO[8] - jiaO[8]) / jiO[8]) assert dar < 10.0**-4.0, ( "actionAngleStaeckel applied to isochrone potential fails for ar at %g%%" % (dar * 100.0) ) assert dap < 10.0**-6.0, ( "actionAngleStaeckel applied to isochrone potential fails for ap at %g%%" % (dap * 100.0) ) assert daz < 10.0**-4.0, ( "actionAngleStaeckel applied to isochrone potential fails for az at %g%%" % (daz * 100.0) ) return None # Test that actionAngleStaeckel at very small u works okay def test_actionAngleStaeckel_smallu(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014, vcirc aAS = actionAngleStaeckel(pot=MWPotential2014, c=False, delta=0.45) rmin = 8e-9 o = Orbit([rmin, 0.0, vcirc(MWPotential2014, rmin) / 20, 1e-8, 0.0]) ezrpra = aAS.EccZmaxRperiRap(o) # Check that rperi is close to zero assert numpy.fabs(ezrpra[2]) < 1e-8, ( "actionAngleStaeckel at very small u does not give rperi=0" ) return None # Basic sanity checking of the actionAngleStaeckelGrid actions (incl. conserved and ecc etc., bc takes a lot of time) def test_actionAngleStaeckelGrid_basicAndConserved_actions(): from galpy.actionAngle import actionAngleStaeckelGrid from galpy.orbit import Orbit from galpy.potential import MWPotential aAA = actionAngleStaeckelGrid( pot=MWPotential, delta=0.71, c=False, nLz=20, interpecc=True ) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 assert numpy.fabs(aAA.JR(R, vR, vT, z, vz, 0.0)) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jr=0" ) assert numpy.fabs(aAA.Jz(R, vR, vT, z, vz, 0.0)) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have Jz=0" ) te, tzmax, _, _ = aAA.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have e=0" ) assert numpy.fabs(tzmax) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have zmax=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAA(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-3.0, ( "Close-to-circular orbit in the MWPotential does not have small Jz" ) te, tzmax, _, _ = aAA.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) # Check that actions are conserved along the orbit obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0]) check_actionAngle_conserved_actions( aAA, obs, MWPotential, -1.2, -8.0, -1.7, ntimes=101 ) # and the eccentricity etc. check_actionAngle_conserved_EccZmaxRperiRap( aAA, obs, MWPotential, -2.0, -2.0, -2.0, -2.0, ntimes=101 ) return None # Basic sanity checking of the actionAngleStaeckel actions def test_actionAngleStaeckelGrid_basic_actions_c(): from galpy.actionAngle import actionAngleStaeckelGrid from galpy.orbit import Orbit from galpy.potential import MWPotential, interpRZPotential rzpot = interpRZPotential( RZPot=MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 201), logR=True, zgrid=(0.0, 1.0, 101), interpPot=True, use_c=True, enable_c=True, zsym=True, ) aAA = actionAngleStaeckelGrid(pot=rzpot, delta=0.71, c=True) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 js = aAA(R, vR, vT, z, vz) assert numpy.fabs(js[0]) < 10.0**-8.0, ( "Circular orbit in the MWPotential does not have Jr=0" ) assert numpy.fabs(js[2]) < 10.0**-8.0, ( "Circular orbit in the MWPotential does not have Jz=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 js = aAA(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(js[0]) < 10.0**-4.0, ( "Close-to-circular orbit in the MWPotential does not have small Jr" ) assert numpy.fabs(js[2]) < 10.0**-3.0, ( "Close-to-circular orbit in the MWPotentialspherical LogarithmicHalo does not have small Jz" ) # Test the actions of an actionAngleStaeckel def test_actionAngleStaeckelGrid_conserved_actions_c(): from galpy.actionAngle import actionAngleStaeckelGrid from galpy.orbit import Orbit from galpy.potential import MWPotential obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0]) aAA = actionAngleStaeckelGrid(pot=MWPotential, delta=0.71, c=True) check_actionAngle_conserved_actions( aAA, obs, MWPotential, -1.4, -8.0, -1.7, ntimes=101 ) return None # Test the setup of an actionAngleStaeckelGrid def test_actionAngleStaeckelGrid_setuperrs(): from galpy.actionAngle import actionAngleStaeckelGrid from galpy.potential import MWPotential try: aAA = actionAngleStaeckelGrid() except OSError: pass else: raise AssertionError("actionAngleStaeckelGrid w/o pot does not give IOError") try: aAA = actionAngleStaeckelGrid(pot=MWPotential) except OSError: pass else: raise AssertionError("actionAngleStaeckelGrid w/o delta does not give IOError") return None # Test the actionAngleStaeckel against an isochrone potential: actions def test_actionAngleStaeckelGrid_Isochrone_actions(): from galpy.actionAngle import actionAngleIsochrone, actionAngleStaeckelGrid from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAA = actionAngleStaeckelGrid(pot=ip, delta=0.1, c=True) R, vR, vT, z, vz, phi = 1.01, 0.05, 1.05, 0.05, 0.0, 2.0 ji = aAI(R, vR, vT, z, vz, phi) jia = aAA(R, vR, vT, z, vz, phi) djr = numpy.fabs((ji[0] - jia[0]) / ji[0]) dlz = numpy.fabs((ji[1] - jia[1]) / ji[1]) djz = numpy.fabs((ji[2] - jia[2]) / ji[2]) assert djr < 10.0**-1.2, ( "actionAngleStaeckel applied to isochrone potential fails for Jr at %f%%" % (djr * 100.0) ) # Lz and Jz are easy, because ip is a spherical potential assert dlz < 10.0**-10.0, ( "actionAngleStaeckel applied to isochrone potential fails for Lz at %f%%" % (dlz * 100.0) ) assert djz < 10.0**-1.2, ( "actionAngleStaeckel applied to isochrone potential fails for Jz at %f%%" % (djz * 100.0) ) return None # Basic sanity checking of the actionAngleStaeckelGrid eccentricity etc. def test_actionAngleStaeckelGrid_basic_EccZmaxRperiRap_c(): from galpy.actionAngle import actionAngleStaeckelGrid from galpy.orbit import Orbit from galpy.potential import MWPotential, interpRZPotential rzpot = interpRZPotential( RZPot=MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 201), logR=True, zgrid=(0.0, 1.0, 101), interpPot=True, use_c=True, enable_c=True, zsym=True, ) aAA = actionAngleStaeckelGrid(pot=rzpot, delta=0.71, c=True, interpecc=True) # circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.0, 0.0 te, tzmax, _, _ = aAA.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have e=0" ) assert numpy.fabs(tzmax) < 10.0**-16.0, ( "Circular orbit in the MWPotential does not have zmax=0" ) # Close-to-circular orbit R, vR, vT, z, vz = 1.01, 0.01, 1.0, 0.01, 0.01 te, tzmax, _, _ = aAA.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) # Another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.0, 0.99, 0.0, 0.0 te, tzmax, _, _ = aAA.EccZmaxRperiRap(R, vR, vT, z, vz) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) # Another close-to-circular orbit R, vR, vT, z, vz = 1.0, 0.0, 1.0, 0.01, 0.0 te, tzmax, _, _ = aAA.EccZmaxRperiRap(Orbit([R, vR, vT, z, vz])) assert numpy.fabs(te) < 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small eccentricity" ) assert numpy.fabs(tzmax) < 2.0 * 10.0**-2.0, ( "Close-to-circular orbit in the MWPotential does not have small zmax" ) return None # Test the actions of an actionAngleStaeckel def test_actionAngleStaeckelGrid_conserved_EccZmaxRperiRap_c(): from galpy.actionAngle import actionAngleStaeckelGrid from galpy.orbit import Orbit from galpy.potential import MWPotential obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) aAA = actionAngleStaeckelGrid(pot=MWPotential, delta=0.71, c=True, interpecc=True) check_actionAngle_conserved_EccZmaxRperiRap( aAA, obs, MWPotential, -2.0, -2.0, -2.0, -2.0, ntimes=101, inclphi=True ) return None # Test the actionAngleIsochroneApprox against an isochrone potential: actions def test_actionAngleIsochroneApprox_otherIsochrone_actions(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneApprox from galpy.orbit.Orbits import ext_loaded from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAIA = actionAngleIsochroneApprox(pot=ip, b=0.8) R, vR, vT, z, vz, phi = 1.1, 0.3, 1.2, 0.2, 0.5, 2.0 ji = aAI(R, vR, vT, z, vz, phi) jia = aAIA(R, vR, vT, z, vz, phi) djr = numpy.fabs((ji[0] - jia[0]) / ji[0]) dlz = numpy.fabs((ji[1] - jia[1]) / ji[1]) djz = numpy.fabs((ji[2] - jia[2]) / ji[2]) assert djr < 10.0**-2.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Jr at %f%%" % (djr * 100.0) ) # Lz and Jz are easy, because ip is a spherical potential assert dlz < 10.0**-10.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Lz at %f%%" % (dlz * 100.0) ) if not ext_loaded: # odeint is less accurate than dopr54_c assert djz < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Jz at %f%%" % (djz * 100.0) ) else: assert djz < 10.0**-10.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Jz at %f%%" % (djz * 100.0) ) return None # Test the actionAngleIsochroneApprox against an isochrone potential: frequencies def test_actionAngleIsochroneApprox_otherIsochrone_freqs(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneApprox from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAIA = actionAngleIsochroneApprox(pot=ip, b=0.8) R, vR, vT, z, vz, phi = 1.1, 0.3, 1.2, 0.2, 0.5, 2.0 jiO = aAI.actionsFreqs(R, vR, vT, z, vz, phi) jiaO = aAIA.actionsFreqs(R, vR, vT, z, vz, phi) dOr = numpy.fabs((jiO[3] - jiaO[3]) / jiO[3]) dOp = numpy.fabs((jiO[4] - jiaO[4]) / jiO[4]) dOz = numpy.fabs((jiO[5] - jiaO[5]) / jiO[5]) assert dOr < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Or at %f%%" % (dOr * 100.0) ) assert dOp < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Op at %f%%" % (dOp * 100.0) ) assert dOz < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Oz at %f%%" % (dOz * 100.0) ) # Same with _firstFlip, shouldn't be different bc doesn't do anything for R,vR,... input jiaO = aAIA.actionsFreqs(R, vR, vT, z, vz, phi, _firstFlip=True) dOr = numpy.fabs((jiO[3] - jiaO[3]) / jiO[3]) dOp = numpy.fabs((jiO[4] - jiaO[4]) / jiO[4]) dOz = numpy.fabs((jiO[5] - jiaO[5]) / jiO[5]) assert dOr < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Or at %f%%" % (dOr * 100.0) ) assert dOp < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Op at %f%%" % (dOp * 100.0) ) assert dOz < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Oz at %f%%" % (dOz * 100.0) ) return None # Test the actionAngleIsochroneApprox against an isochrone potential: angles def test_actionAngleIsochroneApprox_otherIsochrone_angles(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneApprox from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAIA = actionAngleIsochroneApprox(pot=ip, b=0.8) R, vR, vT, z, vz, phi = 1.1, 0.3, 1.2, 0.2, 0.5, 2.0 jiO = aAI.actionsFreqsAngles(R, vR, vT, z, vz, phi) jiaO = aAIA.actionsFreqsAngles(R, vR, vT, z, vz, phi) dar = numpy.fabs((jiO[6] - jiaO[6]) / jiO[6]) dap = numpy.fabs((jiO[7] - jiaO[7]) / jiO[7]) daz = numpy.fabs((jiO[8] - jiaO[8]) / jiO[8]) assert dar < 10.0**-4.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for ar at %f%%" % (dar * 100.0) ) assert dap < 10.0**-4.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for ap at %f%%" % (dap * 100.0) ) assert daz < 10.0**-4.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for az at %f%%" % (daz * 100.0) ) # Same with _firstFlip, shouldn't be different bc doesn't do anything for R,vR,... input jiaO = aAIA.actionsFreqsAngles(R, vR, vT, z, vz, phi, _firstFlip=True) dar = numpy.fabs((jiO[6] - jiaO[6]) / jiO[6]) dap = numpy.fabs((jiO[7] - jiaO[7]) / jiO[7]) daz = numpy.fabs((jiO[8] - jiaO[8]) / jiO[8]) assert dar < 10.0**-4.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for ar at %f%%" % (dar * 100.0) ) assert dap < 10.0**-4.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for ap at %f%%" % (dap * 100.0) ) assert daz < 10.0**-4.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for az at %f%%" % (daz * 100.0) ) return None # Test the actionAngleIsochroneApprox against an isochrone potential: actions, cumul def test_actionAngleIsochroneApprox_otherIsochrone_actions_cumul(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneApprox from galpy.orbit.Orbits import ext_loaded from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAIA = actionAngleIsochroneApprox(pot=ip, b=0.8) R, vR, vT, z, vz, phi = 1.1, 0.3, 1.2, 0.2, 0.5, 2.0 ji = aAI(R, vR, vT, z, vz, phi) jia = aAIA(R, vR, vT, z, vz, phi, cumul=True) djr = numpy.fabs((ji[0] - jia[0][0, -1]) / ji[0]) djz = numpy.fabs((ji[2] - jia[2][0, -1]) / ji[2]) assert djr < 10.0**-2.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Jr at %f%%" % (djr * 100.0) ) # Lz and Jz are easy, because ip is a spherical potential if not ext_loaded: # odeint is less accurate than dopr54_c assert djz < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Jz at %f%%" % (djz * 100.0) ) else: assert djz < 10.0**-10.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Jz at %f%%" % (djz * 100.0) ) return None # Test the actionAngleIsochroneApprox against an isochrone potential: actions; planarOrbit def test_actionAngleIsochroneApprox_otherIsochrone_planarOrbit_actions(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneApprox from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAIA = actionAngleIsochroneApprox(pot=ip, b=0.8) R, vR, vT, phi = 1.1, 0.3, 1.2, 2.0 ji = aAI(R, vR, vT, 0.0, 0.0, phi) jia = aAIA(R, vR, vT, phi) djr = numpy.fabs((ji[0] - jia[0]) / ji[0]) dlz = numpy.fabs((ji[1] - jia[1]) / ji[1]) assert djr < 10.0**-2.0, ( "actionAngleIsochroneApprox applied to isochrone potential for planarOrbit fails for Jr at %f%%" % (djr * 100.0) ) # Lz and Jz are easy, because ip is a spherical potential assert dlz < 10.0**-10.0, ( "actionAngleIsochroneApprox applied to isochrone potential for planarOrbit fails for Lz at %f%%" % (dlz * 100.0) ) return None # Test the actionAngleIsochroneApprox against an isochrone potential: actions; integrated planarOrbit def test_actionAngleIsochroneApprox_otherIsochrone_planarOrbit_integratedOrbit_actions(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneApprox from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAIA = actionAngleIsochroneApprox(pot=ip, b=0.8) R, vR, vT, phi = 1.1, 0.3, 1.2, 2.0 ji = aAI(R, vR, vT, 0.0, 0.0, phi) o = Orbit([R, vR, vT, phi]) ts = numpy.linspace(0.0, 250.0, 25000) o.integrate(ts, ip) jia = aAIA(o) djr = numpy.fabs((ji[0] - jia[0]) / ji[0]) dlz = numpy.fabs((ji[1] - jia[1]) / ji[1]) assert djr < 10.0**-2.0, ( "actionAngleIsochroneApprox applied to isochrone potential for planarOrbit fails for Jr at %f%%" % (djr * 100.0) ) # Lz and Jz are easy, because ip is a spherical potential assert dlz < 10.0**-10.0, ( "actionAngleIsochroneApprox applied to isochrone potential for planarOrbit fails for Lz at %f%%" % (dlz * 100.0) ) return None # Test the actionAngleIsochroneApprox against an isochrone potential: actions; for an integrated orbit def test_actionAngleIsochroneApprox_otherIsochrone_integratedOrbit_actions(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneApprox from galpy.orbit import Orbit from galpy.orbit.Orbits import ext_loaded from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAIA = actionAngleIsochroneApprox(pot=ip, b=0.8) R, vR, vT, z, vz, phi = 1.1, 0.3, 1.2, 0.2, 0.5, 2.0 ji = aAI(R, vR, vT, z, vz, phi) # Setup an orbit, and integrated it first o = Orbit([R, vR, vT, z, vz, phi]) ts = numpy.linspace(0.0, 250.0, 25000) # Integrate for a long time, not the default o.integrate(ts, ip) jia = aAIA(o) # actions, with an integrated orbit djr = numpy.fabs((ji[0] - jia[0]) / ji[0]) dlz = numpy.fabs((ji[1] - jia[1]) / ji[1]) djz = numpy.fabs((ji[2] - jia[2]) / ji[2]) assert djr < 10.0**-2.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Jr at %f%%" % (djr * 100.0) ) # Lz and Jz are easy, because ip is a spherical potential assert dlz < 10.0**-10.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Lz at %f%%" % (dlz * 100.0) ) if not ext_loaded: # odeint is less accurate than dopr54_c assert djz < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Jz at %f%%" % (djz * 100.0) ) else: assert djz < 10.0**-10.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Jz at %f%%" % (djz * 100.0) ) return None # Test the actionAngleIsochroneApprox against an isochrone potential: frequencies; for an integrated orbit def test_actionAngleIsochroneApprox_otherIsochrone_integratedOrbit_freqs(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneApprox from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAIA = actionAngleIsochroneApprox(pot=ip, b=0.8) R, vR, vT, z, vz, phi = 1.1, 0.3, 1.2, 0.2, 0.5, 2.0 jiO = aAI.actionsFreqs(R, vR, vT, z, vz, phi) # Setup an orbit, and integrated it first o = Orbit([R, vR, vT, z, vz, phi]) ts = numpy.linspace(0.0, 250.0, 25000) # Integrate for a long time, not the default o.integrate(ts, ip) jiaO = aAIA.actionsFreqs([o]) # for list dOr = numpy.fabs((jiO[3] - jiaO[3]) / jiO[3]) dOp = numpy.fabs((jiO[4] - jiaO[4]) / jiO[4]) dOz = numpy.fabs((jiO[5] - jiaO[5]) / jiO[5]) assert dOr < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Or at %f%%" % (dOr * 100.0) ) assert dOp < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Op at %f%%" % (dOp * 100.0) ) assert dOz < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Oz at %f%%" % (dOz * 100.0) ) # Same with specifying ts jiaO = aAIA.actionsFreqs(o, ts=ts) dOr = numpy.fabs((jiO[3] - jiaO[3]) / jiO[3]) dOp = numpy.fabs((jiO[4] - jiaO[4]) / jiO[4]) dOz = numpy.fabs((jiO[5] - jiaO[5]) / jiO[5]) assert dOr < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Or at %f%%" % (dOr * 100.0) ) assert dOp < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Op at %f%%" % (dOp * 100.0) ) assert dOz < 10.0**-6.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for Oz at %f%%" % (dOz * 100.0) ) return None # Test the actionAngleIsochroneApprox against an isochrone potential: angles; for an integrated orbit def test_actionAngleIsochroneApprox_otherIsochrone_integratedOrbit_angles(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneApprox from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAIA = actionAngleIsochroneApprox(pot=ip, b=0.8) R, vR, vT, z, vz, phi = 1.1, 0.3, 1.2, 0.2, 0.5, 2.0 jiO = aAI.actionsFreqsAngles(R, vR, vT, z, vz, phi) # Setup an orbit, and integrated it first o = Orbit([R, vR, vT, z, vz, phi]) ts = numpy.linspace(0.0, 250.0, 25000) # Integrate for a long time, not the default o.integrate(ts, ip) jiaO = aAIA.actionsFreqsAngles(o) dar = numpy.fabs((jiO[6] - jiaO[6]) / jiO[6]) dap = numpy.fabs((jiO[7] - jiaO[7]) / jiO[7]) daz = numpy.fabs((jiO[8] - jiaO[8]) / jiO[8]) assert dar < 10.0**-4.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for ar at %f%%" % (dar * 100.0) ) assert dap < 10.0**-4.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for ap at %f%%" % (dap * 100.0) ) assert daz < 10.0**-4.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for az at %f%%" % (daz * 100.0) ) # Same with specifying ts jiaO = aAIA.actionsFreqsAngles(o, ts=ts) dar = numpy.fabs((jiO[6] - jiaO[6]) / jiO[6]) dap = numpy.fabs((jiO[7] - jiaO[7]) / jiO[7]) daz = numpy.fabs((jiO[8] - jiaO[8]) / jiO[8]) assert dar < 10.0**-4.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for ar at %f%%" % (dar * 100.0) ) assert dap < 10.0**-4.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for ap at %f%%" % (dap * 100.0) ) assert daz < 10.0**-4.0, ( "actionAngleIsochroneApprox applied to isochrone potential fails for az at %f%%" % (daz * 100.0) ) return None # Check that actionAngleIsochroneApprox gives the same answer for different setups def test_actionAngleIsochroneApprox_diffsetups(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneApprox from galpy.orbit import Orbit from galpy.potential import IsochronePotential, LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) # Different setups aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) aAIip = actionAngleIsochroneApprox( pot=lp, ip=IsochronePotential(normalize=1.0, b=0.8) ) aAIaAIip = actionAngleIsochroneApprox( pot=lp, aAI=actionAngleIsochrone(ip=IsochronePotential(normalize=1.0, b=0.8)) ) aAIrk6 = actionAngleIsochroneApprox(pot=lp, b=0.8, integrate_method="rk6_c") aAIlong = actionAngleIsochroneApprox(pot=lp, b=0.8, tintJ=200.0) aAImany = actionAngleIsochroneApprox(pot=lp, b=0.8, ntintJ=20000) # Orbit to test on obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) # Actions, frequencies, angles acfs = numpy.array(list(aAI.actionsFreqsAngles(obs()))).flatten() acfsip = numpy.array(list(aAIip.actionsFreqsAngles(obs()))).flatten() acfsaAIip = numpy.array(list(aAIaAIip.actionsFreqsAngles(obs()))).flatten() acfsrk6 = numpy.array(list(aAIrk6.actionsFreqsAngles(obs()))).flatten() acfslong = numpy.array(list(aAIlong.actionsFreqsAngles(obs()))).flatten() acfsmany = numpy.array(list(aAImany.actionsFreqsAngles(obs()))).flatten() acfsfirstFlip = numpy.array( list(aAI.actionsFreqsAngles(obs(), _firstFlip=True)) ).flatten() # Check that they are the same assert numpy.amax(numpy.fabs((acfs - acfsip) / acfs)) < 10.0**-15.0, ( "actionAngleIsochroneApprox calculated w/ b= and ip= set to the equivalent IsochronePotential do not agree" ) assert numpy.amax(numpy.fabs((acfs - acfsaAIip) / acfs)) < 10.0**-15.0, ( "actionAngleIsochroneApprox calculated w/ b= and aAI= set to the equivalent IsochronePotential do not agree" ) assert numpy.amax(numpy.fabs((acfs - acfsrk6) / acfs)) < 10.0**-8.0, ( "actionAngleIsochroneApprox calculated w/ integrate_method=dopr54_c and rk6_c do not agree at %g%%" % (100.0 * numpy.amax(numpy.fabs((acfs - acfsrk6) / acfs))) ) assert numpy.amax(numpy.fabs((acfs - acfslong) / acfs)) < 10.0**-2.0, ( "actionAngleIsochroneApprox calculated w/ tintJ=100 and 200 do not agree at %g%%" % (100.0 * numpy.amax(numpy.fabs((acfs - acfslong) / acfs))) ) assert numpy.amax(numpy.fabs((acfs - acfsmany) / acfs)) < 10.0**-4.0, ( "actionAngleIsochroneApprox calculated w/ ntintJ=10000 and 20000 do not agree at %g%%" % (100.0 * numpy.amax(numpy.fabs((acfs - acfsmany) / acfs))) ) assert numpy.amax(numpy.fabs((acfs - acfsfirstFlip) / acfs)) < 10.0**-4.0, ( "actionAngleIsochroneApprox calculated w/ _firstFlip and w/o do not agree at %g%%" % (100.0 * numpy.amax(numpy.fabs((acfs - acfsmany) / acfs))) ) return None # Check that actionAngleIsochroneApprox gives the same answer w/ and w/o firstFlip def test_actionAngleIsochroneApprox_firstFlip(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneApprox from galpy.orbit import Orbit from galpy.potential import IsochronePotential, LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) # Orbit to test on obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) # Actions, frequencies, angles acfs = numpy.array(list(aAI.actionsFreqsAngles(obs()))).flatten() acfsfirstFlip = numpy.array( list(aAI.actionsFreqsAngles(obs(), _firstFlip=True)) ).flatten() # Check that they are the same assert numpy.amax(numpy.fabs((acfs - acfsfirstFlip) / acfs)) < 10.0**-4.0, ( "actionAngleIsochroneApprox calculated w/ _firstFlip and w/o do not agree at %g%%" % (100.0 * numpy.amax(numpy.fabs((acfs - acfsfirstFlip) / acfs))) ) # Also test that this still works when the orbit was already integrated obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) ts = numpy.linspace(0.0, 250.0, 25000) obs.integrate(ts, lp) acfs = numpy.array(list(aAI.actionsFreqsAngles(obs()))).flatten() acfsfirstFlip = numpy.array( list(aAI.actionsFreqsAngles(obs(), _firstFlip=True)) ).flatten() # Check that they are the same assert numpy.amax(numpy.fabs((acfs - acfsfirstFlip) / acfs)) < 10.0**-4.0, ( "actionAngleIsochroneApprox calculated w/ _firstFlip and w/o do not agree at %g%%" % (100.0 * numpy.amax(numpy.fabs((acfs - acfsfirstFlip) / acfs))) ) return None # Test the actionAngleIsochroneApprox used in Bovy (2014) def test_actionAngleIsochroneApprox_bovy14(): from galpy.actionAngle import actionAngleIsochroneApprox from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) times = numpy.linspace(0.0, 100.0, 51) obs.integrate(times, lp, method="dopr54_c") js = aAI( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times), obs.phi(times), ) maxdj = numpy.amax( numpy.fabs(js - numpy.tile(numpy.mean(js, axis=1), (len(times), 1)).T), axis=1 ) / numpy.mean(js, axis=1) assert maxdj[0] < 3.0 * 10.0**-2.0, ( "Jr conservation for the GD-1 like orbit of Bovy (2014) fails at %f%%" % (100.0 * maxdj[0]) ) assert maxdj[1] < 10.0**-2.0, ( "Lz conservation for the GD-1 like orbit of Bovy (2014) fails at %f%%" % (100.0 * maxdj[1]) ) assert maxdj[2] < 2.0 * 10.0**-2.0, ( "Jz conservation for the GD-1 like orbit of Bovy (2014) fails at %f%%" % (100.0 * maxdj[2]) ) return None # Test the actionAngleIsochroneApprox for a triaxial potential def test_actionAngleIsochroneApprox_triaxialnfw_conserved_actions(): from galpy.actionAngle import actionAngleIsochroneApprox from galpy.orbit import Orbit from galpy.potential import TriaxialNFWPotential tnp = TriaxialNFWPotential(b=0.9, c=0.8, normalize=1.0) aAI = actionAngleIsochroneApprox(pot=tnp, b=0.8, tintJ=200.0) obs = Orbit([1.0, 0.2, 1.1, 0.1, 0.1, 0.0]) check_actionAngle_conserved_actions( aAI, obs, tnp, -1.7, -2.0, -1.7, ntimes=51, inclphi=True ) return None def test_actionAngleIsochroneApprox_triaxialnfw_linear_angles(): from galpy.actionAngle import actionAngleIsochroneApprox from galpy.orbit import Orbit from galpy.potential import TriaxialNFWPotential tnp = TriaxialNFWPotential(b=0.9, c=0.8, normalize=1.0) aAI = actionAngleIsochroneApprox(pot=tnp, b=0.8, tintJ=200.0) obs = Orbit([1.0, 0.2, 1.1, 0.1, 0.1, 0.0]) check_actionAngle_linear_angles( aAI, obs, tnp, -5.0, -5.0, -5.0, -5.0, -5.0, -5.0, -4.0, -4.0, -4.0, separate_times=True, maxt=4.0, ntimes=51, ) # quick, essentially tests that nothing is grossly wrong return None def test_actionAngleIsochroneApprox_plotting(): from matplotlib import pyplot from galpy.actionAngle import actionAngleIsochroneApprox from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) # Various plots that should be produced aAI.plot(obs) aAI.plot(obs, type="jr") aAI.plot( numpy.reshape(obs.R(obs.t), (1, len(obs.t))), numpy.reshape(obs.vR(obs.t), (1, len(obs.t))), numpy.reshape(obs.vT(obs.t), (1, len(obs.t))), numpy.reshape(obs.z(obs.t), (1, len(obs.t))), numpy.reshape(obs.vz(obs.t), (1, len(obs.t))), numpy.reshape(obs.phi(obs.t), (1, len(obs.t))), type="lz", ) aAI.plot(obs, type="jz") aAI.plot(obs, type="jr", downsample=True) aAI.plot(obs, type="lz", downsample=True) aAI.plot(obs, type="jz", downsample=True) aAI.plot(obs, type="araz") aAI.plot(obs, type="araz", downsample=True) aAI.plot(obs, type="araz", deperiod=True) aAI.plot(obs, type="araphi", deperiod=True) aAI.plot(obs, type="azaphi", deperiod=True) aAI.plot(obs, type="araphi", deperiod=True, downsample=True) aAI.plot(obs, type="azaphi", deperiod=True, downsample=True) # With integrated orbit, just to make sure we're covering this obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) obs.integrate(numpy.linspace(0.0, 200.0, 20001), lp) aAI.plot(obs, type="jr") pyplot.close("all") return None # Test the Orbit interface def test_orbit_interface_spherical(): from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential, NFWPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) obs = Orbit([1.0, 0.2, 1.5, 0.3, 0.1, 2.0]) # resetaA has been deprecated # assert not obs.resetaA(), 'obs.resetaA() does not return False when called before having set up an actionAngle instance' aAS = actionAngleSpherical(pot=lp) acfs = numpy.array(list(aAS.actionsFreqsAngles(obs))).reshape(9) type = "spherical" try: obs.jr(type=type) except AttributeError: pass # should raise this, as we have not specified a potential else: raise AssertionError( "obs.jr w/o pot= does not raise AttributeError before the orbit was integrated" ) acfso = numpy.array( [ obs.jr(pot=lp, type=type), obs.jp(pot=lp, type=type), obs.jz(pot=lp, type=type), obs.Or(pot=lp, type=type), obs.Op(pot=lp, type=type), obs.Oz(pot=lp, type=type), obs.wr(pot=lp, type=type), obs.wp(pot=lp, type=type), obs.wz(pot=lp, type=type), ] ) maxdev = numpy.amax(numpy.abs(acfs - acfso)) assert maxdev < 10.0**-16.0, ( "Orbit interface for actionAngleSpherical does not return the same as actionAngle interface" ) assert ( numpy.abs(obs.Tr(pot=lp, type=type) - 2.0 * numpy.pi / acfs[3]) < 10.0**-16.0 ), "Orbit.Tr does not agree with actionAngleSpherical frequency" assert ( numpy.abs(obs.Tp(pot=lp, type=type) - 2.0 * numpy.pi / acfs[4]) < 10.0**-16.0 ), "Orbit.Tp does not agree with actionAngleSpherical frequency" assert ( numpy.abs(obs.Tz(pot=lp, type=type) - 2.0 * numpy.pi / acfs[5]) < 10.0**-16.0 ), "Orbit.Tz does not agree with actionAngleSpherical frequency" assert ( numpy.abs(obs.TrTp(pot=lp, type=type) - acfs[4] / acfs[3] * numpy.pi) < 10.0**-16.0 ), "Orbit.TrTp does not agree with actionAngleSpherical frequency" # Different spherical potential np = NFWPotential(normalize=1.0) aAS = actionAngleSpherical(pot=np) acfs = numpy.array(list(aAS.actionsFreqsAngles(obs))).reshape(9) type = "spherical" # resetaA has been deprecated # assert obs.resetaA(pot=np), 'obs.resetaA() does not return True after having set up an actionAngle instance' obs.integrate( numpy.linspace(0.0, 1.0, 11), np ) # to test that not specifying the potential works acfso = numpy.array( [ obs.jr(type=type), obs.jp(type=type), obs.jz(type=type), obs.Or(type=type), obs.Op(type=type), obs.Oz(type=type), obs.wr(type=type), obs.wp(type=type), obs.wz(type=type), ] ) maxdev = numpy.amax(numpy.abs(acfs - acfso)) assert maxdev < 10.0**-16.0, ( "Orbit interface for actionAngleSpherical does not return the same as actionAngle interface" ) # Directly test _resetaA --> deprecated # assert obs._orb._resetaA(pot=lp), 'OrbitTop._resetaA does not return True when resetting the actionAngle instance' # Test that unit conversions to physical units are handled correctly ro, vo = 8.0, 220.0 obs = Orbit([1.0, 0.2, 1.5, 0.3, 0.1, 2.0], ro=ro, vo=vo) aAS = actionAngleSpherical(pot=lp) acfs = numpy.array(list(aAS.actionsFreqsAngles(obs))).reshape(9) type = "spherical" acfso = numpy.array( [ obs.jr(pot=lp, type=type) / ro / vo, obs.jp(pot=lp, type=type) / ro / vo, obs.jz(pot=lp, type=type) / ro / vo, obs.Or(pot=lp, type=type) / vo * ro / 1.0227121655399913, obs.Op(pot=lp, type=type) / vo * ro / 1.0227121655399913, obs.Oz(pot=lp, type=type) / vo * ro / 1.0227121655399913, obs.wr(pot=lp, type=type), obs.wp(pot=lp, type=type), obs.wz(pot=lp, type=type), ] ) maxdev = numpy.amax(numpy.abs(acfs - acfso)) assert maxdev < 10.0**-9.0, ( "Orbit interface for actionAngleSpherical does not return the same as actionAngle interface when using physical coordinates" ) assert ( numpy.abs( obs.Tr(pot=lp, type=type) / ro * vo * 1.0227121655399913 - 2.0 * numpy.pi / acfs[3] ) < 10.0**-8.0 ), ( "Orbit.Tr does not agree with actionAngleSpherical frequency when using physical coordinates" ) assert ( numpy.abs( obs.Tp(pot=lp, type=type) / ro * vo * 1.0227121655399913 - 2.0 * numpy.pi / acfs[4] ) < 10.0**-8.0 ), ( "Orbit.Tp does not agree with actionAngleSpherical frequency when using physical coordinates" ) assert ( numpy.abs( obs.Tz(pot=lp, type=type) / ro * vo * 1.0227121655399913 - 2.0 * numpy.pi / acfs[5] ) < 10.0**-8.0 ), ( "Orbit.Tz does not agree with actionAngleSpherical frequency when using physical coordinates" ) assert ( numpy.abs(obs.TrTp(pot=lp, type=type) - acfs[4] / acfs[3] * numpy.pi) < 10.0**-8.0 ), ( "Orbit.TrTp does not agree with actionAngleSpherical frequency when using physical coordinates" ) # Test frequency in km/s/kpc assert ( numpy.abs(obs.Or(pot=lp, type=type, kmskpc=True) / vo * ro - acfs[3]) < 10.0**-8.0 ), ( "Orbit.Or does not agree with actionAngleSpherical frequency when using physical coordinates with km/s/kpc" ) assert ( numpy.abs(obs.Op(pot=lp, type=type, kmskpc=True) / vo * ro - acfs[4]) < 10.0**-8.0 ), ( "Orbit.Op does not agree with actionAngleSpherical frequency when using physical coordinates with km/s/kpc" ) assert ( numpy.abs(obs.Oz(pot=lp, type=type, kmskpc=True) / vo * ro - acfs[5]) < 10.0**-8.0 ), ( "Orbit.Oz does not agree with actionAngleSpherical frequency when using physical coordinates with km/s/kpc" ) return None # Test the Orbit interface for actionAngleStaeckel def test_orbit_interface_staeckel(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) aAS = actionAngleStaeckel(pot=MWPotential, delta=0.71) acfs = numpy.array(list(aAS.actionsFreqsAngles(obs))).reshape(9) type = "staeckel" acfso = numpy.array( [ obs.jr(pot=MWPotential, type=type, delta=0.71), obs.jp(pot=MWPotential, type=type, delta=0.71), obs.jz(pot=MWPotential, type=type, delta=0.71), obs.Or(pot=MWPotential, type=type, delta=0.71), obs.Op(pot=MWPotential, type=type, delta=0.71), obs.Oz(pot=MWPotential, type=type, delta=0.71), obs.wr(pot=MWPotential, type=type, delta=0.71), obs.wp(pot=MWPotential, type=type, delta=0.71), obs.wz(pot=MWPotential, type=type, delta=0.71), ] ) maxdev = numpy.amax(numpy.abs(acfs - acfso)) assert maxdev < 10.0**-16.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface" ) return None # Further tests of the Orbit interface for actionAngleStaeckel def test_orbit_interface_staeckel_defaultdelta(): from galpy.actionAngle import actionAngleStaeckel, estimateDeltaStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) est_delta = estimateDeltaStaeckel(MWPotential2014, obs.R(), obs.z()) # Just need to trigger delta estimation in orbit jr_orb = obs.jr(pot=MWPotential2014, type="staeckel") assert numpy.fabs(est_delta - obs._aA._delta) < 1e-10, ( "Directly estimated delta does not agree with Orbit-interface-estimated delta" ) aAS = actionAngleStaeckel(pot=MWPotential2014, delta=est_delta) acfs = numpy.array(list(aAS.actionsFreqsAngles(obs))).reshape(9) type = "staeckel" acfso = numpy.array( [ obs.jr(pot=MWPotential2014, type=type), obs.jp(pot=MWPotential2014, type=type), obs.jz(pot=MWPotential2014, type=type), obs.Or(pot=MWPotential2014, type=type), obs.Op(pot=MWPotential2014, type=type), obs.Oz(pot=MWPotential2014, type=type), obs.wr(pot=MWPotential2014, type=type), obs.wp(pot=MWPotential2014, type=type), obs.wz(pot=MWPotential2014, type=type), ] ) maxdev = numpy.amax(numpy.abs(acfs - acfso)) assert maxdev < 10.0**-16.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface" ) return None def test_orbit_interface_staeckel_PotentialErrors(): # staeckel approx. w/ automatic delta should fail if delta cannot be found from galpy.orbit import Orbit from galpy.potential import ( PotentialError, SpiralArmsPotential, TwoPowerSphericalPotential, ) obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) # Version of TwoPowerSphericalPotential that does not have R2deriv class TwoPowerSphericalPotentialNoR2deriv(TwoPowerSphericalPotential): _R2deriv = property() # turns it off! tp = TwoPowerSphericalPotentialNoR2deriv(normalize=1.0, alpha=1.2, beta=2.5) # Check that this potential indeed does not have second derivs with pytest.raises(PotentialError) as excinfo: dummy = tp.R2deriv(1.0, 0.1) pytest.fail( "TwoPowerSphericalPotentialNoR2deriv appears to now have second derivatives, means that it cannot be used to test exceptions based on not having the second derivatives any longer" ) # Now check that estimating delta fails with pytest.raises(PotentialError) as excinfo: obs.jr(pot=tp, type="staeckel") pytest.fail( "TwoPowerSphericalPotentialNoR2deriv appears to now have second derivatives, means that it cannot be used to test exceptions based on not having the second derivatives any longer" ) assert "second derivatives" in str(excinfo.value), ( "Estimating delta for potential lacking second derivatives should have failed with a message about the lack of second derivatives" ) # Generic non-axi sp = SpiralArmsPotential() with pytest.raises(PotentialError) as excinfo: obs.jr(pot=sp, type="staeckel") pytest.fail( "TwoPowerSphericalPotentialNoR2deriv appears to now have second derivatives, means that it cannot be used to test exceptions based on not having the second derivatives any longer" ) assert "not axisymmetric" in str(excinfo.value), ( "Estimating delta for a non-axi potential should have failed with a message about the fact that the potential is non-axisymmetric" ) return None def test_orbits_interface_staeckel_PotentialErrors(): # staeckel approx. w/ automatic delta should fail if delta cannot be found from galpy.orbit import Orbit from galpy.potential import ( PotentialError, SpiralArmsPotential, TwoPowerSphericalPotential, ) obs = Orbit( [[1.05, 0.02, 1.05, 0.03, 0.0, 2.0], [1.15, -0.02, 1.02, -0.03, 0.0, 2.0]] ) # Version of TwoPowerSphericalPotential that does not have R2deriv class TwoPowerSphericalPotentialNoR2deriv(TwoPowerSphericalPotential): _R2deriv = property() # turns it off! tp = TwoPowerSphericalPotentialNoR2deriv(normalize=1.0, alpha=1.2, beta=2.5) # Check that this potential indeed does not have second derivs with pytest.raises(PotentialError) as excinfo: dummy = tp.R2deriv(1.0, 0.1) pytest.fail( "TwoPowerSphericalPotentialNoR2deriv appears to now have second derivatives, means that it cannot be used to test exceptions based on not having the second derivatives any longer" ) # Now check that estimating delta fails with pytest.raises(PotentialError) as excinfo: obs.jr(pot=tp, type="staeckel") pytest.fail( "TwoPowerSphericalPotentialNoR2deriv appears to now have second derivatives, means that it cannot be used to test exceptions based on not having the second derivatives any longer" ) assert "second derivatives" in str(excinfo.value), ( "Estimating delta for potential lacking second derivatives should have failed with a message about the lack of second derivatives" ) # Generic non-axi sp = SpiralArmsPotential() with pytest.raises(PotentialError) as excinfo: obs.jr(pot=sp, type="staeckel") pytest.fail( "SpiralArms appears to now have second derivatives, means that it cannot be used to test exceptions based on not having the second derivatives any longer" ) assert "not axisymmetric" in str(excinfo.value), ( "Estimating delta for a non-axi potential should have failed with a message about the fact that the potential is non-axisymmetric" ) return None # Test the Orbit interface for actionAngleAdiabatic def test_orbit_interface_adiabatic(): from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) aAS = actionAngleAdiabatic(pot=MWPotential) acfs = numpy.array(list(aAS(obs))).reshape(3) type = "adiabatic" acfso = numpy.array( [ obs.jr(pot=MWPotential, type=type), obs.jp(pot=MWPotential, type=type), obs.jz(pot=MWPotential, type=type), ] ) maxdev = numpy.amax(numpy.abs(acfs - acfso)) assert maxdev < 10.0**-16.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface" ) return None def test_orbit_interface_adiabatic_2d(): # Test with 2D orbit from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential obs = Orbit([1.05, 0.02, 1.05, 2.0]) aAS = actionAngleAdiabatic(pot=MWPotential) acfs = numpy.array(list(aAS(obs))).reshape(3) type = "adiabatic" acfso = numpy.array( [ obs.jr(pot=MWPotential, type=type), obs.jp(pot=MWPotential, type=type), obs.jz(pot=MWPotential, type=type), ] ) maxdev = numpy.amax(numpy.abs(acfs - acfso)) assert maxdev < 10.0**-16.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface" ) return None def test_orbit_interface_adiabatic_2d_2dpot(): # Test with 2D orbit from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential, toPlanarPotential obs = Orbit([1.05, 0.02, 1.05, 2.0]) aAS = actionAngleAdiabatic(pot=toPlanarPotential(MWPotential)) acfs = numpy.array(list(aAS(obs))).reshape(3) type = "adiabatic" acfso = numpy.array( [ obs.jr(pot=toPlanarPotential(MWPotential), type=type), obs.jp(pot=toPlanarPotential(MWPotential), type=type), obs.jz(pot=toPlanarPotential(MWPotential), type=type), ] ) maxdev = numpy.amax(numpy.abs(acfs - acfso)) assert maxdev < 10.0**-16.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface" ) return None def test_orbit_interface_actionAngleIsochroneApprox(): from galpy.actionAngle import actionAngleIsochroneApprox from galpy.orbit import Orbit from galpy.potential import MWPotential obs = Orbit([1.05, 0.02, 1.05, 0.03, 0.0, 2.0]) aAS = actionAngleIsochroneApprox(pot=MWPotential, b=0.8) acfs = aAS.actionsFreqsAngles([obs()]) acfs = numpy.array(acfs).reshape(9) type = "isochroneApprox" acfso = numpy.array( [ obs.jr(pot=MWPotential, type=type, b=0.8), obs.jp(pot=MWPotential, type=type, b=0.8), obs.jz(pot=MWPotential, type=type, b=0.8), obs.Or(pot=MWPotential, type=type, b=0.8), obs.Op(pot=MWPotential, type=type, b=0.8), obs.Oz(pot=MWPotential, type=type, b=0.8), obs.wr(pot=MWPotential, type=type, b=0.8), obs.wp(pot=MWPotential, type=type, b=0.8), obs.wz(pot=MWPotential, type=type, b=0.8), ] ) maxdev = numpy.amax(numpy.abs(acfs - acfso)) assert maxdev < 10.0**-13.0, ( "Orbit interface for actionAngleIsochroneApprox does not return the same as actionAngle interface" ) assert ( numpy.abs(obs.Tr(pot=MWPotential, type=type, b=0.8) - 2.0 * numpy.pi / acfso[3]) < 10.0**-13.0 ), "Orbit.Tr does not agree with actionAngleIsochroneApprox frequency" assert ( numpy.abs(obs.Tp(pot=MWPotential, type=type, b=0.8) - 2.0 * numpy.pi / acfso[4]) < 10.0**-13.0 ), "Orbit.Tp does not agree with actionAngleIsochroneApprox frequency" assert ( numpy.abs(obs.Tz(pot=MWPotential, type=type, b=0.8) - 2.0 * numpy.pi / acfso[5]) < 10.0**-13.0 ), "Orbit.Tz does not agree with actionAngleIsochroneApprox frequency" assert ( numpy.abs( obs.TrTp(pot=MWPotential, type=type, b=0.8) - acfso[4] / acfso[3] * numpy.pi ) < 10.0**-13.0 ), "Orbit.TrTp does not agree with actionAngleIsochroneApprox frequency" return None def test_orbit_interface_unbound_simple_adiabatic_noc(): # Test that an unbound orbit in a set of orbits is handled correctly from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential2014 obs = Orbit( [[1.05, 0.02, 1.05, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0]] ) aAAnoc = actionAngleAdiabatic(pot=MWPotential2014, c=False) jr, jp, jz, e, zmax, rperi, rap = ( obs.jr(pot=MWPotential2014, type="adiabatic", c=False), obs.jp(pot=MWPotential2014, type="adiabatic", c=False), obs.jz(pot=MWPotential2014, type="adiabatic", c=False), obs.e(pot=MWPotential2014, type="adiabatic", analytic=True, c=False), obs.zmax(pot=MWPotential2014, type="adiabatic", analytic=True, c=False), obs.rperi(pot=MWPotential2014, type="adiabatic", analytic=True, c=False), obs.rap(pot=MWPotential2014, type="adiabatic", analytic=True, c=False), ) assert numpy.fabs(jr[0] - aAAnoc(obs[0])[0]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(jp[0] - aAAnoc(obs[0])[1]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(jz[0] - aAAnoc(obs[0])[2]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(e[0] - aAAnoc.EccZmaxRperiRap(obs[0])[0]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(zmax[0] - aAAnoc.EccZmaxRperiRap(obs[0])[1]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(rperi[0] - aAAnoc.EccZmaxRperiRap(obs[0])[2]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(rap[0] - aAAnoc.EccZmaxRperiRap(obs[0])[3]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jr[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jp[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jz[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(e[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(zmax[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(rperi[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(rap[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) return None def test_orbit_interface_unbound_simple_adiabatic_c(): # Test that an unbound orbit in a set of orbits is handled correctly from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential2014 obs = Orbit( [[1.05, 0.02, 1.05, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0]] ) aAAc = actionAngleAdiabatic(pot=MWPotential2014, c=True) jr, jp, jz, e, zmax, rperi, rap = ( obs.jr(pot=MWPotential2014, type="adiabatic", c=True), obs.jp(pot=MWPotential2014, type="adiabatic", c=True), obs.jz(pot=MWPotential2014, type="adiabatic", c=True), obs.e(pot=MWPotential2014, type="adiabatic", analytic=True, c=True), obs.zmax(pot=MWPotential2014, type="adiabatic", analytic=True, c=True), obs.rperi(pot=MWPotential2014, type="adiabatic", analytic=True, c=True), obs.rap(pot=MWPotential2014, type="adiabatic", analytic=True, c=True), ) assert numpy.fabs(jr[0] - aAAc(obs[0])[0]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(jp[0] - aAAc(obs[0])[1]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(jz[0] - aAAc(obs[0])[2]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(e[0] - aAAc.EccZmaxRperiRap(obs[0])[0]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(zmax[0] - aAAc.EccZmaxRperiRap(obs[0])[1]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(rperi[0] - aAAc.EccZmaxRperiRap(obs[0])[2]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(rap[0] - aAAc.EccZmaxRperiRap(obs[0])[3]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jr[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jp[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jz[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(e[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(zmax[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(rperi[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(rap[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) return None def test_orbit_interface_unbound_simple_staeckel_noc(): # Test that an unbound orbit in a set of orbits is handled correctly from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 obs = Orbit( [[1.05, 0.02, 1.05, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0]] ) aASnoc = actionAngleStaeckel(pot=MWPotential2014, delta=0.71, c=False) jr, jp, jz, e, zmax, rperi, rap = ( obs.jr(pot=MWPotential2014, type="staeckel", delta=0.71, c=False), obs.jp(pot=MWPotential2014, type="staeckel", delta=0.71, c=False), obs.jz(pot=MWPotential2014, type="staeckel", delta=0.71, c=False), obs.e(pot=MWPotential2014, type="staeckel", delta=0.71, analytic=True, c=False), obs.zmax( pot=MWPotential2014, type="staeckel", delta=0.71, analytic=True, c=False ), obs.rperi( pot=MWPotential2014, type="staeckel", delta=0.71, analytic=True, c=False ), obs.rap( pot=MWPotential2014, type="staeckel", delta=0.71, analytic=True, c=False ), ) assert numpy.fabs(jr[0] - aASnoc(obs[0])[0]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(jp[0] - aASnoc(obs[0])[1]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(jz[0] - aASnoc(obs[0])[2]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(e[0] - aASnoc.EccZmaxRperiRap(obs[0])[0]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(zmax[0] - aASnoc.EccZmaxRperiRap(obs[0])[1]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(rperi[0] - aASnoc.EccZmaxRperiRap(obs[0])[2]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(rap[0] - aASnoc.EccZmaxRperiRap(obs[0])[3]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jr[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jp[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jz[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(e[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(zmax[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(rperi[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(rap[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) return None def test_orbit_interface_unbound_simple_staeckel_c(): # Test that an unbound orbit in a set of orbits is handled correctly from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 obs = Orbit( [[1.05, 0.02, 1.05, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0]] ) aASc = actionAngleStaeckel(pot=MWPotential2014, delta=0.71, c=True) jr, jp, jz, omr, omp, omz, wr, wp, wz, e, zmax, rperi, rap = ( obs.jr(pot=MWPotential2014, type="staeckel", delta=0.71, c=True), obs.jp(pot=MWPotential2014, type="staeckel", delta=0.71, c=True), obs.jz(pot=MWPotential2014, type="staeckel", delta=0.71, c=True), obs.Or(pot=MWPotential2014, type="staeckel", delta=0.71, c=True), obs.Op(pot=MWPotential2014, type="staeckel", delta=0.71, c=True), obs.Oz(pot=MWPotential2014, type="staeckel", delta=0.71, c=True), obs.wr(pot=MWPotential2014, type="staeckel", delta=0.71, c=True), obs.wp(pot=MWPotential2014, type="staeckel", delta=0.71, c=True), obs.wz(pot=MWPotential2014, type="staeckel", delta=0.71, c=True), obs.e(pot=MWPotential2014, type="staeckel", delta=0.71, analytic=True, c=True), obs.zmax( pot=MWPotential2014, type="staeckel", delta=0.71, analytic=True, c=True ), obs.rperi( pot=MWPotential2014, type="staeckel", delta=0.71, analytic=True, c=True ), obs.rap( pot=MWPotential2014, type="staeckel", delta=0.71, analytic=True, c=True ), ) assert numpy.fabs(jr[0] - aASc(obs[0])[0]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(jp[0] - aASc(obs[0])[1]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(jz[0] - aASc(obs[0])[2]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(omr[0] - aASc.actionsFreqs(obs[0])[3]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(omp[0] - aASc.actionsFreqs(obs[0])[4]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(omz[0] - aASc.actionsFreqs(obs[0])[5]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(wr[0] - aASc.actionsFreqsAngles(obs[0])[6]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(wp[0] - aASc.actionsFreqsAngles(obs[0])[7]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(wz[0] - aASc.actionsFreqsAngles(obs[0])[8]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(e[0] - aASc.EccZmaxRperiRap(obs[0])[0]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(zmax[0] - aASc.EccZmaxRperiRap(obs[0])[1]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(rperi[0] - aASc.EccZmaxRperiRap(obs[0])[2]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(rap[0] - aASc.EccZmaxRperiRap(obs[0])[3]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jr[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jp[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jz[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(omr[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(omp[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(omz[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(wr[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(wp[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(wz[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) return None def test_orbit_interface_unbound_simple_2d_adiabatic(): # Test that an unbound orbit in a set of orbits is handled correctly from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import MWPotential2014 obs = Orbit([[1.05, 0.02, 1.05, 2.0], [1.05, 0.02, 10.05, 2.0]]) # in 2D, adiabatic and Staeckel are the same and the same as spherical aAS = actionAngleSpherical(pot=MWPotential2014) jr, jp, jz = ( obs.jr(pot=MWPotential2014, type="adiabatic"), obs.jp(pot=MWPotential2014, type="adiabatic"), obs.jz(pot=MWPotential2014, type="adiabatic"), ) assert numpy.fabs(jr[0] - aAS(obs[0])[0]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(jp[0] - aAS(obs[0])[1]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(jz[0] - aAS(obs[0])[2]) < 10.0**-10.0, ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jr[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jp[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jz[1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) return None def test_orbit_interface_unbound_simple_2d_staeckel(): # Test that an unbound orbit in a set of orbits is handled correctly from galpy.actionAngle import actionAngleSpherical from galpy.orbit import Orbit from galpy.potential import MWPotential2014 obs = Orbit([[1.05, 0.02, 1.05, 2.0], [1.05, 0.02, 10.05, 2.0]]) # in 2D, adiabatic and Staeckel are the same and the same as spherical aAS = actionAngleSpherical(pot=MWPotential2014) jr, jp, jz = ( obs.jr(pot=MWPotential2014, type="staeckel", delta=0.71), obs.jp(pot=MWPotential2014, type="staeckel", delta=0.71), obs.jz(pot=MWPotential2014, type="staeckel", delta=0.71), ) assert numpy.fabs(jr[0] - aAS(obs[0])[0]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(jp[0] - aAS(obs[0])[1]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.fabs(jz[0] - aAS(obs[0])[2]) < 10.0**-10.0, ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jr[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jp[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jz[1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) return None def test_orbit_interface_unbound_complexshape_adiabatic(): # Test that an unbound orbit in a set of orbits is handled correctly from galpy.actionAngle import actionAngleAdiabatic from galpy.orbit import Orbit from galpy.potential import MWPotential2014 obs = Orbit( numpy.array( [ [ [1.05, 0.02, 1.05, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0], ], [ [1.05, 0.02, 1.05, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0], ], [ [1.05, 0.02, 1.05, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0], ], ] ) ) aAA = actionAngleAdiabatic(pot=MWPotential2014) jr, jp, jz, e, zmax, rperi, rap = ( obs.jr(pot=MWPotential2014, type="adiabatic"), obs.jp(pot=MWPotential2014, type="adiabatic"), obs.jz(pot=MWPotential2014, type="adiabatic"), obs.e(pot=MWPotential2014, type="adiabatic", analytic=True), obs.zmax(pot=MWPotential2014, type="adiabatic", analytic=True), obs.rperi(pot=MWPotential2014, type="adiabatic", analytic=True), obs.rap(pot=MWPotential2014, type="adiabatic", analytic=True), ) assert numpy.all(numpy.fabs(jr[:, 0] - aAA(obs[:, 0])[0]) < 10.0**-10.0), ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.fabs(jp[:, 0] - aAA(obs[:, 0])[1]) < 10.0**-10.0), ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.fabs(jz[:, 0] - aAA(obs[:, 0])[2]) < 10.0**-10.0), ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(e[:, 0] - aAA.EccZmaxRperiRap(obs[:, 0])[0]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(zmax[:, 0] - aAA.EccZmaxRperiRap(obs[:, 0])[1]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(rperi[:, 0] - aAA.EccZmaxRperiRap(obs[:, 0])[2]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(rap[:, 0] - aAA.EccZmaxRperiRap(obs[:, 0])[3]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleAdiabatic does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jr[:, 1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jp[:, 1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jz[:, 1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(e[:, 1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(zmax[:, 1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(rperi[:, 1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(rap[:, 1])), ( "Orbit interface for actionAngleAdiabatic does not return NaN for unbound orbit in a collection with an unbound orbit" ) return None def test_orbit_interface_unbound_complexshape_staeckel(): # Test that an unbound orbit in a set of orbits is handled correctly from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 obs = Orbit( numpy.array( [ [ [1.05, 0.02, 1.05, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0], ], [ [1.05, 0.02, 1.05, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0], ], [ [1.05, 0.02, 1.05, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0], ], ] ) ) aAS = actionAngleStaeckel(pot=MWPotential2014, delta=0.71) jr, jp, jz, omr, omp, omz, wr, wp, wz, e, zmax, rperi, rap = ( obs.jr(pot=MWPotential2014, type="staeckel", delta=0.71), obs.jp(pot=MWPotential2014, type="staeckel", delta=0.71), obs.jz(pot=MWPotential2014, type="staeckel", delta=0.71), obs.Or(pot=MWPotential2014, type="staeckel", delta=0.71), obs.Op(pot=MWPotential2014, type="staeckel", delta=0.71), obs.Oz(pot=MWPotential2014, type="staeckel", delta=0.71), obs.wr(pot=MWPotential2014, type="staeckel", delta=0.71), obs.wp(pot=MWPotential2014, type="staeckel", delta=0.71), obs.wz(pot=MWPotential2014, type="staeckel", delta=0.71), obs.e(pot=MWPotential2014, type="staeckel", delta=0.71, analytic=True), obs.zmax(pot=MWPotential2014, type="staeckel", delta=0.71, analytic=True), obs.rperi(pot=MWPotential2014, type="staeckel", delta=0.71, analytic=True), obs.rap(pot=MWPotential2014, type="staeckel", delta=0.71, analytic=True), ) assert numpy.all(numpy.fabs(jr[:, 0] - aAS(obs[:, 0])[0]) < 10.0**-10.0), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.fabs(jp[:, 0] - aAS(obs[:, 0])[1]) < 10.0**-10.0), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.fabs(jz[:, 0] - aAS(obs[:, 0])[2]) < 10.0**-10.0), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(omr[:, 0] - aAS.actionsFreqs(obs[:, 0])[3]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(omp[:, 0] - aAS.actionsFreqs(obs[:, 0])[4]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(omz[:, 0] - aAS.actionsFreqs(obs[:, 0])[5]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(wr[:, 0] - aAS.actionsFreqsAngles(obs[:, 0])[6]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(wp[:, 0] - aAS.actionsFreqsAngles(obs[:, 0])[7]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(wz[:, 0] - aAS.actionsFreqsAngles(obs[:, 0])[8]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(e[:, 0] - aAS.EccZmaxRperiRap(obs[:, 0])[0]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(zmax[:, 0] - aAS.EccZmaxRperiRap(obs[:, 0])[1]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(rperi[:, 0] - aAS.EccZmaxRperiRap(obs[:, 0])[2]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(rap[:, 0] - aAS.EccZmaxRperiRap(obs[:, 0])[3]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jr[:, 1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jp[:, 1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jz[:, 1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) return None def test_orbit_interface_unbound_staeckeldelta_handling(): # Test that the automagically determined delta is handled correctly when there are unbound orbits # Use a complex shape from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 obs = Orbit( numpy.array( [ [ [1.15, 0.02, 1.15, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0], ], [ [1.02, 0.02, 0.95, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0], ], [ [0.97, 0.02, 1.25, 0.03, 0.0, 2.0], [1.05, 0.02, 10.05, 0.03, 0.0, 2.0], ], ] ) ) # Compute the actions with the automagically determined delta using the orbit interface jr, jp, jz, omr, omp, omz, wr, wp, wz, e, zmax, rperi, rap = ( obs.jr(pot=MWPotential2014, type="staeckel"), obs.jp(pot=MWPotential2014, type="staeckel"), obs.jz(pot=MWPotential2014, type="staeckel"), obs.Or(pot=MWPotential2014, type="staeckel"), obs.Op(pot=MWPotential2014, type="staeckel"), obs.Oz(pot=MWPotential2014, type="staeckel"), obs.wr(pot=MWPotential2014, type="staeckel"), obs.wp(pot=MWPotential2014, type="staeckel"), obs.wz(pot=MWPotential2014, type="staeckel"), obs.e(pot=MWPotential2014, type="staeckel", analytic=True), obs.zmax(pot=MWPotential2014, type="staeckel", analytic=True), obs.rperi(pot=MWPotential2014, type="staeckel", analytic=True), obs.rap(pot=MWPotential2014, type="staeckel", analytic=True), ) # Now do the same with the actionAngle interface aAS = actionAngleStaeckel(pot=MWPotential2014, delta=0.71) # just a dummy delta bound_indx = numpy.array([True, False, True, False, True, False]) assert numpy.all( numpy.fabs(jr[:, 0] - aAS(obs[:, 0], delta=obs._aA._delta[bound_indx])[0]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(jp[:, 0] - aAS(obs[:, 0], delta=obs._aA._delta[bound_indx])[1]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs(jz[:, 0] - aAS(obs[:, 0], delta=obs._aA._delta[bound_indx])[2]) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs( omr[:, 0] - aAS.actionsFreqs(obs[:, 0], delta=obs._aA._delta[bound_indx])[3] ) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs( omp[:, 0] - aAS.actionsFreqs(obs[:, 0], delta=obs._aA._delta[bound_indx])[4] ) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs( omz[:, 0] - aAS.actionsFreqs(obs[:, 0], delta=obs._aA._delta[bound_indx])[5] ) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs( wr[:, 0] - aAS.actionsFreqsAngles(obs[:, 0], delta=obs._aA._delta[bound_indx])[6] ) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs( wp[:, 0] - aAS.actionsFreqsAngles(obs[:, 0], delta=obs._aA._delta[bound_indx])[7] ) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs( wz[:, 0] - aAS.actionsFreqsAngles(obs[:, 0], delta=obs._aA._delta[bound_indx])[8] ) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs( e[:, 0] - aAS.EccZmaxRperiRap(obs[:, 0], delta=obs._aA._delta[bound_indx])[0] ) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs( zmax[:, 0] - aAS.EccZmaxRperiRap(obs[:, 0], delta=obs._aA._delta[bound_indx])[1] ) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs( rperi[:, 0] - aAS.EccZmaxRperiRap(obs[:, 0], delta=obs._aA._delta[bound_indx])[2] ) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all( numpy.fabs( rap[:, 0] - aAS.EccZmaxRperiRap(obs[:, 0], delta=obs._aA._delta[bound_indx])[3] ) < 10.0**-10.0 ), ( "Orbit interface for actionAngleStaeckel does not return the same as actionAngle interface for bound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jr[:, 1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jp[:, 1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) assert numpy.all(numpy.isnan(jz[:, 1])), ( "Orbit interface for actionAngleStaeckel does not return NaN for unbound orbit in a collection with an unbound orbit" ) return None # Test physical output for actionAngleStaeckel def test_physical_staeckel(): from galpy.actionAngle import actionAngleStaeckel from galpy.potential import MWPotential from galpy.util import conversion ro, vo = 7.0, 230.0 aA = actionAngleStaeckel(pot=MWPotential, delta=0.71, ro=ro, vo=vo) aAnu = actionAngleStaeckel(pot=MWPotential, delta=0.71) for ii in range(3): assert ( numpy.fabs( aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] - aAnu(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), "actionAngle function __call__ does not return Quantity with the right value" for ii in range(3): assert ( numpy.fabs( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) for ii in range(6, 9): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) return None # Test the b estimation def test_estimateBIsochrone(): from galpy.actionAngle import estimateBIsochrone from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) o = Orbit([1.1, 0.3, 1.2, 0.2, 0.5, 2.0]) times = numpy.linspace(0.0, 100.0, 1001) o.integrate(times, ip) bmin, bmed, bmax = estimateBIsochrone(ip, o.R(times), o.z(times)) assert numpy.fabs(bmed - 1.2) < 10.0**-15.0, ( "Estimated scale parameter b when estimateBIsochrone is applied to an IsochronePotential is wrong" ) return None # Test the focal delta estimation def test_estimateDeltaStaeckel(): from galpy.actionAngle import estimateDeltaStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential o = Orbit([1.1, 0.05, 1.1, 0.05, 0.0, 2.0]) times = numpy.linspace(0.0, 100.0, 1001) o.integrate(times, MWPotential) delta = estimateDeltaStaeckel(MWPotential, o.R(times), o.z(times)) assert numpy.fabs(delta - 0.71) < 10.0**-3.0, ( "Estimated focal parameter delta when estimateDeltaStaeckel is applied to the MWPotential is wrong" ) return None # Test the focal delta estimation def test_estimateDeltaStaeckel_spherical(): from galpy.actionAngle import estimateDeltaStaeckel from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential o = Orbit([1.1, 0.05, 1.1, 0.05, 0.0, 2.0]) times = numpy.linspace(0.0, 100.0, 1001) lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) o.integrate(times, lp) # Need to set delta0=0 so spherical actualluy returns 0 delta = estimateDeltaStaeckel(lp, o.R(), o.z(), delta0=0.0) assert numpy.fabs(delta) < 10.0**-6.0, ( "Estimated focal parameter delta when estimateDeltaStaeckel is applied to a spherical potential is wrong" ) delta = estimateDeltaStaeckel(lp, o.R(times), o.z(times), delta0=0.0) assert numpy.fabs(delta) < 10.0**-16.0, ( "Estimated focal parameter delta when estimateDeltaStaeckel is applied to a spherical potential is wrong" ) return None # Test that setting up the non-spherical actionAngle routines raises a warning when using MWPotential, see #229 def test_MWPotential_warning_adiabatic(): # Test that using MWPotential throws a warning, see #229 from galpy.actionAngle import actionAngleAdiabatic, actionAngleAdiabaticGrid from galpy.potential import MWPotential with warnings.catch_warnings(record=True) as w: if PY2: reset_warning_registry("galpy") warnings.simplefilter("always", galpyWarning) aAA = actionAngleAdiabatic(pot=MWPotential, gamma=1.0) # Should raise warning bc of MWPotential, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "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" ) if raisedWarning: break assert raisedWarning, ( "actionAngleAdiabatic with MWPotential should have thrown a warning, but didn't" ) # Grid with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) aAA = actionAngleAdiabaticGrid( pot=MWPotential, gamma=1.0, nEz=5, nEr=5, nLz=5, nR=5 ) # Should raise warning bc of MWPotential, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "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" ) if raisedWarning: break assert raisedWarning, ( "actionAngleAdiabaticGrid with MWPotential should have thrown a warning, but didn't" ) return None def test_MWPotential_warning_staeckel(): # Test that using MWPotential throws a warning, see #229 from galpy.actionAngle import actionAngleStaeckel, actionAngleStaeckelGrid from galpy.potential import MWPotential with warnings.catch_warnings(record=True) as w: if PY2: reset_warning_registry("galpy") warnings.simplefilter("always", galpyWarning) aAA = actionAngleStaeckel(pot=MWPotential, delta=0.5) # Should raise warning bc of MWPotential, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "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" ) if raisedWarning: break assert raisedWarning, ( "actionAngleStaeckel with MWPotential should have thrown a warning, but didn't" ) # Grid with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) aAA = actionAngleStaeckelGrid(pot=MWPotential, delta=0.5, nE=5, npsi=5, nLz=5) # Should raise warning bc of MWPotential, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "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" ) if raisedWarning: break assert raisedWarning, ( "actionAngleStaeckelGrid with MWPotential should have thrown a warning, but didn't" ) return None def test_MWPotential_warning_isochroneapprox(): # Test that using MWPotential throws a warning, see #229 from galpy.actionAngle import actionAngleIsochroneApprox from galpy.potential import MWPotential with warnings.catch_warnings(record=True) as w: if PY2: reset_warning_registry("galpy") warnings.simplefilter("always", galpyWarning) aAA = actionAngleIsochroneApprox(pot=MWPotential, b=1.0) # Should raise warning bc of MWPotential, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "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" ) if raisedWarning: break assert raisedWarning, ( "actionAngleIsochroneApprox with MWPotential should have thrown a warning, but didn't" ) return None # Test of the fix to issue 361 def test_actionAngleAdiabatic_issue361(): from galpy import actionAngle from galpy.potential import MWPotential2014 aA_adi = actionAngle.actionAngleAdiabatic(pot=MWPotential2014, c=True) R = 8.7007 / 8.0 vT = 188.5 / 220.0 jr_good, _, _ = aA_adi(R, -0.1 / 220.0, vT, 0, 0) jr_bad, _, _ = aA_adi(R, -0.09 / 220.0, vT, 0, 0) assert numpy.fabs(jr_good - jr_bad) < 1e-6, ( f"Nearby JR for orbit near apocenter disagree too much, likely because one completely fails: Jr_good = {jr_good}, Jr_bad = {jr_bad}" ) return None # Test that evaluating actionAngle with multi-dimensional orbit doesn't work def test_actionAngle_orbitInput_multid_error(): from galpy.actionAngle import actionAngleStaeckel from galpy.orbit import Orbit from galpy.potential import MWPotential2014 orbits = Orbit( numpy.array( [ [[1.0, 0.1, 1.1, -0.1, -0.2, 0.0], [1.0, 0.2, 1.2, 0.0, -0.1, 1.0]], [[1.0, -0.2, 0.9, 0.2, 0.2, 2.0], [1.2, -0.4, 1.1, -0.1, 0.0, -2.0]], [[1.0, 0.2, 0.9, 0.3, -0.2, 0.1], [1.2, 0.4, 1.1, -0.2, 0.05, 4.0]], ] ) ) aAS = actionAngleStaeckel(pot=MWPotential2014, delta=0.45, c=True) with pytest.raises(RuntimeError) as excinfo: aAS(orbits) pytest.fail( "Evaluating actionAngle methods with Orbit instances with multi-dimensional shapes is not support" ) return None # Test that actionAngleHarmonicInverse is the inverse of actionAngleHarmonic def test_actionAngleHarmonicInverse_wrtHarmonic(): # Create harmonic oscillator potential as isochrone w/ large b --> 1D from galpy.actionAngle import actionAngleHarmonic, actionAngleHarmonicInverse from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=5.0, b=10000.0) ipz = ip.toVertical(1.2) # Omega = sqrt(4piG density / 3) aAH = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) aAHI = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) # Check a few orbits x, vx = 0.1, -0.3 obs = Orbit([x, vx]) times = numpy.linspace(0.0, 30.0, 1001) obs.integrate(times, ipz) j, _, a = aAH.actionsFreqsAngles(obs.x(times), obs.vx(times)) xi, vxi = aAHI(numpy.median(j), a) assert numpy.amax(numpy.fabs(obs.x(times) - xi)) < 10.0**-6.0, ( "actionAngleHarmonicInverse is not the inverse of actionAngleHarmonic for an example orbit" ) assert numpy.amax(numpy.fabs(obs.vx(times) - vxi)) < 10.0**-6.0, ( "actionAngleHarmonicInverse is not the inverse of actionAngleHarmonic for an example orbit" ) return None def test_actionAngleHarmonicInverse_freqs_wrtHarmonic(): # Create harmonic oscillator potential as isochrone w/ large b --> 1D from galpy.actionAngle import actionAngleHarmonic, actionAngleHarmonicInverse from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=5.0, b=10000.0) # Omega = sqrt(4piG density / 3) aAH = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) aAHI = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) tol = -10.0 j = 0.1 Om = aAHI.Freqs(j) # Compute frequency with actionAngleHarmonic _, Omi = aAH.actionsFreqs(*aAHI(j, 0.0)) assert numpy.fabs((Om - Omi) / Om) < 10.0**tol, ( "Frequency computed using actionAngleHarmonicInverse does not agree with that computed by actionAngleHarmonic" ) return None # Test that orbit from actionAngleHarmonicInverse is the same as an integrated orbit def test_actionAngleHarmonicInverse_orbit(): # Create harmonic oscillator potential as isochrone w/ large b --> 1D from galpy.actionAngle import actionAngleHarmonicInverse from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=5.0, b=10000.0) ipz = ip.toVertical(1.2) # Omega = sqrt(4piG density / 3) aAHI = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) j = 0.01 # First calculate frequencies and the initial x,v xvom = aAHI.xvFreqs(j, numpy.array([0.1])) om = xvom[2:] # Angles along an orbit ts = numpy.linspace(0.0, 20.0, 1001) angle = 0.1 + ts * om[0] # Calculate the orbit using actionAngleHarmonicInverse xv = aAHI(j, angle) # Calculate the orbit using orbit integration orb = Orbit([xvom[0][0], xvom[1][0]]) orb.integrate(ts, ipz, method="dopr54_c") # Compare tol = -7.0 assert numpy.all(numpy.fabs(orb.x(ts) - xv[0]) < 10.0**tol), ( "Integrated orbit does not agree with actionAngleHarmmonicInverse orbit in x" ) assert numpy.all(numpy.fabs(orb.vx(ts) - xv[1]) < 10.0**tol), ( "Integrated orbit does not agree with actionAngleHarmmonicInverse orbit in v" ) return None # Test physical output for actionAngleHarmonicInverse def test_physical_actionAngleHarmonicInverse(): # Create harmonic oscillator potential as isochrone w/ large b --> 1D from galpy.actionAngle import actionAngleHarmonicInverse from galpy.potential import IsochronePotential from galpy.util import conversion ip = IsochronePotential(normalize=5.0, b=10000.0) ro, vo = 7.0, 230.0 aAHI = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=ro, vo=vo ) aAHInu = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) correct_fac = [ro, vo] for ii in range(2): assert ( numpy.fabs(aAHI(0.1, -0.2)[ii] - aAHInu(0.1, -0.2)[ii] * correct_fac[ii]) < 10.0**-8.0 ), ( "actionAngleInverse function __call__ does not return Quantity with the right value" ) correct_fac = [ro, vo, conversion.freq_in_Gyr(vo, ro)] for ii in range(3): assert ( numpy.fabs( aAHI.xvFreqs(0.1, -0.2)[ii] - aAHInu.xvFreqs(0.1, -0.2)[ii] * correct_fac[ii] ) < 10.0**-8.0 ), ( "actionAngleInverse function xvFreqs does not return Quantity with the right value" ) assert ( numpy.fabs(aAHI.Freqs(0.1) - aAHInu.Freqs(0.1) * conversion.freq_in_Gyr(vo, ro)) < 10.0**-8.0 ), "actionAngleInverse function Freqs does not return Quantity with the right value" return None # Test that actionAngleIsochroneInverse is the inverse of actionAngleIsochrone def test_actionAngleIsochroneInverse_wrtIsochrone(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneInverse from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=2.0, b=1.5) aAI = actionAngleIsochrone(ip=ip) aAII = actionAngleIsochroneInverse(ip=ip) # Check a few orbits tol = -7.0 R, vR, vT, z, vz, phi = 1.1, 0.1, 1.1, 0.1, 0.2, 2.3 o = Orbit([R, vR, vT, z, vz, phi]) check_actionAngleIsochroneInverse_wrtIsochrone(ip, aAI, aAII, o, tol, ntimes=1001) R, vR, vT, z, vz, phi = 1.1, 0.1, -1.1, 0.1, 0.2, 2.3 o = Orbit([R, vR, vT, z, vz, phi]) check_actionAngleIsochroneInverse_wrtIsochrone(ip, aAI, aAII, o, tol, ntimes=1001) R, vR, vT, z, vz, phi = 1.1, -0.1, 1.1, 0.1, 0.2, 0.3 o = Orbit([R, vR, vT, z, vz, phi]) check_actionAngleIsochroneInverse_wrtIsochrone(ip, aAI, aAII, o, tol, ntimes=1001) R, vR, vT, z, vz, phi = 1.1, -0.1, 1.1, 0.1, -0.2, 0.3 o = Orbit([R, vR, vT, z, vz, phi]) check_actionAngleIsochroneInverse_wrtIsochrone(ip, aAI, aAII, o, tol, ntimes=1001) R, vR, vT, z, vz, phi = 1.1, -4.1, 1.1, 0.1, -0.2, 0.3 o = Orbit([R, vR, vT, z, vz, phi]) check_actionAngleIsochroneInverse_wrtIsochrone(ip, aAI, aAII, o, tol, ntimes=1001) return None # Test that actionAngleIsochroneInverse is the inverse of actionAngleIsochrone, # for an orbit that is not inclined (at z=0); possibly problematic, because # the longitude of the ascending node is ambiguous; set to zero by convention # in actionAngleIsochrone def test_actionAngleIsochroneInverse_wrtIsochrone_noninclinedorbit(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneInverse from galpy.orbit import Orbit from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=2.0, b=1.5) aAI = actionAngleIsochrone(ip=ip) aAII = actionAngleIsochroneInverse(ip=ip) # Check a few orbits tol = -7.0 R, vR, vT, z, vz, phi = 1.1, 0.1, 1.1, 0.0, 0.0, 2.3 o = Orbit([R, vR, vT, z, vz, phi]) check_actionAngleIsochroneInverse_wrtIsochrone(ip, aAI, aAII, o, tol, ntimes=1001) R, vR, vT, z, vz, phi = 1.1, 0.1, -1.1, 0.0, 0.0, 2.3 o = Orbit([R, vR, vT, z, vz, phi]) check_actionAngleIsochroneInverse_wrtIsochrone(ip, aAI, aAII, o, tol, ntimes=1001) # also some almost non-inclined orbits eps = 1e-10 R, vR, vT, z, vz, phi = 1.1, 0.1, 1.1, 0.0, eps, 2.3 o = Orbit([R, vR, vT, z, vz, phi]) check_actionAngleIsochroneInverse_wrtIsochrone(ip, aAI, aAII, o, tol, ntimes=1001) R, vR, vT, z, vz, phi = 1.1, 0.1, -1.1, 0.0, eps, 2.3 o = Orbit([R, vR, vT, z, vz, phi]) check_actionAngleIsochroneInverse_wrtIsochrone(ip, aAI, aAII, o, tol, ntimes=1001) return None # Basic sanity checking: close-to-circular orbit should have freq. = epicycle freq. def test_actionAngleIsochroneInverse_basic_freqs(): from galpy.actionAngle import actionAngleIsochroneInverse from galpy.potential import IsochronePotential, epifreq, omegac, rl, verticalfreq jr = 10.0**-6.0 jz = 10.0**-6.0 ip = IsochronePotential(normalize=1.0) aAII = actionAngleIsochroneInverse(ip=ip) tol = -5.0 # at Lz=1 jphi = 1.0 om = aAII.Freqs(jr, jphi, jz) assert numpy.fabs((om[0] - epifreq(ip, rl(ip, jphi))) / om[0]) < 10.0**tol, ( "Close-to-circular orbit does not have Or=kappa for actionAngleTorus" ) assert numpy.fabs((om[1] - omegac(ip, rl(ip, jphi))) / om[1]) < 10.0**tol, ( "Close-to-circular orbit does not have Ophi=omega for actionAngleTorus" ) assert numpy.fabs((om[2] - verticalfreq(ip, rl(ip, jphi))) / om[2]) < 10.0**tol, ( "Close-to-circular orbit does not have Oz=nu for actionAngleTorus" ) # at Lz=1.5, w/ different potential normalization ip = IsochronePotential(normalize=1.2) aAII = actionAngleIsochroneInverse(ip=ip) jphi = 1.5 om = aAII.Freqs(jr, jphi, jz) assert numpy.fabs((om[0] - epifreq(ip, rl(ip, jphi))) / om[0]) < 10.0**tol, ( "Close-to-circular orbit does not have Or=kappa for actionAngleTorus" ) assert numpy.fabs((om[1] - omegac(ip, rl(ip, jphi))) / om[1]) < 10.0**tol, ( "Close-to-circular orbit does not have Ophi=omega for actionAngleTorus" ) assert numpy.fabs((om[2] - verticalfreq(ip, rl(ip, jphi))) / om[2]) < 10.0**tol, ( "Close-to-circular orbit does not have Oz=nu for actionAngleTorus" ) return None def test_actionAngleIsochroneInverse_freqs_wrtIsochrone(): from galpy.actionAngle import actionAngleIsochrone, actionAngleIsochroneInverse from galpy.potential import IsochronePotential jr = 0.1 jz = 0.2 ip = IsochronePotential(normalize=1.04, b=1.2) aAI = actionAngleIsochrone(ip=ip) aAII = actionAngleIsochroneInverse(ip=ip) # at Lz=1 tol = -10.0 jphi = 1.0 Or, Op, Oz = aAII.Freqs(jr, jphi, jz) # Compute frequency with actionAngleIsochrone _, _, _, Ori, Opi, Ozi = aAI.actionsFreqs(*aAII(jr, jphi, jz, 0.0, 1.0, 2.0)[:6]) assert numpy.fabs((Or - Ori) / Or) < 10.0**tol, ( "Radial frequency computed using actionAngleIsochroneInverse does not agree with that computed by actionAngleIsochrone" ) assert numpy.fabs((Op - Opi) / Op) < 10.0**tol, ( "Azimuthal frequency computed using actionAngleIsochroneInverse does not agree with that computed by actionAngleIsochrone" ) assert numpy.fabs((Oz - Ozi) / Oz) < 10.0**tol, ( "Vertical frequency computed using actionAngleIsochroneInverse does not agree with that computed by actionAngleIsochrone" ) # at Lz=1.5 jphi = 1.51 Or, Op, Oz = aAII.Freqs(jr, jphi, jz) # Compute frequency with actionAngleIsochrone _, _, _, Ori, Opi, Ozi = aAI.actionsFreqs(*aAII(jr, jphi, jz, 0.0, 1.0, 2.0)[:6]) assert numpy.fabs((Or - Ori) / Or) < 10.0**tol, ( "Radial frequency computed using actionAngleIsochroneInverse does not agree with that computed by actionAngleIsochrone" ) assert numpy.fabs((Op - Opi) / Op) < 10.0**tol, ( "Azimuthal frequency computed using actionAngleIsochroneInverse does not agree with that computed by actionAngleIsochrone" ) assert numpy.fabs((Oz - Ozi) / Oz) < 10.0**tol, ( "Vertical frequency computed using actionAngleIsochroneInverse does not agree with that computed by actionAngleIsochrone" ) return None # Test that orbit from actionAngleIsochroneInverse is the same as an integrated orbit def test_actionAngleIsochroneInverse_orbit(): from galpy.actionAngle.actionAngleIsochroneInverse import ( actionAngleIsochroneInverse, ) from galpy.orbit import Orbit from galpy.potential import IsochronePotential # Set up instance ip = IsochronePotential(normalize=1.03, b=1.2) aAII = actionAngleIsochroneInverse(ip=ip) jr, jphi, jz = 0.05, 1.1, 0.025 # First calculate frequencies and the initial RvR RvRom = aAII.xvFreqs( jr, jphi, jz, numpy.array([0.0]), numpy.array([1.0]), numpy.array([2.0]) ) om = RvRom[6:] # Angles along an orbit ts = numpy.linspace(0.0, 100.0, 1001) angler = ts * om[0] anglephi = 1.0 + ts * om[1] anglez = 2.0 + ts * om[2] # Calculate the orbit using actionAngleTorus RvR = aAII(jr, jphi, jz, angler, anglephi, anglez) # Calculate the orbit using orbit integration orb = Orbit( [RvRom[0][0], RvRom[1][0], RvRom[2][0], RvRom[3][0], RvRom[4][0], RvRom[5][0]] ) orb.integrate(ts, ip) # Compare tol = -3.0 assert numpy.all(numpy.fabs(orb.R(ts) - RvR[0]) < 10.0**tol), ( "Integrated orbit does not agree with torus orbit in R" ) assert numpy.all(numpy.fabs(orb.vR(ts) - RvR[1]) < 10.0**tol), ( "Integrated orbit does not agree with torus orbit in vR" ) assert numpy.all(numpy.fabs(orb.vT(ts) - RvR[2]) < 10.0**tol), ( "Integrated orbit does not agree with torus orbit in vT" ) assert numpy.all(numpy.fabs(orb.z(ts) - RvR[3]) < 10.0**tol), ( "Integrated orbit does not agree with torus orbit in z" ) assert numpy.all(numpy.fabs(orb.vz(ts) - RvR[4]) < 10.0**tol), ( "Integrated orbit does not agree with torus orbit in vz" ) assert numpy.all( numpy.fabs((orb.phi(ts) - RvR[5] + numpy.pi) % (2.0 * numpy.pi) - numpy.pi) < 10.0**tol ), "Integrated orbit does not agree with torus orbit in phi" return None # Test physical output for actionAngleIsochroneInverse def test_physical_actionAngleIsochroneInverse(): from galpy.actionAngle import actionAngleIsochroneInverse from galpy.potential import IsochronePotential from galpy.util import conversion ro, vo = 7.0, 230.0 ip = IsochronePotential(normalize=1.01, b=1.02) aAII = actionAngleIsochroneInverse(ip=ip, ro=ro, vo=vo) aAIInu = actionAngleIsochroneInverse(ip=ip) correct_fac = [ro, vo, vo, ro, vo, 1.0] for ii in range(6): assert ( numpy.fabs( aAII(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii] - aAIInu(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii] * correct_fac[ii] ) < 10.0**-8.0 ), ( "actionAngleInverse function __call__ does not return Quantity with the right value" ) correct_fac = [ ro, vo, vo, ro, vo, 1.0, conversion.freq_in_Gyr(vo, ro), conversion.freq_in_Gyr(vo, ro), conversion.freq_in_Gyr(vo, ro), ] for ii in range(9): assert ( numpy.fabs( aAII.xvFreqs(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii] - aAIInu.xvFreqs(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii] * correct_fac[ii] ) < 10.0**-8.0 ), ( "actionAngleInverse function xvFreqs does not return Quantity with the right value" ) for ii in range(3): assert ( numpy.fabs( aAII.Freqs(0.1, 1.1, 0.1)[ii] - aAIInu.Freqs(0.1, 1.1, 0.1)[ii] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngleInverse function Freqs does not return Quantity with the right value" ) return None # Test that actionAngleVerticalInverse is the inverse of actionAngleVertical def test_actionAngleVerticalInverse_wrtVertical(): from galpy.actionAngle import actionAngleVertical, actionAngleVerticalInverse from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAV = actionAngleVertical(pot=isopot) # Check a few orbits x, vx = 0.1, -0.3 obs = Orbit([x, vx]) times = numpy.linspace(0.0, 30.0, 1001) obs.integrate(times, isopot) j, _, a = aAV.actionsFreqsAngles(obs.x(times), obs.vx(times)) # Set up actionAngleVerticalInverse for this energy aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[obs.E()], use_pointtransform=False ) xi, vxi = aAVI(aAVI.J(obs.E()), a) assert numpy.amax(numpy.fabs(obs.x(times) - xi)) < 10.0**-6.0, ( "actionAngleVerticalInverse is not the inverse of actionAngleVertical for an example orbit" ) assert numpy.amax(numpy.fabs(obs.vx(times) - vxi)) < 10.0**-6.0, ( "actionAngleVerticalInverse is not the inverse of actionAngleVertical for an example orbit" ) return None def test_actionAngleVerticalInverse_freqs_wrtVertical(): from galpy.actionAngle import actionAngleVertical, actionAngleVerticalInverse from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAV = actionAngleVertical(pot=isopot) x, vx = 0.1, -0.3 obs = Orbit([x, vx]) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[obs.E(pot=isopot)], use_pointtransform=False ) tol = -10.0 Om = aAVI.Freqs(aAVI.J(obs.E(pot=isopot))) # Compute frequency with actionAngleHarmonic _, Omi = aAV.actionsFreqs(*aAVI(aAVI.J(obs.E(pot=isopot)), 0.0)) assert numpy.fabs((Om - Omi) / Om) < 10.0**tol, ( "Frequency computed using actionAngleVerticalInverse does not agree with that computed by actionAngleVertical" ) return None # Test that orbit from actionAngleVerticalInverse is the same as an integrated orbit def test_actionAngleVerticalInverse_orbit(): from galpy.actionAngle import actionAngleVerticalInverse from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential, evaluatelinearPotentials # Set up instance isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[0.1, 1.0, 10.0], use_pointtransform=False ) ta = numpy.linspace(0.0, 2.0 * numpy.pi, 1001) x, v = aAVI(aAVI.J(1.0), ta) # Compute energy and check whether it's conserved E = evaluatelinearPotentials(isopot, x) + v**2.0 / 2.0 assert numpy.std(E) / numpy.mean(E) < 1e-10, ( "Energy is not conserved along the actionAngleVerticalInverse torus for the IsothermalDiskPotential when using a point transform" ) # Now traverse the orbit at the frequency rate and check against orbit integration Om = aAVI.Freqs(aAVI.J(1.0)) ts = numpy.linspace(0.0, 2.0 * numpy.pi / Om, 1001) x, v = aAVI(aAVI.J(1.0), Om * ts) orb = Orbit([x[0], v[0]]) orb.integrate(ts, isopot) assert numpy.amax(numpy.fabs(orb.x(ts) - x)) < 1e-8, ( "Position does not agree with that of the integrated orbit along the torus of the IsothermalDiskPotential when using a point transform" ) assert numpy.amax(numpy.fabs(orb.vx(ts) - v)) < 1e-8, ( "Velocity does not agree with that of the integrated orbit along the torus of the IsothermalDiskPotential when using a point transform" ) return None # Test that actionAngleVerticalInverse is the inverse of actionAngleVertical # when using a point transformation def test_actionAngleVerticalInverse_wrtVertical_pointtransform(): from galpy.actionAngle import actionAngleVertical, actionAngleVerticalInverse from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAV = actionAngleVertical(pot=isopot) # Check a few orbits x, vx = 0.1, -0.3 obs = Orbit([x, vx]) times = numpy.linspace(0.0, 30.0, 1001) obs.integrate(times, isopot) j, _, a = aAV.actionsFreqsAngles(obs.x(times), obs.vx(times)) # Set up actionAngleVerticalInverse for this energy aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[obs.E()], use_pointtransform=True ) xi, vxi = aAVI(aAVI.J(obs.E()), a) assert numpy.amax(numpy.fabs(obs.x(times) - xi)) < 10.0**-6.0, ( "actionAngleVerticalInverse is not the inverse of actionAngleVertical for an example orbit when using a point transform" ) assert numpy.amax(numpy.fabs(obs.vx(times) - vxi)) < 10.0**-6.0, ( "actionAngleVerticalInverse is not the inverse of actionAngleVertical for an example orbit when using a point transform" ) return None def test_actionAngleVerticalInverse_freqs_wrtVertical_pointtransform(): from galpy.actionAngle import actionAngleVertical, actionAngleVerticalInverse from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAV = actionAngleVertical(pot=isopot) x, vx = 0.1, -0.3 obs = Orbit([x, vx]) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[obs.E(pot=isopot)], use_pointtransform=True ) tol = -10.0 Om = aAVI.Freqs(aAVI.J(obs.E(pot=isopot))) # Compute frequency with actionAngleHarmonic _, Omi = aAV.actionsFreqs(*aAVI(aAVI.J(obs.E(pot=isopot)), 0.0)) assert numpy.fabs((Om - Omi) / Om) < 10.0**tol, ( "Frequency computed using actionAngleVerticalInverse does not agree with that computed by actionAngleVertical when using a point transform" ) return None # Test that orbit from actionAngleVerticalInverse is the same as an integrated # orbit when using a point transformation def test_actionAngleVerticalInverse_orbit_pointtransform(): from galpy.actionAngle import actionAngleVerticalInverse from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential, evaluatelinearPotentials # Set up instance isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[0.1, 1.0, 10.0], use_pointtransform=True ) ta = numpy.linspace(0.0, 2.0 * numpy.pi, 1001) x, v = aAVI(aAVI.J(1.0), ta) # Compute energy and check whether it's conserved E = evaluatelinearPotentials(isopot, x) + v**2.0 / 2.0 assert numpy.std(E) / numpy.mean(E) < 1e-10, ( "Energy is not conserved along the actionAngleVerticalInverse torus for the IsothermalDiskPotential when using a point transform" ) # Now traverse the orbit at the frequency rate and check against orbit integration Om = aAVI.Freqs(aAVI.J(1.0)) ts = numpy.linspace(0.0, 2.0 * numpy.pi / Om, 1001) x, v = aAVI(aAVI.J(1.0), Om * ts) orb = Orbit([x[0], v[0]]) orb.integrate(ts, isopot) assert numpy.amax(numpy.fabs(orb.x(ts) - x)) < 1e-8, ( "Position does not agree with that of the integrated orbit along the torus of the IsothermalDiskPotential when using a point transform" ) assert numpy.amax(numpy.fabs(orb.vx(ts) - v)) < 1e-8, ( "Velocity does not agree with that of the integrated orbit along the torus of the IsothermalDiskPotential when using a point transform" ) return None # Test that actionAngleVerticalInverse is the inverse of actionAngleVertical # when using only bisection to solve equations def test_actionAngleVerticalInverse_wrtVertical_bisect(): from galpy.actionAngle import actionAngleVertical, actionAngleVerticalInverse from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAV = actionAngleVertical(pot=isopot) # Check a few orbits x, vx = 0.1, -0.3 obs = Orbit([x, vx]) times = numpy.linspace(0.0, 30.0, 1001) obs.integrate(times, isopot) j, _, a = aAV.actionsFreqsAngles(obs.x(times), obs.vx(times)) # Set up actionAngleVerticalInverse for this energy aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[obs.E()], use_pointtransform=False, bisect=True ) xi, vxi = aAVI(aAVI.J(obs.E()), a) assert numpy.amax(numpy.fabs(obs.x(times) - xi)) < 10.0**-6.0, ( "actionAngleVerticalInverse is not the inverse of actionAngleVertical for an example orbit when using bisection" ) assert numpy.amax(numpy.fabs(obs.vx(times) - vxi)) < 10.0**-6.0, ( "actionAngleVerticalInverse is not the inverse of actionAngleVertical for an example orbit when using bisection" ) return None def test_actionAngleVerticalInverse_freqs_wrtVertical_bisect(): from galpy.actionAngle import actionAngleVertical, actionAngleVerticalInverse from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAV = actionAngleVertical(pot=isopot) x, vx = 0.1, -0.3 obs = Orbit([x, vx]) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[obs.E(pot=isopot)], use_pointtransform=False, bisect=True, ) tol = -10.0 Om = aAVI.Freqs(aAVI.J(obs.E(pot=isopot))) # Compute frequency with actionAngleHarmonic _, Omi = aAV.actionsFreqs(*aAVI(aAVI.J(obs.E(pot=isopot)), 0.0)) assert numpy.fabs((Om - Omi) / Om) < 10.0**tol, ( "Frequency computed using actionAngleVerticalInverse does not agree with that computed by actionAngleVertical when using bisection" ) return None # Test that orbit from actionAngleVerticalInverse is the same as an integrated orbit def test_actionAngleVerticalInverse_orbit_bisect(): from galpy.actionAngle import actionAngleVerticalInverse from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential, evaluatelinearPotentials # Set up instance isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[0.1, 1.0, 10.0], use_pointtransform=False, bisect=True, ) ta = numpy.linspace(0.0, 2.0 * numpy.pi, 1001) x, v = aAVI(aAVI.J(1.0), ta) # Compute energy and check whether it's conserved E = evaluatelinearPotentials(isopot, x) + v**2.0 / 2.0 assert numpy.std(E) / numpy.mean(E) < 1e-10, ( "Energy is not conserved along the actionAngleVerticalInverse torus for the IsothermalDiskPotential when using bisection" ) # Now traverse the orbit at the frequency rate and check against orbit integration Om = aAVI.Freqs(aAVI.J(1.0)) ts = numpy.linspace(0.0, 2.0 * numpy.pi / Om, 1001) x, v = aAVI(aAVI.J(1.0), Om * ts) orb = Orbit([x[0], v[0]]) orb.integrate(ts, isopot) assert numpy.amax(numpy.fabs(orb.x(ts) - x)) < 1e-8, ( "Position does not agree with that of the integrated orbit along the torus of the IsothermalDiskPotential when using bisection" ) assert numpy.amax(numpy.fabs(orb.vx(ts) - v)) < 1e-8, ( "Velocity does not agree with that of the integrated orbit along the torus of the IsothermalDiskPotential when using bisection" ) return None # Test that actionAngleVerticalInverse is the inverse of actionAngleVertical # when using a point transformation def test_actionAngleVerticalInverse_wrtVertical_pointtransform_bisect(): from galpy.actionAngle import actionAngleVertical, actionAngleVerticalInverse from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAV = actionAngleVertical(pot=isopot) # Check a few orbits x, vx = 0.1, -0.3 obs = Orbit([x, vx]) times = numpy.linspace(0.0, 30.0, 1001) obs.integrate(times, isopot) j, _, a = aAV.actionsFreqsAngles(obs.x(times), obs.vx(times)) # Set up actionAngleVerticalInverse for this energy aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[obs.E()], use_pointtransform=True, bisect=True ) xi, vxi = aAVI(aAVI.J(obs.E()), a) assert numpy.amax(numpy.fabs(obs.x(times) - xi)) < 10.0**-6.0, ( "actionAngleVerticalInverse is not the inverse of actionAngleVertical for an example orbit when using bisection and a point transformation" ) assert numpy.amax(numpy.fabs(obs.vx(times) - vxi)) < 10.0**-6.0, ( "actionAngleVerticalInverse is not the inverse of actionAngleVertical for an example orbit when using bisection and a point transformation" ) return None def test_actionAngleVerticalInverse_freqs_wrtVertical_pointtransform_bisect(): from galpy.actionAngle import actionAngleVertical, actionAngleVerticalInverse from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAV = actionAngleVertical(pot=isopot) x, vx = 0.1, -0.3 obs = Orbit([x, vx]) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[obs.E(pot=isopot)], use_pointtransform=True, bisect=True, ) tol = -10.0 Om = aAVI.Freqs(aAVI.J(obs.E(pot=isopot))) # Compute frequency with actionAngleHarmonic _, Omi = aAV.actionsFreqs(*aAVI(aAVI.J(obs.E(pot=isopot)), 0.0)) assert numpy.fabs((Om - Omi) / Om) < 10.0**tol, ( "Frequency computed using actionAngleVerticalInverse does not agree with that computed by actionAngleVertical when using bisection and a point transformation" ) return None # Test that orbit from actionAngleVerticalInverse is the same as an integrated # orbit when using a point transformation def test_actionAngleVerticalInverse_orbit_pointtransform_bisect(): from galpy.actionAngle import actionAngleVerticalInverse from galpy.orbit import Orbit from galpy.potential import IsothermalDiskPotential, evaluatelinearPotentials # Set up instance isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[0.1, 1.0, 10.0], use_pointtransform=True, bisect=True, ) ta = numpy.linspace(0.0, 2.0 * numpy.pi, 1001) x, v = aAVI(aAVI.J(1.0), ta) # Compute energy and check whether it's conserved E = evaluatelinearPotentials(isopot, x) + v**2.0 / 2.0 assert numpy.std(E) / numpy.mean(E) < 1e-10, ( "Energy is not conserved along the actionAngleVerticalInverse torus for the IsothermalDiskPotential when using bisection and a point transformation" ) # Now traverse the orbit at the frequency rate and check against orbit integration Om = aAVI.Freqs(aAVI.J(1.0)) ts = numpy.linspace(0.0, 2.0 * numpy.pi / Om, 1001) x, v = aAVI(aAVI.J(1.0), Om * ts) orb = Orbit([x[0], v[0]]) orb.integrate(ts, isopot) assert numpy.amax(numpy.fabs(orb.x(ts) - x)) < 1e-8, ( "Position does not agree with that of the integrated orbit along the torus of the IsothermalDiskPotential when using bisection and a point transformation" ) assert numpy.amax(numpy.fabs(orb.vx(ts) - v)) < 1e-8, ( "Velocity does not agree with that of the integrated orbit along the torus of the IsothermalDiskPotential when using bisection and a point transformation" ) return None # Tests of interpolated actionAngleVerticalInverse need fixture to set up the # interpolated actionAngleVerticalInverse @pytest.fixture(scope="module") def setup_actionAngleVerticalInverse_interpolated(): from galpy.actionAngle import actionAngleVerticalInverse from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aA1Dinv = actionAngleVerticalInverse( pot=isopot, nta=2 * 128, Es=numpy.linspace(0.0, 4.0, 1001), setup_interp=True, use_pointtransform=False, ) return aA1Dinv, isopot @pytest.fixture(scope="module") def setup_actionAngleVerticalInverse_interpolated_pointtransform(): from galpy.actionAngle import actionAngleVerticalInverse from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aA1Dinv = actionAngleVerticalInverse( pot=isopot, nta=2 * 128, Es=numpy.linspace(0.0, 4.0, 1001), setup_interp=True, use_pointtransform=True, pt_deg=7, ) return aA1Dinv, isopot # Test that actionAngleVerticalInverse is the inverse of actionAngleVertical def test_actionAngleVerticalInverse_wrtVertical_interpolation( setup_actionAngleVerticalInverse_interpolated, ): from galpy.actionAngle import actionAngleVertical from galpy.orbit import Orbit aAVI, isopot = setup_actionAngleVerticalInverse_interpolated aAV = actionAngleVertical(pot=isopot) # Check a few orbits x, vx = 0.1, -0.3 obs = Orbit([x, vx]) times = numpy.linspace(0.0, 30.0, 1001) obs.integrate(times, isopot) j, _, a = aAV.actionsFreqsAngles(obs.x(times), obs.vx(times)) xi, vxi = aAVI(aAVI.J(obs.E()), a) assert numpy.amax(numpy.fabs(obs.x(times) - xi)) < 10.0**-6.0, ( "actionAngleVerticalInverse is not the inverse of actionAngleVertical for an example orbit when using interpolation" ) assert numpy.amax(numpy.fabs(obs.vx(times) - vxi)) < 10.0**-6.0, ( "actionAngleVerticalInverse is not the inverse of actionAngleVertical for an example orbit when using interpolation" ) return None def test_actionAngleVerticalInverse_freqs_wrtVertical_interpolation( setup_actionAngleVerticalInverse_interpolated, ): # Create harmonic oscillator potential as isochrone w/ large b --> 1D from galpy.actionAngle import actionAngleVertical from galpy.orbit import Orbit aAVI, isopot = setup_actionAngleVerticalInverse_interpolated aAV = actionAngleVertical(pot=isopot) x, vx = 0.1, -0.3 obs = Orbit([x, vx]) tol = -10.0 Om = aAVI.Freqs(aAVI.J(obs.E(pot=isopot))) # Compute frequency with actionAngleHarmonic _, Omi = aAV.actionsFreqs(*aAVI(aAVI.J(obs.E(pot=isopot)), 0.0)) assert numpy.fabs((Om - Omi) / Om) < 10.0**tol, ( "Frequency computed using actionAngleVerticalInverse does not agree with that computed by actionAngleVertical when using interpolation" ) return None # Test that orbit from actionAngleVerticalInverse is the same as an integrated orbit def test_actionAngleVerticalInverse_orbit_interpolation( setup_actionAngleVerticalInverse_interpolated, ): from galpy.orbit import Orbit from galpy.potential import evaluatelinearPotentials aAVI, isopot = setup_actionAngleVerticalInverse_interpolated ta = numpy.linspace(0.0, 2.0 * numpy.pi, 1001) Ei = 1.3132 x, v = aAVI(aAVI.J(Ei), ta) # Compute energy and check whether it's conserved E = evaluatelinearPotentials(isopot, x) + v**2.0 / 2.0 assert numpy.std(E) / numpy.mean(E) < 1e-10, ( "Energy is not conserved along the actionAngleVerticalInverse torus for the IsothermalDiskPotential when using interpolation" ) # Now traverse the orbit at the frequency rate and check against orbit integration Om = aAVI.Freqs(aAVI.J(Ei)) ts = numpy.linspace(0.0, 2.0 * numpy.pi / Om, 1001) x, v = aAVI(aAVI.J(Ei), Om * ts) orb = Orbit([x[0], v[0]]) orb.integrate(ts, isopot) assert numpy.amax(numpy.fabs(orb.x(ts) - x)) < 1e-8, ( "Position does not agree with that of the integrated orbit along the torus of the IsothermalDiskPotential when using interpolation" ) assert numpy.amax(numpy.fabs(orb.vx(ts) - v)) < 1e-8, ( "Velocity does not agree with that of the integrated orbit along the torus of the IsothermalDiskPotential when using interpolation" ) return None # Test that actionAngleVerticalInverse is the inverse of actionAngleVertical def test_actionAngleVerticalInverse_wrtVertical_interpolation_pointtransform( setup_actionAngleVerticalInverse_interpolated_pointtransform, ): from galpy.actionAngle import actionAngleVertical from galpy.orbit import Orbit aAVI, isopot = setup_actionAngleVerticalInverse_interpolated_pointtransform aAV = actionAngleVertical(pot=isopot) # Check a few orbits x, vx = 0.1, -0.3 obs = Orbit([x, vx]) times = numpy.linspace(0.0, 30.0, 1001) obs.integrate(times, isopot) j, _, a = aAV.actionsFreqsAngles(obs.x(times), obs.vx(times)) xi, vxi = aAVI(aAVI.J(obs.E()), a) assert numpy.amax(numpy.fabs(obs.x(times) - xi)) < 10.0**-6.0, ( "actionAngleVerticalInverse is not the inverse of actionAngleVertical for an example orbit when using interpolation and a point transformation" ) assert numpy.amax(numpy.fabs(obs.vx(times) - vxi)) < 10.0**-6.0, ( "actionAngleVerticalInverse is not the inverse of actionAngleVertical for an example orbit when using interpolation and a point transformation" ) return None def test_actionAngleVerticalInverse_freqs_wrtVertical_interpolation_pointtransform( setup_actionAngleVerticalInverse_interpolated_pointtransform, ): # Create harmonic oscillator potential as isochrone w/ large b --> 1D from galpy.actionAngle import actionAngleVertical from galpy.orbit import Orbit aAVI, isopot = setup_actionAngleVerticalInverse_interpolated_pointtransform aAV = actionAngleVertical(pot=isopot) x, vx = 0.1, -0.3 obs = Orbit([x, vx]) tol = -8.0 Om = aAVI.Freqs(aAVI.J(obs.E(pot=isopot))) # Compute frequency with actionAngleHarmonic _, Omi = aAV.actionsFreqs(*aAVI(aAVI.J(obs.E(pot=isopot)), 0.0)) assert numpy.fabs((Om - Omi) / Om) < 10.0**tol, ( "Frequency computed using actionAngleVerticalInverse does not agree with that computed by actionAngleVertical when using interpolation and a point transformation" ) return None # Test that orbit from actionAngleVerticalInverse is the same as an integrated orbit def test_actionAngleVerticalInverse_orbit_interpolation_pointtransform( setup_actionAngleVerticalInverse_interpolated_pointtransform, ): from galpy.orbit import Orbit from galpy.potential import evaluatelinearPotentials aAVI, isopot = setup_actionAngleVerticalInverse_interpolated_pointtransform ta = numpy.linspace(0.0, 2.0 * numpy.pi, 1001) Ei = 1.3132 x, v = aAVI(aAVI.J(Ei), ta) # Compute energy and check whether it's conserved E = evaluatelinearPotentials(isopot, x) + v**2.0 / 2.0 assert numpy.std(E) / numpy.mean(E) < 1e-8, ( "Energy is not conserved along the actionAngleVerticalInverse torus for the IsothermalDiskPotential when using interpolation and a point transformation" ) # Now traverse the orbit at the frequency rate and check against orbit integration Om = aAVI.Freqs(aAVI.J(Ei)) ts = numpy.linspace(0.0, 2.0 * numpy.pi / Om, 1001) x, v = aAVI(aAVI.J(Ei), Om * ts) orb = Orbit([x[0], v[0]]) orb.integrate(ts, isopot) assert numpy.amax(numpy.fabs(orb.x(ts) - x)) < 1e-7, ( "Position does not agree with that of the integrated orbit along the torus of the IsothermalDiskPotential when using interpolation and a point transformation" ) assert numpy.amax(numpy.fabs(orb.vx(ts) - v)) < 1e-7, ( "Velocity does not agree with that of the integrated orbit along the torus of the IsothermalDiskPotential when using interpolation and a point transformation" ) return None def test_actionAngleVerticalInverse_plotting(): import matplotlib.pyplot as pyplot from galpy.actionAngle import actionAngleVerticalInverse from galpy.potential import IsothermalDiskPotential # Set up instance isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[0.1, 1.0, 10.0], use_pointtransform=False ) aAVIpt = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[0.1, 1.0, 10.0], use_pointtransform=True ) gs = aAVI.plot_convergence(1.0, return_gridspec=True) aAVIpt.plot_convergence(1.0, overplot=gs) pyplot.close() gs = aAVI.plot_power(0.1, return_gridspec=True) gs = aAVI.plot_power([0.1, 1.0, 10.0], overplot=gs) pyplot.close() aAVI.plot_orbit(1.0) pyplot.close() return None # Test that actionAngleVerticalInverse is the inverse of actionAngleVertical def test_actionAngleVerticalInverse_interpolation_plotting( setup_actionAngleVerticalInverse_interpolated, ): import matplotlib.pyplot as pyplot aAVI, _ = setup_actionAngleVerticalInverse_interpolated gs = aAVI.plot_convergence(3.7, return_gridspec=True) pyplot.close() aAVI.plot_power(numpy.linspace(0.0, 4.0, 1001)) pyplot.close() aAVI.plot_orbit(3.706) pyplot.close() aAVI.plot_interp(3.706) pyplot.close() return None def test_actionAngleVerticalInverse_convergence_warnings(): from galpy.actionAngle import actionAngleVerticalInverse from galpy.potential import IsothermalDiskPotential isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) # Setup warnings with warnings.catch_warnings(record=True) as w: if PY2: reset_warning_registry("galpy") warnings.simplefilter("always", galpyWarning) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[300.0], use_pointtransform=False, maxiter=100 ) # Should raise convergence warnings raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "Torus mapping with Newton-Raphson did not converge in 100 iterations, falling back onto simple bisection (increase maxiter to try harder with Newton-Raphson)" ) if raisedWarning: break assert raisedWarning, ( "actionAngleVerticalInverse for large energy should have raised convergence warning, but didn't" ) for wa in w: raisedWarning = ( str(wa.message) == "Torus mapping with bisection did not converge in 100 iterations for energies: 300" ) if raisedWarning: break assert raisedWarning, ( "actionAngleVerticalInverse for large energy should have raised convergence warning, but didn't" ) return None def test_actionAngleVerticalInverse_plotting_errors(): from galpy.actionAngle import actionAngleVerticalInverse from galpy.potential import IsothermalDiskPotential # Set up instance isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[0.1, 1.0, 10.0, 20.0, 30.0], use_pointtransform=False, ) with pytest.raises(ValueError) as excinfo: gs = aAVI.plot_convergence(1.1, return_gridspec=True) pytest.fail( "Calling plot_convergence with an energy not given should have given a ValueError, but did not" ) with pytest.raises(ValueError) as excinfo: aAVI.plot_power(1.1) pytest.fail( "Calling plot_convergence with an energy not given should have given a ValueError, but did not" ) with pytest.raises(RuntimeError) as excinfo: aAVI.plot_power(numpy.linspace(0.0, 4.0, 1001), overplot=True) pytest.fail( "Calling plot_power with overplot=True and many Es should have raised a RuntimeError, but didn't" ) with pytest.raises(ValueError) as excinfo: aAVI.plot_orbit(1.1) pytest.fail( "Calling plot_convergence with an energy not given should have given a ValueError, but did not" ) return None def test_actionAngleVerticalInverse_interpolation_errors(): from galpy.actionAngle import actionAngleVerticalInverse from galpy.potential import IsothermalDiskPotential # Set up instance isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[0.1, 1.0, 10.0], use_pointtransform=True ) # Interpolation not being set up should lead to a bunch of errors with pytest.raises(RuntimeError) as excinfo: aAVI.nSn(0.1) pytest.fail( "Calling nSn without interpolation should have raised a RuntimeError, but did not" ) with pytest.raises(RuntimeError) as excinfo: aAVI.dSndJ(0.1) pytest.fail( "Calling dSndJ without interpolation should have raised a RuntimeError, but did not" ) with pytest.raises(RuntimeError) as excinfo: aAVI.pt_coeffs(0.1) pytest.fail( "Calling pt_coeffs without interpolation should have raised a RuntimeError, but did not" ) with pytest.raises(RuntimeError) as excinfo: aAVI.pt_deriv_coeffs(0.1) pytest.fail( "Calling pt_deriv_coeffs without interpolation should have raised a RuntimeError, but did not" ) return None # Test that evaluating various functions for an actionAngleVerticalInverse instance for an E not in the instantiation raises an error def test_actionAngleVerticalInverse_notE_errors(): from galpy.actionAngle import actionAngleVerticalInverse from galpy.potential import IsothermalDiskPotential # Set up instance isopot = IsothermalDiskPotential(amp=1.0, sigma=0.5) aAVI = actionAngleVerticalInverse( pot=isopot, nta=4 * 128, Es=[0.1, 1.0, 10.0], use_pointtransform=True ) with pytest.raises(ValueError) as excinfo: aAVI.J(0.11) pytest.fail( "Calling J with an energy not given should have given a ValueError, but did not" ) with pytest.raises(ValueError) as excinfo: # actually action input here, but this is fine aAVI.xvFreqs(0.11, 0.0) pytest.fail( "Calling xvFreqs with an energy not given should have given a ValueError, but did not" ) with pytest.raises(ValueError) as excinfo: # actually action input here, but this is fine aAVI.Freqs(0.11) pytest.fail( "Calling Freqs with an energy not given should have given a ValueError, but did not" ) return None # Test that computing actionAngle coordinates in C for a NullPotential leads to an error def test_nullpotential_error(): from galpy.actionAngle import actionAngleStaeckel from galpy.potential import NullPotential np = NullPotential() aAS = actionAngleStaeckel(pot=np, delta=1.0) with pytest.raises(NotImplementedError) as excinfo: aAS(1.0, 0.0, 1.0, 0.1, 0.0) pytest.fail( "Calculating actionAngle coordinates in C for a NullPotential should have given a NotImplementedError, but did not" ) return None def check_actionAngleIsochroneInverse_wrtIsochrone( pot, aAI, aAII, obs, tol, ntimes=1001 ): times = numpy.linspace(0.0, 30.0, ntimes) obs.integrate(times, pot) jr, jp, jz, _, _, _, ar, ap, az = aAI.actionsFreqsAngles( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times), obs.phi(times), ) Ri, vRi, vTi, zi, vzi, phii = aAII( numpy.median(jr), numpy.median(jp), numpy.median(jz), ar, ap, az ) assert numpy.amax(numpy.fabs(obs.R(times) - Ri)) < 10.0**tol, ( "actionAngleIsochroneInverse is not the inverse of actionAngleIsochrone for an example orbit" ) assert ( numpy.amax( numpy.fabs((obs.phi(times) - phii + numpy.pi) % (2.0 * numpy.pi) - numpy.pi) ) < 10.0**tol ), ( "actionAngleIsochroneInverse is not the inverse of actionAngleIsochrone for an example orbit" ) assert numpy.amax(numpy.fabs(obs.z(times) - zi)) < 10.0**tol, ( "actionAngleIsochroneInverse is not the inverse of actionAngleIsochrone for an example orbit" ) assert numpy.amax(numpy.fabs(obs.vR(times) - vRi)) < 10.0**tol, ( "actionAngleIsochroneInverse is not the inverse of actionAngleIsochrone for an example orbit" ) assert numpy.amax(numpy.fabs(obs.vT(times) - vTi)) < 10.0**tol, ( "actionAngleIsochroneInverse is not the inverse of actionAngleIsochrone for an example orbit" ) assert numpy.amax(numpy.fabs(obs.vz(times) - vzi)) < 10.0**tol, ( "actionAngleIsochroneInverse is not the inverse of actionAngleIsochrone for an example orbit" ) return None # Test that the actions are conserved along an orbit def check_actionAngle_conserved_actions( aA, obs, pot, toljr, toljp, toljz, ntimes=1001, fixed_quad=False, inclphi=False ): times = numpy.linspace(0.0, 100.0, ntimes) obs.integrate(times, pot, method="dopr54_c") if fixed_quad and inclphi: js = aA( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times), obs.phi(times), fixed_quad=True, ) elif fixed_quad and not inclphi: js = aA( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times), fixed_quad=True, ) elif inclphi: js = aA( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times), obs.phi(times), ) else: # Test Orbit with multiple objects case, but calling js = aA(obs(times)) maxdj = numpy.amax( numpy.fabs(js - numpy.tile(numpy.mean(js, axis=1), (len(times), 1)).T), axis=1 ) / numpy.mean(js, axis=1) assert maxdj[0] < 10.0**toljr, "Jr conservation fails at %g%%" % (100.0 * maxdj[0]) assert maxdj[1] < 10.0**toljp, "Lz conservation fails at %g%%" % (100.0 * maxdj[1]) assert maxdj[2] < 10.0**toljz, "Jz conservation fails at %g%%" % (100.0 * maxdj[2]) return None # Test that the angles increase linearly def check_actionAngle_linear_angles( aA, obs, pot, tolinitar, tolinitap, tolinitaz, tolor, tolop, toloz, toldar, toldap, toldaz, maxt=100.0, ntimes=1001, separate_times=False, fixed_quad=False, u0=None, ): from galpy.actionAngle import dePeriod times = numpy.linspace(0.0, maxt, ntimes) obs.integrate(times, pot, method="dopr54_c") if fixed_quad: acfs_init = aA.actionsFreqsAngles( obs, fixed_quad=True ) # to check the init. angles acfs = aA.actionsFreqsAngles( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times), obs.phi(times), fixed_quad=True, ) elif not u0 is None: acfs_init = aA.actionsFreqsAngles(obs, u0=u0) # to check the init. angles acfs = aA.actionsFreqsAngles( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times), obs.phi(times), u0=(u0 + times * 0.0), ) # array else: acfs_init = aA.actionsFreqsAngles(obs()) # to check the init. angles if separate_times: acfs = numpy.array( [ aA.actionsFreqsAngles( obs.R(t), obs.vR(t), obs.vT(t), obs.z(t), obs.vz(t), obs.phi(t) ) for t in times ] )[:, :, 0].T acfs = ( acfs[0], acfs[1], acfs[2], acfs[3], acfs[4], acfs[5], acfs[6], acfs[7], acfs[8], ) else: acfs = aA.actionsFreqsAngles( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times), obs.phi(times), ) ar = dePeriod(numpy.reshape(acfs[6], (1, len(times)))).flatten() ap = dePeriod(numpy.reshape(acfs[7], (1, len(times)))).flatten() az = dePeriod(numpy.reshape(acfs[8], (1, len(times)))).flatten() # Do linear fit to radial angle, check that deviations are small, check # that the slope is the frequency linfit = numpy.polyfit(times, ar, 1) assert numpy.fabs((linfit[1] - acfs_init[6]) / acfs_init[6]) < 10.0**tolinitar, ( "Radial angle obtained by fitting linear trend to the orbit does not agree with the initially-calculated angle by %g%%" % (100.0 * numpy.fabs((linfit[1] - acfs_init[6]) / acfs_init[6])) ) assert numpy.fabs(linfit[0] - acfs_init[3]) < 10.0**tolor, ( "Radial frequency obtained by fitting linear trend to the orbit does not agree with the initially-calculated frequency by %g%%" % (100.0 * numpy.fabs((linfit[0] - acfs_init[3]) / acfs_init[3])) ) devs = ar - linfit[0] * times - linfit[1] maxdev = numpy.amax(numpy.fabs(devs)) assert maxdev < 10.0**toldar, ( "Maximum deviation from linear trend in the radial angles is %g" % maxdev ) # Do linear fit to azimuthal angle, check that deviations are small, check # that the slope is the frequency linfit = numpy.polyfit(times, ap, 1) assert numpy.fabs((linfit[1] - acfs_init[7]) / acfs_init[7]) < 10.0**tolinitap, ( "Azimuthal angle obtained by fitting linear trend to the orbit does not agree with the initially-calculated angle by %g%%" % (100.0 * numpy.fabs((linfit[1] - acfs_init[7]) / acfs_init[7])) ) assert numpy.fabs(linfit[0] - acfs_init[4]) < 10.0**tolop, ( "Azimuthal frequency obtained by fitting linear trend to the orbit does not agree with the initially-calculated frequency by %g%%" % (100.0 * numpy.fabs((linfit[0] - acfs_init[4]) / acfs_init[4])) ) devs = ap - linfit[0] * times - linfit[1] maxdev = numpy.amax(numpy.fabs(devs)) assert maxdev < 10.0**toldap, ( "Maximum deviation from linear trend in the azimuthal angle is %g" % maxdev ) # Do linear fit to vertical angle, check that deviations are small, check # that the slope is the frequency linfit = numpy.polyfit(times, az, 1) assert numpy.fabs((linfit[1] - acfs_init[8]) / acfs_init[8]) < 10.0**tolinitaz, ( "Vertical angle obtained by fitting linear trend to the orbit does not agree with the initially-calculated angle by %g%%" % (100.0 * numpy.fabs((linfit[1] - acfs_init[8]) / acfs_init[8])) ) assert numpy.fabs(linfit[0] - acfs_init[5]) < 10.0**toloz, ( "Vertical frequency obtained by fitting linear trend to the orbit does not agree with the initially-calculated frequency by %g%%" % (100.0 * numpy.fabs((linfit[0] - acfs_init[5]) / acfs_init[5])) ) devs = az - linfit[0] * times - linfit[1] maxdev = numpy.amax(numpy.fabs(devs)) assert maxdev < 10.0**toldaz, ( "Maximum deviation from linear trend in the vertical angles is %g" % maxdev ) return None # Test that the ecc, zmax, rperi, rap are conserved along an orbit def check_actionAngle_conserved_EccZmaxRperiRap( aA, obs, pot, tole, tolzmax, tolrperi, tolrap, ntimes=1001, inclphi=False ): times = numpy.linspace(0.0, 100.0, ntimes) obs.integrate(times, pot, method="dopr54_c") if inclphi: es, zmaxs, rperis, raps = aA.EccZmaxRperiRap( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times), obs.phi(times), ) else: es, zmaxs, rperis, raps = aA.EccZmaxRperiRap( obs.R(times), obs.vR(times), obs.vT(times), obs.z(times), obs.vz(times) ) assert numpy.amax(numpy.fabs(es / numpy.mean(es) - 1)) < 10.0**tole, ( "Eccentricity conservation fails at %g%%" % (100.0 * numpy.amax(numpy.fabs(es / numpy.mean(es) - 1))) ) assert numpy.amax(numpy.fabs(zmaxs / numpy.mean(zmaxs) - 1)) < 10.0**tolzmax, ( "Zmax conservation fails at %g%%" % (100.0 * numpy.amax(numpy.fabs(zmaxs / numpy.mean(zmaxs) - 1))) ) assert numpy.amax(numpy.fabs(rperis / numpy.mean(rperis) - 1)) < 10.0**tolrperi, ( "Rperi conservation fails at %g%%" % (100.0 * numpy.amax(numpy.fabs(rperis / numpy.mean(rperis) - 1))) ) assert numpy.amax(numpy.fabs(raps / numpy.mean(raps) - 1)) < 10.0**tolrap, ( "Rap conservation fails at %g%%" % (100.0 * numpy.amax(numpy.fabs(raps / numpy.mean(raps) - 1))) ) return None # Python 2 bug: setting simplefilter to 'always' still does not display # warnings that were already displayed using 'once' or 'default', so some # warnings tests fail; need to reset the registry # Has become an issue at pytest 3.8.0, which seems to have changed the scope of # filterwarnings (global one at the start is ignored) def reset_warning_registry(pattern=".*"): "clear warning registry for all match modules" import re import sys key = "__warningregistry__" for mod in sys.modules.values(): if hasattr(mod, key) and re.match(pattern, mod.__name__): getattr(mod, key).clear() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_actionAngleTorus.py0000644000175100001660000010204514761352023020353 0ustar00runnerdockerimport sys import warnings import numpy import pytest from test_actionAngle import reset_warning_registry from galpy.util import galpyWarning PY2 = sys.version < "3" # Print all galpyWarnings always for tests of warnings warnings.simplefilter("always", galpyWarning) # Basic sanity checking: circular orbit should have constant R, zero vR, vT=vc def test_actionAngleTorus_basic(): from galpy.actionAngle import actionAngleTorus from galpy.potential import ( FlattenedPowerPotential, MWPotential, PlummerPotential, rl, vcirc, ) tol = -4.0 jr = 10.0**-10.0 jz = 10.0**-10.0 aAT = actionAngleTorus(pot=MWPotential) # at R=1, Lz=1 jphi = 1.0 angler = numpy.linspace(0.0, 2.0 * numpy.pi, 101) anglephi = numpy.linspace(0.0, 2.0 * numpy.pi, 101) + 1.0 anglez = numpy.linspace(0.0, 2.0 * numpy.pi, 101) + 2.0 RvR = aAT(jr, jphi, jz, angler, anglephi, anglez).T assert numpy.all(numpy.fabs(RvR[0] - rl(MWPotential, jphi)) < 10.0**tol), ( "circular orbit does not have constant radius for actionAngleTorus" ) assert numpy.all(numpy.fabs(RvR[1]) < 10.0**tol), ( "circular orbit does not have zero radial velocity for actionAngleTorus" ) assert numpy.all( numpy.fabs(RvR[2] - vcirc(MWPotential, rl(MWPotential, jphi))) < 10.0**tol ), "circular orbit does not have constant vT=vc for actionAngleTorus" assert numpy.all(numpy.fabs(RvR[3]) < 10.0**tol), ( "circular orbit does not have zero vertical height for actionAngleTorus" ) assert numpy.all(numpy.fabs(RvR[4]) < 10.0**tol), ( "circular orbit does not have zero vertical velocity for actionAngleTorus" ) # at Lz=1.5, using Plummer tol = -3.25 pp = PlummerPotential(normalize=1.0) aAT = actionAngleTorus(pot=pp) jphi = 1.5 RvR = aAT(jr, jphi, jz, angler, anglephi, anglez).T assert numpy.all(numpy.fabs(RvR[0] - rl(pp, jphi)) < 10.0**tol), ( "circular orbit does not have constant radius for actionAngleTorus" ) assert numpy.all(numpy.fabs(RvR[1]) < 10.0**tol), ( "circular orbit does not have zero radial velocity for actionAngleTorus" ) assert numpy.all(numpy.fabs(RvR[2] - vcirc(pp, rl(pp, jphi))) < 10.0**tol), ( "circular orbit does not have constant vT=vc for actionAngleTorus" ) assert numpy.all(numpy.fabs(RvR[3]) < 10.0**tol), ( "circular orbit does not have zero vertical height for actionAngleTorus" ) assert numpy.all(numpy.fabs(RvR[4]) < 10.0**tol), ( "circular orbit does not have zero vertical velocity for actionAngleTorus" ) # at Lz=0.5, using FlattenedPowerPotential tol = -4.0 fp = FlattenedPowerPotential(normalize=1.0) aAT = actionAngleTorus(pot=fp) jphi = 0.5 RvR = aAT(jr, jphi, jz, angler, anglephi, anglez).T assert numpy.all(numpy.fabs(RvR[0] - rl(fp, jphi)) < 10.0**tol), ( "circular orbit does not have constant radius for actionAngleTorus" ) assert numpy.all(numpy.fabs(RvR[1]) < 10.0**tol), ( "circular orbit does not have zero radial velocity for actionAngleTorus" ) assert numpy.all(numpy.fabs(RvR[2] - vcirc(fp, rl(fp, jphi))) < 10.0**tol), ( "circular orbit does not have constant vT=vc for actionAngleTorus" ) assert numpy.all(numpy.fabs(RvR[3]) < 10.0**tol), ( "circular orbit does not have zero vertical height for actionAngleTorus" ) assert numpy.all(numpy.fabs(RvR[4]) < 10.0**tol), ( "circular orbit does not have zero vertical velocity for actionAngleTorus" ) return None # Basic sanity checking: close-to-circular orbit should have freq. = epicycle freq. def test_actionAngleTorus_basic_freqs(): from galpy.actionAngle import actionAngleTorus from galpy.potential import ( HernquistPotential, JaffePotential, PowerSphericalPotential, epifreq, omegac, rl, verticalfreq, ) tol = -3.0 jr = 10.0**-6.0 jz = 10.0**-6.0 jp = JaffePotential(normalize=1.0) aAT = actionAngleTorus(pot=jp) # at Lz=1 jphi = 1.0 om = aAT.Freqs(jr, jphi, jz) assert numpy.fabs((om[0] - epifreq(jp, rl(jp, jphi))) / om[0]) < 10.0**tol, ( "Close-to-circular orbit does not have Or=kappa for actionAngleTorus" ) assert numpy.fabs((om[1] - omegac(jp, rl(jp, jphi))) / om[1]) < 10.0**tol, ( "Close-to-circular orbit does not have Ophi=omega for actionAngleTorus" ) assert numpy.fabs((om[2] - verticalfreq(jp, rl(jp, jphi))) / om[2]) < 10.0**tol, ( "Close-to-circular orbit does not have Oz=nu for actionAngleTorus" ) # at Lz=1.5, w/ different potential pp = PowerSphericalPotential(normalize=1.0) aAT = actionAngleTorus(pot=pp) jphi = 1.5 om = aAT.Freqs(jr, jphi, jz) assert numpy.fabs((om[0] - epifreq(pp, rl(pp, jphi))) / om[0]) < 10.0**tol, ( "Close-to-circular orbit does not have Or=kappa for actionAngleTorus" ) assert numpy.fabs((om[1] - omegac(pp, rl(pp, jphi))) / om[1]) < 10.0**tol, ( "Close-to-circular orbit does not have Ophi=omega for actionAngleTorus" ) assert numpy.fabs((om[2] - verticalfreq(pp, rl(pp, jphi))) / om[2]) < 10.0**tol, ( "Close-to-circular orbit does not have Oz=nu for actionAngleTorus" ) # at Lz=0.5, w/ different potential tol = -2.5 # appears more difficult hp = HernquistPotential(normalize=1.0) aAT = actionAngleTorus(pot=hp) jphi = 0.5 om = aAT.Freqs(jr, jphi, jz) assert numpy.fabs((om[0] - epifreq(hp, rl(hp, jphi))) / om[0]) < 10.0**tol, ( "Close-to-circular orbit does not have Or=kappa for actionAngleTorus" ) assert numpy.fabs((om[1] - omegac(hp, rl(hp, jphi))) / om[1]) < 10.0**tol, ( "Close-to-circular orbit does not have Ophi=omega for actionAngleTorus" ) assert numpy.fabs((om[2] - verticalfreq(hp, rl(hp, jphi))) / om[2]) < 10.0**tol, ( "Close-to-circular orbit does not have Oz=nu for actionAngleTorus" ) return None # Test that orbit from actionAngleTorus is the same as an integrated orbit def test_actionAngleTorus_orbit(): from galpy.actionAngle import actionAngleTorus from galpy.orbit import Orbit from galpy.potential import MWPotential2014 # Set up instance aAT = actionAngleTorus(pot=MWPotential2014, tol=10.0**-5.0) jr, jphi, jz = 0.05, 1.1, 0.025 # First calculate frequencies and the initial RvR RvRom = aAT.xvFreqs( jr, jphi, jz, numpy.array([0.0]), numpy.array([1.0]), numpy.array([2.0]) ) om = RvRom[1:] # Angles along an orbit ts = numpy.linspace(0.0, 100.0, 1001) angler = ts * om[0] anglephi = 1.0 + ts * om[1] anglez = 2.0 + ts * om[2] # Calculate the orbit using actionAngleTorus RvR = aAT(jr, jphi, jz, angler, anglephi, anglez).T # Calculate the orbit using orbit integration orb = Orbit( [ RvRom[0][0, 0], RvRom[0][0, 1], RvRom[0][0, 2], RvRom[0][0, 3], RvRom[0][0, 4], RvRom[0][0, 5], ] ) orb.integrate(ts, MWPotential2014) # Compare tol = -3.0 assert numpy.all(numpy.fabs(orb.R(ts) - RvR[0]) < 10.0**tol), ( "Integrated orbit does not agree with torus orbit in R" ) assert numpy.all(numpy.fabs(orb.vR(ts) - RvR[1]) < 10.0**tol), ( "Integrated orbit does not agree with torus orbit in vR" ) assert numpy.all(numpy.fabs(orb.vT(ts) - RvR[2]) < 10.0**tol), ( "Integrated orbit does not agree with torus orbit in vT" ) assert numpy.all(numpy.fabs(orb.z(ts) - RvR[3]) < 10.0**tol), ( "Integrated orbit does not agree with torus orbit in z" ) assert numpy.all(numpy.fabs(orb.vz(ts) - RvR[4]) < 10.0**tol), ( "Integrated orbit does not agree with torus orbit in vz" ) assert numpy.all( numpy.fabs((orb.phi(ts) - RvR[5] + numpy.pi) % (2.0 * numpy.pi) - numpy.pi) < 10.0**tol ), "Integrated orbit does not agree with torus orbit in phi" return None # Test that actionAngleTorus w/ interp pot gives same freqs as regular pot # Doesn't work well: TM aborts because our interpolated forces aren't # consistent enough with the potential for TM's taste, but we test that it at # at least works somewhat def test_actionAngleTorus_interppot_freqs(): from galpy.actionAngle import actionAngleTorus from galpy.potential import LogarithmicHaloPotential, interpRZPotential lp = LogarithmicHaloPotential(normalize=1.0) ip = interpRZPotential( RZPot=lp, interpPot=True, interpDens=True, interpRforce=True, interpzforce=True, enable_c=True, ) aAT = actionAngleTorus(pot=lp) aATi = actionAngleTorus(pot=ip) jr, jphi, jz = 0.05, 1.1, 0.02 om = aAT.Freqs(jr, jphi, jz) omi = aATi.Freqs(jr, jphi, jz) assert numpy.fabs((om[0] - omi[0]) / om[0]) < 0.2, ( "Radial frequency computed using the torus machine does not agree between potential and interpolated potential" ) assert numpy.fabs((om[1] - omi[1]) / om[1]) < 0.2, ( "Azimuthal frequency computed using the torus machine does not agree between potential and interpolated potential" ) assert numpy.fabs((om[2] - omi[2]) / om[2]) < 0.8, ( "Vertical frequency computed using the torus machine does not agree between potential and interpolated potential" ) return None # Test the actionAngleTorus against an isochrone potential: actions def test_actionAngleTorus_Isochrone_actions(): from galpy.actionAngle import actionAngleIsochrone, actionAngleTorus from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) tol = -6.0 aAT = actionAngleTorus(pot=ip, tol=tol) jr, jphi, jz = 0.075, 1.1, 0.05 angler = numpy.array([0.0]) anglephi = numpy.array([numpy.pi]) anglez = numpy.array([numpy.pi / 2.0]) # Calculate position from aAT RvR = aAT(jr, jphi, jz, angler, anglephi, anglez).T # Calculate actions from aAI ji = aAI(*RvR) djr = numpy.fabs((ji[0] - jr) / jr) dlz = numpy.fabs((ji[1] - jphi) / jphi) djz = numpy.fabs((ji[2] - jz) / jz) assert djr < 10.0**tol, ( "actionAngleTorus and actionAngleIsochrone applied to isochrone potential disagree for Jr at %f%%" % (djr * 100.0) ) assert dlz < 10.0**tol, ( "actionAngleTorus and actionAngleIsochrone applied to isochrone potential disagree for Jr at %f%%" % (dlz * 100.0) ) assert djz < 10.0**tol, ( "actionAngleTorus and actionAngleIsochrone applied to isochrone potential disagree for Jr at %f%%" % (djz * 100.0) ) return None # Test the actionAngleTorus against an isochrone potential: frequencies and angles def test_actionAngleTorus_Isochrone_freqsAngles(): from galpy.actionAngle import actionAngleIsochrone, actionAngleTorus from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=1.2) aAI = actionAngleIsochrone(ip=ip) tol = -6.0 aAT = actionAngleTorus(pot=ip, tol=tol) jr, jphi, jz = 0.075, 1.1, 0.05 angler = numpy.array([0.1]) + numpy.linspace(0.0, numpy.pi, 101) angler = angler % (2.0 * numpy.pi) anglephi = numpy.array([numpy.pi]) + numpy.linspace(0.0, numpy.pi, 101) anglephi = anglephi % (2.0 * numpy.pi) anglez = numpy.array([numpy.pi / 2.0]) + numpy.linspace(0.0, numpy.pi, 101) anglez = anglez % (2.0 * numpy.pi) # Calculate position from aAT RvRom = aAT.xvFreqs(jr, jphi, jz, angler, anglephi, anglez) # Calculate actions, frequencies, and angles from aAI ws = aAI.actionsFreqsAngles(*RvRom[0].T) dOr = numpy.fabs(ws[3] - RvRom[1]) dOp = numpy.fabs(ws[4] - RvRom[2]) dOz = numpy.fabs(ws[5] - RvRom[3]) dar = numpy.fabs(ws[6] - angler) dap = numpy.fabs(ws[7] - anglephi) daz = numpy.fabs(ws[8] - anglez) dar[dar > numpy.pi] -= 2.0 * numpy.pi dar[dar < -numpy.pi] += 2.0 * numpy.pi dap[dap > numpy.pi] -= 2.0 * numpy.pi dap[dap < -numpy.pi] += 2.0 * numpy.pi daz[daz > numpy.pi] -= 2.0 * numpy.pi daz[daz < -numpy.pi] += 2.0 * numpy.pi assert numpy.all(dOr < 10.0**tol), ( "actionAngleTorus and actionAngleIsochrone applied to isochrone potential disagree for Or at %f%%" % (numpy.nanmax(dOr) * 100.0) ) assert numpy.all(dOp < 10.0**tol), ( "actionAngleTorus and actionAngleIsochrone applied to isochrone potential disagree for Ophi at %f%%" % (numpy.nanmax(dOp) * 100.0) ) assert numpy.all(dOz < 10.0**tol), ( "actionAngleTorus and actionAngleIsochrone applied to isochrone potential disagree for Oz at %f%%" % (numpy.nanmax(dOz) * 100.0) ) assert numpy.all(dar < 10.0**tol), ( "actionAngleTorus and actionAngleIsochrone applied to isochrone potential disagree for ar at %f" % (numpy.nanmax(dar)) ) assert numpy.all(dap < 10.0**tol), ( "actionAngleTorus and actionAngleIsochrone applied to isochrone potential disagree for aphi at %f" % (numpy.nanmax(dap)) ) assert numpy.all(daz < 10.0**tol), ( "actionAngleTorus and actionAngleIsochrone applied to isochrone potential disagree for az at %f" % (numpy.nanmax(daz)) ) return None # Test the actionAngleTorus against a Staeckel potential: actions def test_actionAngleTorus_Staeckel_actions(): from galpy.actionAngle import actionAngleStaeckel, actionAngleTorus from galpy.potential import KuzminKutuzovStaeckelPotential delta = 1.2 kp = KuzminKutuzovStaeckelPotential(normalize=1.0, Delta=delta) aAS = actionAngleStaeckel(pot=kp, delta=delta, c=True) tol = -3.0 aAT = actionAngleTorus(pot=kp, tol=tol) jr, jphi, jz = 0.075, 1.1, 0.05 angler = numpy.array([0.0]) anglephi = numpy.array([numpy.pi]) anglez = numpy.array([numpy.pi / 2.0]) # Calculate position from aAT RvR = aAT(jr, jphi, jz, angler, anglephi, anglez).T # Calculate actions from aAI ji = aAS(*RvR) djr = numpy.fabs((ji[0] - jr) / jr) dlz = numpy.fabs((ji[1] - jphi) / jphi) djz = numpy.fabs((ji[2] - jz) / jz) assert djr < 10.0**tol, ( "actionAngleTorus and actionAngleStaeckel applied to Staeckel potential disagree for Jr at %f%%" % (djr * 100.0) ) assert dlz < 10.0**tol, ( "actionAngleTorus and actionAngleStaeckel applied to Staeckel potential disagree for Jr at %f%%" % (dlz * 100.0) ) assert djz < 10.0**tol, ( "actionAngleTorus and actionAngleStaeckel applied to Staeckel potential disagree for Jr at %f%%" % (djz * 100.0) ) return None # Test the actionAngleTorus against an isochrone potential: frequencies and angles def test_actionAngleTorus_Staeckel_freqsAngles(): from galpy.actionAngle import actionAngleStaeckel, actionAngleTorus from galpy.potential import KuzminKutuzovStaeckelPotential delta = 1.2 kp = KuzminKutuzovStaeckelPotential(normalize=1.0, Delta=delta) aAS = actionAngleStaeckel(pot=kp, delta=delta, c=True) tol = -3.0 aAT = actionAngleTorus(pot=kp, tol=tol) jr, jphi, jz = 0.075, 1.1, 0.05 angler = numpy.array([0.1]) + numpy.linspace(0.0, numpy.pi, 101) angler = angler % (2.0 * numpy.pi) anglephi = numpy.array([numpy.pi]) + numpy.linspace(0.0, numpy.pi, 101) anglephi = anglephi % (2.0 * numpy.pi) anglez = numpy.array([numpy.pi / 2.0]) + numpy.linspace(0.0, numpy.pi, 101) anglez = anglez % (2.0 * numpy.pi) # Calculate position from aAT RvRom = aAT.xvFreqs(jr, jphi, jz, angler, anglephi, anglez) # Calculate actions, frequencies, and angles from aAI ws = aAS.actionsFreqsAngles(*RvRom[0].T) dOr = numpy.fabs(ws[3] - RvRom[1]) dOp = numpy.fabs(ws[4] - RvRom[2]) dOz = numpy.fabs(ws[5] - RvRom[3]) dar = numpy.fabs(ws[6] - angler) dap = numpy.fabs(ws[7] - anglephi) daz = numpy.fabs(ws[8] - anglez) dar[dar > numpy.pi] -= 2.0 * numpy.pi dar[dar < -numpy.pi] += 2.0 * numpy.pi dap[dap > numpy.pi] -= 2.0 * numpy.pi dap[dap < -numpy.pi] += 2.0 * numpy.pi daz[daz > numpy.pi] -= 2.0 * numpy.pi daz[daz < -numpy.pi] += 2.0 * numpy.pi assert numpy.all(dOr < 10.0**tol), ( "actionAngleTorus and actionAngleStaeckel applied to Staeckel potential disagree for Or at %f%%" % (numpy.nanmax(dOr) * 100.0) ) assert numpy.all(dOp < 10.0**tol), ( "actionAngleTorus and actionAngleStaeckel applied to Staeckel potential disagree for Ophi at %f%%" % (numpy.nanmax(dOp) * 100.0) ) assert numpy.all(dOz < 10.0**tol), ( "actionAngleTorus and actionAngleStaeckel applied to Staeckel potential disagree for Oz at %f%%" % (numpy.nanmax(dOz) * 100.0) ) assert numpy.all(dar < 10.0**tol), ( "actionAngleTorus and actionAngleStaeckel applied to Staeckel potential disagree for ar at %f" % (numpy.nanmax(dar)) ) assert numpy.all(dap < 10.0**tol), ( "actionAngleTorus and actionAngleStaeckel applied to Staeckel potential disagree for aphi at %f" % (numpy.nanmax(dap)) ) assert numpy.all(daz < 10.0**tol), ( "actionAngleTorus and actionAngleStaeckel applied to Staeckel potential disagree for az at %f" % (numpy.nanmax(daz)) ) return None # Test the actionAngleTorus against a general potential w/ actionAngleIsochroneApprox: actions def test_actionAngleTorus_isochroneApprox_actions(): from galpy.actionAngle import actionAngleIsochroneApprox, actionAngleTorus from galpy.potential import MWPotential2014 aAIA = actionAngleIsochroneApprox(pot=MWPotential2014, b=0.8) tol = -2.5 aAT = actionAngleTorus(pot=MWPotential2014, tol=tol) jr, jphi, jz = 0.075, 1.1, 0.05 angler = numpy.array([0.0]) anglephi = numpy.array([numpy.pi]) anglez = numpy.array([numpy.pi / 2.0]) # Calculate position from aAT RvR = aAT(jr, jphi, jz, angler, anglephi, anglez).T # Calculate actions from aAIA ji = aAIA(*RvR) djr = numpy.fabs((ji[0] - jr) / jr) dlz = numpy.fabs((ji[1] - jphi) / jphi) djz = numpy.fabs((ji[2] - jz) / jz) assert djr < 10.0**tol, ( "actionAngleTorus and actionAngleIsochroneApprox applied to MWPotential2014 potential disagree for Jr at %f%%" % (djr * 100.0) ) assert dlz < 10.0**tol, ( "actionAngleTorus and actionAngleIsochroneApprox applied to MWPotential2014 potential disagree for Jr at %f%%" % (dlz * 100.0) ) assert djz < 10.0**tol, ( "actionAngleTorus and actionAngleMWPotential2014 applied to MWPotential2014 potential disagree for Jr at %f%%" % (djz * 100.0) ) return None # Test the actionAngleTorus against a general potential w/ actionAngleIsochrone: frequencies and angles def test_actionAngleTorus_isochroneApprox_freqsAngles(): from galpy.actionAngle import actionAngleIsochroneApprox, actionAngleTorus from galpy.potential import MWPotential2014 aAIA = actionAngleIsochroneApprox(pot=MWPotential2014, b=0.8) tol = -3.5 aAT = actionAngleTorus(pot=MWPotential2014, tol=tol) jr, jphi, jz = 0.075, 1.1, 0.05 angler = numpy.array([0.1]) + numpy.linspace(0.0, numpy.pi, 21) angler = angler % (2.0 * numpy.pi) anglephi = numpy.array([numpy.pi]) + numpy.linspace(0.0, numpy.pi, 21) anglephi = anglephi % (2.0 * numpy.pi) anglez = numpy.array([numpy.pi / 2.0]) + numpy.linspace(0.0, numpy.pi, 21) anglez = anglez % (2.0 * numpy.pi) # Calculate position from aAT RvRom = aAT.xvFreqs(jr, jphi, jz, angler, anglephi, anglez) # Calculate actions, frequencies, and angles from aAI ws = aAIA.actionsFreqsAngles(*RvRom[0].T) dOr = numpy.fabs(ws[3] - RvRom[1]) dOp = numpy.fabs(ws[4] - RvRom[2]) dOz = numpy.fabs(ws[5] - RvRom[3]) dar = numpy.fabs(ws[6] - angler) dap = numpy.fabs(ws[7] - anglephi) daz = numpy.fabs(ws[8] - anglez) dar[dar > numpy.pi] -= 2.0 * numpy.pi dar[dar < -numpy.pi] += 2.0 * numpy.pi dap[dap > numpy.pi] -= 2.0 * numpy.pi dap[dap < -numpy.pi] += 2.0 * numpy.pi daz[daz > numpy.pi] -= 2.0 * numpy.pi daz[daz < -numpy.pi] += 2.0 * numpy.pi assert numpy.all(dOr < 10.0**tol), ( "actionAngleTorus and actionAngleIsochroneApprox applied to MWPotential2014 potential disagree for Or at %f%%" % (numpy.nanmax(dOr) * 100.0) ) assert numpy.all(dOp < 10.0**tol), ( "actionAngleTorus and actionAngleIsochroneApprox applied to MWPotential2014 potential disagree for Ophi at %f%%" % (numpy.nanmax(dOp) * 100.0) ) assert numpy.all(dOz < 10.0**tol), ( "actionAngleTorus and actionAngleIsochroneApprox applied to MWPotential2014 potential disagree for Oz at %f%%" % (numpy.nanmax(dOz) * 100.0) ) assert numpy.all(dar < 10.0**tol), ( "actionAngleTorus and actionAngleIsochroneApprox applied to MWPotential2014 potential disagree for ar at %f" % (numpy.nanmax(dar)) ) assert numpy.all(dap < 10.0**tol), ( "actionAngleTorus and actionAngleIsochroneApprox applied to MWPotential2014 potential disagree for aphi at %f" % (numpy.nanmax(dap)) ) assert numpy.all(daz < 10.0**tol), ( "actionAngleTorus and actionAngleIsochroneApprox applied to MWPotential2014 potential disagree for az at %f" % (numpy.nanmax(daz)) ) return None # Test that the frequencies returned by hessianFreqs are the same as those returned by Freqs def test_actionAngleTorus_hessian_freqs(): from galpy.actionAngle import actionAngleTorus from galpy.potential import MWPotential2014 aAT = actionAngleTorus(pot=MWPotential2014) jr, jphi, jz = 0.075, 1.1, 0.05 freqO = aAT.Freqs(jr, jphi, jz)[:3] hessO = aAT.hessianFreqs(jr, jphi, jz)[1:4] assert numpy.all( numpy.fabs(numpy.array(freqO) - numpy.array(hessO)) < 10.0**-8.0 ), "actionAngleTorus methods Freqs and hessianFreqs return different frequencies" return None # Test that the Hessian is approximately symmetric def test_actionAngleTorus_hessian_symm(): from galpy.actionAngle import actionAngleTorus from galpy.potential import MWPotential2014 aAT = actionAngleTorus(pot=MWPotential2014) jr, jphi, jz = 0.075, 1.1, 0.05 h = aAT.hessianFreqs(jr, jphi, jz, tol=0.0001, nosym=True)[0] assert numpy.all(numpy.fabs((h - h.T) / h) < 0.03), ( "actionAngleTorus Hessian is not symmetric" ) return None # Test that the Hessian is approximately correct def test_actionAngleTorus_hessian_linear(): from galpy.actionAngle import actionAngleTorus from galpy.potential import MWPotential2014 aAT = actionAngleTorus(pot=MWPotential2014) jr, jphi, jz = 0.075, 1.1, 0.05 h = aAT.hessianFreqs(jr, jphi, jz, tol=0.0001, nosym=True)[0] dj = numpy.array([0.02, 0.005, -0.01]) do_fromhessian = numpy.dot(h, dj) O = numpy.array(aAT.Freqs(jr, jphi, jz)[:3]) do = numpy.array(aAT.Freqs(jr + dj[0], jphi + dj[1], jz + dj[2])[:3]) - O assert numpy.all(numpy.fabs((do_fromhessian - do) / O) < 0.001), ( "actionAngleTorus Hessian does not return good approximation to dO/dJ" ) return None # Test that the frequencies returned by xvJacobianFreqs are the same as those returned by Freqs def test_actionAngleTorus_jacobian_freqs(): from galpy.actionAngle import actionAngleTorus from galpy.potential import MWPotential2014 aAT = actionAngleTorus(pot=MWPotential2014) jr, jphi, jz = 0.075, 1.1, 0.05 freqO = aAT.Freqs(jr, jphi, jz)[:3] hessO = aAT.xvJacobianFreqs( jr, jphi, jz, numpy.array([0.0]), numpy.array([1.0]), numpy.array([2.0]) )[3:6] assert numpy.all( numpy.fabs(numpy.array(freqO) - numpy.array(hessO)) < 10.0**-8.0 ), "actionAngleTorus methods Freqs and xvJacobianFreqs return different frequencies" return None # Test that the Hessian returned by xvJacobianFreqs are the same as those returned by hessianFreqs def test_actionAngleTorus_jacobian_hessian(): from galpy.actionAngle import actionAngleTorus from galpy.potential import MWPotential2014 aAT = actionAngleTorus(pot=MWPotential2014) jr, jphi, jz = 0.075, 1.1, 0.05 freqO = aAT.hessianFreqs(jr, jphi, jz)[0] hessO = aAT.xvJacobianFreqs( jr, jphi, jz, numpy.array([0.0]), numpy.array([1.0]), numpy.array([2.0]) )[2] assert numpy.all( numpy.fabs(numpy.array(freqO) - numpy.array(hessO)) < 10.0**-8.0 ), ( "actionAngleTorus methods hessianFreqs and xvJacobianFreqs return different Hessians" ) return None # Test that the xv returned by xvJacobianFreqs are the same as those returned by __call__ def test_actionAngleTorus_jacobian_xv(): from galpy.actionAngle import actionAngleTorus from galpy.potential import MWPotential2014 aAT = actionAngleTorus(pot=MWPotential2014) jr, jphi, jz = 0.075, 1.1, 0.05 angler = numpy.array([0.0, 1.0]) anglephi = numpy.array([1.0, 2.0]) anglez = numpy.array([2.0, 3.0]) freqO = aAT(jr, jphi, jz, angler, anglephi, anglez) hessO = aAT.xvJacobianFreqs(jr, jphi, jz, angler, anglephi, anglez)[0] assert numpy.all( numpy.fabs(numpy.array(freqO) - numpy.array(hessO)) < 10.0**-8.0 ), "actionAngleTorus methods __call__ and xvJacobianFreqs return different xv" return None # Test that the determinant of the Jacobian returned by xvJacobianFreqs is close to 1/R (should be 1 for rectangular coordinates, 1/R for cylindrical def test_actionAngleTorus_jacobian_detone(): from galpy.actionAngle import actionAngleTorus from galpy.potential import MWPotential2014 aAT = actionAngleTorus(pot=MWPotential2014, dJ=0.0001) jr, jphi, jz = 0.075, 1.1, 0.05 angler = numpy.array([0.0, 1.0]) anglephi = numpy.array([1.0, 2.0]) anglez = numpy.array([2.0, 3.0]) jf = aAT.xvJacobianFreqs(jr, jphi, jz, angler, anglephi, anglez) assert ( numpy.fabs(jf[0][0, 0] * numpy.fabs(numpy.linalg.det(jf[1][0])) - 1) < 0.01 ), ( "Jacobian returned by actionAngleTorus method xvJacobianFreqs does not have the expected determinant" ) assert ( numpy.fabs(jf[0][1, 0] * numpy.fabs(numpy.linalg.det(jf[1][1])) - 1) < 0.01 ), ( "Jacobian returned by actionAngleTorus method xvJacobianFreqs does not have the expected determinant" ) return None # Test that Jacobian returned by xvJacobianFreqs is approximately correct def test_actionAngleTorus_jacobian_linear(): from galpy.actionAngle import actionAngleTorus from galpy.potential import MWPotential2014 aAT = actionAngleTorus(pot=MWPotential2014) jr, jphi, jz = 0.075, 1.1, 0.05 angler = numpy.array([0.5]) anglephi = numpy.array([1.0]) anglez = numpy.array([2.0]) jf = aAT.xvJacobianFreqs(jr, jphi, jz, angler, anglephi, anglez) xv = aAT(jr, jphi, jz, angler, anglephi, anglez) dja = 2.0 * numpy.array([0.001, 0.002, 0.003, -0.002, 0.004, 0.002]) xv_direct = aAT( jr + dja[0], jphi + dja[1], jz + dja[2], angler + dja[3], anglephi + dja[4], anglez + dja[5], ) xv_fromjac = xv + numpy.dot(jf[1], dja) assert numpy.all(numpy.fabs((xv_fromjac - xv_direct) / xv_direct) < 0.01), ( "Jacobian returned by actionAngleTorus method xvJacobianFreqs does not appear to be correct" ) return None # Test error when potential is not implemented in C def test_actionAngleTorus_nocerr(): from test_potential import BurkertPotentialNoC from galpy.actionAngle import actionAngleTorus bp = BurkertPotentialNoC() try: aAT = actionAngleTorus(pot=bp) except RuntimeError: pass else: raise AssertionError( "actionAngleTorus initialization with potential w/o C should have given a RuntimeError, but didn't" ) return None # Test error when potential is not axisymmetric def test_actionAngleTorus_nonaxierr(): from galpy.actionAngle import actionAngleTorus from galpy.potential import TriaxialNFWPotential np = TriaxialNFWPotential(normalize=1.0, b=0.9) try: aAT = actionAngleTorus(pot=np) except RuntimeError: pass else: raise AssertionError( "actionAngleTorus initialization with non-axisymmetric potential should have given a RuntimeError, but didn't" ) return None # Test the Autofit torus warnings def test_actionAngleTorus_AutoFitWarning(): from galpy.actionAngle import actionAngleTorus from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAT = actionAngleTorus(pot=lp, tol=10.0**-8.0) # These should give warnings jr, jp, jz = 0.27209033, 1.80253892, 0.6078445 ar, ap, az = ( numpy.array([1.95732492]), numpy.array([6.16753224]), numpy.array([4.08233059]), ) # Turn warnings into errors to test for them import warnings with warnings.catch_warnings(record=True) as w: if PY2: reset_warning_registry("galpy") warnings.simplefilter("always", galpyWarning) aAT(jr, jp, jz, ar, ap, az) # Should raise warning bc of Autofit, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "actionAngleTorus' AutoFit exited with non-zero return status -3: Fit failed the goal by more than 2" ) if raisedWarning: break assert raisedWarning, ( "actionAngleTorus with flattened LogarithmicHaloPotential and a particular orbit should have thrown a warning, but didn't" ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) aAT.xvFreqs(jr, jp, jz, ar, ap, az) # Should raise warning bc of Autofit, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "actionAngleTorus' AutoFit exited with non-zero return status -3: Fit failed the goal by more than 2" ) if raisedWarning: break assert raisedWarning, ( "actionAngleTorus with flattened LogarithmicHaloPotential and a particular orbit should have thrown a warning, but didn't" ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) aAT.Freqs(jr, jp, jz) # Should raise warning bc of Autofit, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "actionAngleTorus' AutoFit exited with non-zero return status -3: Fit failed the goal by more than 2" ) if raisedWarning: break assert raisedWarning, ( "actionAngleTorus with flattened LogarithmicHaloPotential and a particular orbit should have thrown a warning, but didn't" ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) aAT.hessianFreqs(jr, jp, jz) # Should raise warning bc of Autofit, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "actionAngleTorus' AutoFit exited with non-zero return status -3: Fit failed the goal by more than 2" ) if raisedWarning: break assert raisedWarning, ( "actionAngleTorus with flattened LogarithmicHaloPotential and a particular orbit should have thrown a warning, but didn't" ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) aAT.xvJacobianFreqs(jr, jp, jz, ar, ap, az) # Should raise warning bc of Autofit, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "actionAngleTorus' AutoFit exited with non-zero return status -3: Fit failed the goal by more than 2" ) if raisedWarning: break assert raisedWarning, ( "actionAngleTorus with flattened LogarithmicHaloPotential and a particular orbit should have thrown a warning, but didn't" ) return None def test_MWPotential_warning_torus(): # Test that using MWPotential throws a warning, see #229 from galpy.actionAngle import actionAngleTorus from galpy.potential import MWPotential if PY2: reset_warning_registry("galpy") warnings.simplefilter("error", galpyWarning) try: aAA = actionAngleTorus(pot=MWPotential) except: pass else: raise AssertionError( "actionAngleTorus with MWPotential should have thrown a warning, but didn't" ) # Turn warnings back into warnings warnings.simplefilter("always", galpyWarning) return None def test_load_library(): # Test that loading the library again gives the same library as the first # time from galpy.util._load_extension_libs import load_libgalpy_actionAngleTorus first_lib = load_libgalpy_actionAngleTorus()[0] second_lib = load_libgalpy_actionAngleTorus()[0] assert first_lib == second_lib, ( "libgalpy_actionAngleTorus loaded second time is not the same as first time" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_amuse.py0000644000175100001660000002377514761352023016220 0ustar00runnerdocker# Test consistency between galpy and amuse import numpy from amuse.couple import bridge from amuse.datamodel import Particles from amuse.lab import * # nopycln: import from astropy import units as apy_u from galpy import potential from galpy.orbit import Orbit from galpy.potential import to_amuse from galpy.util import conversion def test_amuse_potential_with_physical(): ro, vo = 8.0, 220.0 amp = 1e8 / conversion.mass_in_msol(ro=ro, vo=vo) a = 0.8 / ro amp_u = 1e8 * apy_u.solMass a_u = 0.8 * apy_u.kpc ro_u, vo_u = 8.0 * apy_u.kpc, 220.0 * apy_u.km / apy_u.s x, y, z = 3 | units.kpc, 4 | units.kpc, 2 | units.kpc r = 5 | units.kpc # ------------------------------------ # get_potential_at_point pot1 = potential.TwoPowerSphericalPotential(amp=amp, a=a, ro=ro, vo=vo) gg1 = pot1(5 / ro, 2 / ro) gg1 = gg1.to_value(apy_u.km**2 / apy_u.s**2) if hasattr(gg1, "unit") else gg1 amuse_pot1 = to_amuse(pot1) ag1 = amuse_pot1.get_potential_at_point(0, x, y, z) assert numpy.abs(gg1 - ag1.value_in(units.kms**2)) < 1e-10 pot2 = potential.TwoPowerSphericalPotential(amp=amp_u, a=a_u, ro=ro_u, vo=vo_u) gg2 = pot1(5 * apy_u.kpc, 2 * apy_u.kpc) gg2 = gg2.to_value(apy_u.km**2 / apy_u.s**2) if hasattr(gg2, "unit") else gg2 amuse_pot2 = to_amuse(pot2) ag2 = amuse_pot2.get_potential_at_point(0, x, y, z) assert numpy.abs(gg2 - ag2.value_in(units.kms**2)) < 1e-10 assert numpy.abs(ag1 - ag2) < 1e-10 | units.kms**2 # ------------------------------------ # test get_gravity_at_point pot1 = potential.TwoPowerSphericalPotential(amp=amp, a=a, ro=ro, vo=vo) amuse_pot1 = to_amuse(pot1) ax1, ay1, az1 = amuse_pot1.get_gravity_at_point(0, x, y, z) pot2 = potential.TwoPowerSphericalPotential(amp=amp_u, a=a_u, ro=ro_u, vo=vo_u) amuse_pot2 = to_amuse(pot2) ax2, ay2, az2 = amuse_pot2.get_gravity_at_point(0, x, y, z) assert numpy.abs(ax1 - ax2) < 1e-10 | units.kms / units.Myr assert numpy.abs(ay1 - ay2) < 1e-10 | units.kms / units.Myr assert numpy.abs(az1 - az2) < 1e-10 | units.kms / units.Myr # ------------------------------------ # test mass_density pot1 = potential.TwoPowerSphericalPotential(amp=amp, a=a, ro=ro, vo=vo) grho1 = pot1.dens(5 / ro, 2 / ro) grho1 = ( grho1.to_value(apy_u.solMass / apy_u.pc**3) if hasattr(grho1, "unit") else grho1 ) amuse_pot1 = to_amuse(pot1) arho1 = amuse_pot1.mass_density(x, y, z) assert numpy.abs(grho1 - arho1.value_in(units.MSun / units.parsec**3)) < 1e-10 pot2 = potential.TwoPowerSphericalPotential(amp=amp_u, a=a_u, ro=ro_u, vo=vo_u) grho2 = pot2.dens(5 * apy_u.kpc, 2 * apy_u.kpc) grho2 = ( grho2.to_value(apy_u.solMass / apy_u.pc**3) if hasattr(grho2, "unit") else grho2 ) amuse_pot2 = to_amuse(pot2) arho2 = amuse_pot2.mass_density(x, y, z) assert numpy.abs(grho2 - arho2.value_in(units.MSun / units.parsec**3)) < 1e-10 assert numpy.abs(arho1 - arho2) < 1e-10 | units.MSun / units.parsec**3 # ------------------------------------ # test circular_velocity pot1 = potential.TwoPowerSphericalPotential(amp=amp, a=a, ro=ro, vo=vo) gv1 = pot1.vcirc(1 * apy_u.kpc) gv1 = gv1.to_value(apy_u.km / apy_u.s) if hasattr(gv1, "unit") else gv1 amuse_pot1 = to_amuse(pot1) av1 = amuse_pot1.circular_velocity(1 | units.kpc) assert numpy.abs(gv1 - av1.value_in(units.kms)) < 1e-10 pot2 = potential.TwoPowerSphericalPotential(amp=amp_u, a=a_u, ro=ro_u, vo=vo_u) gv2 = pot2.vcirc(1 * apy_u.kpc) gv2 = gv2.to_value(apy_u.km / apy_u.s) if hasattr(gv2, "unit") else gv2 amuse_pot2 = to_amuse(pot2) av2 = amuse_pot2.circular_velocity(1 | units.kpc) assert numpy.abs(gv2 - av2.value_in(units.kms)) < 1e-10 assert numpy.abs(av1 - av2) < 1e-10 | units.kms # ------------------------------------ # test enclosed_mass pot1 = potential.TwoPowerSphericalPotential(amp=amp, a=a, ro=ro, vo=vo) gm1 = pot1.mass(1 / ro) gm1 = gm1.to_value(apy_u.solMass) if hasattr(gm1, "unit") else gm1 amuse_pot1 = to_amuse(pot1) am1 = amuse_pot1.enclosed_mass(1 | units.kpc) assert numpy.abs(gm1 - am1.value_in(units.MSun)) < 3e-8 pot2 = potential.TwoPowerSphericalPotential(amp=amp_u, a=a_u, ro=ro_u, vo=vo_u) gm2 = pot2.mass(1 * apy_u.kpc) gm2 = gm2.to_value(apy_u.solMass) if hasattr(gm2, "unit") else gm2 amuse_pot2 = to_amuse(pot2) am2 = amuse_pot2.enclosed_mass(1 | units.kpc) assert numpy.abs(gm2 - am2.value_in(units.MSun)) < 3e-8 assert numpy.abs(am1 - am2) < 1e-10 | units.MSun return None def test_amuse_MN3ExponentialDiskPotential(): mn = potential.MN3ExponentialDiskPotential(normalize=1.0, hr=0.5, hz=0.1) tmax = 3.0 vo, ro = 215.0, 8.75 o = Orbit([1.0, 0.1, 1.1, 0.3, 0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, mn, tmax, vo, ro) return None def test_amuse_MiyamotoNagaiPotential(): mp = potential.MiyamotoNagaiPotential(normalize=1.0, a=0.5, b=0.1) tmax = 4.0 vo, ro = 220.0, 8.0 o = Orbit([1.0, 0.1, 1.1, 0.3, 0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, mp, tmax, vo, ro) return None def test_amuse_NFWPotential(): np = potential.NFWPotential(normalize=1.0, a=3.0) tmax = 3.0 vo, ro = 200.0, 7.0 o = Orbit([1.0, 0.5, 1.3, 0.3, 0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, np, tmax, vo, ro) return None def test_amuse_HernquistPotential(): hp = potential.HernquistPotential(normalize=1.0, a=3.0) tmax = 3.0 vo, ro = 210.0, 7.5 o = Orbit([1.0, 0.25, 1.4, 0.3, -0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, hp, tmax, vo, ro, tol=0.02) return None def test_amuse_PowerSphericalPotentialwCutoffPotential(): pp = potential.PowerSphericalPotentialwCutoff(normalize=1.0, alpha=1.0, rc=0.4) tmax = 2.0 vo, ro = 180.0, 9.0 o = Orbit([1.0, 0.03, 1.03, 0.2, 0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, pp, tmax, vo, ro) return None def test_amuse_LogarithmicHaloPotential(): lp = potential.LogarithmicHaloPotential(normalize=1.0) tmax = 2.0 vo, ro = 210.0, 8.5 o = Orbit([1.0, 0.1, 1.1, 0.3, 0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, lp, tmax, vo, ro, tol=0.03) return None def test_amuse_PlummerPotential(): pp = potential.PlummerPotential(normalize=1.0, b=2.0) tmax = 3.0 vo, ro = 213.0, 8.23 o = Orbit([1.0, 0.1, 1.1, 0.3, 0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, pp, tmax, vo, ro, tol=0.03) return None def test_amuse_MWPotential2014(): mp = potential.MWPotential2014 tmax = 3.5 vo, ro = 220.0, 8.0 o = Orbit([1.0, 0.1, 1.1, 0.2, 0.1, 1.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, mp, tmax, vo, ro) return None def run_orbitIntegration_comparison(orb, pot, tmax, vo, ro, tol=0.01): # Integrate in galpy ts = numpy.linspace(0.0, tmax / conversion.time_in_Gyr(vo, ro), 1001) orb.integrate(ts, pot) # Integrate with amuse x, y, z, vx, vy, vz = integrate_amuse(orb, pot, tmax | units.Gyr, vo, ro) # Read and compare xdiff = numpy.fabs((x - orb.x(ts[-1])) / x) ydiff = numpy.fabs((y - orb.y(ts[-1])) / y) zdiff = numpy.fabs((z - orb.z(ts[-1])) / z) vxdiff = numpy.fabs((vx - orb.vx(ts[-1])) / vx) vydiff = numpy.fabs((vy - orb.vy(ts[-1])) / vy) vzdiff = numpy.fabs((vz - orb.vz(ts[-1])) / vz) assert xdiff < tol, ( "galpy and amuse orbit integration inconsistent for x by %g" % xdiff ) assert ydiff < tol, ( "galpy and amuse orbit integration inconsistent for y by %g" % ydiff ) assert zdiff < tol, ( "galpy and amuse orbit integration inconsistent for z by %g" % zdiff ) assert vxdiff < tol, ( "galpy and amuse orbit integration inconsistent for vx by %g" % vxdiff ) assert vydiff < tol, ( "galpy and amuse orbit integration inconsistent for vy by %g" % vydiff ) assert vzdiff < tol, ( "galpy and amuse orbit integration inconsistent for vz by %g" % vzdiff ) return None def integrate_amuse(orb, pot, tmax, vo, ro): """Integrate a snapshot in infile until tmax in Gyr, save to outfile""" time = 0.0 | tmax.unit dt = tmax / 10001.0 orbit = Particles(1) orbit.mass = 1.0 | units.MSun orbit.radius = 1.0 | units.RSun orbit.position = [orb.x(), orb.y(), orb.z()] | units.kpc orbit.velocity = [orb.vx(), orb.vy(), orb.vz()] | units.kms galaxy_code = to_amuse(pot, ro=ro, vo=vo) orbit_gravity = drift_without_gravity(orbit) orbit_gravity.particles.add_particles(orbit) channel_from_gravity_to_orbit = orbit_gravity.particles.new_channel_to(orbit) gravity = bridge.Bridge(use_threading=False) gravity.add_system(orbit_gravity, (galaxy_code,)) gravity.add_system( galaxy_code, ) gravity.timestep = dt while time <= tmax: time += dt gravity.evolve_model(time) channel_from_gravity_to_orbit.copy() gravity.stop() return ( orbit.x[0].value_in(units.kpc), orbit.y[0].value_in(units.kpc), orbit.z[0].value_in(units.kpc), orbit.vx[0].value_in(units.kms), orbit.vy[0].value_in(units.kms), orbit.vz[0].value_in(units.kms), ) class drift_without_gravity: def __init__(self, convert_nbody, time=0 | units.Myr): self.model_time = time self.particles = Particles() def evolve_model(self, t_end): dt = t_end - self.model_time self.particles.position += self.particles.velocity * dt self.model_time = t_end @property def potential_energy(self): return quantities.zero @property def get_potential_at_point(self): return quantities.zero @property def kinetic_energy(self): return ( 0.5 * self.particles.mass * self.particles.velocity.lengths() ** 2 ).sum() @property def angular_momenum(self): return numpy.cross(self.particles.position, self.particles.velocity) def stop(self): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_conversion.py0000644000175100001660000007435214761352023017270 0ustar00runnerdockerimport numpy from galpy.util import conversion def test_dens_in_criticaldens(): # Test the scaling, as a 2nd derivative of the potential / G, should scale as velocity^2/position^2 vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 4.0 * conversion.dens_in_criticaldens(vofid, rofid) / conversion.dens_in_criticaldens(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "dens_in_criticaldens did not work as expected" assert ( numpy.fabs( 0.25 * conversion.dens_in_criticaldens(vofid, rofid) / conversion.dens_in_criticaldens(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "dens_in_critical did not work as expected" return None def test_dens_in_meanmatterdens(): # Test the scaling, as a 2nd derivative of the potential / G, should scale as velocity^2/position^2 vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 4.0 * conversion.dens_in_meanmatterdens(vofid, rofid) / conversion.dens_in_meanmatterdens(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "dens_in_meanmatterdens did not work as expected" assert ( numpy.fabs( 0.25 * conversion.dens_in_meanmatterdens(vofid, rofid) / conversion.dens_in_meanmatterdens(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "dens_in_meanmatter did not work as expected" return None def test_dens_in_gevcc(): # Test the scaling, as a 2nd derivative of the potential / G, should scale as velocity^2/position^2 vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 4.0 * conversion.dens_in_gevcc(vofid, rofid) / conversion.dens_in_gevcc(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "dens_in_gevcc did not work as expected" assert ( numpy.fabs( 0.25 * conversion.dens_in_gevcc(vofid, rofid) / conversion.dens_in_gevcc(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "dens_in_gevcc did not work as expected" return None def test_dens_in_msolpc3(): # Test the scaling, as a 2nd derivative of the potential / G, should scale as velocity^2/position^2 vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 4.0 * conversion.dens_in_msolpc3(vofid, rofid) / conversion.dens_in_msolpc3(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "dens_in_msolpc3 did not work as expected" assert ( numpy.fabs( 0.25 * conversion.dens_in_msolpc3(vofid, rofid) / conversion.dens_in_msolpc3(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "dens_in_msolpc3 did not work as expected" return None def test_force_in_2piGmsolpc2(): # Test the scaling, as a 1st derivative of the potential / G, should scale as velocity^2/position vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 4.0 * conversion.force_in_2piGmsolpc2(vofid, rofid) / conversion.force_in_2piGmsolpc2(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "force_in_2piGmsolpc2 did not work as expected" assert ( numpy.fabs( 0.5 * conversion.force_in_2piGmsolpc2(vofid, rofid) / conversion.force_in_2piGmsolpc2(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "force_in_2piGmsolpc2 did not work as expected" return None def test_force_in_pcMyr2(): # Test the scaling, as a 1st derivative of the potential, should scale as velocity^2/position vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 4.0 * conversion.force_in_pcMyr2(vofid, rofid) / conversion.force_in_pcMyr2(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "force_in_pcMyr2 did not work as expected" assert ( numpy.fabs( 0.5 * conversion.force_in_pcMyr2(vofid, rofid) / conversion.force_in_pcMyr2(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "force_in_pcMyr2 did not work as expected" return None def test_force_in_kmsMyr(): # Test the scaling, as a 1st derivative of the potential, should scale as velocity^2/position vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 4.0 * conversion.force_in_kmsMyr(vofid, rofid) / conversion.force_in_kmsMyr(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "force_in_kmsMyr did not work as expected" assert ( numpy.fabs( 0.5 * conversion.force_in_kmsMyr(vofid, rofid) / conversion.force_in_kmsMyr(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "force_in_kmsMyr did not work as expected" return None def test_force_in_10m13kms2(): # Test the scaling, as a 1st derivative of the potential, should scale as velocity^2/position vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 4.0 * conversion.force_in_10m13kms2(vofid, rofid) / conversion.force_in_10m13kms2(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "force_in_10m13kms2 did not work as expected" assert ( numpy.fabs( 0.5 * conversion.force_in_10m13kms2(vofid, rofid) / conversion.force_in_10m13kms2(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "force_in_10m13kms2 did not work as expected" return None def test_freq_in_Gyr(): # Test the scaling, as 1/time, should scale as velocity/position vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 2.0 * conversion.freq_in_Gyr(vofid, rofid) / conversion.freq_in_Gyr(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "freq_in_Gyr did not work as expected" assert ( numpy.fabs( 0.5 * conversion.freq_in_Gyr(vofid, rofid) / conversion.freq_in_Gyr(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "freq_in_Gyr did not work as expected" return None def test_freq_in_kmskpc(): # Test the scaling, as 1/time, should scale as velocity/position vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 2.0 * conversion.freq_in_kmskpc(vofid, rofid) / conversion.freq_in_kmskpc(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "freq_in_kmskpc did not work as expected" assert ( numpy.fabs( 0.5 * conversion.freq_in_kmskpc(vofid, rofid) / conversion.freq_in_kmskpc(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "freq_in_kmskpc did not work as expected" return None def test_surfdens_in_msolpc2(): # Test the scaling, as a 1st derivative of the potential, should scale as velocity^2/position vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 4.0 * conversion.surfdens_in_msolpc2(vofid, rofid) / conversion.surfdens_in_msolpc2(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "surfdens_in_msolpc2 did not work as expected" assert ( numpy.fabs( 0.5 * conversion.surfdens_in_msolpc2(vofid, rofid) / conversion.surfdens_in_msolpc2(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "surfdens_in_msolpc2 did not work as expected" return None def test_mass_in_msol(): # Test the scaling, should be velocity^2 x position vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 4.0 * conversion.mass_in_msol(vofid, rofid) / conversion.mass_in_msol(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "mass_in_msol did not work as expected" assert ( numpy.fabs( 2.0 * conversion.mass_in_msol(vofid, rofid) / conversion.mass_in_msol(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "mass_in_msol did not work as expected" return None def test_mass_in_1010msol(): # Test the scaling, should be velocity^2 x position vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 4.0 * conversion.mass_in_1010msol(vofid, rofid) / conversion.mass_in_1010msol(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "mass_in_1010msol did not work as expected" assert ( numpy.fabs( 2.0 * conversion.mass_in_1010msol(vofid, rofid) / conversion.mass_in_1010msol(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "mass_in_1010msol did not work as expected" return None def test_time_in_Gyr(): # Test the scaling, should scale as position/velocity vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 0.5 * conversion.time_in_Gyr(vofid, rofid) / conversion.time_in_Gyr(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "time_in_Gyr did not work as expected" assert ( numpy.fabs( 2.0 * conversion.time_in_Gyr(vofid, rofid) / conversion.time_in_Gyr(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "time_in_Gyr did not work as expected" return None def test_velocity_in_kpcGyr(): # Test the scaling, should scale as velocity vofid, rofid = 200.0, 8.0 assert ( numpy.fabs( 2.0 * conversion.velocity_in_kpcGyr(vofid, rofid) / conversion.velocity_in_kpcGyr(2.0 * vofid, rofid) - 1.0 ) < 10.0**-10.0 ), "velocity_in_kpcGyr did not work as expected" assert ( numpy.fabs( conversion.velocity_in_kpcGyr(vofid, rofid) / conversion.velocity_in_kpcGyr(vofid, 2 * rofid) - 1.0 ) < 10.0**-10.0 ), "velocity_in_kpcGyr did not work as expected" return None def test_get_physical(): # Test that the get_physical function returns the right scaling parameters # Potential and variations thereof from galpy.potential import DehnenBarPotential, MWPotential2014 from galpy.util.conversion import get_physical dp = DehnenBarPotential assert numpy.fabs(get_physical(MWPotential2014[0]).get("ro") - 8.0) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a Potential" ) assert numpy.fabs(get_physical(MWPotential2014[0]).get("vo") - 220.0) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a Potential" ) ro, vo = 9.0, 230.0 dp = DehnenBarPotential(ro=ro, vo=vo) assert numpy.fabs(get_physical(dp).get("ro") - ro) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a Potential" ) assert numpy.fabs(get_physical(dp).get("vo") - vo) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a Potential" ) assert numpy.fabs(get_physical(MWPotential2014).get("ro") - 8.0) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a Potential" ) assert numpy.fabs(get_physical(MWPotential2014).get("vo") - 220.0) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a Potential" ) assert numpy.fabs(get_physical(MWPotential2014 + dp).get("ro") - 8.0) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a Potential" ) assert numpy.fabs(get_physical(MWPotential2014 + dp).get("vo") - 220.0) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a Potential" ) assert numpy.fabs(get_physical(MWPotential2014 + dp).get("ro") - 8.0) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a Potential" ) assert numpy.fabs(get_physical(MWPotential2014 + dp).get("vo") - 220.0) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a Potential" ) # Orbits from galpy.orbit import Orbit ro, vo = 10.0, 210.0 o = Orbit(ro=ro, vo=vo) assert numpy.fabs(get_physical(o).get("ro") - ro) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for an Orbit" ) assert numpy.fabs(get_physical(o).get("vo") - vo) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for an Orbit" ) # even though one shouldn't do this, let's test a list assert numpy.fabs(get_physical([o, o]).get("ro") - ro) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for an Orbit" ) assert numpy.fabs(get_physical([o, o]).get("vo") - vo) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for an Orbit" ) # actionAngle from galpy.actionAngle import actionAngleStaeckel aAS = actionAngleStaeckel(pot=MWPotential2014, delta=0.45) assert numpy.fabs(get_physical(aAS).get("ro") - 8.0) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for an actionAngle instance" ) assert numpy.fabs(get_physical(aAS).get("vo") - 220.0) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for an actionAngle instance" ) # This doesn't make much sense, but let's test... ro, vo = 19.0, 130.0 dp = DehnenBarPotential(ro=ro, vo=vo) aAS = actionAngleStaeckel(pot=dp, delta=0.45, ro=ro, vo=vo) assert numpy.fabs(get_physical(aAS).get("ro") - ro) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for an actionAngle instance" ) assert numpy.fabs(get_physical(aAS).get("vo") - vo) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for an actionAngle instance" ) # DF from galpy.df import quasiisothermaldf aAS = actionAngleStaeckel(pot=MWPotential2014, delta=0.45) qdf = quasiisothermaldf(1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, aA=aAS, pot=MWPotential2014) assert numpy.fabs(get_physical(qdf).get("ro") - 8.0) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a DF instance" ) assert numpy.fabs(get_physical(qdf).get("vo") - 220.0) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a DF instance" ) # non-standard ro,vo from galpy.potential import MiyamotoNagaiPotential ro, vo = 4.0, 330.0 mp = MiyamotoNagaiPotential(a=0.5, b=0.1, ro=ro, vo=vo) aAS = actionAngleStaeckel(pot=mp, delta=0.45, ro=ro, vo=vo) qdf = quasiisothermaldf(1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, aA=aAS, pot=mp, ro=ro, vo=vo) assert numpy.fabs(get_physical(qdf).get("ro") - ro) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a DF instance" ) assert numpy.fabs(get_physical(qdf).get("vo") - vo) < 1e-10, ( "get_physical does not return the correct unit conversion parameter for a DF instance" ) return None def test_physical_compatible_potential(): # Test that physical_compatible acts as expected from galpy.potential import HernquistPotential from galpy.util.conversion import physical_compatible # Set up potentials for all possible cases pot_default_phys = HernquistPotential(amp=0.55, a=2.0, ro=8.0, vo=220.0) pot_nonstandardro = HernquistPotential(amp=0.55, a=2.0, ro=9.0, vo=220.0) pot_nonstandardvo = HernquistPotential(amp=0.55, a=2.0, ro=8.0, vo=230.0) pot_nonstandardrovo = HernquistPotential(amp=0.55, a=2.0, ro=9.0, vo=230.0) pot_nophys = HernquistPotential(amp=0.55) pot_default_noro = HernquistPotential(amp=0.55, vo=220.0) pot_default_novo = HernquistPotential(amp=0.55, ro=8.0) pot_nonstandardro_novo = HernquistPotential(amp=0.55, ro=9.0) pot_nonstandardvo_noro = HernquistPotential(amp=0.55, vo=230.0) # Test expected behavior for single potentials assert physical_compatible(pot_default_phys, pot_default_phys), ( "pot_default_phys does not behave as expected" ) assert not physical_compatible(pot_default_phys, pot_nonstandardro), ( "pot_default_phys does not behave as expected" ) assert not physical_compatible(pot_nonstandardro, pot_default_phys), ( "pot_default_phys does not behave as expected" ) assert not physical_compatible(pot_default_phys, pot_nonstandardvo), ( "pot_default_phys does not behave as expected" ) assert not physical_compatible(pot_default_phys, pot_nonstandardrovo), ( "pot_default_phys does not behave as expected" ) assert physical_compatible(pot_default_phys, pot_nophys), ( "pot_default_phys does not behave as expected" ) assert physical_compatible(pot_default_phys, pot_default_noro), ( "pot_default_phys does not behave as expected" ) assert physical_compatible(pot_default_phys, pot_default_novo), ( "pot_default_phys does not behave as expected" ) assert not physical_compatible(pot_default_phys, pot_nonstandardro_novo), ( "pot_default_phys does not behave as expected" ) assert not physical_compatible(pot_default_phys, pot_nonstandardvo_noro), ( "pot_default_phys does not behave as expected" ) # Test expected behavior for single,list pairs assert physical_compatible( pot_default_phys, [pot_default_phys, pot_default_phys] ), "pot_default_phys does not behave as expected" assert not physical_compatible( pot_default_phys, [pot_nonstandardro, pot_nonstandardro] ), "pot_default_phys does not behave as expected" assert not physical_compatible( pot_default_phys, [pot_nonstandardro, pot_default_phys] ), "pot_default_phys does not behave as expected" assert not physical_compatible( pot_nonstandardro, [pot_default_phys, pot_default_phys] ), "pot_default_phys does not behave as expected" assert not physical_compatible( pot_default_phys, [pot_nonstandardvo, pot_default_phys] ), "pot_default_phys does not behave as expected" assert not physical_compatible( pot_default_phys, [pot_nonstandardrovo, pot_nonstandardro] ), "pot_default_phys does not behave as expected" assert physical_compatible(pot_default_phys, [pot_nophys, pot_nophys]), ( "pot_default_phys does not behave as expected" ) assert physical_compatible( pot_default_phys, [pot_default_noro, pot_default_phys] ), "pot_default_phys does not behave as expected" assert physical_compatible(pot_default_phys, [pot_default_novo, pot_nophys]), ( "pot_default_phys does not behave as expected" ) assert not physical_compatible( pot_default_phys, [pot_nonstandardro_novo, pot_nophys] ), "pot_default_phys does not behave as expected" assert not physical_compatible( pot_default_phys, [pot_nonstandardvo_noro, pot_nophys] ), "pot_default_phys does not behave as expected" # Test expected behavior for list,list pairs assert physical_compatible( [pot_default_phys, pot_default_phys], [pot_default_phys, pot_default_phys] ), "pot_default_phys does not behave as expected" assert not physical_compatible( [pot_default_phys, pot_default_phys], [pot_nonstandardro, pot_nonstandardro] ), "pot_default_phys does not behave as expected" assert not physical_compatible( [pot_default_phys, pot_default_phys], [pot_nonstandardro, pot_default_phys] ), "pot_default_phys does not behave as expected" assert not physical_compatible( [pot_nonstandardro, pot_default_phys], [pot_default_phys, pot_default_phys] ), "pot_default_phys does not behave as expected" assert not physical_compatible( [pot_default_phys, pot_default_phys], [pot_nonstandardvo, pot_default_phys] ), "pot_default_phys does not behave as expected" assert not physical_compatible( [pot_default_phys, pot_default_phys], [pot_nonstandardrovo, pot_nonstandardro] ), "pot_default_phys does not behave as expected" assert physical_compatible( [pot_default_phys, pot_default_phys], [pot_nophys, pot_nophys] ), "pot_default_phys does not behave as expected" assert physical_compatible( [pot_default_phys, pot_default_phys], [pot_default_noro, pot_default_phys] ), "pot_default_phys does not behave as expected" assert physical_compatible( [pot_default_phys, pot_default_phys], [pot_default_novo, pot_nophys] ), "pot_default_phys does not behave as expected" assert not physical_compatible( [pot_default_phys, pot_default_phys], [pot_nonstandardro_novo, pot_nophys] ), "pot_default_phys does not behave as expected" assert not physical_compatible( [pot_default_phys, pot_default_phys], [pot_nonstandardvo_noro, pot_nophys] ), "pot_default_phys does not behave as expected" return None # ADD OTHER COMBINATIONS, e.g., potential and orbit def test_physical_compatible_combos(): # Test that physical_compatible acts as expected for combinations of # different types of objects from galpy.actionAngle import actionAngleSpherical from galpy.df import quasiisothermaldf from galpy.orbit import Orbit from galpy.potential import HernquistPotential from galpy.util.conversion import physical_compatible # Set up different objects for possible cases # Potentials pot_default_phys = HernquistPotential(amp=0.55, a=2.0, ro=8.0, vo=220.0) pot_nonstandardro = HernquistPotential(amp=0.55, a=2.0, ro=9.0, vo=220.0) pot_nonstandardvo = HernquistPotential(amp=0.55, a=2.0, ro=8.0, vo=230.0) pot_nonstandardrovo = HernquistPotential(amp=0.55, a=2.0, ro=9.0, vo=230.0) pot_nophys = HernquistPotential(amp=0.55) pot_default_noro = HernquistPotential(amp=0.55, vo=220.0) pot_default_novo = HernquistPotential(amp=0.55, ro=8.0) pot_nonstandardro_novo = HernquistPotential(amp=0.55, ro=9.0) pot_nonstandardvo_noro = HernquistPotential(amp=0.55, vo=230.0) pot_nonstandardvo_noro = HernquistPotential(amp=0.55, vo=230.0) # Orbits orb_default_phys = Orbit([1.0, 0.1, 1.1, 0.1, 0.3, -0.9], ro=8.0, vo=220.0) orb_nonstandardro = Orbit([1.0, 0.1, 1.1, 0.1, 0.3, -0.9], ro=9.0, vo=220.0) orb_nonstandardvo = Orbit([1.0, 0.1, 1.1, 0.1, 0.3, -0.9], ro=8.0, vo=230.0) orb_nonstandardrovo = Orbit([1.0, 0.1, 1.1, 0.1, 0.3, -0.9], ro=9.0, vo=230.0) orb_nophys = Orbit([1.0, 0.1, 1.1, 0.1, 0.3, -0.9]) orb_default_noro = Orbit([1.0, 0.1, 1.1, 0.1, 0.3, -0.9], vo=220.0) orb_nonstandardvo_noro = Orbit([1.0, 0.1, 1.1, 0.1, 0.3, -0.9], vo=230.0) # aAs aA_default_phys = actionAngleSpherical(pot=pot_default_phys, ro=8.0, vo=220.0) aA_nonstandardro = actionAngleSpherical(pot=pot_nonstandardro, ro=9.0, vo=220.0) aA_nonstandardvo = actionAngleSpherical(pot=pot_nonstandardvo, ro=8.0, vo=230.0) aA_nonstandardrovo = actionAngleSpherical(pot=pot_nonstandardrovo, ro=9.0, vo=230.0) aA_nophys = actionAngleSpherical(pot=pot_nophys) aA_default_novo = actionAngleSpherical(pot=pot_default_novo, ro=8.0) aA_nonstandardvo_noro = actionAngleSpherical(pot=pot_nonstandardvo_noro, vo=230.0) # DFs qdf_default_phys = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=pot_default_phys, aA=aA_default_phys, ro=8.0, vo=220.0, ) qdf_nonstandardro = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=pot_nonstandardro, aA=aA_nonstandardro, ro=9.0, vo=220.0, ) qdf_nonstandardvo = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=pot_nonstandardvo, aA=aA_nonstandardvo, ro=8.0, vo=230.0, ) qdf_nonstandardrovo = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=pot_nonstandardrovo, aA=aA_nonstandardrovo, ro=9.0, vo=230.0, ) qdf_nophys = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=pot_nophys, aA=aA_nophys ) # Now do some tests! assert physical_compatible(pot_default_phys, orb_default_phys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_default_phys, aA_default_phys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_default_phys, qdf_default_phys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_nonstandardro, orb_nonstandardro), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_nonstandardro, aA_nonstandardro), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_nonstandardro, qdf_nonstandardro), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert not physical_compatible(pot_default_phys, orb_nonstandardro), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert not physical_compatible(pot_default_phys, aA_nonstandardro), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert not physical_compatible(pot_default_phys, qdf_nonstandardro), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert not physical_compatible(pot_default_phys, orb_nonstandardvo), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert not physical_compatible(pot_default_phys, aA_nonstandardvo), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert not physical_compatible(pot_default_phys, qdf_nonstandardvo), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert not physical_compatible(pot_default_phys, orb_nonstandardrovo), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert not physical_compatible(pot_default_phys, aA_nonstandardrovo), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert not physical_compatible(pot_default_phys, qdf_nonstandardrovo), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible([pot_nophys, pot_nophys], orb_nonstandardrovo), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_nophys, aA_nonstandardrovo), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_nophys, qdf_nonstandardrovo), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_nophys, orb_default_phys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_nophys, aA_default_phys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_nophys, qdf_default_phys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_nophys, orb_nophys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_nophys, orb_nophys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_nophys, qdf_nophys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_default_noro, qdf_nonstandardro), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_nonstandardro_novo, orb_default_noro), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(aA_nonstandardvo_noro, orb_nonstandardvo_noro), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert not physical_compatible(aA_default_novo, qdf_nonstandardrovo), ( "pot_default_phys does not behave as expected for combinations of different objects" ) # Also test agained None! assert physical_compatible(None, pot_default_phys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(None, orb_default_phys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(None, aA_default_phys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(None, qdf_default_phys), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(pot_default_phys, None), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(orb_default_phys, None), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(aA_default_phys, None), ( "pot_default_phys does not behave as expected for combinations of different objects" ) assert physical_compatible(qdf_default_phys, None), ( "pot_default_phys does not behave as expected for combinations of different objects" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_coords.py0000644000175100001660000036062014761352023016370 0ustar00runnerdockerimport numpy from packaging.version import parse as parse_version _NUMPY_VERSION = parse_version(numpy.__version__) _NUMPY_GE_1_22 = (_NUMPY_VERSION > parse_version("1.21")) * ( _NUMPY_VERSION < parse_version("1.25") ) import astropy import pytest from galpy.util import coords _APY3 = astropy.__version__ > "3" def test_radec_to_lb_ngp(): _turn_off_apy() # Test that the NGP is at b=90 ra, dec = 192.25, 27.4 lb = coords.radec_to_lb(ra, dec, degree=True, epoch=1950.0) assert not coords._APY_LOADED, "_APY_LOADED should be False, but isn't" assert ( numpy.fabs(lb[1] - 90.0) < _NUMPY_GE_1_22 * 1e-5 + (1 - _NUMPY_GE_1_22) * 1e-6 ), "Galactic latitude of the NGP given in ra,dec is not 90" # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=1950.0 ) assert numpy.fabs(lb[1] - numpy.pi / 2.0) < 10.0**-7.0, ( "Galactic latitude of the NGP given in ra,dec is not pi/2" ) _turn_on_apy() assert coords._APY_LOADED, "_APY_LOADED should be True, but isn't" return None def test_radec_to_lb_ngp_apyangles(): # Test, but using transformation angles derived from astropy _turn_off_apy(keep_loaded=True) # Test that the NGP is at b=90 ra, dec = 192.25, 27.4 assert coords._APY_LOADED, "_APY_LOADED should be True, but isn't" lb = coords.radec_to_lb(ra, dec, degree=True, epoch="B1950") assert numpy.fabs(lb[1] - 90.0) < 10.0**-4.0, ( "Galactic latitude of the NGP given in ra,dec is not 90" ) # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch="B1950" ) assert numpy.fabs(lb[1] - numpy.pi / 2.0) < 10.0**-4.0, ( "Galactic latitude of the NGP given in ra,dec is not pi/2" ) _turn_on_apy() return None def test_radec_to_lb_ngp_apy(): # Test that the NGP is at b=90, using astropy's coordinate transformations ra, dec = 192.25, 27.4 lb = coords.radec_to_lb(ra, dec, degree=True, epoch=1950.0) assert numpy.fabs(lb[1] - 90.0) < 10.0**-4.0, ( "Galactic latitude of the NGP given in ra,dec is not 90" ) # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=1950.0 ) assert numpy.fabs(lb[1] - numpy.pi / 2.0) < 10.0**-4.0, ( "Galactic latitude of the NGP given in ra,dec is not pi/2" ) return None def test_radec_to_lb_ngp_j2000(): _turn_off_apy() # Test that the NGP is at b=90 ra, dec = 192.8594812065348, 27.12825118085622 lb = coords.radec_to_lb(ra, dec, degree=True, epoch=2000.0) assert numpy.fabs(lb[1] - 90.0) < 10.0**-8.0, ( "Galactic latitude of the NGP given in ra,dec is not 90" ) # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=2000.0 ) assert numpy.fabs(lb[1] - numpy.pi / 2.0) < 10.0**-8.0, ( "Galactic latitude of the NGP given in ra,dec is not pi/2" ) _turn_on_apy() return None def test_radec_to_lb_ngp_j2000_apy(): # Test that the NGP is at b=90 ra, dec = 192.8594812065348, 27.12825118085622 lb = coords.radec_to_lb(ra, dec, degree=True, epoch=2000.0) assert numpy.fabs(lb[1] - 90.0) < 10.0**-4.0, ( "Galactic latitude of the NGP given in ra,dec is not 90" ) # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=2000.0 ) assert numpy.fabs(lb[1] - numpy.pi / 2.0) < 10.0**-4.0, ( "Galactic latitude of the NGP given in ra,dec is not pi/2" ) return None def test_radec_to_lb_ngp_j2000_apyangles(): # Same test, but using transformation angles derived from astropy _turn_off_apy(keep_loaded=True) # Test that the NGP is at b=90 ra, dec = 192.8594812065348, 27.12825118085622 lb = coords.radec_to_lb(ra, dec, degree=True, epoch="J2000") assert numpy.fabs(lb[1] - 90.0) < 10.0**-4.0, ( "Galactic latitude of the NGP given in ra,dec is not 90" ) # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch="J2000" ) assert numpy.fabs(lb[1] - numpy.pi / 2.0) < 10.0**-4.0, ( "Galactic latitude of the NGP given in ra,dec is not pi/2" ) _turn_on_apy() return None def test_radec_to_lb_ngp_j2000_apyangles_icrs(): # Test, but using transformation angles derived from astropy, for ICRS _turn_off_apy(keep_loaded=True) # Test that the NGP is at b=90 ra, dec = 192.8594812065348, 27.12825118085622 lb = coords.radec_to_lb(ra, dec, degree=True, epoch=None) assert numpy.fabs(lb[1] - 90.0) < 10.0**-4.0, ( "Galactic latitude of the NGP given in ra,dec is not 90" ) # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=None ) assert numpy.fabs(lb[1] - numpy.pi / 2.0) < 10.0**-4.0, ( "Galactic latitude of the NGP given in ra,dec is not pi/2" ) _turn_on_apy() return None def test_radec_to_lb_sgp(): _turn_off_apy() # Test that the SGP is at b=90 ra, dec = 12.25, -27.4 assert not coords._APY_LOADED, "_APY_LOADED should be False, but isn't" lb = coords.radec_to_lb(ra, dec, degree=True, epoch=1950.0) assert ( numpy.fabs(lb[1] + 90.0) < _NUMPY_GE_1_22 * 1e-5 + (1 - _NUMPY_GE_1_22) * 1e-6 ), "Galactic latitude of the SGP given in ra,dec is not 90" # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=1950.0 ) assert numpy.fabs(lb[1] + numpy.pi / 2.0) < 10.0**-7.0, ( "Galactic latitude of the SGP given in ra,dec is not pi/2" ) _turn_on_apy() return None # Test the longitude of the north celestial pole def test_radec_to_lb_ncp(): _turn_off_apy() ra, dec = 180.0, 90.0 lb = coords.radec_to_lb(ra, dec, degree=True, epoch=1950.0) assert numpy.fabs(lb[0] - 123.0) < 10.0**-8.0, ( "Galactic longitude of the NCP given in ra,dec is not 123" ) # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=1950.0 ) assert numpy.fabs(lb[0] - 123.0 / 180.0 * numpy.pi) < 10.0**-8.0, ( "Galactic longitude of the NCP given in ra,dec is not 123" ) # Also test the latter for vector inputs os = numpy.ones(2) lb = coords.radec_to_lb( os * ra / 180.0 * numpy.pi, os * dec / 180.0 * numpy.pi, degree=False, epoch=1950.0, ) assert numpy.all(numpy.fabs(lb[:, 0] - 123.0 / 180.0 * numpy.pi) < 10.0**-8.0), ( "Galactic longitude of the NCP given in ra,dec is not 123" ) _turn_on_apy() return None def test_radec_to_lb_ncp_apyangles(): _turn_off_apy(keep_loaded=True) ra, dec = 180.0, 90.0 lb = coords.radec_to_lb(ra, dec, degree=True, epoch="B1950") assert numpy.fabs(lb[0] - 123.0) < 10.0**-4.0, ( "Galactic longitude of the NCP given in ra,dec is not 123" ) _turn_on_apy() return None # Test the longitude of the north celestial pole def test_radec_to_lb_ncp_j2000(): _turn_off_apy() ra, dec = 180.0, 90.0 lb = coords.radec_to_lb(ra, dec, degree=True, epoch=2000.0) assert numpy.fabs(lb[0] - 122.9319185680026) < 10.0**-8.0, ( "Galactic longitude of the NCP given in ra,dec is not 122.9319185680026" ) # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=2000.0 ) assert numpy.fabs(lb[0] - 122.9319185680026 / 180.0 * numpy.pi) < 10.0**-8.0, ( "Galactic longitude of the NCP given in ra,dec is not 122.9319185680026" ) # Also test the latter for vector inputs os = numpy.ones(2) lb = coords.radec_to_lb( os * ra / 180.0 * numpy.pi, os * dec / 180.0 * numpy.pi, degree=False, epoch=2000.0, ) assert numpy.all( numpy.fabs(lb[:, 0] - 122.9319185680026 / 180.0 * numpy.pi) < 10.0**-8.0 ), "Galactic longitude of the NCP given in ra,dec is not 122.9319185680026" _turn_on_apy() return None def test_radec_to_lb_ncp_j2000_apyangles(): _turn_off_apy(keep_loaded=True) ra, dec = 180.0, 90.0 lb = coords.radec_to_lb(ra, dec, degree=True, epoch="J2000") assert numpy.fabs(lb[0] - 122.9319185680026) < 10.0**-4.0, ( "Galactic longitude of the NCP given in ra,dec is not 122.9319185680026" ) _turn_on_apy() return None # Test that other epochs do not work when not using astropy def test_radec_to_lb_otherepochs(): _turn_off_apy() ra, dec = 180.0, 90.0 try: lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=1975.0 ) except OSError: pass else: raise AssertionError( "radec functions with epoch not equal to 1950 or 2000 did not raise IOError" ) _turn_on_apy() return None # Test that other epochs do work when using astropy def test_radec_to_lb_otherepochs_apy(): _turn_off_apy(keep_loaded=True) ra, dec = 180.0, 90.0 try: lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch="J2015" ) except OSError: raise AssertionError( "radec functions with epoch not equal to 1950 or 2000 did not raise IOError" ) else: pass _turn_on_apy() return None # Test that radec_to_lb and lb_to_radec are each other's inverse def test_lb_to_radec(): _turn_off_apy() ra, dec = 120, 60.0 lb = coords.radec_to_lb(ra, dec, degree=True, epoch=2000.0) rat, dect = coords.lb_to_radec(lb[0], lb[1], degree=True, epoch=2000.0) assert numpy.fabs(ra - rat) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) assert numpy.fabs(dec - dect) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=2000.0 ) rat, dect = coords.lb_to_radec(lb[0], lb[1], degree=False, epoch=2000.0) assert numpy.fabs(ra / 180.0 * numpy.pi - rat) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) assert numpy.fabs(dec / 180.0 * numpy.pi - dect) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) # And also test this for arrays os = numpy.ones(2) lb = coords.radec_to_lb( os * ra / 180.0 * numpy.pi, os * dec / 180.0 * numpy.pi, degree=False, epoch=2000.0, ) ratdect = coords.lb_to_radec(lb[:, 0], lb[:, 1], degree=False, epoch=2000.0) rat = ratdect[:, 0] dect = ratdect[:, 1] assert numpy.all(numpy.fabs(ra / 180.0 * numpy.pi - rat) < 10.0**-10.0), ( "lb_to_radec is not the inverse of radec_to_lb" ) assert numpy.all(numpy.fabs(dec / 180.0 * numpy.pi - dect) < 10.0**-10.0), ( "lb_to_radec is not the inverse of radec_to_lb" ) # Also test for a negative l l, b = 240.0, 60.0 ra, dec = coords.lb_to_radec(l, b, degree=True) lt, bt = coords.radec_to_lb(ra, dec, degree=True) assert numpy.fabs(lt - l) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) assert numpy.fabs(bt - b) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) _turn_on_apy() return None # Test that radec_to_lb and lb_to_radec are each other's inverse, using astropy def test_lb_to_radec_apy(): ra, dec = 120, 60.0 lb = coords.radec_to_lb(ra, dec, degree=True, epoch=2000.0) rat, dect = coords.lb_to_radec(lb[0], lb[1], degree=True, epoch=2000.0) assert numpy.fabs(ra - rat) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) assert numpy.fabs(dec - dect) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=2000.0 ) rat, dect = coords.lb_to_radec(lb[0], lb[1], degree=False, epoch=2000.0) assert numpy.fabs(ra / 180.0 * numpy.pi - rat) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) assert numpy.fabs(dec / 180.0 * numpy.pi - dect) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) # And also test this for arrays os = numpy.ones(2) lb = coords.radec_to_lb( os * ra / 180.0 * numpy.pi, os * dec / 180.0 * numpy.pi, degree=False, epoch=2000.0, ) ratdect = coords.lb_to_radec(lb[:, 0], lb[:, 1], degree=False, epoch=2000.0) rat = ratdect[:, 0] dect = ratdect[:, 1] assert numpy.all(numpy.fabs(ra / 180.0 * numpy.pi - rat) < 10.0**-10.0), ( "lb_to_radec is not the inverse of radec_to_lb" ) assert numpy.all(numpy.fabs(dec / 180.0 * numpy.pi - dect) < 10.0**-10.0), ( "lb_to_radec is not the inverse of radec_to_lb" ) # Also test for a negative l l, b = 240.0, 60.0 ra, dec = coords.lb_to_radec(l, b, degree=True) lt, bt = coords.radec_to_lb(ra, dec, degree=True) assert numpy.fabs(lt - l) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) assert numpy.fabs(bt - b) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) return None # Test that radec_to_lb and lb_to_radec are each other's inverse, using astropy def test_lb_to_radec_apy_icrs(): ra, dec = 120, 60.0 lb = coords.radec_to_lb(ra, dec, degree=True, epoch=None) rat, dect = coords.lb_to_radec(lb[0], lb[1], degree=True, epoch=None) assert numpy.fabs(ra - rat) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) assert numpy.fabs(dec - dect) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) # Also test this for degree=False lb = coords.radec_to_lb( ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=None ) rat, dect = coords.lb_to_radec(lb[0], lb[1], degree=False, epoch=None) assert numpy.fabs(ra / 180.0 * numpy.pi - rat) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) assert numpy.fabs(dec / 180.0 * numpy.pi - dect) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) # And also test this for arrays os = numpy.ones(2) lb = coords.radec_to_lb( os * ra / 180.0 * numpy.pi, os * dec / 180.0 * numpy.pi, degree=False, epoch=None, ) ratdect = coords.lb_to_radec(lb[:, 0], lb[:, 1], degree=False, epoch=None) rat = ratdect[:, 0] dect = ratdect[:, 1] assert numpy.all(numpy.fabs(ra / 180.0 * numpy.pi - rat) < 10.0**-10.0), ( "lb_to_radec is not the inverse of radec_to_lb" ) assert numpy.all(numpy.fabs(dec / 180.0 * numpy.pi - dect) < 10.0**-10.0), ( "lb_to_radec is not the inverse of radec_to_lb" ) # Also test for a negative l l, b = 240.0, 60.0 ra, dec = coords.lb_to_radec(l, b, degree=True) lt, bt = coords.radec_to_lb(ra, dec, degree=True) assert numpy.fabs(lt - l) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) assert numpy.fabs(bt - b) < 10.0**-10.0, ( "lb_to_radec is not the inverse of radec_to_lb" ) return None def test_radec_to_lb_galpyvsastropy(): # Test that galpy's radec_to_lb agrees with astropy's import astropy.units as u from astropy.coordinates import SkyCoord _turn_off_apy(keep_loaded=True) ra, dec = 33.0, -20.0 # using galpy lg, bg = coords.radec_to_lb(ra, dec, degree=True, epoch=2000.0) # using astropy c = SkyCoord(ra=ra * u.deg, dec=dec * u.deg, frame="fk5", equinox="J2000") c = c.transform_to("galactic") lla, bba = c.l.to(u.deg).value, c.b.to(u.deg).value assert numpy.fabs(lg - lla) < 1e-12, ( "radec_to_lb using galpy's own transformations does not agree with astropy's" ) assert numpy.fabs(bg - bba) < 1e-12, ( "radec_to_lb using galpy's own transformations does not agree with astropy's" ) _turn_on_apy() return None def test_radec_to_lb__1950_galpyvsastropy(): # Test that galpy's radec_to_lb agrees with astropy's import astropy.units as u from astropy.coordinates import SkyCoord _turn_off_apy(keep_loaded=True) ra, dec = 33.0, -20.0 # using galpy lg, bg = coords.radec_to_lb(ra, dec, degree=True, epoch=1950.0) # using astropy c = SkyCoord(ra=ra * u.deg, dec=dec * u.deg, frame="fk4noeterms", equinox="B1950") c = c.transform_to("galactic") lla, bba = c.l.to(u.deg).value, c.b.to(u.deg).value assert numpy.fabs(lg - lla) < 1e-12, ( "radec_to_lb using galpy's own transformations does not agree with astropy's" ) assert numpy.fabs(bg - bba) < 1e-12, ( "radec_to_lb using galpy's own transformations does not agree with astropy's" ) _turn_on_apy() return None # Test lb_to_XYZ def test_lbd_to_XYZ(): l, b, d = 90.0, 30.0, 1.0 XYZ = coords.lbd_to_XYZ(l, b, d, degree=True) assert numpy.fabs(XYZ[0]) < 10.0**-10.0, ( "lbd_to_XYZ conversion does not work as expected" ) assert numpy.fabs(XYZ[1] - numpy.sqrt(3.0) / 2.0) < 10.0**-10.0, ( "lbd_to_XYZ conversion does not work as expected" ) assert numpy.fabs(XYZ[2] - 0.5) < 10.0**-10.0, ( "lbd_to_XYZ conversion does not work as expected" ) # Also test for degree=False XYZ = coords.lbd_to_XYZ(l / 180.0 * numpy.pi, b / 180.0 * numpy.pi, d, degree=False) assert numpy.fabs(XYZ[0]) < 10.0**-10.0, ( "lbd_to_XYZ conversion does not work as expected" ) assert numpy.fabs(XYZ[1] - numpy.sqrt(3.0) / 2.0) < 10.0**-10.0, ( "lbd_to_XYZ conversion does not work as expected" ) assert numpy.fabs(XYZ[2] - 0.5) < 10.0**-10.0, ( "lbd_to_XYZ conversion does not work as expected" ) # Also test for arrays os = numpy.ones(2) XYZ = coords.lbd_to_XYZ( os * l / 180.0 * numpy.pi, os * b / 180.0 * numpy.pi, os * d, degree=False ) assert numpy.all(numpy.fabs(XYZ[:, 0]) < 10.0**-10.0), ( "lbd_to_XYZ conversion does not work as expected" ) assert numpy.all(numpy.fabs(XYZ[:, 1] - numpy.sqrt(3.0) / 2.0) < 10.0**-10.0), ( "lbd_to_XYZ conversion does not work as expected" ) assert numpy.all(numpy.fabs(XYZ[:, 2] - 0.5) < 10.0**-10.0), ( "lbd_to_XYZ conversion does not work as expected" ) return None # Test that XYZ_to_lbd is the inverse of lbd_to_XYZ def test_XYZ_to_lbd(): l, b, d = 90.0, 30.0, 1.0 XYZ = coords.lbd_to_XYZ(l, b, d, degree=True) lt, bt, dt = coords.XYZ_to_lbd(XYZ[0], XYZ[1], XYZ[2], degree=True) assert numpy.fabs(lt - l) < 10.0**-10.0, ( "XYZ_to_lbd conversion does not work as expected" ) assert numpy.fabs(bt - b) < 10.0**-10.0, ( "XYZ_to_lbd conversion does not work as expected" ) assert numpy.fabs(dt - d) < 10.0**-10.0, ( "XYZ_to_lbd conversion does not work as expected" ) # Also test for degree=False XYZ = coords.lbd_to_XYZ(l / 180.0 * numpy.pi, b / 180.0 * numpy.pi, d, degree=False) lt, bt, dt = coords.XYZ_to_lbd(XYZ[0], XYZ[1], XYZ[2], degree=False) assert numpy.fabs(lt - l / 180.0 * numpy.pi) < 10.0**-10.0, ( "XYZ_to_lbd conversion does not work as expected" ) assert numpy.fabs(bt - b / 180.0 * numpy.pi) < 10.0**-10.0, ( "XYZ_to_lbd conversion does not work as expected" ) assert numpy.fabs(dt - d) < 10.0**-10.0, ( "XYZ_to_lbd conversion does not work as expected" ) # Also test for arrays os = numpy.ones(2) XYZ = coords.lbd_to_XYZ( os * l / 180.0 * numpy.pi, os * b / 180.0 * numpy.pi, os * d, degree=False ) lbdt = coords.XYZ_to_lbd(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], degree=False) assert numpy.all(numpy.fabs(lbdt[:, 0] - l / 180.0 * numpy.pi) < 10.0**-10.0), ( "XYZ_to_lbd conversion does not work as expected" ) assert numpy.all(numpy.fabs(lbdt[:, 1] - b / 180.0 * numpy.pi) < 10.0**-10.0), ( "XYZ_to_lbd conversion does not work as expected" ) assert numpy.all(numpy.fabs(lbdt[:, 2] - d) < 10.0**-10.0), ( "XYZ_to_lbd conversion does not work as expected" ) return None def test_vrpmllpmbb_to_vxvyvz(): l, b, d = 90.0, 0.0, 1.0 vr, pmll, pmbb = 10.0, 20.0 / 4.740470463496208, -10.0 / 4.740470463496208 vxvyvz = coords.vrpmllpmbb_to_vxvyvz( vr, pmll, pmbb, l, b, d, degree=True, XYZ=False ) assert numpy.fabs(vxvyvz[0] + 20.0) < 10.0**-9.0, ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) assert numpy.fabs(vxvyvz[1] - 10.0) < 10.0**-9.0, ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) assert numpy.fabs(vxvyvz[2] + 10.0) < 10.0**-9.0, ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) vxvyvz = coords.vrpmllpmbb_to_vxvyvz( vr, pmll, pmbb, l / 180.0 * numpy.pi, b / 180.0 * numpy.pi, d, degree=False, XYZ=False, ) assert numpy.fabs(vxvyvz[0] + 20.0) < 10.0**-9.0, ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) assert numpy.fabs(vxvyvz[1] - 10.0) < 10.0**-9.0, ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) assert numpy.fabs(vxvyvz[2] + 10.0) < 10.0**-9.0, ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) vxvyvz = coords.vrpmllpmbb_to_vxvyvz(vr, pmll, pmbb, 0.0, 1, 0.0, XYZ=True) assert numpy.fabs(vxvyvz[0] + 20.0) < 10.0**-9.0, ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) assert numpy.fabs(vxvyvz[1] - 10.0) < 10.0**-9.0, ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) assert numpy.fabs(vxvyvz[2] + 10.0) < 10.0**-9.0, ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) vxvyvz = coords.vrpmllpmbb_to_vxvyvz( vr, pmll, pmbb, 0.0, 1, 0.0, XYZ=True, degree=True ) assert numpy.fabs(vxvyvz[0] + 20.0) < 10.0**-9.0, ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) assert numpy.fabs(vxvyvz[1] - 10.0) < 10.0**-9.0, ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) assert numpy.fabs(vxvyvz[2] + 10.0) < 10.0**-9.0, ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) # Also test for arrays os = numpy.ones(2) vxvyvz = coords.vrpmllpmbb_to_vxvyvz( os * vr, os * pmll, os * pmbb, os * l, os * b, os * d, degree=True, XYZ=False ) assert numpy.all(numpy.fabs(vxvyvz[:, 0] + 20.0) < 10.0**-9.0), ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) assert numpy.all(numpy.fabs(vxvyvz[:, 1] - 10.0) < 10.0**-9.0), ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) assert numpy.all(numpy.fabs(vxvyvz[:, 2] + 10.0) < 10.0**-9.0), ( "vrpmllpmbb_to_vxvyvz conversion did not work as expected" ) return None def test_vxvyvz_to_vrpmllpmbb(): vx, vy, vz = -20.0 * 4.740470463496208, 10.0, -10.0 * 4.740470463496208 X, Y, Z = 0.0, 1.0, 0.0 vrpmllpmbb = coords.vxvyvz_to_vrpmllpmbb(vx, vy, vz, X, Y, Z, XYZ=True) assert numpy.fabs(vrpmllpmbb[0] - 10.0) < 10.0**-9.0, ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) assert numpy.fabs(vrpmllpmbb[1] - 20.0) < 10.0**-9.0, ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) assert numpy.fabs(vrpmllpmbb[2] + 10.0) < 10.0**-9.0, ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) # also try with degree=True (that shouldn't fail!) vrpmllpmbb = coords.vxvyvz_to_vrpmllpmbb(vx, vy, vz, X, Y, Z, XYZ=True, degree=True) assert numpy.fabs(vrpmllpmbb[0] - 10.0) < 10.0**-9.0, ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) assert numpy.fabs(vrpmllpmbb[1] - 20.0) < 10.0**-9.0, ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) assert numpy.fabs(vrpmllpmbb[2] + 10.0) < 10.0**-9.0, ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) # also for lbd vrpmllpmbb = coords.vxvyvz_to_vrpmllpmbb( vx, vy, vz, 90.0, 0.0, 1.0, XYZ=False, degree=True ) assert numpy.fabs(vrpmllpmbb[0] - 10.0) < 10.0**-9.0, ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) assert numpy.fabs(vrpmllpmbb[1] - 20.0) < 10.0**-9.0, ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) assert numpy.fabs(vrpmllpmbb[2] + 10.0) < 10.0**-9.0, ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) # also for lbd, not in degree vrpmllpmbb = coords.vxvyvz_to_vrpmllpmbb( vx, vy, vz, numpy.pi / 2.0, 0.0, 1.0, XYZ=False, degree=False ) assert numpy.fabs(vrpmllpmbb[0] - 10.0) < 10.0**-9.0, ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) assert numpy.fabs(vrpmllpmbb[1] - 20.0) < 10.0**-9.0, ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) assert numpy.fabs(vrpmllpmbb[2] + 10.0) < 10.0**-9.0, ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) # and for arrays os = numpy.ones(2) vrpmllpmbb = coords.vxvyvz_to_vrpmllpmbb( os * vx, os * vy, os * vz, os * numpy.pi / 2.0, os * 0.0, os, XYZ=False, degree=False, ) assert numpy.all(numpy.fabs(vrpmllpmbb[:, 0] - 10.0) < 10.0**-9.0), ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) assert numpy.all(numpy.fabs(vrpmllpmbb[:, 1] - 20.0) < 10.0**-9.0), ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) assert numpy.all(numpy.fabs(vrpmllpmbb[:, 2] + 10.0) < 10.0**-10.0), ( "vxvyvz_to_vrpmllpmbb conversion did not work as expected" ) return None def test_XYZ_to_galcenrect(): X, Y, Z = 1.0, 3.0, -2.0 gcXYZ = coords.XYZ_to_galcenrect(X, Y, Z, Xsun=1.0, Zsun=0.0) assert numpy.fabs(gcXYZ[0]) < 10.0**-5.0, ( "XYZ_to_galcenrect conversion did not work as expected" ) assert numpy.fabs(gcXYZ[1] - 3.0) < 10.0**-5.0, ( "XYZ_to_galcenrect conversion did not work as expected" ) assert numpy.fabs(gcXYZ[2] + 2.0) < 10.0**-5.0, ( "XYZ_to_galcenrect conversion did not work as expected" ) # Another test X, Y, Z = -1.0, 3.0, -2.0 gcXYZ = coords.XYZ_to_galcenrect(X, Y, Z, Xsun=1.0, Zsun=0.0) assert numpy.fabs(gcXYZ[0] - 2.0) < 10.0**-5.0, ( "XYZ_to_galcenrect conversion did not work as expected" ) assert numpy.fabs(gcXYZ[1] - 3.0) < 10.0**-5.0, ( "XYZ_to_galcenrect conversion did not work as expected" ) assert numpy.fabs(gcXYZ[2] + 2.0) < 10.0**-5.0, ( "XYZ_to_galcenrect conversion did not work as expected" ) return None def test_XYZ_to_galcenrect_negXsun(): # Check that XYZ_to_galcenrect works for negative Xsun X, Y, Z = 0.3, 2.1, -1.2 gcXYZ = coords.XYZ_to_galcenrect(X, Y, Z, Xsun=1.2, Zsun=0.2) gcXYZn = coords.XYZ_to_galcenrect(X, Y, Z, Xsun=-1.2, Zsun=0.2) assert numpy.fabs(gcXYZ[0] + gcXYZn[0]) < 10.0**-10.0, ( "XYZ_to_galcenrect conversion did not work as expected for negative Xsun" ) assert numpy.fabs(gcXYZ[1] - gcXYZn[1]) < 10.0**-10.0, ( "XYZ_to_galcenrect conversion did not work as expected for negative Xsun" ) assert numpy.fabs(gcXYZ[2] - gcXYZn[2]) < 10.0**-10.0, ( "XYZ_to_galcenrect conversion did not work as expected for negative Xsun" ) def test_XYZ_to_galcenrect_vecSun(): X, Y, Z = ( numpy.array([1.0, 2.0]), numpy.array([3.0, 3.0]), numpy.array([-2.0, -2.0]), ) gcXYZ = coords.XYZ_to_galcenrect( X, Y, Z, Xsun=numpy.array([1.0, -2.0]), Zsun=numpy.array([0.0, 0.0]) ) assert numpy.all(numpy.fabs(gcXYZ[:, 0]) < 10.0**-5.0), ( "XYZ_to_galcenrect conversion did not work as expected" ) assert numpy.all(numpy.fabs(gcXYZ[:, 1] - 3.0) < 10.0**-5.0), ( "XYZ_to_galcenrect conversion did not work as expected" ) assert numpy.all(numpy.fabs(gcXYZ[:, 2] + 2.0) < 10.0**-5.0), ( "XYZ_to_galcenrect conversion did not work as expected" ) return None def test_lbd_to_galcenrect_galpyvsastropy(): # Test that galpy's transformations agree with astropy's import astropy.units as u from astropy.coordinates import Galactocentric, SkyCoord _turn_off_apy() l, b, d = 32.0, -12.0, 3.0 Zsun = 0.025 # Using galpy X, Y, Z = coords.lbd_to_XYZ(l, b, d, degree=True) gcXYZ = coords.XYZ_to_galcenrect(X, Y, Z, Xsun=8.0, Zsun=Zsun) # Using astropy c = SkyCoord(l=l * u.deg, b=b * u.deg, distance=d * u.kpc, frame="galactic") gc_frame = Galactocentric( galcen_distance=numpy.sqrt(8.0**2.0 + Zsun**2.0) * u.kpc, z_sun=Zsun * u.kpc ) c = c.transform_to(gc_frame) # galpy is left-handed, astropy right-handed assert numpy.fabs(gcXYZ[0] + c.x.to(u.kpc).value) < 10.0**-10.0, ( "lbd to galcenrect conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(gcXYZ[1] - c.y.to(u.kpc).value) < 10.0**-10.0, ( "lbd to galcenrect conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(gcXYZ[2] - c.z.to(u.kpc).value) < 10.0**-9.5, ( "lbd to galcenrect conversion using galpy's methods does not agree with astropy" ) # Also with negative Xsun l, b, d = 32.0, -12.0, 3.0 Zsun = 0.025 # Using galpy X, Y, Z = coords.lbd_to_XYZ(l, b, d, degree=True) gcXYZ = coords.XYZ_to_galcenrect(X, Y, Z, Xsun=-8.0, Zsun=Zsun) # Using astropy c = SkyCoord(l=l * u.deg, b=b * u.deg, distance=d * u.kpc, frame="galactic") gc_frame = Galactocentric( galcen_distance=numpy.sqrt(8.0**2.0 + Zsun**2.0) * u.kpc, z_sun=Zsun * u.kpc ) c = c.transform_to(gc_frame) # galpy is now right-handed, astropy right-handed assert numpy.fabs(gcXYZ[0] - c.x.to(u.kpc).value) < 10.0**-10.0, ( "lbd to galcenrect conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(gcXYZ[1] - c.y.to(u.kpc).value) < 10.0**-10.0, ( "lbd to galcenrect conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(gcXYZ[2] - c.z.to(u.kpc).value) < 10.0**-9.5, ( "lbd to galcenrect conversion using galpy's methods does not agree with astropy" ) _turn_on_apy() return None def test_lbd_to_galcencyl_galpyvsastropy(): # Test that galpy's transformations agree with astropy's import astropy.units as u from astropy.coordinates import Galactocentric, SkyCoord _turn_off_apy() l, b, d = 32.0, -12.0, 3.0 Zsun = 0.025 # Using galpy X, Y, Z = coords.lbd_to_XYZ(l, b, d, degree=True) gcRpZ = coords.XYZ_to_galcencyl(X, Y, Z, Xsun=8.0, Zsun=Zsun) # Using astropy c = SkyCoord(l=l * u.deg, b=b * u.deg, distance=d * u.kpc, frame="galactic") gc_frame = Galactocentric( galcen_distance=numpy.sqrt(8.0**2.0 + Zsun**2.0) * u.kpc, z_sun=Zsun * u.kpc ) c = c.transform_to(gc_frame) c.representation_type = "cylindrical" # galpy is left-handed, astropy right-handed assert numpy.fabs(gcRpZ[0] - c.rho.to(u.kpc).value) < 10.0**-10.0, ( "lbd to galcencyl conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(gcRpZ[1] - numpy.pi + c.phi.to(u.rad).value) < 10.0**-10.0, ( "lbd to galcencyl conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(gcRpZ[2] - c.z.to(u.kpc).value) < 10.0**-9.5, ( "lbd to galcencyl conversion using galpy's methods does not agree with astropy" ) # Also with negative Xsun l, b, d = 32.0, -12.0, 3.0 Zsun = 0.025 # Using galpy X, Y, Z = coords.lbd_to_XYZ(l, b, d, degree=True) gcRpZ = coords.XYZ_to_galcencyl(X, Y, Z, Xsun=-8.0, Zsun=Zsun) # Using astropy c = SkyCoord(l=l * u.deg, b=b * u.deg, distance=d * u.kpc, frame="galactic") gc_frame = Galactocentric( galcen_distance=numpy.sqrt(8.0**2.0 + Zsun**2.0) * u.kpc, z_sun=Zsun * u.kpc ) c = c.transform_to(gc_frame) c.representation_type = "cylindrical" # galpy is now right-handed, astropy right-handed assert numpy.fabs(gcRpZ[0] - c.rho.to(u.kpc).value) < 10.0**-10.0, ( "lbd to galcencyl conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(gcRpZ[1] - c.phi.to(u.rad).value) < 10.0**-10.0, ( "lbd to galcencyl conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(gcRpZ[2] - c.z.to(u.kpc).value) < 10.0**-9.5, ( "lbd to galcencyl conversion using galpy's methods does not agree with astropy" ) _turn_on_apy() return None def test_galcenrect_to_XYZ_negXsun(): gcX, gcY, gcZ = -1.0, 4.0, 2.0 XYZ = numpy.array(coords.galcenrect_to_XYZ(gcX, gcY, gcZ, Xsun=1.0, Zsun=0.2)) XYZn = numpy.array(coords.galcenrect_to_XYZ(-gcX, gcY, gcZ, Xsun=-1.0, Zsun=0.2)) assert numpy.all(numpy.fabs(XYZ - XYZn) < 10.0**-10.0), ( "galcenrect_to_XYZ conversion did not work as expected for negative Xsun" ) return None def test_galcenrect_to_XYZ(): gcX, gcY, gcZ = -1.0, 4.0, 2.0 XYZ = coords.galcenrect_to_XYZ(gcX, gcY, gcZ, Xsun=1.0, Zsun=0.0) assert numpy.fabs(XYZ[0] - 2.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[1] - 4.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[2] - 2.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) # Also for arrays s = numpy.arange(2) + 1 XYZ = coords.galcenrect_to_XYZ(gcX * s, gcY * s, gcZ * s, Xsun=1.0, Zsun=0.0) assert numpy.fabs(XYZ[0, 0] - 2.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[0, 1] - 4.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[0, 2] - 2.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) # Check 2nd one assert numpy.fabs(XYZ[1, 0] - 3.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[1, 1] - 8.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[1, 2] - 4.0) < 10.0**-4.7, ( "galcenrect_to_XYZ conversion did not work as expected" ) # Also for arrays with Xsun/Zsun also arrays s = numpy.arange(2) + 1 XYZ = coords.galcenrect_to_XYZ( gcX * s, gcY * s, gcZ * s, Xsun=1.0 * s, Zsun=0.0 * s ) assert numpy.fabs(XYZ[0, 0] - 2.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[0, 1] - 4.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[0, 2] - 2.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) # Check 2nd one assert numpy.fabs(XYZ[1, 0] - 4.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[1, 1] - 8.0) < 10.0**-5.0, ( "galcenrect_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[1, 2] - 4.0) < 10.0**-4.7, ( "galcenrect_to_XYZ conversion did not work as expected" ) return None def test_galcenrect_to_XYZ_asInverse(): # Test that galcenrect_to_XYZ is the inverse of XYZ_to_galcenrect X, Y, Z = 1.0, 3.0, -2.0 gcXYZ = coords.XYZ_to_galcenrect(X, Y, Z, Xsun=1.0, Zsun=0.1) Xt, Yt, Zt = coords.galcenrect_to_XYZ( gcXYZ[0], gcXYZ[1], gcXYZ[2], Xsun=1.0, Zsun=0.1 ) assert numpy.fabs(X - Xt) < 1e-14, ( "galcenrect_to_XYZ is not the exact inverse of XYZ_to_galcenrect" ) assert numpy.fabs(Y - Yt) < 1e-14, ( "galcenrect_to_XYZ is not the exact inverse of XYZ_to_galcenrect" ) assert numpy.fabs(Z - Zt) < 1e-14, ( "galcenrect_to_XYZ is not the exact inverse of XYZ_to_galcenrect" ) return None def test_XYZ_to_galcencyl(): X, Y, Z = 5.0, 4.0, -2.0 gcRpZ = coords.XYZ_to_galcencyl(X, Y, Z, Xsun=8.0, Zsun=0.0) assert numpy.fabs(gcRpZ[0] - 5.0) < 10.0**-5.0, ( "XYZ_to_galcencyl conversion did not work as expected" ) assert numpy.fabs(gcRpZ[1] - numpy.arctan(4.0 / 3.0)) < 10.0**-5.0, ( "XYZ_to_galcencyl conversion did not work as expected" ) assert numpy.fabs(gcRpZ[2] + 2.0) < 10.0**-4.8, ( "XYZ_to_galcencyl conversion did not work as expected" ) # Another X X, Y, Z = 11.0, 4.0, -2.0 gcRpZ = coords.XYZ_to_galcencyl(X, Y, Z, Xsun=8.0, Zsun=0.0) assert numpy.fabs(gcRpZ[0] - 5.0) < 10.0**-5.0, ( "XYZ_to_galcencyl conversion did not work as expected" ) assert numpy.fabs(gcRpZ[1] - numpy.pi + numpy.arctan(4.0 / 3.0)) < 10.0**-5.0, ( "XYZ_to_galcencyl conversion did not work as expected" ) assert numpy.fabs(gcRpZ[2] + 2.0) < 10.0**-4.6, ( "XYZ_to_galcencyl conversion did not work as expected" ) return None def test_XYZ_to_galcencyl_vecSun(): X, Y, Z = ( numpy.array([5.0, 4.0]), numpy.array([4.0, 4.0]), numpy.array([-2.0, -2.0]), ) gcRpZ = coords.XYZ_to_galcencyl( X, Y, Z, Xsun=numpy.array([8.0, 7.0]), Zsun=numpy.array([0.0, 0.0]) ) assert numpy.all(numpy.fabs(gcRpZ[:, 0] - 5.0) < 10.0**-5.0), ( "XYZ_to_galcencyl conversion did not work as expected" ) assert numpy.all(numpy.fabs(gcRpZ[:, 1] - numpy.arctan(4.0 / 3.0)) < 10.0**-5.0), ( "XYZ_to_galcencyl conversion did not work as expected" ) assert numpy.all(numpy.fabs(gcRpZ[:, 2] + 2.0) < 10.0**-4.8), ( "XYZ_to_galcencyl conversion did not work as expected" ) return None def test_galcencyl_to_XYZ(): gcR, gcp, gcZ = 5.0, numpy.arctan(4.0 / 3.0), 2.0 XYZ = coords.galcencyl_to_XYZ(gcR, gcp, gcZ, Xsun=8.0, Zsun=0.0) assert numpy.fabs(XYZ[0] - 5.0) < 10.0**-5.0, ( "galcencyl_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[1] - 4.0) < 10.0**-5.0, ( "galcencyl_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[2] - 2.0) < 10.0**-4.7, ( "galcencyl_to_XYZ conversion did not work as expected" ) # Also for arrays s = numpy.arange(2) + 1 XYZ = coords.galcencyl_to_XYZ(gcR * s, gcp * s, gcZ * s, Xsun=8.0, Zsun=0.0) assert numpy.fabs(XYZ[0, 0] - 5.0) < 10.0**-5.0, ( "galcencyl_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[0, 1] - 4.0) < 10.0**-5.0, ( "galcencyl_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[0, 2] - 2.0) < 10.0**-4.7, ( "galcencyl_to_XYZ conversion did not work as expected" ) # Also test the second one assert numpy.fabs(XYZ[1, 0] - 10.8) < 10.0**-5.0, ( "galcencyl_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[1, 1] - 9.6) < 10.0**-4.7, ( "galcencyl_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[1, 2] - 4.0) < 10.0**-4.5, ( "galcencyl_to_XYZ conversion did not work as expected" ) # Also for arrays where Xsun/Zsun are also arrays s = numpy.arange(2) + 1 XYZ = coords.galcencyl_to_XYZ(gcR * s, gcp * s, gcZ * s, Xsun=8.0 * s, Zsun=0.0 * s) assert numpy.fabs(XYZ[0, 0] - 5.0) < 10.0**-5.0, ( "galcencyl_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[0, 1] - 4.0) < 10.0**-5.0, ( "galcencyl_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[0, 2] - 2.0) < 10.0**-4.7, ( "galcencyl_to_XYZ conversion did not work as expected" ) # Also test the second one assert numpy.fabs(XYZ[1, 0] - 18.8) < 10.0**-5.0, ( "galcencyl_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[1, 1] - 9.6) < 10.0**-4.5, ( "galcencyl_to_XYZ conversion did not work as expected" ) assert numpy.fabs(XYZ[1, 2] - 4.0) < 10.0**-4.0, ( "galcencyl_to_XYZ conversion did not work as expected" ) return None def test_galcencyl_to_XYZ_asInverse(): # Test that galcencyl_to_XYZ is the inverse of XYZ_to_galcencyl X, Y, Z = 1.0, 3.0, -2.0 gcRpZ = coords.XYZ_to_galcencyl(X, Y, Z, Xsun=1.0, Zsun=0.1) Xt, Yt, Zt = coords.galcencyl_to_XYZ( gcRpZ[0], gcRpZ[1], gcRpZ[2], Xsun=1.0, Zsun=0.1 ) assert numpy.fabs(X - Xt) < 1e-14, ( "galcencyl_to_XYZ is not the exact inverse of XYZ_to_galcencyl" ) assert numpy.fabs(Y - Yt) < 1e-14, ( "galcencyl_to_XYZ is not the exact inverse of XYZ_to_galcencyl" ) assert numpy.fabs(Z - Zt) < 1e-14, ( "galcencyl_to_XYZ is not the exact inverse of XYZ_to_galcencyl" ) # Also for arrays where Xsun/Zsun are also arrays s = numpy.arange(2) + 1 gcRpZ1 = coords.XYZ_to_galcencyl( X * s[0], Y * s[0], Z * s[0], Xsun=1.0 * s[0], Zsun=0.1 * s[0] ) gcRpZ2 = coords.XYZ_to_galcencyl( X * s[1], Y * s[1], Z * s[1], Xsun=1.0 * s[1], Zsun=0.1 * s[1] ) XYZt = coords.galcencyl_to_XYZ( numpy.hstack((gcRpZ1[0], gcRpZ2[0])), numpy.hstack((gcRpZ1[1], gcRpZ2[1])), numpy.hstack((gcRpZ1[2], gcRpZ2[2])), Xsun=1.0 * s, Zsun=0.1 * s, ) # first one assert numpy.fabs(XYZt[0, 0] - Xt) < 1e-14, ( "galcencyl_to_XYZ is not the exact inverse of XYZ_to_galcencyl" ) assert numpy.fabs(XYZt[0, 1] - Yt) < 1e-14, ( "galcencyl_to_XYZ is not the exact inverse of XYZ_to_galcencyl" ) assert numpy.fabs(XYZt[0, 2] - Zt) < 1e-14, ( "galcencyl_to_XYZ is not the exact inverse of XYZ_to_galcencyl" ) # second one assert numpy.fabs(XYZt[1, 0] - Xt * s[1]) < 1e-14, ( "galcencyl_to_XYZ is not the exact inverse of XYZ_to_galcencyl" ) assert numpy.fabs(XYZt[1, 1] - Yt * s[1]) < 1e-14, ( "galcencyl_to_XYZ is not the exact inverse of XYZ_to_galcencyl" ) assert numpy.fabs(XYZt[1, 2] - Zt * s[1]) < 1e-14, ( "galcencyl_to_XYZ is not the exact inverse of XYZ_to_galcencyl" ) return None def test_vxvyvz_to_galcenrect(): vx, vy, vz = 10.0, -20.0, 30 vgc = coords.vxvyvz_to_galcenrect(vx, vy, vz, vsun=[-5.0, 10.0, 5.0]) assert numpy.fabs(vgc[0] + 15.0) < 10.0**-4.0, ( "vxvyvz_to_galcenrect conversion did not work as expected" ) assert numpy.fabs(vgc[1] + 10.0) < 10.0**-4.0, ( "vxvyvz_to_galcenrect conversion did not work as expected" ) assert numpy.fabs(vgc[2] - 35.0) < 10.0**-4.0, ( "vxvyvz_to_galcenrect conversion did not work as expected" ) return None def test_vxvyvz_to_galcenrect_negXsun(): vx, vy, vz = 10.0, -20.0, 30 vgc = coords.vxvyvz_to_galcenrect( vx, vy, vz, vsun=[-5.0, 10.0, 5.0], Xsun=1.1, Zsun=0.2 ) vgcn = coords.vxvyvz_to_galcenrect( vx, vy, vz, vsun=[5.0, 10.0, 5.0], Xsun=-1.1, Zsun=0.2 ) assert numpy.fabs(vgc[0] + vgcn[0]) < 10.0**-4.0, ( "vxvyvz_to_galcenrect conversion did not work as expected for negative Xsun" ) assert numpy.fabs(vgc[1] - vgcn[1]) < 10.0**-4.0, ( "vxvyvz_to_galcenrect conversion did not work as expected for negative Xsun" ) assert numpy.fabs(vgc[2] - vgcn[2]) < 10.0**-4.0, ( "vxvyvz_to_galcenrect conversion did not work as expected for negative Xsun" ) return None def test_vxvyvz_to_galcenrect_vecXsun(): vx, vy, vz = ( numpy.array([10.0, 10.0]), numpy.array([-20.0, -20.0]), numpy.array([30.0, 30.0]), ) vgc = coords.vxvyvz_to_galcenrect( vx, vy, vz, vsun=[-5.0, 10.0, 5.0], Xsun=numpy.array([1.1, 1.0]), Zsun=numpy.array([0.0, 0.0]), ) assert numpy.all(numpy.fabs(vgc[:, 0] + 15.0) < 10.0**-4.0), ( "vxvyvz_to_galcenrect conversion did not work as expected" ) assert numpy.all(numpy.fabs(vgc[:, 1] + 10.0) < 10.0**-4.0), ( "vxvyvz_to_galcenrect conversion did not work as expected" ) assert numpy.all(numpy.fabs(vgc[:, 2] - 35.0) < 10.0**-4.0), ( "vxvyvz_to_galcenrect conversion did not work as expected" ) return None def test_vxvyvz_to_galcenrect_vecvsun(): vx, vy, vz = ( numpy.array([10.0, 5.0]), numpy.array([-20.0, -10.0]), numpy.array([30.0, 25.0]), ) vgc = coords.vxvyvz_to_galcenrect( vx, vy, vz, vsun=numpy.array([[-5.0, 10.0, 5.0], [-10.0, 0.0, 10.0]]).T ) assert numpy.all(numpy.fabs(vgc[:, 0] + 15.0) < 10.0**-4.0), ( "vxvyvz_to_galcenrect conversion did not work as expected" ) assert numpy.all(numpy.fabs(vgc[:, 1] + 10.0) < 10.0**-4.0), ( "vxvyvz_to_galcenrect conversion did not work as expected" ) assert numpy.all(numpy.fabs(vgc[:, 2] - 35.0) < 10.0**-4.0), ( "vxvyvz_to_galcenrect conversion did not work as expected" ) return None def test_vxvyvz_to_galcenrect_vecXsunvsun(): vx, vy, vz = ( numpy.array([10.0, 5.0]), numpy.array([-20.0, -10.0]), numpy.array([30.0, 25.0]), ) vgc = coords.vxvyvz_to_galcenrect( vx, vy, vz, Xsun=numpy.array([1.1, 1.0]), Zsun=numpy.array([0.0, 0.0]), vsun=numpy.array([[-5.0, 10.0, 5.0], [-10.0, 0.0, 10.0]]).T, ) assert numpy.all(numpy.fabs(vgc[:, 0] + 15.0) < 10.0**-4.0), ( "vxvyvz_to_galcenrect conversion did not work as expected" ) assert numpy.all(numpy.fabs(vgc[:, 1] + 10.0) < 10.0**-4.0), ( "vxvyvz_to_galcenrect conversion did not work as expected" ) assert numpy.all(numpy.fabs(vgc[:, 2] - 35.0) < 10.0**-4.0), ( "vxvyvz_to_galcenrect conversion did not work as expected" ) return None def test_vrpmllpmbb_to_galcenrect_galpyvsastropy(): # Only run this for astropy>3 if not _APY3: return None # Test that galpy's transformations agree with astropy's import astropy.units as u from astropy.coordinates import CartesianDifferential, Galactocentric, SkyCoord _turn_off_apy() l, b, d = 32.0, -12.0, 3.0 vr, pmll, pmbb = -112.0, -13.0, 5.0 Zsun = 0.025 Rsun = 8.0 vsun = [-10.0, 230.0, 7.0] # Using galpy vx, vy, vz = coords.vrpmllpmbb_to_vxvyvz(vr, pmll, pmbb, l, b, d, degree=True) vXYZg = coords.vxvyvz_to_galcenrect(vx, vy, vz, vsun=vsun, Xsun=Rsun, Zsun=Zsun) # Using astropy c = SkyCoord( l=l * u.deg, b=b * u.deg, distance=d * u.kpc, radial_velocity=vr * u.km / u.s, pm_l_cosb=pmll * u.mas / u.yr, pm_b=pmbb * u.mas / u.yr, frame="galactic", ) gc_frame = Galactocentric( galcen_distance=numpy.sqrt(Rsun**2.0 + Zsun**2.0) * u.kpc, z_sun=Zsun * u.kpc, galcen_v_sun=CartesianDifferential( numpy.array([-vsun[0], vsun[1], vsun[2]]) * u.km / u.s ), ) c = c.transform_to(gc_frame) c.representation_type = "cartesian" # galpy is left-handed, astropy right-handed assert numpy.fabs(vXYZg[0] + c.v_x.to(u.km / u.s).value) < 10.0**-8.0, ( "vrpmllpmbblbd to galcenrect conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(vXYZg[1] - c.v_y.to(u.km / u.s).value) < 10.0**-8.0, ( "vrpmllpmbb to galcenrect conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(vXYZg[2] - c.v_z.to(u.km / u.s).value) < 10.0**-8.0, ( "vrpmllpmbb to galcenrect conversion using galpy's methods does not agree with astropy" ) # Also with negative Xsun l, b, d = 32.0, -12.0, 3.0 Zsun = 0.025 Rsun = -8.0 vsun = numpy.array([-10.0, 230.0, 7.0]) # Using galpy vx, vy, vz = coords.vrpmllpmbb_to_vxvyvz(vr, pmll, pmbb, l, b, d, degree=True) vXYZg = coords.vxvyvz_to_galcenrect(vx, vy, vz, vsun=vsun, Xsun=Rsun, Zsun=Zsun) # Using astropy c = SkyCoord( l=l * u.deg, b=b * u.deg, distance=d * u.kpc, radial_velocity=vr * u.km / u.s, pm_l_cosb=pmll * u.mas / u.yr, pm_b=pmbb * u.mas / u.yr, frame="galactic", ) gc_frame = Galactocentric( galcen_distance=numpy.sqrt(Rsun**2.0 + Zsun**2.0) * u.kpc, z_sun=Zsun * u.kpc, galcen_v_sun=CartesianDifferential( numpy.array([vsun[0], vsun[1], vsun[2]]) * u.km / u.s ), ) c = c.transform_to(gc_frame) c.representation_type = "cartesian" # galpy is now right-handed, astropy right-handed assert numpy.fabs(vXYZg[0] - c.v_x.to(u.km / u.s).value) < 10.0**-8.0, ( "vrpmllpmbblbd to galcenrect conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(vXYZg[1] - c.v_y.to(u.km / u.s).value) < 10.0**-8.0, ( "vrpmllpmbb to galcenrect conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(vXYZg[2] - c.v_z.to(u.km / u.s).value) < 10.0**-8.0, ( "vrpmllpmbb to galcenrect conversion using galpy's methods does not agree with astropy" ) _turn_on_apy() return None def test_vxvyvz_to_galcencyl(): X, Y, Z = 3.0, 4.0, 2.0 vx, vy, vz = 10.0, -20.0, 30 vgc = coords.vxvyvz_to_galcencyl(vx, vy, vz, X, Y, Z, vsun=[-5.0, 10.0, 5.0]) assert numpy.fabs(vgc[0] + 17.0) < 10.0**-4.0, ( "vxvyvz_to_galcenrect conversion did not work as expected" ) assert numpy.fabs(vgc[1] - 6.0) < 10.0**-4.0, ( "vxvyvz_to_galcenrect conversion did not work as expected" ) assert numpy.fabs(vgc[2] - 35.0) < 10.0**-4.0, ( "vxvyvz_to_galcenrect conversion did not work as expected" ) # with galcen=True vgc = coords.vxvyvz_to_galcencyl( vx, vy, vz, 5.0, numpy.arctan(4.0 / 3.0), Z, vsun=[-5.0, 10.0, 5.0], galcen=True ) assert numpy.fabs(vgc[0] + 17.0) < 10.0**-4.0, ( "vxvyvz_to_galcenrect conversion did not work as expected" ) assert numpy.fabs(vgc[1] - 6.0) < 10.0**-4.0, ( "vxvyvz_to_galcenrect conversion did not work as expected" ) assert numpy.fabs(vgc[2] - 35.0) < 10.0**-4.0, ( "vxvyvz_to_galcenrect conversion did not work as expected" ) return None def test_vrpmllpmbb_to_galcencyl_galpyvsastropy(): # Only run this for astropy>3 if not _APY3: return None # Test that galpy's transformations agree with astropy's import astropy.units as u from astropy.coordinates import CartesianDifferential, Galactocentric, SkyCoord _turn_off_apy() l, b, d = 32.0, -12.0, 3.0 vr, pmll, pmbb = -112.0, -13.0, 5.0 Zsun = 0.025 Rsun = 8.0 vsun = [-10.0, 230.0, 7.0] # Using galpy X, Y, Z = coords.lbd_to_XYZ(l, b, d, degree=True) gcXYZ = coords.XYZ_to_galcenrect(X, Y, Z, Xsun=Rsun, Zsun=Zsun) vx, vy, vz = coords.vrpmllpmbb_to_vxvyvz(vr, pmll, pmbb, l, b, d, degree=True) vRTZg = coords.vxvyvz_to_galcencyl( vx, vy, vz, gcXYZ[0], gcXYZ[1], gcXYZ[2], vsun=vsun, Xsun=Rsun, Zsun=Zsun ) # Using astropy c = SkyCoord( l=l * u.deg, b=b * u.deg, distance=d * u.kpc, radial_velocity=vr * u.km / u.s, pm_l_cosb=pmll * u.mas / u.yr, pm_b=pmbb * u.mas / u.yr, frame="galactic", ) gc_frame = Galactocentric( galcen_distance=numpy.sqrt(Rsun**2.0 + Zsun**2.0) * u.kpc, z_sun=Zsun * u.kpc, galcen_v_sun=CartesianDifferential( numpy.array([-vsun[0], vsun[1], vsun[2]]) * u.km / u.s ), ) c = c.transform_to(gc_frame) c.representation_type = "cylindrical" # galpy is left-handed, astropy right-handed assert numpy.fabs(vRTZg[0] - c.d_rho.to(u.km / u.s).value) < 10.0**-8.0, ( "vrpmllpmbblbd to galcencyl conversion using galpy's methods does not agree with astropy" ) assert ( numpy.fabs( vRTZg[1] + (c.d_phi * c.rho) .to(u.km / u.s, equivalencies=u.dimensionless_angles()) .value ) < 10.0**-8.0 ), ( "vrpmllpmbb to galcencyl conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(vRTZg[2] - c.d_z.to(u.km / u.s).value) < 10.0**-8.0, ( "vrpmllpmbb to galcencyl conversion using galpy's methods does not agree with astropy" ) # Also with negative Xsun l, b, d = 32.0, -12.0, 3.0 Zsun = 0.025 Rsun = -8.0 vsun = numpy.array([-10.0, 230.0, 7.0]) # Using galpy X, Y, Z = coords.lbd_to_XYZ(l, b, d, degree=True) gcXYZ = coords.XYZ_to_galcenrect(X, Y, Z, Xsun=Rsun, Zsun=Zsun) vx, vy, vz = coords.vrpmllpmbb_to_vxvyvz(vr, pmll, pmbb, l, b, d, degree=True) vRTZg = coords.vxvyvz_to_galcencyl( vx, vy, vz, gcXYZ[0], gcXYZ[1], gcXYZ[2], vsun=vsun, Xsun=Rsun, Zsun=Zsun ) # Using astropy c = SkyCoord( l=l * u.deg, b=b * u.deg, distance=d * u.kpc, radial_velocity=vr * u.km / u.s, pm_l_cosb=pmll * u.mas / u.yr, pm_b=pmbb * u.mas / u.yr, frame="galactic", ) gc_frame = Galactocentric( galcen_distance=numpy.sqrt(Rsun**2.0 + Zsun**2.0) * u.kpc, z_sun=Zsun * u.kpc, galcen_v_sun=CartesianDifferential( numpy.array([vsun[0], vsun[1], vsun[2]]) * u.km / u.s ), ) c = c.transform_to(gc_frame) c.representation_type = "cylindrical" # galpy is left-handed, astropy right-handed assert numpy.fabs(vRTZg[0] - c.d_rho.to(u.km / u.s).value) < 10.0**-8.0, ( "vrpmllpmbblbd to galcencyl conversion using galpy's methods does not agree with astropy" ) assert ( numpy.fabs( vRTZg[1] - (c.d_phi * c.rho) .to(u.km / u.s, equivalencies=u.dimensionless_angles()) .value ) < 10.0**-8.0 ), ( "vrpmllpmbb to galcencyl conversion using galpy's methods does not agree with astropy" ) assert numpy.fabs(vRTZg[2] - c.d_z.to(u.km / u.s).value) < 10.0**-8.0, ( "vrpmllpmbb to galcencyl conversion using galpy's methods does not agree with astropy" ) _turn_on_apy() return None def test_galcenrect_to_vxvyvz(): vxg, vyg, vzg = -15.0, -10.0, 35.0 vxyz = coords.galcenrect_to_vxvyvz(vxg, vyg, vzg, vsun=[-5.0, 10.0, 5.0]) assert numpy.fabs(vxyz[0] - 10.0) < 10.0**-4.0, ( "galcenrect_to_vxvyvz conversion did not work as expected" ) assert numpy.fabs(vxyz[1] + 20.0) < 10.0**-4.0, ( "galcenrect_to_vxvyvz conversion did not work as expected" ) assert numpy.fabs(vxyz[2] - 30.0) < 10.0**-4.0, ( "galcenrect_to_vxvyvz conversion did not work as expected" ) # Also for arrays os = numpy.ones(2) vxyz = coords.galcenrect_to_vxvyvz( os * vxg, os * vyg, os * vzg, vsun=[-5.0, 10.0, 5.0] ) assert numpy.all(numpy.fabs(vxyz[:, 0] - 10.0) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) assert numpy.all(numpy.fabs(vxyz[:, 1] + 20.0) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) assert numpy.all(numpy.fabs(vxyz[:, 2] - 30.0) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) return None def test_galcenrect_to_vxvyvz_negXsun(): vxg, vyg, vzg = -15.0, -10.0, 35.0 vxyz = coords.galcenrect_to_vxvyvz( vxg, vyg, vzg, vsun=[-5.0, 10.0, 5.0], Xsun=1.1, Zsun=0.2 ) vxyzn = coords.galcenrect_to_vxvyvz( -vxg, vyg, vzg, vsun=[5.0, 10.0, 5.0], Xsun=-1.1, Zsun=0.2 ) assert numpy.all(numpy.fabs(numpy.array(vxyz) - numpy.array(vxyzn)) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) return None def test_galcenrect_to_vxvyvz_asInverse(): # Test that galcenrect_to_vxvyvz is the inverse of vxvyvz_to_galcenrect vx, vy, vz = -15.0, -10.0, 35.0 vxg, vyg, vzg = coords.vxvyvz_to_galcenrect(vx, vy, vz, vsun=[-5.0, 10.0, 5.0]) vxt, vyt, vzt = coords.galcenrect_to_vxvyvz(vxg, vyg, vzg, vsun=[-5.0, 10.0, 5.0]) assert numpy.fabs(vx - vxt) < 10.0**-14.0, ( "galcenrect_to_vxvyvz is not the inverse of vxvyvz_to_galcenrect" ) assert numpy.fabs(vy - vyt) < 10.0**-14.0, ( "galcenrect_to_vxvyvz is not the inverse of vxvyvz_to_galcenrect" ) assert numpy.fabs(vz - vzt) < 10.0**-14.0, ( "galcenrect_to_vxvyvz is not the inverse of vxvyvz_to_galcenrect" ) # Also for arrays os = numpy.ones(2) vxyzg = coords.vxvyvz_to_galcenrect( vx * os, vy * os, vz * os, vsun=[-5.0, 10.0, 5.0] ) vxyzt = coords.galcenrect_to_vxvyvz( vxyzg[:, 0], vxyzg[:, 1], vxyzg[:, 2], vsun=[-5.0, 10.0, 5.0] ) assert numpy.all(numpy.fabs(vxyzt[:, 0] - vx * os) < 10.0**-10.0), ( "galcenrect_to_vxvyvz is not the inverse of vxvyvz_to_galcenrect" ) assert numpy.all(numpy.fabs(vxyzt[:, 1] - vy * os) < 10.0**-10.0), ( "galcenrect_to_vxvyvz is not the inverse of vxvyvz_to_galcenrect" ) assert numpy.all(numpy.fabs(vxyzt[:, 2] - vz * os) < 10.0**-10.0), ( "galcenrect_to_vxvyvz is not the inverse of vxvyvz_to_galcenrect" ) return None def test_galcenrect_to_vxvyvz_vecXsun(): vxg, vyg, vzg = ( numpy.array([-15.0, -15.0]), numpy.array([-10.0, -10.0]), numpy.array([35.0, 35.0]), ) vxyz = coords.galcenrect_to_vxvyvz( vxg, vyg, vzg, vsun=[-5.0, 10.0, 5.0], Xsun=numpy.array([1.1, 1.0]), Zsun=numpy.array([0.0, 0.0]), ) assert numpy.all(numpy.fabs(vxyz[:, 0] - 10.0) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) assert numpy.all(numpy.fabs(vxyz[:, 1] + 20.0) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) assert numpy.all(numpy.fabs(vxyz[:, 2] - 30.0) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) return None def test_galcenrect_to_vxvyvz_vecvsun(): vxg, vyg, vzg = ( numpy.array([-15.0, -5.0]), numpy.array([-10.0, -20.0]), numpy.array([35.0, 32.5]), ) vxyz = coords.galcenrect_to_vxvyvz( vxg, vyg, vzg, vsun=numpy.array([[-5.0, 10.0, 5.0], [5.0, 0.0, 2.5]]).T ) assert numpy.all(numpy.fabs(vxyz[:, 0] - 10.0) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) assert numpy.all(numpy.fabs(vxyz[:, 1] + 20.0) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) assert numpy.all(numpy.fabs(vxyz[:, 2] - 30.0) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) return None def test_galcenrect_to_vxvyvz_vecXsunvsun(): vxg, vyg, vzg = ( numpy.array([-15.0, -5.0]), numpy.array([-10.0, -20.0]), numpy.array([35.0, 32.5]), ) vxyz = coords.galcenrect_to_vxvyvz( vxg, vyg, vzg, vsun=numpy.array([[-5.0, 10.0, 5.0], [5.0, 0.0, 2.5]]).T, Xsun=numpy.array([1.1, 1.0]), Zsun=numpy.array([0.0, 0.0]), ) assert numpy.all(numpy.fabs(vxyz[:, 0] - 10.0) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) assert numpy.all(numpy.fabs(vxyz[:, 1] + 20.0) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) assert numpy.all(numpy.fabs(vxyz[:, 2] - 30.0) < 10.0**-4.0), ( "galcenrect_to_vxvyvz conversion did not work as expected" ) return None def test_galcencyl_to_vxvyvz(): vr, vp, vz = -17.0, 6.0, 35.0 phi = numpy.arctan(4.0 / 3.0) vxyz = coords.galcencyl_to_vxvyvz(vr, vp, vz, phi, vsun=[-5.0, 10.0, 5.0]) assert numpy.fabs(vxyz[0] - 10.0) < 10.0**-4.0, ( "galcenrect_to_vxvyvz conversion did not work as expected" ) assert numpy.fabs(vxyz[1] + 20.0) < 10.0**-4.0, ( "galcenrect_to_vxvyvz conversion did not work as expected" ) assert numpy.fabs(vxyz[2] - 30.0) < 10.0**-4.0, ( "galcenrect_to_vxvyvz conversion did not work as expected" ) return None def test_galcencyl_to_vxvyvz_asInverse(): # Test that galcencyl_to_vxvyvz is the inverse of vxvyvz_to_galcencyl vx, vy, vz = -15.0, -10.0, 35.0 phi = numpy.arctan(4.0 / 3.0) vrg, vtg, vzg = coords.vxvyvz_to_galcencyl( vx, vy, vz, 0.0, phi, 0.0, vsun=[-5.0, 10.0, 5.0], galcen=True ) vxt, vyt, vzt = coords.galcencyl_to_vxvyvz( vrg, vtg, vzg, phi, vsun=[-5.0, 10.0, 5.0] ) assert numpy.fabs(vx - vxt) < 10.0**-14.0, ( "galcencyl_to_vxvyvz is not the inverse of vxvyvz_to_galcencyl" ) assert numpy.fabs(vy - vyt) < 10.0**-14.0, ( "galcencyl_to_vxvyvz is not the inverse of vxvyvz_to_galcencyl" ) assert numpy.fabs(vz - vzt) < 10.0**-14.0, ( "galcencyl_to_vxvyvz is not the inverse of vxvyvz_to_galcencyl" ) # Also for arrays os = numpy.ones(2) vx, vy, vz = -15.0, -10.0, 35.0 phi = numpy.arctan(4.0 / 3.0) vrtzg = coords.vxvyvz_to_galcencyl( vx * os, vy * os, vz * os, 0.0, phi * os, 0.0, vsun=[-5.0, 10.0, 5.0], galcen=True, ) vxyzt = coords.galcencyl_to_vxvyvz( vrtzg[:, 0], vrtzg[:, 1], vrtzg[:, 2], phi * os, vsun=[-5.0, 10.0, 5.0] ) assert numpy.all(numpy.fabs(vxyzt[:, 0] - vx * os) < 10.0**-10.0), ( "galcencyl_to_vxvyvz is not the inverse of vxvyvz_to_galcencyl" ) assert numpy.all(numpy.fabs(vxyzt[:, 1] - vy * os) < 10.0**-10.0), ( "galcencyl_to_vxvyvz is not the inverse of vxvyvz_to_galcencyl" ) assert numpy.all(numpy.fabs(vxyzt[:, 2] - vz * os) < 10.0**-10.0), ( "galcencyl_to_vxvyvz is not the inverse of vxvyvz_to_galcencyl" ) return None def test_sphergal_to_rectgal(): l, b, d = 90.0, 0.0, 1.0 vr, pmll, pmbb = 10.0, -20.0 / 4.740470463496208, 30.0 / 4.740470463496208 X, Y, Z, vx, vy, vz = coords.sphergal_to_rectgal( l, b, d, vr, pmll, pmbb, degree=True ) assert numpy.fabs(X - 0.0) < 10.0**-9.0, ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.fabs(Y - 1.0) < 10.0**-9.0, ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.fabs(Z - 0.0) < 10.0**-9.0, ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.fabs(vx - 20.0) < 10.0**-9.0, ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.fabs(vy - 10.0) < 10.0**-9.0, ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.fabs(vz - 30.0) < 10.0**-9.0, ( "sphergal_to_rectgal conversion did not work as expected" ) # Also test for degree=False X, Y, Z, vx, vy, vz = coords.sphergal_to_rectgal( l / 180.0 * numpy.pi, b / 180.0 * numpy.pi, d, vr, pmll, pmbb, degree=False ) assert numpy.fabs(X - 0.0) < 10.0**-9.0, ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.fabs(Y - 1.0) < 10.0**-9.0, ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.fabs(Z - 0.0) < 10.0**-9.0, ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.fabs(vx - 20.0) < 10.0**-9.0, ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.fabs(vy - 10.0) < 10.0**-9.0, ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.fabs(vz - 30.0) < 10.0**-9.0, ( "sphergal_to_rectgal conversion did not work as expected" ) # Also test for arrays os = numpy.ones(2) XYZvxvyvz = coords.sphergal_to_rectgal( os * l, os * b, os * d, os * vr, os * pmll, os * pmbb, degree=True ) X = XYZvxvyvz[:, 0] Y = XYZvxvyvz[:, 1] Z = XYZvxvyvz[:, 2] vx = XYZvxvyvz[:, 3] vy = XYZvxvyvz[:, 4] vz = XYZvxvyvz[:, 5] assert numpy.all(numpy.fabs(X - 0.0) < 10.0**-9.0), ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.all(numpy.fabs(Y - 1.0) < 10.0**-9.0), ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.all(numpy.fabs(Z - 0.0) < 10.0**-9.0), ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.all(numpy.fabs(vx - 20.0) < 10.0**-9.0), ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.all(numpy.fabs(vy - 10.0) < 10.0**-9.0), ( "sphergal_to_rectgal conversion did not work as expected" ) assert numpy.all(numpy.fabs(vz - 30.0) < 10.0**-9.0), ( "sphergal_to_rectgal conversion did not work as expected" ) return None def test_rectgal_to_sphergal(): # Test that this is the inverse of sphergal_to_rectgal l, b, d = 90.0, 30.0, 1.0 vr, pmll, pmbb = 10.0, -20.0, 30.0 X, Y, Z, vx, vy, vz = coords.sphergal_to_rectgal( l, b, d, vr, pmll, pmbb, degree=True ) lt, bt, dt, vrt, pmllt, pmbbt = coords.rectgal_to_sphergal( X, Y, Z, vx, vy, vz, degree=True ) assert numpy.fabs(lt - l) < 10.0**-10.0, ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.fabs(bt - b) < 10.0**-10.0, ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.fabs(dt - d) < 10.0**-10.0, ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.fabs(vrt - vr) < 10.0**-10.0, ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.fabs(pmllt - pmll) < 10.0**-10.0, ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.fabs(pmbbt - pmbb) < 10.0**-10.0, ( "rectgal_to_sphergal conversion did not work as expected" ) # Also test for degree=False lt, bt, dt, vrt, pmllt, pmbbt = coords.rectgal_to_sphergal( X, Y, Z, vx, vy, vz, degree=False ) assert numpy.fabs(lt - l / 180.0 * numpy.pi) < 10.0**-10.0, ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.fabs(bt - b / 180.0 * numpy.pi) < 10.0**-10.0, ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.fabs(dt - d) < 10.0**-10.0, ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.fabs(vrt - vr) < 10.0**-10.0, ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.fabs(pmllt - pmll) < 10.0**-10.0, ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.fabs(pmbbt - pmbb) < 10.0**-10.0, ( "rectgal_to_sphergal conversion did not work as expected" ) # Also test for arrays os = numpy.ones(2) lbdvrpmllpmbbt = coords.rectgal_to_sphergal( os * X, os * Y, os * Z, os * vx, os * vy, os * vz, degree=True ) lt = lbdvrpmllpmbbt[:, 0] bt = lbdvrpmllpmbbt[:, 1] dt = lbdvrpmllpmbbt[:, 2] vrt = lbdvrpmllpmbbt[:, 3] pmllt = lbdvrpmllpmbbt[:, 4] pmbbt = lbdvrpmllpmbbt[:, 5] assert numpy.all(numpy.fabs(lt - l) < 10.0**-10.0), ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.all(numpy.fabs(bt - b) < 10.0**-10.0), ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.all(numpy.fabs(dt - d) < 10.0**-10.0), ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.all(numpy.fabs(vrt - vr) < 10.0**-10.0), ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.all(numpy.fabs(pmllt - pmll) < 10.0**-10.0), ( "rectgal_to_sphergal conversion did not work as expected" ) assert numpy.all(numpy.fabs(pmbbt - pmbb) < 10.0**-10.0), ( "rectgal_to_sphergal conversion did not work as expected" ) return None def test_pmrapmdec_to_pmllpmbb(): # This is a random ra,dec ra, dec = 132.0, -20.4 pmra, pmdec = 10.0, 20.0 pmll, pmbb = coords.pmrapmdec_to_pmllpmbb( pmra, pmdec, ra, dec, degree=True, epoch=1950.0 ) assert ( numpy.fabs( numpy.sqrt(pmll**2.0 + pmbb**2.0) - numpy.sqrt(pmra**2.0 + pmdec**2.0) ) < 10.0**-10.0 ), "pmrapmdec_to_pmllpmbb conversion did not work as expected" # This is close to the NGP at 1950. ra, dec = 192.24, 27.39 pmra, pmdec = 10.0, 20.0 os = numpy.ones(2) pmllpmbb = coords.pmrapmdec_to_pmllpmbb( os * pmra, os * pmdec, os * ra, os * dec, degree=True, epoch=1950.0 ) pmll = pmllpmbb[:, 0] pmbb = pmllpmbb[:, 1] assert numpy.all( numpy.fabs( numpy.sqrt(pmll**2.0 + pmbb**2.0) - numpy.sqrt(pmra**2.0 + pmdec**2.0) ) < 10.0**-10.0 ), "pmrapmdec_to_pmllpmbb conversion did not work as expected close to the NGP" # This is the NGP at 1950. ra, dec = 192.25, 27.4 pmra, pmdec = 10.0, 20.0 os = numpy.ones(2) pmllpmbb = coords.pmrapmdec_to_pmllpmbb( os * pmra, os * pmdec, os * ra, os * dec, degree=True, epoch=1950.0 ) pmll = pmllpmbb[:, 0] pmbb = pmllpmbb[:, 1] assert numpy.all( numpy.fabs( numpy.sqrt(pmll**2.0 + pmbb**2.0) - numpy.sqrt(pmra**2.0 + pmdec**2.0) ) < 10.0**-10.0 ), "pmrapmdec_to_pmllpmbb conversion did not work as expected for the NGP" # This is the NCP ra, dec = numpy.pi, numpy.pi / 2.0 pmra, pmdec = 10.0, 20.0 pmll, pmbb = coords.pmrapmdec_to_pmllpmbb( pmra, pmdec, ra, dec, degree=False, epoch=1950.0 ) assert ( numpy.fabs( numpy.sqrt(pmll**2.0 + pmbb**2.0) - numpy.sqrt(pmra**2.0 + pmdec**2.0) ) < 10.0**-10.0 ), "pmrapmdec_to_pmllpmbb conversion did not work as expected for the NCP" return None def test_pmllpmbb_to_pmrapmdec(): # This is a random l,b ll, bb = 132.0, -20.4 pmll, pmbb = 10.0, 20.0 pmra, pmdec = coords.pmllpmbb_to_pmrapmdec( pmll, pmbb, ll, bb, degree=True, epoch=1950.0 ) assert ( numpy.fabs( numpy.sqrt(pmll**2.0 + pmbb**2.0) - numpy.sqrt(pmra**2.0 + pmdec**2.0) ) < 10.0**-10.0 ), "pmllpmbb_to_pmrapmdec conversion did not work as expected for a random l,b" # This is close to the NGP ll, bb = numpy.pi - 0.001, numpy.pi / 2.0 - 0.001 pmll, pmbb = 10.0, 20.0 os = numpy.ones(2) pmrapmdec = coords.pmllpmbb_to_pmrapmdec( os * pmll, os * pmbb, os * ll, os * bb, degree=False, epoch=1950.0 ) pmra = pmrapmdec[:, 0] pmdec = pmrapmdec[:, 1] assert numpy.all( numpy.fabs( numpy.sqrt(pmll**2.0 + pmbb**2.0) - numpy.sqrt(pmra**2.0 + pmdec**2.0) ) < 10.0**-10.0 ), "pmllpmbb_to_pmrapmdec conversion did not work as expected close to the NGP" # This is the NGP ll, bb = numpy.pi, numpy.pi / 2.0 pmll, pmbb = 10.0, 20.0 os = numpy.ones(2) pmrapmdec = coords.pmllpmbb_to_pmrapmdec( os * pmll, os * pmbb, os * ll, os * bb, degree=False, epoch=1950.0 ) pmra = pmrapmdec[:, 0] pmdec = pmrapmdec[:, 1] assert numpy.all( numpy.fabs( numpy.sqrt(pmll**2.0 + pmbb**2.0) - numpy.sqrt(pmra**2.0 + pmdec**2.0) ) < 10.0**-10.0 ), "pmllpmbb_to_pmrapmdec conversion did not work as expected at the NGP" # This is the NCP ra, dec = numpy.pi, numpy.pi / 2.0 ll, bb = coords.radec_to_lb(ra, dec, degree=False, epoch=1950.0) pmll, pmbb = 10.0, 20.0 pmra, pmdec = coords.pmllpmbb_to_pmrapmdec( pmll, pmbb, ll, bb, degree=False, epoch=1950.0 ) assert ( numpy.fabs( numpy.sqrt(pmll**2.0 + pmbb**2.0) - numpy.sqrt(pmra**2.0 + pmdec**2.0) ) < 10.0**-10.0 ), "pmllpmbb_to_pmrapmdec conversion did not work as expected at the NCP" return None def test_cov_pmradec_to_pmllbb(): # This is the NGP at 1950., for this the parallactic angle is 180 ra, dec = 192.25, 27.4 cov_pmrapmdec = numpy.array([[100.0, 100.0], [100.0, 400.0]]) cov_pmllpmbb = coords.cov_pmrapmdec_to_pmllpmbb( cov_pmrapmdec, ra, dec, degree=True, epoch=1950.0 ) assert numpy.fabs(cov_pmllpmbb[0, 0] - 100.0) < 10.0**-10.0, ( "cov_pmradec_to_pmllbb conversion did not work as expected" ) assert numpy.fabs(cov_pmllpmbb[0, 1] - 100.0) < 10.0**-10.0, ( "cov_pmradec_to_pmllbb conversion did not work as expected" ) assert numpy.fabs(cov_pmllpmbb[1, 0] - 100.0) < 10.0**-10.0, ( "cov_pmradec_to_pmllbb conversion did not work as expected" ) assert numpy.fabs(cov_pmllpmbb[1, 1] - 400.0) < 10.0**-10.0, ( "cov_pmradec_to_pmllbb conversion did not work as expected" ) # This is a random position, check that the conversion makes sense ra, dec = 132.25, -23.4 cov_pmrapmdec = numpy.array([[100.0, 100.0], [100.0, 400.0]]) cov_pmllpmbb = coords.cov_pmrapmdec_to_pmllpmbb( cov_pmrapmdec, ra / 180.0 * numpy.pi, dec / 180.0 * numpy.pi, degree=False, epoch=1950.0, ) assert ( numpy.fabs(numpy.linalg.det(cov_pmllpmbb) - numpy.linalg.det(cov_pmrapmdec)) < 10.0**-10.0 ), "cov_pmradec_to_pmllbb conversion did not work as expected" assert ( numpy.fabs(numpy.trace(cov_pmllpmbb) - numpy.trace(cov_pmrapmdec)) < 10.0**-10.0 ), "cov_pmradec_to_pmllbb conversion did not work as expected" # This is a random position, check that the conversion makes sense, arrays (using einsum) ra, dec = 132.25, -23.4 icov_pmrapmdec = numpy.array([[100.0, 100.0], [100.0, 400.0]]) cov_pmrapmdec = numpy.empty((3, 2, 2)) for ii in range(3): cov_pmrapmdec[ii, :, :] = icov_pmrapmdec os = numpy.ones(3) cov_pmllpmbb = coords.cov_pmrapmdec_to_pmllpmbb( cov_pmrapmdec, os * ra, os * dec, degree=True, epoch=1950.0 ) for ii in range(3): assert ( numpy.fabs( numpy.linalg.det(cov_pmllpmbb[ii, :, :]) - numpy.linalg.det(cov_pmrapmdec[ii, :, :]) ) < 10.0**-10.0 ), "cov_pmradec_to_pmllbb conversion did not work as expected" assert ( numpy.fabs( numpy.trace(cov_pmllpmbb[ii, :, :]) - numpy.trace(cov_pmrapmdec[ii, :, :]) ) < 10.0**-10.0 ), "cov_pmradec_to_pmllbb conversion did not work as expected" return None def test_cov_dvrpmllbb_to_vxyz(): l, b, d = 90.0, 0.0, 2.0 e_d, e_vr = 0.2, 2.0 cov_pmllpmbb = numpy.array([[100.0, 0.0], [0.0, 400.0]]) pmll, pmbb = 20.0, 30.0 cov_vxvyvz = coords.cov_dvrpmllbb_to_vxyz( d, e_d, e_vr, pmll, pmbb, cov_pmllpmbb, l, b, degree=True, plx=False ) assert ( numpy.fabs( numpy.sqrt(cov_vxvyvz[0, 0]) - d * 4.740470463496208 * pmll * numpy.sqrt((e_d / d) ** 2.0 + (10.0 / pmll) ** 2.0) ) < 10.0**-8.0 ), "cov_dvrpmllbb_to_vxyz conversion did not work as expected" assert numpy.fabs(numpy.sqrt(cov_vxvyvz[1, 1]) - e_vr) < 10.0**-10.0, ( "cov_dvrpmllbb_to_vxyz conversion did not work as expected" ) assert ( numpy.fabs( numpy.sqrt(cov_vxvyvz[2, 2]) - d * 4.740470463496208 * pmbb * numpy.sqrt((e_d / d) ** 2.0 + (20.0 / pmbb) ** 2.0) ) < 10.0**-8.0 ), "cov_dvrpmllbb_to_vxyz conversion did not work as expected" # Another one l, b, d = 180.0, 0.0, 1.0 / 2.0 e_d, e_vr = 0.05, 2.0 cov_pmllpmbb = numpy.array([[100.0, 0.0], [0.0, 400.0]]) pmll, pmbb = 20.0, 30.0 cov_vxvyvz = coords.cov_dvrpmllbb_to_vxyz( d, e_d, e_vr, pmll, pmbb, cov_pmllpmbb, l / 180.0 * numpy.pi, b / 180.0 * numpy.pi, degree=False, plx=True, ) assert numpy.fabs(numpy.sqrt(cov_vxvyvz[0, 0]) - e_vr) < 10.0**-8.0, ( "cov_dvrpmllbb_to_vxyz conversion did not work as expected" ) assert ( numpy.fabs( numpy.sqrt(cov_vxvyvz[1, 1]) - 1.0 / d * 4.740470463496208 * pmll * numpy.sqrt((e_d / d) ** 2.0 + (10.0 / pmll) ** 2.0) ) < 10.0**-8.0 ), "cov_dvrpmllbb_to_vxyz conversion did not work as expected" assert ( numpy.fabs( numpy.sqrt(cov_vxvyvz[2, 2]) - 1.0 / d * 4.740470463496208 * pmbb * numpy.sqrt((e_d / d) ** 2.0 + (20.0 / pmbb) ** 2.0) ) < 10.0**-8.0 ), "cov_dvrpmllbb_to_vxyz conversion did not work as expected" # Another one, w/ arrays (using einsum) l, b, d = 90.0, 90.0, 2.0 e_d, e_vr = 0.2, 2.0 tcov_pmllpmbb = numpy.array([[100.0, 0.0], [0.0, 400.0]]) cov_pmllpmbb = numpy.empty((3, 2, 2)) for ii in range(3): cov_pmllpmbb[ii, :, :] = tcov_pmllpmbb pmll, pmbb = 20.0, 30.0 os = numpy.ones(3) cov_vxvyvz = coords.cov_dvrpmllbb_to_vxyz( os * d, os * e_d, os * e_vr, os * pmll, os * pmbb, cov_pmllpmbb, os * l, os * b, degree=True, plx=False, ) for ii in range(3): assert ( numpy.fabs( numpy.sqrt(cov_vxvyvz[ii, 0, 0]) - d * 4.740470463496208 * pmll * numpy.sqrt((e_d / d) ** 2.0 + (10.0 / pmll) ** 2.0) ) < 10.0**-8.0 ), "cov_dvrpmllbb_to_vxyz conversion did not work as expected" assert ( numpy.fabs( numpy.sqrt(cov_vxvyvz[ii, 1, 1]) - d * 4.740470463496208 * pmbb * numpy.sqrt((e_d / d) ** 2.0 + (20.0 / pmbb) ** 2.0) ) < 10.0**-8.0 ), "cov_dvrpmllbb_to_vxyz conversion did not work as expected" assert numpy.fabs(numpy.sqrt(cov_vxvyvz[ii, 2, 2]) - e_vr) < 10.0**-10.0, ( "cov_dvrpmllbb_to_vxyz conversion did not work as expected" ) return None def test_cov_vxyz_to_galcencyl(): # test point - system should flip with phi = pi cov_vxyz = numpy.array([[1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0]]) phi = numpy.pi cov_galcencyl = coords.cov_vxyz_to_galcencyl(cov_vxyz, phi) assert (cov_galcencyl[0, 2] + 1 < 10.0**-8).all(), ( "cov_vxyz_to_galcencyl conversion did not work as expected" ) assert (cov_galcencyl[1, 2] + 1 < 10.0**-8).all(), ( "cov_vxyz_to_galcencyl conversion did not work as expected" ) assert (cov_galcencyl[2, 0] + 1 < 10.0**-8).all(), ( "cov_vxyz_to_galcencyl conversion did not work as expected" ) assert (cov_galcencyl[2, 1] + 1 < 10.0**-8).all(), ( "cov_vxyz_to_galcencyl conversion did not work as expected" ) # try an array cov_vxyz = numpy.array( [ [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0]], ] ) phis = [0.0, numpy.pi] cov_galcencyl = coords.cov_vxyz_to_galcencyl(cov_vxyz, phis) assert (cov_galcencyl[1, 0, 2] + 1 < 10.0**-8).all(), ( "cov_vxyz_to_galcencyl conversion did not work as expected" ) assert (cov_galcencyl[1, 1, 2] + 1 < 10.0**-8).all(), ( "cov_vxyz_to_galcencyl conversion did not work as expected" ) assert (cov_galcencyl[1, 2, 0] + 1 < 10.0**-8).all(), ( "cov_vxyz_to_galcencyl conversion did not work as expected" ) assert (cov_galcencyl[1, 2, 1] + 1 < 10.0**-8).all(), ( "cov_vxyz_to_galcencyl conversion did not work as expected" ) assert (cov_galcencyl[0] - 1.0 < 10**-8).all() def test_dl_to_rphi_2d(): # This is a tangent point l = numpy.arcsin(0.75) d = 6.0 / numpy.tan(l) r, phi = coords.dl_to_rphi_2d(d, l, degree=False, ro=8.0, phio=0.0) assert numpy.fabs(r - 6.0) < 10.0**-10.0, ( "dl_to_rphi_2d conversion did not work as expected" ) assert numpy.fabs(phi - numpy.arccos(0.75)) < 10.0**-10.0, ( "dl_to_rphi_2d conversion did not work as expected" ) # This is a different point d, l = 2.0, 45.0 r, phi = coords.dl_to_rphi_2d( d, l, degree=True, ro=2.0 * numpy.sqrt(2.0), phio=10.0 ) assert numpy.fabs(r - 2.0) < 10.0**-10.0, ( "dl_to_rphi_2d conversion did not work as expected" ) assert numpy.fabs(phi - 55.0) < 10.0**-10.0, ( "dl_to_rphi_2d conversion did not work as expected" ) # This is a different point, for array d, l = 2.0, 45.0 os = numpy.ones(2) r, phi = coords.dl_to_rphi_2d( os * d, os * l, degree=True, ro=2.0 * numpy.sqrt(2.0), phio=0.0 ) assert numpy.all(numpy.fabs(r - 2.0) < 10.0**-10.0), ( "dl_to_rphi_2d conversion did not work as expected" ) assert numpy.all(numpy.fabs(phi - 45.0) < 10.0**-10.0), ( "dl_to_rphi_2d conversion did not work as expected" ) # This is a different point, for list (which I support for some reason) d, l = 2.0, 45.0 r, phi = coords.dl_to_rphi_2d( [d, d], [l, l], degree=True, ro=2.0 * numpy.sqrt(2.0), phio=0.0 ) r = numpy.array(r) phi = numpy.array(phi) assert numpy.all(numpy.fabs(r - 2.0) < 10.0**-10.0), ( "dl_to_rphi_2d conversion did not work as expected" ) assert numpy.all(numpy.fabs(phi - 45.0) < 10.0**-10.0), ( "dl_to_rphi_2d conversion did not work as expected" ) return None def test_rphi_to_dl_2d(): # This is a tangent point r, phi = 6.0, numpy.arccos(0.75) d, l = coords.rphi_to_dl_2d(r, phi, degree=False, ro=8.0, phio=0.0) l = numpy.arcsin(0.75) d = 6.0 / numpy.tan(l) assert numpy.fabs(d - 6.0 / numpy.tan(numpy.arcsin(0.75))) < 10.0**-10.0, ( "dl_to_rphi_2d conversion did not work as expected" ) assert numpy.fabs(l - numpy.arcsin(0.75)) < 10.0**-10.0, ( "rphi_to_dl_2d conversion did not work as expected" ) # This is another point r, phi = 2.0, 55.0 d, l = coords.rphi_to_dl_2d( r, phi, degree=True, ro=2.0 * numpy.sqrt(2.0), phio=10.0 ) assert numpy.fabs(d - 2.0) < 10.0**-10.0, ( "rphi_to_dl_2d conversion did not work as expected" ) assert numpy.fabs(l - 45.0) < 10.0**-10.0, ( "rphi_to_dl_2d conversion did not work as expected" ) # This is another point, for arrays r, phi = 2.0, 45.0 os = numpy.ones(2) d, l = coords.rphi_to_dl_2d( os * r, os * phi, degree=True, ro=2.0 * numpy.sqrt(2.0), phio=0.0 ) assert numpy.all(numpy.fabs(d - 2.0) < 10.0**-10.0), ( "rphi_to_dl_2d conversion did not work as expected" ) assert numpy.all(numpy.fabs(l - 45.0) < 10.0**-10.0), ( "rphi_to_dl_2d conversion did not work as expected" ) # This is another point, for lists, which for some reason I support r, phi = 2.0, 45.0 d, l = coords.rphi_to_dl_2d( [r, r], [phi, phi], degree=True, ro=2.0 * numpy.sqrt(2.0), phio=0.0 ) d = numpy.array(d) l = numpy.array(l) assert numpy.all(numpy.fabs(d - 2.0) < 10.0**-10.0), ( "rphi_to_dl_2d conversion did not work as expected" ) assert numpy.all(numpy.fabs(l - 45.0) < 10.0**-10.0), ( "rphi_to_dl_2d conversion did not work as expected" ) return None def test_uv_to_Rz(): u, v = numpy.arccosh(5.0 / 3.0), numpy.pi / 6.0 R, z = coords.uv_to_Rz(u, v, delta=3.0) assert numpy.fabs(R - 2.0) < 10.0**-10.0, ( "uv_to_Rz conversion did not work as expected" ) assert numpy.fabs(z - 2.5 * numpy.sqrt(3.0)) < 10.0**-10.0, ( "uv_to_Rz conversion did not work as expected" ) # Also test for arrays os = numpy.ones(2) R, z = coords.uv_to_Rz(os * u, os * v, delta=3.0) assert numpy.all(numpy.fabs(R - 2.0) < 10.0**-10.0), ( "uv_to_Rz conversion did not work as expected" ) assert numpy.all(numpy.fabs(z - 2.5 * numpy.sqrt(3.0)) < 10.0**-10.0), ( "uv_to_Rz conversion did not work as expected" ) return None def test_Rz_to_uv(): u, v = numpy.arccosh(5.0 / 3.0), numpy.pi / 6.0 ut, vt = coords.Rz_to_uv(*coords.uv_to_Rz(u, v, delta=3.0), delta=3.0) assert numpy.fabs(ut - u) < 10.0**-10.0, ( "Rz_to_uvz conversion did not work as expected" ) assert numpy.fabs(vt - v) < 10.0**-10.0, ( "Rz_to_uv conversion did not work as expected" ) # Also test for arrays os = numpy.ones(2) ut, vt = coords.Rz_to_uv(*coords.uv_to_Rz(u * os, v * os, delta=3.0), delta=3.0) assert numpy.all(numpy.fabs(ut - u) < 10.0**-10.0), ( "Rz_to_uvz conversion did not work as expected" ) assert numpy.all(numpy.fabs(vt - v) < 10.0**-10.0), ( "Rz_to_uv conversion did not work as expected" ) return None def test_Rz_to_coshucosv(): u, v = numpy.arccosh(5.0 / 3.0), numpy.pi / 3.0 R, z = coords.uv_to_Rz(u, v, delta=3.0) coshu, cosv = coords.Rz_to_coshucosv(R, z, delta=3.0) assert numpy.fabs(coshu - 5.0 / 3.0) < 10.0**-10.0, ( "Rz_to_coshucosv conversion did notwork as expected" ) assert numpy.fabs(cosv - 0.5) < 10.0**-10.0, ( "Rz_to_coshucosv conversion did notwork as expected" ) # Also test for arrays os = numpy.ones(2) coshu, cosv = coords.Rz_to_coshucosv(R * os, z * os, delta=3.0) assert numpy.all(numpy.fabs(coshu - 5.0 / 3.0) < 10.0**-10.0), ( "Rz_to_coshucosv conversion did notwork as expected" ) assert numpy.all(numpy.fabs(cosv - 0.5) < 10.0**-10.0), ( "Rz_to_coshucosv conversion did notwork as expected" ) return None def test_uv_to_Rz_oblate(): u, v = numpy.arccosh(5.0 / 3.0), numpy.pi / 6.0 R, z = coords.uv_to_Rz(u, v, delta=3.0, oblate=True) assert numpy.fabs(R - 2.5) < 10.0**-10.0, ( "uv_to_Rz conversion did not work as expected" ) assert numpy.fabs(z - 2.0 * numpy.sqrt(3.0)) < 10.0**-10.0, ( "uv_to_Rz conversion did not work as expected" ) # Also test for arrays os = numpy.ones(2) R, z = coords.uv_to_Rz(os * u, os * v, delta=3.0, oblate=True) assert numpy.all(numpy.fabs(R - 2.5) < 10.0**-10.0), ( "uv_to_Rz conversion did not work as expected" ) assert numpy.all(numpy.fabs(z - 2.0 * numpy.sqrt(3.0)) < 10.0**-10.0), ( "uv_to_Rz conversion did not work as expected" ) return None def test_Rz_to_uv_oblate(): u, v = numpy.arccosh(5.0 / 3.0), numpy.pi / 6.0 ut, vt = coords.Rz_to_uv( *coords.uv_to_Rz(u, v, delta=3.0, oblate=True), delta=3.0, oblate=True ) assert numpy.fabs(ut - u) < 10.0**-10.0, ( "Rz_to_uvz conversion did not work as expected" ) assert numpy.fabs(vt - v) < 10.0**-10.0, ( "Rz_to_uv conversion did not work as expected" ) # Also test for arrays os = numpy.ones(2) ut, vt = coords.Rz_to_uv( *coords.uv_to_Rz(u * os, v * os, delta=3.0, oblate=True), delta=3.0, oblate=True ) assert numpy.all(numpy.fabs(ut - u) < 10.0**-10.0), ( "Rz_to_uvz conversion did not work as expected" ) assert numpy.all(numpy.fabs(vt - v) < 10.0**-10.0), ( "Rz_to_uv conversion did not work as expected" ) return None def test_Rz_to_coshucosv_oblate(): u, v = numpy.arccosh(5.0 / 3.0), numpy.pi / 3.0 R, z = coords.uv_to_Rz(u, v, delta=3.0, oblate=True) coshu, cosv = coords.Rz_to_coshucosv(R, z, delta=3.0, oblate=True) assert numpy.fabs(coshu - 5.0 / 3.0) < 10.0**-10.0, ( "Rz_to_coshucosv conversion did notwork as expected" ) assert numpy.fabs(cosv - 0.5) < 10.0**-10.0, ( "Rz_to_coshucosv conversion did notwork as expected" ) # Also test for arrays os = numpy.ones(2) coshu, cosv = coords.Rz_to_coshucosv(R * os, z * os, delta=3.0, oblate=True) assert numpy.all(numpy.fabs(coshu - 5.0 / 3.0) < 10.0**-10.0), ( "Rz_to_coshucosv conversion did notwork as expected" ) assert numpy.all(numpy.fabs(cosv - 0.5) < 10.0**-10.0), ( "Rz_to_coshucosv conversion did notwork as expected" ) return None def test_vRvz_to_pupv(): # Some sanity checks # At R,z << Delta --> p_u ~ delta vR, p_v ~ -delta vz delta = 0.5 R, z = delta / 100.0, delta / 300.0 vR, vz = 0.2, -0.5 assert ( numpy.fabs(coords.vRvz_to_pupv(vR, vz, R, z, delta=delta)[0] - delta * vR) < 10.0**-3.0 ), "vRvz_to_pupv at small R,z does not behave as expected" assert ( numpy.fabs(coords.vRvz_to_pupv(vR, vz, R, z, delta=delta)[1] + delta * vz) < 10.0**-3.0 ), "vRvz_to_pupv at small R,z does not behave as expected" # At R,z >> Delta --> p_u ~ r v_r, p_v ~ r v_theta, spherical velocities delta = 0.5 R, z = delta * 100.0, delta * 300.0 vR, vz = 0.2, -0.5 # Compute spherical velocities r = numpy.sqrt(R**2.0 + z**2.0) costheta = z / r sintheta = R / r vr = vR * sintheta + vz * costheta vt = -vz * sintheta + vR * costheta assert ( numpy.fabs(coords.vRvz_to_pupv(vR, vz, R, z, delta=delta)[0] - r * vr) < 10.0**-3.0 ), "vRvz_to_pupv at large R,z does not behave as expected" assert ( numpy.fabs(coords.vRvz_to_pupv(vR, vz, R, z, delta=delta)[1] - r * vt) < 10.0**-3.0 ), "vRvz_to_pupv at large R,z does not behave as expected" # Also check that it does not matter whether we give R,z or u,v delta = 0.5 R, z = delta * 2.0, delta / 3.0 vR, vz = 0.2, -0.5 assert ( numpy.fabs( coords.vRvz_to_pupv(vR, vz, R, z, delta=delta)[0] - coords.vRvz_to_pupv( vR, vz, *coords.Rz_to_uv(R, z, delta=delta), delta=delta, uv=True )[0] ) < 10.0**-3.0 ), "vRvz_to_pupv with and without pre-computed u,v do not agree" assert ( numpy.fabs( coords.vRvz_to_pupv(vR, vz, R, z, delta=delta)[1] - coords.vRvz_to_pupv( vR, vz, *coords.Rz_to_uv(R, z, delta=delta), delta=delta, uv=True )[1] ) < 10.0**-3.0 ), "vRvz_to_pupv with and without pre-computed u,v do not agree" return None def test_vRvz_to_pupv_oblate(): # Some sanity checks # At R,z << Delta --> p_u ~ delta vz, p_v ~ delta vR delta = 0.5 R, z = delta / 100.0, delta / 300.0 vR, vz = 0.2, -0.5 assert ( numpy.fabs( coords.vRvz_to_pupv(vR, vz, R, z, delta=delta, oblate=True)[0] - delta * vz ) < 10.0**-3.0 ), ( "vRvz_to_pupv at small R,z does not behave as expected for oblate spheroidal coordinates" ) assert ( numpy.fabs( coords.vRvz_to_pupv(vR, vz, R, z, delta=delta, oblate=True)[1] - delta * vR ) < 10.0**-3.0 ), ( "vRvz_to_pupv at small R,z does not behave as expected for oblate spheroidal coordinates" ) # At R,z >> Delta --> p_u ~ r v_r, p_v ~ r v_theta, spherical velocities delta = 0.5 R, z = delta * 100.0, delta * 300.0 vR, vz = 0.2, -0.5 # Compute spherical velocities r = numpy.sqrt(R**2.0 + z**2.0) costheta = z / r sintheta = R / r vr = vR * sintheta + vz * costheta vt = -vz * sintheta + vR * costheta assert ( numpy.fabs( coords.vRvz_to_pupv(vR, vz, R, z, delta=delta, oblate=True)[0] - r * vr ) < 10.0**-3.0 ), ( "vRvz_to_pupv at large R,z does not behave as expected for oblate spheroidal coordinates" ) assert ( numpy.fabs( coords.vRvz_to_pupv(vR, vz, R, z, delta=delta, oblate=True)[1] - r * vt ) < 10.0**-3.0 ), ( "vRvz_to_pupv at large R,z does not behave as expected for oblate spheroidal coordinates" ) # Also check that it does not matter whether we give R,z or u,v delta = 0.5 R, z = delta * 2.0, delta / 3.0 vR, vz = 0.2, -0.5 assert ( numpy.fabs( coords.vRvz_to_pupv(vR, vz, R, z, delta=delta, oblate=True)[0] - coords.vRvz_to_pupv( vR, vz, *coords.Rz_to_uv(R, z, delta=delta, oblate=True), delta=delta, oblate=True, uv=True, )[0] ) < 10.0**-3.0 ), ( "vRvz_to_pupv with and without pre-computed u,v do not agree for oblate spheroidal coordinates" ) assert ( numpy.fabs( coords.vRvz_to_pupv(vR, vz, R, z, delta=delta, oblate=True)[1] - coords.vRvz_to_pupv( vR, vz, *coords.Rz_to_uv(R, z, delta=delta, oblate=True), delta=delta, oblate=True, uv=True, )[1] ) < 10.0**-3.0 ), ( "vRvz_to_pupv with and without pre-computed u,v do not agree for oblate spheroidal coordinates" ) return None def test_pupv_to_vRvz(): # Test that this is the inverse of vRvz_to_pupv delta = 0.5 R, z = delta / 2.0, delta * 3.0 vR, vz = 0.2, -0.5 u, v = coords.Rz_to_uv(R, z, delta=delta) pu, pv = coords.vRvz_to_pupv(vR, vz, R, z, delta=delta) assert numpy.fabs(coords.pupv_to_vRvz(pu, pv, u, v, delta=delta)[0] - vR) < 1e-8, ( "pupv_to_vRvz is not the inverse of vRvz_to_pupv" ) assert numpy.fabs(coords.pupv_to_vRvz(pu, pv, u, v, delta=delta)[1] - vz) < 1e-8, ( "pupv_to_vRvz is not the inverse of vRvz_to_pupv" ) # Another one delta = 1.5 R, z = delta * 2.0, -delta / 3.0 vR, vz = -0.2, 0.5 u, v = coords.Rz_to_uv(R, z, delta=delta) pu, pv = coords.vRvz_to_pupv(vR, vz, R, z, delta=delta) assert numpy.fabs(coords.pupv_to_vRvz(pu, pv, u, v, delta=delta)[0] - vR) < 1e-8, ( "pupv_to_vRvz is not the inverse of vRvz_to_pupv" ) assert numpy.fabs(coords.pupv_to_vRvz(pu, pv, u, v, delta=delta)[1] - vz) < 1e-8, ( "pupv_to_vRvz is not the inverse of vRvz_to_pupv" ) return None def test_pupv_to_vRvz_oblate(): # Test that this is the inverse of vRvz_to_pupv delta = 0.5 R, z = delta / 2.0, delta * 3.0 vR, vz = 0.2, -0.5 u, v = coords.Rz_to_uv(R, z, delta=delta, oblate=True) pu, pv = coords.vRvz_to_pupv(vR, vz, R, z, delta=delta, oblate=True) assert ( numpy.fabs(coords.pupv_to_vRvz(pu, pv, u, v, delta=delta, oblate=True)[0] - vR) < 1e-8 ), "pupv_to_vRvz is not the inverse of vRvz_to_pupv" assert ( numpy.fabs(coords.pupv_to_vRvz(pu, pv, u, v, delta=delta, oblate=True)[1] - vz) < 1e-8 ), "pupv_to_vRvz is not the inverse of vRvz_to_pupv" # Another one delta = 1.5 R, z = delta * 2.0, -delta / 3.0 vR, vz = -0.2, 0.5 u, v = coords.Rz_to_uv(R, z, delta=delta, oblate=True) pu, pv = coords.vRvz_to_pupv(vR, vz, R, z, delta=delta, oblate=True) assert ( numpy.fabs(coords.pupv_to_vRvz(pu, pv, u, v, delta=delta, oblate=True)[0] - vR) < 1e-8 ), "pupv_to_vRvz is not the inverse of vRvz_to_pupv" assert ( numpy.fabs(coords.pupv_to_vRvz(pu, pv, u, v, delta=delta, oblate=True)[1] - vz) < 1e-8 ), "pupv_to_vRvz is not the inverse of vRvz_to_pupv" return None def test_lbd_to_XYZ_jac(): # Just position l, b, d = 180.0, 30.0, 2.0 jac = coords.lbd_to_XYZ_jac(l, b, d, degree=True) assert numpy.fabs(jac[0, 0] - 0.0) < 10.0**-10.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[0, 1] - numpy.pi / 180.0) < 10.0**-10.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[0, 2] + numpy.sqrt(3.0) / 2.0) < 10.0**-10.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[1, 0] + numpy.sqrt(3.0) * numpy.pi / 180.0) < 10.0**-10.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[1, 1] - 0.0) < 10.0**-10.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[1, 2] - 0.0) < 10.0**-10.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[2, 0] - 0.0) < 10.0**-10.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[2, 1] - numpy.sqrt(3.0) * numpy.pi / 180.0) < 10.0**-10.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[2, 2] - 0.5) < 10.0**-10.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) # 6D l, b, d = 3.0 * numpy.pi / 2.0, numpy.pi / 6.0, 2.0 vr, pmll, pmbb = 10.0, 20.0, -30.0 jac = coords.lbd_to_XYZ_jac(l, b, d, vr, pmll, pmbb, degree=False) assert numpy.fabs(jac[0, 0] - numpy.sqrt(3.0)) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[0, 1] - 0.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[0, 2] - 0.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[1, 0] - 0.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[1, 1] - 1.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[1, 2] + numpy.sqrt(3.0) / 2.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[2, 0] - 0.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[2, 1] - numpy.sqrt(3.0)) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[2, 2] - 0.5) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.all(numpy.fabs(jac[:3, 3:]) < 10.0**-9.0), ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert ( numpy.fabs( jac[3, 0] - numpy.sqrt(3.0) / 2.0 * vr + 0.5 * pmbb * d * 4.740470463496208 ) < 10.0**-8.0 ), "lbd_to_XYZ_jac calculation did not work as expected" assert numpy.fabs(jac[3, 1] - 0.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[3, 2] - pmll * 4.740470463496208) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[3, 3] - 0.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[3, 4] - d * 4.740470463496208) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[3, 5] - 0.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[4, 0] - pmll * d * 4.740470463496208) < 10.0**-8.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert ( numpy.fabs( jac[4, 1] - vr / 2.0 - numpy.sqrt(3.0) / 2.0 * d * pmbb * 4.740470463496208 ) < 10.0**-8.0 ), "lbd_to_XYZ_jac calculation did not work as expected" assert numpy.fabs(jac[4, 2] - 0.5 * 4.740470463496208 * pmbb) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[4, 3] + numpy.sqrt(3.0) / 2.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[4, 4] - 0.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[4, 5] - 4.740470463496208) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[5, 0] - 0.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert ( numpy.fabs( jac[5, 1] + 0.5 * d * 4.740470463496208 * pmbb - numpy.sqrt(3.0) / 2.0 * vr ) < 10.0**-8.0 ), "lbd_to_XYZ_jac calculation did not work as expected" assert ( numpy.fabs(jac[5, 2] - numpy.sqrt(3.0) / 2.0 * 4.740470463496208 * pmbb) < 10.0**-9.0 ), "lbd_to_XYZ_jac calculation did not work as expected" assert numpy.fabs(jac[5, 3] - 0.5) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert numpy.fabs(jac[5, 4] - 0.0) < 10.0**-9.0, ( "lbd_to_XYZ_jac calculation did not work as expected" ) assert ( numpy.fabs(jac[5, 5] - numpy.sqrt(3.0) / 2.0 * d * 4.740470463496208) < 10.0**-9.0 ), "lbd_to_XYZ_jac calculation did not work as expected" return None def test_cyl_to_spher_vec(): # Test 45 degrees, disk plane, & polar location vr, vT, vtheta = coords.cyl_to_spher_vec(0.6, 1.3, 0.6, 1.0, 1.0) assert numpy.fabs(vr - 0.6 * 2**0.5) < 10.0**-8, ( "cyl_to_spher_vec does not work as expected" ) assert numpy.fabs(vtheta - 0) < 10.0**-8, ( "cyl_to_spher_vec does not work as expected" ) assert numpy.fabs(vT - 1.3) < 10.0**-8, "cyl_to_spher_vec does not work as expected" vr, vT, vtheta = coords.cyl_to_spher_vec(-1.2, -0.7, -0.8, 1.0, 0.0) assert numpy.fabs(vr + 1.2) < 10.0**-8, "cyl_to_spher_vec does not work as expected" assert numpy.fabs(vtheta - 0.8) < 10.0**-8, ( "cyl_to_spher_vec does not work as expected" ) assert numpy.fabs(vT + 0.7) < 10.0**-8, "cyl_to_spher_vec does not work as expected" vr, vT, vtheta = coords.cyl_to_spher_vec(-1.2, -0.7, -0.8, 0.0, 1.0) assert numpy.fabs(vr + 0.8) < 10.0**-8, "cyl_to_spher_vec does not work as expected" assert numpy.fabs(vtheta + 1.2) < 10.0**-8, ( "cyl_to_spher_vec does not work as expected" ) assert numpy.fabs(vT + 0.7) < 10.0**-8, "cyl_to_spher_vec does not work as expected" return None def test_spher_to_cyl_vec(): # Test 45 degrees, disk plane, & polar location vR, vT, vz = coords.spher_to_cyl_vec(0.7, 1.4, 0.7, numpy.pi / 4.0) assert numpy.fabs(vR - 0.7 * 2**0.5) < 10.0**-8, ( "spher_to_cyl_vec does not work as expected" ) assert numpy.fabs(vT - 1.4) < 10.0**-8, "spher_to_cyl_vec does not work as expected" assert numpy.fabs(vz - 0.0) < 10.0**-8, "spher_to_cyl_vec does not work as expected" vR, vT, vz = coords.spher_to_cyl_vec(0.5, -1.3, 0.7, 0.0) assert numpy.fabs(vR - 0.7) < 10.0**-8, "spher_to_cyl_vec does not work as expected" assert numpy.fabs(vT + 1.3) < 10.0**-8, "spher_to_cyl_vec does not work as expected" assert numpy.fabs(vz - 0.5) < 10.0**-8, "spher_to_cyl_vec does not work as expected" vR, vT, vz = coords.spher_to_cyl_vec(0.5, -1.3, 0.7, numpy.pi / 2.0) assert numpy.fabs(vR - 0.5) < 10.0**-8, "spher_to_cyl_vec does not work as expected" assert numpy.fabs(vT + 1.3) < 10.0**-8, "spher_to_cyl_vec does not work as expected" assert numpy.fabs(vz + 0.7) < 10.0**-8, "spher_to_cyl_vec does not work as expected" return None def test_cyl_to_spher(): # Just a few quick tests r, t, p = coords.cyl_to_spher(1.2, 3.2, 1.0) assert numpy.fabs(r**2.0 - 1.2**2.0 - 3.2**2.0) < 10.0**-8.0, ( "cyl_to_spher does not work as expected" ) assert numpy.fabs(r * numpy.cos(t) - 3.2) < 10.0**-8.0, ( "cyl_to_spher does not work as expected" ) assert numpy.fabs(p - 1.0) < 10.0**-8.0, "cyl_to_spher does not work as expected" r, t, p = coords.cyl_to_spher(1.2, -3.2, 4.0) assert numpy.fabs(r**2.0 - 1.2**2.0 - 3.2**2.0) < 10.0**-8.0, ( "cyl_to_spher does not work as expected" ) assert numpy.fabs(r * numpy.cos(t) + 3.2) < 10.0**-8.0, ( "cyl_to_spher does not work as expected" ) assert numpy.fabs(p - 4.0) < 10.0**-8.0, "cyl_to_spher does not work as expected" return None def test_spher_to_cyl(): # Just a few quick tests R, z, p = coords.spher_to_cyl(5.0, numpy.arccos(3.0 / 5.0), 1.0) assert numpy.fabs(R - 4.0) < 10.0**-8.0, "spher_to_cyl does not work as expected" assert numpy.fabs(z - 3.0) < 10.0**-8.0, "spher_to_cyl does not work as expected" assert numpy.fabs(p - 1.0) < 10.0**-8.0, "spher_to_cyl does not work as expected" R, z, p = coords.spher_to_cyl(5.0, numpy.arccos(-3.0 / 5.0), 4.0) assert numpy.fabs(R - 4.0) < 10.0**-8.0, "spher_to_cyl does not work as expected" assert numpy.fabs(z + 3.0) < 10.0**-8.0, "spher_to_cyl does not work as expected" assert numpy.fabs(p - 4.0) < 10.0**-8.0, "spher_to_cyl does not work as expected" return None def test_cyl_to_rect_jac(): # Just position R, phi, Z = 2.0, numpy.pi, 1.0 jac = coords.cyl_to_rect_jac(R, phi, Z) assert numpy.fabs(numpy.linalg.det(jac) - R) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[0, 0] + 1.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[0, 1] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[0, 2] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[1, 0] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[1, 1] + 2.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[1, 2] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[2, 0] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[2, 1] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[2, 2] - 1.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) # 6D R, phi, Z = 2.0, numpy.pi, 1.0 vR, vT, vZ = 1.0, 2.0, 3.0 jac = coords.cyl_to_rect_jac(R, vR, vT, Z, vZ, phi) vindx = numpy.array([False, True, True, False, True, False], dtype="bool") assert numpy.fabs(numpy.linalg.det(jac) - R) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[0, 0] + 1.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[0, 5] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[0, 3] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.all(numpy.fabs(jac[0, vindx]) < 10.0**-10.0), ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[1, 0] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[1, 5] + 2.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[1, 3] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.all(numpy.fabs(jac[1, vindx]) < 10.0**-10.0), ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[2, 0] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[2, 5] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[2, 3] - 1.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.all(numpy.fabs(jac[2, vindx]) < 10.0**-10.0), ( "cyl_to_rect_jac calculation did not work as expected" ) # Velocities assert numpy.fabs(jac[3, 0] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[3, 1] + 1.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[3, 2] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[3, 3] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[3, 4] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[3, 5] - 2.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[4, 0] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[4, 1] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[4, 2] + 1.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[4, 3] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[4, 4] - 0.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.fabs(jac[4, 5] + 1.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) assert numpy.all( numpy.fabs( jac[5, numpy.array([True, True, True, True, False, True], dtype="bool")] - 0.0 ) < 10.0**-10.0 ), "cyl_to_rect_jac calculation did not work as expected" assert numpy.fabs(jac[5, 4] - 1.0) < 10.0**-10.0, ( "cyl_to_rect_jac calculation did not work as expected" ) return None def test_radec_to_custom_valueerror(): # Test the radec_to_custom without T raises a ValueError with pytest.raises(ValueError): xieta = coords.radec_to_custom(20.0, 30.0) return None def test_radec_to_custom_againstlb(): _turn_off_apy() ra, dec = 20.0, 30.0 theta, dec_ngp, ra_ngp = coords.get_epoch_angles(2000.0) T = numpy.dot( numpy.array( [ [numpy.cos(ra_ngp), -numpy.sin(ra_ngp), 0.0], [numpy.sin(ra_ngp), numpy.cos(ra_ngp), 0.0], [0.0, 0.0, 1.0], ] ), numpy.dot( numpy.array( [ [-numpy.sin(dec_ngp), 0.0, numpy.cos(dec_ngp)], [0.0, 1.0, 0.0], [numpy.cos(dec_ngp), 0.0, numpy.sin(dec_ngp)], ] ), numpy.array( [ [numpy.cos(theta), numpy.sin(theta), 0.0], [numpy.sin(theta), -numpy.cos(theta), 0.0], [0.0, 0.0, 1.0], ] ), ), ) lb_direct = coords.radec_to_lb(ra, dec, degree=True) lb_custom = coords.radec_to_custom(ra, dec, T=T.T, degree=True) assert numpy.fabs(lb_direct[0] - lb_custom[0]) < 10.0**-8.0, ( "radec_to_custom for transformation to l,b does not work properly" ) assert numpy.fabs(lb_direct[1] - lb_custom[1]) < 10.0**-8.0, ( "radec_to_custom for transformation to l,b does not work properly" ) # Array s = numpy.arange(2) lb_direct = coords.radec_to_lb(ra * s, dec * s, degree=True) lb_custom = coords.radec_to_custom(ra * s, dec * s, T=T.T, degree=True) assert numpy.all(numpy.fabs(lb_direct - lb_custom) < 10.0**-8.0), ( "radec_to_custom for transformation to l,b does not work properly" ) _turn_on_apy() return None def test_radec_to_custom_pal5(): # Test the custom ra,dec transformation for Pal 5 _RAPAL5 = 229.018 / 180.0 * numpy.pi _DECPAL5 = -0.124 / 180.0 * numpy.pi _TPAL5 = numpy.dot( numpy.array( [ [numpy.cos(_DECPAL5), 0.0, numpy.sin(_DECPAL5)], [0.0, 1.0, 0.0], [-numpy.sin(_DECPAL5), 0.0, numpy.cos(_DECPAL5)], ] ), numpy.array( [ [numpy.cos(_RAPAL5), numpy.sin(_RAPAL5), 0.0], [-numpy.sin(_RAPAL5), numpy.cos(_RAPAL5), 0.0], [0.0, 0.0, 1.0], ] ), ) xieta = coords.radec_to_custom(_RAPAL5, _DECPAL5, T=_TPAL5, degree=False) def checkrng(x, xpct, dom, shift): return numpy.fabs(((numpy.fabs(x - xpct) + shift) % dom) - shift) # 0 < xieta[0] < 2 * pi assert checkrng(xieta[0], 0, 2 * numpy.pi, 0) < 1e-8, ( "radec_to_custom does not work properly for Pal 5 transformation" ) assert checkrng(xieta[1], 0, numpy.pi, numpy.pi / 2) < 1e-8, ( "radec_to_custom does not work properly for Pal 5 transformation" ) # One more, rough estimate based on visual inspection of plot xieta = coords.radec_to_custom(240.0, 6.0, T=_TPAL5, degree=True) assert checkrng(xieta[0], 11.0, 2 * numpy.pi, 0) < 0.2, ( "radec_to_custom does not work properly for Pal 5 transformation" ) assert checkrng(xieta[1], 6.0, numpy.pi, numpy.pi / 2) < 0.2, ( "radec_to_custom does not work properly for Pal 5 transformation" ) return None def test_pmrapmdec_to_custom_valueerror(): # Test the pmrapmdec_to_custom without T raises a ValueError with pytest.raises(ValueError): xieta = coords.pmrapmdec_to_custom(1.0, 1.0, 20.0, 30.0) return None def test_pmrapmdec_to_custom_againstlb(): _turn_off_apy() ra, dec = 20.0, 30.0 pmra, pmdec = -3.0, 4.0 theta, dec_ngp, ra_ngp = coords.get_epoch_angles(2000.0) T = numpy.dot( numpy.array( [ [numpy.cos(ra_ngp), -numpy.sin(ra_ngp), 0.0], [numpy.sin(ra_ngp), numpy.cos(ra_ngp), 0.0], [0.0, 0.0, 1.0], ] ), numpy.dot( numpy.array( [ [-numpy.sin(dec_ngp), 0.0, numpy.cos(dec_ngp)], [0.0, 1.0, 0.0], [numpy.cos(dec_ngp), 0.0, numpy.sin(dec_ngp)], ] ), numpy.array( [ [numpy.cos(theta), numpy.sin(theta), 0.0], [numpy.sin(theta), -numpy.cos(theta), 0.0], [0.0, 0.0, 1.0], ] ), ), ) pmlb_direct = coords.pmrapmdec_to_pmllpmbb(pmra, pmdec, ra, dec, degree=True) pmlb_custom = coords.pmrapmdec_to_custom(pmra, pmdec, ra, dec, T=T.T, degree=True) assert numpy.fabs(pmlb_direct[0] - pmlb_custom[0]) < 10.0**-8.0, ( "pmrapmdec_to_custom for transformation to pml,pmb does not work properly" ) assert numpy.fabs(pmlb_direct[1] - pmlb_custom[1]) < 10.0**-8.0, ( "pmrapmdec_to_custom for transformation to pml,pmb does not work properly" ) # Array s = numpy.arange(2) pmlb_direct = coords.pmrapmdec_to_pmllpmbb( pmra * s, pmdec * s, ra * s, dec * s, degree=True ) pmlb_custom = coords.pmrapmdec_to_custom( pmra * s, pmdec * s, ra * s, dec * s, T=T.T, degree=True ) assert numpy.all(numpy.fabs(pmlb_direct - pmlb_custom) < 10.0**-8.0), ( "pmrapmdec_to_custom for transformation to pml,pmb does not work properly" ) _turn_on_apy() return None def test_custom_to_radec_valueerror(): # Test the custom_to_radec without T raises a ValueError with pytest.raises(ValueError): xieta = coords.custom_to_radec(20.0, 30.0) return None def test_custom_to_radec_againstlb(): # FIXME COMPARE TO DOCUMENT _turn_off_apy() ra, dec = 20.0, 30.0 theta, dec_ngp, ra_ngp = coords.get_epoch_angles(2000.0) T = numpy.dot( numpy.array( [ [numpy.cos(ra_ngp), -numpy.sin(ra_ngp), 0.0], [numpy.sin(ra_ngp), numpy.cos(ra_ngp), 0.0], [0.0, 0.0, 1.0], ] ), numpy.dot( numpy.array( [ [-numpy.sin(dec_ngp), 0.0, numpy.cos(dec_ngp)], [0.0, 1.0, 0.0], [numpy.cos(dec_ngp), 0.0, numpy.sin(dec_ngp)], ] ), numpy.array( [ [numpy.cos(theta), numpy.sin(theta), 0.0], [numpy.sin(theta), -numpy.cos(theta), 0.0], [0.0, 0.0, 1.0], ] ), ), ) lb_direct = coords.radec_to_lb(ra, dec, degree=True) lb_custom = coords.custom_to_radec(ra, dec, T=T, degree=True) assert numpy.fabs(lb_direct[0] - lb_custom[0]) < 10.0**-8.0, ( "custom_to_radec for transformation to l,b does not work properly" ) assert numpy.fabs(lb_direct[1] - lb_custom[1]) < 10.0**-8.0, ( "custom_to_radec for transformation to l,b does not work properly" ) # Array s = numpy.arange(2) lb_direct = coords.radec_to_lb(ra * s, dec * s, degree=True) lb_custom = coords.custom_to_radec(ra * s, dec * s, T=T, degree=True) assert numpy.all(numpy.fabs(lb_direct - lb_custom) < 10.0**-8.0), ( "radec_to_custom for transformation to l,b does not work properly" ) _turn_on_apy() return None def test_custom_to_radec_pal5(): # FIXME COMPARE TO DOCUMENT # Test the custom ra,dec transformation for Pal 5 _RAPAL5 = 229.018 / 180.0 * numpy.pi _DECPAL5 = -0.124 / 180.0 * numpy.pi _TPAL5 = numpy.dot( numpy.array( [ [numpy.cos(_DECPAL5), 0.0, numpy.sin(_DECPAL5)], [0.0, 1.0, 0.0], [-numpy.sin(_DECPAL5), 0.0, numpy.cos(_DECPAL5)], ] ), numpy.array( [ [numpy.cos(_RAPAL5), numpy.sin(_RAPAL5), 0.0], [-numpy.sin(_RAPAL5), numpy.cos(_RAPAL5), 0.0], [0.0, 0.0, 1.0], ] ), ) xieta = coords.custom_to_radec(_RAPAL5, _DECPAL5, T=_TPAL5.T, degree=False) def checkrng(x, xpct, dom, shift): return numpy.fabs(((numpy.fabs(x - xpct) + shift) % dom) - shift) # 0 < xieta[0] < 2 * pi assert checkrng(xieta[0], 0, 2 * numpy.pi, 0) < 1e-8, ( "custom_to_radec does not work properly for Pal 5 transformation" ) assert checkrng(xieta[1], 0, numpy.pi, numpy.pi / 2) < 1e-8, ( "custom_to_radec does not work properly for Pal 5 transformation" ) # One more, rough estimate based on visual inspection of plot xieta = coords.custom_to_radec(240.0, 6.0, T=_TPAL5.T, degree=True) assert checkrng(xieta[0], 11.0, 2 * numpy.pi, 0) < 0.2, ( "custom_to_radec does not work properly for Pal 5 transformation" ) assert checkrng(xieta[1], 6.0, numpy.pi, numpy.pi / 2) < 0.2, ( "custom_to_radec does not work properly for Pal 5 transformation" ) return None def test_custom_to_pmrapmdec_valueerror(): # Test the pmrapmdec_to_custom without T raises a ValueError with pytest.raises(ValueError): xieta = coords.custom_to_pmrapmdec(1.0, 1.0, 20.0, 30.0) return None def test_custom_to_pmrapmdec_againstlb(): _turn_off_apy() ra, dec = 20.0, 30.0 pmra, pmdec = -3.0, 4.0 theta, dec_ngp, ra_ngp = coords.get_epoch_angles(2000.0) T = numpy.dot( numpy.array( [ [numpy.cos(ra_ngp), -numpy.sin(ra_ngp), 0.0], [numpy.sin(ra_ngp), numpy.cos(ra_ngp), 0.0], [0.0, 0.0, 1.0], ] ), numpy.dot( numpy.array( [ [-numpy.sin(dec_ngp), 0.0, numpy.cos(dec_ngp)], [0.0, 1.0, 0.0], [numpy.cos(dec_ngp), 0.0, numpy.sin(dec_ngp)], ] ), numpy.array( [ [numpy.cos(theta), numpy.sin(theta), 0.0], [numpy.sin(theta), -numpy.cos(theta), 0.0], [0.0, 0.0, 1.0], ] ), ), ) pmlb_direct = coords.pmrapmdec_to_pmllpmbb(pmra, pmdec, ra, dec, degree=True) pmlb_custom = coords.custom_to_pmrapmdec(pmra, pmdec, ra, dec, T=T, degree=True) assert numpy.fabs(pmlb_direct[0] - pmlb_custom[0]) < 10.0**-8.0, ( "custom_to_pmrapmdec for transformation to pml,pmb does not work properly" ) assert numpy.fabs(pmlb_direct[1] - pmlb_custom[1]) < 10.0**-8.0, ( "custom_to_pmrapmdec for transformation to pml,pmb does not work properly" ) # Array s = numpy.arange(2) pmlb_direct = coords.pmrapmdec_to_pmllpmbb( pmra * s, pmdec * s, ra * s, dec * s, degree=True ) pmlb_custom = coords.custom_to_pmrapmdec( pmra * s, pmdec * s, ra * s, dec * s, T=T, degree=True ) assert numpy.all(numpy.fabs(pmlb_direct - pmlb_custom) < 10.0**-8.0), ( "custom_to_pmrapmdec for transformation to pml,pmb does not work properly" ) _turn_on_apy() return None # 02/06/2018 (JB): Edited for cases where astropy coords are always turned off # [case at hand: einsum bug in numpy 1.14 / python2.7 astropy] def _turn_off_apy(keep_loaded=False): coords._APY_COORDS_ORIG = coords._APY_COORDS coords._APY_COORDS = False if not keep_loaded: coords._APY_LOADED = False def _turn_on_apy(): coords._APY_COORDS = coords._APY_COORDS_ORIG coords._APY_LOADED = True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_diskdf.py0000644000175100001660000033737714761352023016360 0ustar00runnerdocker# Tests of the diskdf module: distribution functions from Dehnen (1999) import os import numpy import pytest from scipy import stats from galpy.df import dehnendf, schwarzschilddf, shudf _FEWERLONGINTEGRALS = True # So we can reuse the following ddf_correct_flat = None ddf_correct2_flat = None ddf_correct_powerrise = None sdf_correct_flat = None # First some tests of surfaceSigmaProfile and expSurfaceSigmaProfile def test_expSurfaceSigmaProfile_surfacemass(): from galpy.df import expSurfaceSigmaProfile essp = expSurfaceSigmaProfile(params=(0.25, 0.75, 0.1)) assert numpy.fabs(essp.surfacemass(0.5) - numpy.exp(-0.5 / 0.25)) < 10.0**-8.0, ( "expSurfaceSigmaProfile's surfacemass does not work as expected" ) assert numpy.fabs(essp.surfacemass(1.5, log=True) + 1.5 / 0.25) < 10.0**-8.0, ( "expSurfaceSigmaProfile's surfacemass does not work as expected" ) return None def test_expSurfaceSigmaProfile_surfacemassDerivative(): from galpy.df import expSurfaceSigmaProfile essp = expSurfaceSigmaProfile(params=(0.25, 0.75, 0.1)) assert ( numpy.fabs(essp.surfacemassDerivative(0.5) + numpy.exp(-0.5 / 0.25) / 0.25) < 10.0**-8.0 ), "expSurfaceSigmaProfile's surfacemassDerivative does not work as expected" assert ( numpy.fabs(essp.surfacemassDerivative(1.5, log=True) + 1.0 / 0.25) < 10.0**-8.0 ), "expSurfaceSigmaProfile's surfacemassDerivative does not work as expected" return None def test_expSurfaceSigmaProfile_sigma2(): from galpy.df import expSurfaceSigmaProfile essp = expSurfaceSigmaProfile(params=(0.25, 0.75, 0.1)) assert ( numpy.fabs(essp.sigma2(0.5) - 0.1**2.0 * numpy.exp(-(0.5 - 1.0) / 0.75 * 2.0)) < 10.0**-8.0 ), "expSurfaceSigmaProfile's sigma2 does not work as expected" assert ( numpy.fabs( essp.sigma2(1.5, log=True) - 2.0 * numpy.log(0.1) + (1.5 - 1.0) / 0.75 * 2.0 ) < 10.0**-8.0 ), "expSurfaceSigmaProfile's sigma2 does not work as expected" return None def test_expSurfaceSigmaProfile_sigma2Derivative(): from galpy.df import expSurfaceSigmaProfile essp = expSurfaceSigmaProfile(params=(0.25, 0.75, 0.1)) assert ( numpy.fabs( essp.sigma2Derivative(0.5) + 2.0 * 0.1**2.0 / 0.75 * numpy.exp(-(0.5 - 1.0) / 0.75 * 2.0) ) < 10.0**-8.0 ), "expSurfaceSigmaProfile's sigma2Derivative does not work as expected" assert numpy.fabs(essp.sigma2Derivative(1.5, log=True) + 2.0 / 0.75) < 10.0**-8.0, ( "expSurfaceSigmaProfile's sigma2 does not work as expected" ) return None def test_surfaceSigmaProfile_outputParams(): from galpy.df import expSurfaceSigmaProfile essp = expSurfaceSigmaProfile(params=(0.25, 0.75, 0.1)) assert numpy.fabs(essp.outputParams()[0] - 0.25) < 10.0**-8.0, ( "surfaceSigmaProfile's outputParams does not behave as expected" ) assert numpy.fabs(essp.outputParams()[1] - 0.75) < 10.0**-8.0, ( "surfaceSigmaProfile's outputParams does not behave as expected" ) assert numpy.fabs(essp.outputParams()[2] - 0.1) < 10.0**-8.0, ( "surfaceSigmaProfile's outputParams does not behave as expected" ) return None def test_surfaceSigmaProfile_formatStringParams(): from galpy.df import expSurfaceSigmaProfile essp = expSurfaceSigmaProfile(params=(0.25, 0.75, 0.1)) assert essp.formatStringParams()[0] == r"%6.4f", ( "surfaceSigmaProfile's formatStringParams does not behave as expected" ) assert essp.formatStringParams()[1] == r"%6.4f", ( "surfaceSigmaProfile's formatStringParams does not behave as expected" ) assert essp.formatStringParams()[2] == r"%6.4f", ( "surfaceSigmaProfile's formatStringParams does not behave as expected" ) return None def test_dfsetup_surfaceSigmaProfile(): df = dehnendf(profileParams=(0.25, 0.75, 0.1), beta=0.0, correct=False) from galpy.df import expSurfaceSigmaProfile essp = expSurfaceSigmaProfile(params=(0.25, 0.75, 0.1)) df_alt = dehnendf(surfaceSigma=essp, beta=0.0, correct=False) assert numpy.all( numpy.fabs( numpy.array(df._surfaceSigmaProfile._params) - numpy.array(df_alt._surfaceSigmaProfile._params) ) < 10.0**-10.0 ), ( "diskdf setup with explicit surfaceSigmaProfile class does not give the same profile as with parameters only" ) return None # Tests for cold population, flat rotation curve: =~ v_c def test_dehnendf_cold_flat_vt(): df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) assert numpy.fabs(df.meanvT(1.0) - 1.0) < 10.0**-3.0, ( "mean vT of cold dehnendf in a flat rotation curve is not close to V_c at R=1" ) assert numpy.fabs(df.meanvT(0.5) - 1.0) < 10.0**-3.0, ( "mean vT of cold dehnendf in a flat rotation curve is not close to V_c at R=0.5" ) assert numpy.fabs(df.meanvT(2.0) - 1.0) < 10.0**-3.0, ( "mean vT of cold dehnendf in a flat rotation curve is not close to V_c at R=2" ) # Really close to the center assert numpy.fabs(df.meanvT(0.0001) - 1.0) < 10.0**-3.0, ( "mean vT of cold dehnendf in a flat rotation curve is not close to V_c at R=0.5" ) return None # Tests for cold population, power-law rotation curve: =~ v_c def test_dehnendf_cold_powerrise_vt(): # Rising rotation curve beta = 0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) assert numpy.fabs(df.meanvT(1.0) - 1.0) < 10.0**-3.0, ( "mean vT of cold dehnendf in a power-law rotation curve is not close to V_c at R=1" ) assert numpy.fabs(df.meanvT(0.5) - (0.5) ** beta) < 10.0**-3.0, ( "mean vT of cold dehnendf in a power-law rotation curve is not close to V_c at R=0.5" ) assert numpy.fabs(df.meanvT(2.0) - (2.0) ** beta) < 10.0**-3.0, ( "mean vT of cold dehnendf in a power-law rotation curve is not close to V_c at R=2" ) def test_dehnendf_cold_powerfall_vt(): # Falling rotation curve beta = -0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) assert numpy.fabs(df.meanvT(1.0) - 1.0) < 10.0**-3.0, ( "mean vT of cold dehnendf in a power-law rotation curve is not close to V_c at R=1" ) assert numpy.fabs(df.meanvT(0.5) - (0.5) ** beta) < 10.0**-3.0, ( "mean vT of cold dehnendf in a power-law rotation curve is not close to V_c at R=0.5" ) assert numpy.fabs(df.meanvT(2.0) - (2.0) ** beta) < 10.0**-3.0, ( "mean vT of cold dehnendf in a power-law rotation curve is not close to V_c at R=2" ) return None # Tests for cold population, flat rotation curve: =~ v_c def test_dehnendf_cold_flat_skewvt(): df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.skewvT(1.0)) < 1.0 / 20.0, ( "skew vT of cold dehnendf in a flat rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.skewvT(0.5)) < 1.0 / 20.0, ( "skew vT of cold dehnendf in a flat rotation curve is not close to zero at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.skewvT(2.0)) < 1.0 / 20.0, ( "skew vT of cold dehnendf in a flat rotation curve is not close to zero at R=2" ) return None # Tests for cold population, power-law rotation curve: =~ v_c def test_dehnendf_cold_powerrise_skewvt(): # Rising rotation curve beta = 0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.skewvT(1.0)) < 1.0 / 20.0, ( "skew vT of cold dehnendf in a power-law rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.skewvT(0.5)) < 1.0 / 20.0, ( "skew vT of cold dehnendf in a power-law rotation curve is not close to zero at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.skewvT(2.0)) < 1.0 / 20.0, ( "skew vT of cold dehnendf in a power-law rotation curve is not close to zero at R=2" ) return None def test_dehnendf_cold_powerfall_skewvt(): # Falling rotation curve beta = -0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.skewvT(1.0)) < 1.0 / 20.0, ( "skew vT of cold dehnendf in a power-law rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.skewvT(0.5)) < 1.0 / 20.0, ( "skew vT of cold dehnendf in a power-law rotation curve is not close to zero at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.skewvT(2.0)) < 1.0 / 20.0, ( "skew vT of cold dehnendf in a power-law rotation curve is not close to zero at R=2" ) return None # Tests for cold population, flat rotation curve: = 0 def test_dehnendf_cold_flat_vr(): df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) assert numpy.fabs(df.meanvR(1.0) - 0.0) < 10.0**-3.0, ( "mean vR of cold dehnendf in a flat rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.meanvR(0.5) - 0.0) < 10.0**-3.0, ( "mean vR of cold dehnendf in a flat rotation curve is not close to zero at R=0.5" ) assert numpy.fabs(df.meanvR(2.0) - 0.0) < 10.0**-3.0, ( "mean vR of cold dehnendf in a flat rotation curve is not close to zero at R=2" ) return None # Tests for cold population, flat rotation curve: kurtosis = 0 def test_dehnendf_cold_flat_kurtosisvt(): df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.kurtosisvT(1.0)) < 1.0 / 20.0, ( "kurtosis vT of cold dehnendf in a flat rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.kurtosisvT(0.5)) < 1.0 / 20.0, ( "kurtosis vT of cold dehnendf in a flat rotation curve is not close to zero at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.kurtosisvT(2.0)) < 1.0 / 20.0, ( "kurtosis vT of cold dehnendf in a flat rotation curve is not close to zero at R=2" ) return None # Tests for cold population, power-law rotation curve: kurtosis = 0 def test_dehnendf_cold_powerrise_kurtosisvt(): # Rising rotation curve beta = 0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.kurtosisvT(1.0)) < 1.0 / 20.0, ( "kurtosis vT of cold dehnendf in a power-law rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.kurtosisvT(0.5)) < 1.0 / 20.0, ( "kurtosis vT of cold dehnendf in a power-law rotation curve is not close to zero at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.kurtosisvT(2.0)) < 1.0 / 20.0, ( "kurtosis vT of cold dehnendf in a power-law rotation curve is not close to zero at R=2" ) def test_dehnendf_cold_powerfall_kurtosisvt(): # Falling rotation curve beta = -0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.kurtosisvT(1.0)) < 1.0 / 20.0, ( "kurtosis vT of cold dehnendf in a power-law rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.kurtosisvT(0.5)) < 1.0 / 20.0, ( "kurtosis vT of cold dehnendf in a power-law rotation curve is not close to zero at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.kurtosisvT(2.0)) < 1.0 / 20.0, ( "kurtosis vT of cold dehnendf in a power-law rotation curve is not close to zero at R=2" ) return None # Tests for cold population, power-law rotation curve: = 0 def test_dehnendf_cold_powerrise_vr(): # Rising rotation curve beta = 0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) assert numpy.fabs(df.meanvR(1.0) - 0.0) < 10.0**-3.0, ( "mean vR of cold dehnendf in a power-law rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.meanvR(0.5) - 0.0) < 10.0**-3.0, ( "mean vR of cold dehnendf in a power-law rotation curve is not close to zero at R=0.5" ) assert numpy.fabs(df.meanvR(2.0) - 0.0) < 10.0**-3.0, ( "mean vR of cold dehnendf in a power-law rotation curve is not close to zero at R=2" ) def test_dehnendf_cold_powerfall_vr(): # Falling rotation curve beta = -0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) assert numpy.fabs(df.meanvR(1.0) - 0.0) < 10.0**-3.0, ( "mean vR of cold dehnendf in a power-law rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.meanvR(0.5) - 0.0) < 10.0**-3.0, ( "mean vR of cold dehnendf in a power-law rotation curve is not close to zero at R=0.5" ) assert numpy.fabs(df.meanvR(2.0) - 0.0) < 10.0**-3.0, ( "mean vR of cold dehnendf in a power-law rotation curve is not close to zero at R=2" ) return None # Tests for cold population, flat rotation curve: = 0 def test_dehnendf_cold_flat_skewvr(): df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.skewvR(1.0) - 0.0) < 10.0**-3.0, ( "skew vR of cold dehnendf in a flat rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.skewvR(0.5) - 0.0) < 10.0**-3.0, ( "skew vR of cold dehnendf in a flat rotation curve is not close to zero at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.skewvR(2.0) - 0.0) < 10.0**-3.0, ( "skew vR of cold dehnendf in a flat rotation curve is not close to zero at R=2" ) return None # Tests for cold population, power-law rotation curve: = 0 def test_dehnendf_cold_powerrise_skewvr(): # Rising rotation curve beta = 0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.skewvR(1.0) - 0.0) < 10.0**-3.0, ( "skew vR of cold dehnendf in a power-law rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.skewvR(0.5) - 0.0) < 10.0**-3.0, ( "skew vR of cold dehnendf in a power-law rotation curve is not close to zero at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.skewvR(2.0) - 0.0) < 10.0**-3.0, ( "skew vR of cold dehnendf in a power-law rotation curve is not close to zero at R=2" ) def test_dehnendf_cold_powerfall_skewvr(): # Falling rotation curve beta = -0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.skewvR(1.0) - 0.0) < 10.0**-3.0, ( "skew vR of cold dehnendf in a power-law rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.skewvR(0.5) - 0.0) < 10.0**-3.0, ( "skew vR of cold dehnendf in a power-law rotation curve is not close to zero at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.skewvR(2.0) - 0.0) < 10.0**-3.0, ( "skew vR of cold dehnendf in a power-law rotation curve is not close to zero at R=2" ) return None # Tests for cold population, flat rotation curve: kurtosis = 0 def test_dehnendf_cold_flat_kurtosisvr(): df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.kurtosisvR(1.0)) < 1.0 / 20.0, ( "kurtosis vR of cold dehnendf in a flat rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.kurtosisvR(0.5)) < 1.0 / 20.0, ( "kurtosis vR of cold dehnendf in a flat rotation curve is not close to zero at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.kurtosisvR(2.0)) < 1.0 / 20.0, ( "kurtosis vR of cold dehnendf in a flat rotation curve is not close to zero at R=2" ) return None # Tests for cold population, power-law rotation curve: kurtosis = 0 def test_dehnendf_cold_powerrise_kurtosisvr(): # Rising rotation curve beta = 0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.kurtosisvR(1.0)) < 1.0 / 20.0, ( "kurtosis vR of cold dehnendf in a power-law rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.kurtosisvR(0.5)) < 1.0 / 20.0, ( "kurtosis vR of cold dehnendf in a power-law rotation curve is not close to zero at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.kurtosisvR(2.0)) < 1.0 / 20.0, ( "kurtosis vR of cold dehnendf in a power-law rotation curve is not close to zero at R=2" ) def test_dehnendf_cold_powerfall_kurtosisvr(): # Falling rotation curve beta = -0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.kurtosisvR(1.0)) < 1.0 / 20.0, ( "kurtosis vR of cold dehnendf in a power-law rotation curve is not close to zero at R=1" ) assert numpy.fabs(df.kurtosisvR(0.5)) < 1.0 / 20.0, ( "kurtosis vR of cold dehnendf in a power-law rotation curve is not close to zero at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.kurtosisvR(2.0)) < 1.0 / 20.0, ( "kurtosis vR of cold dehnendf in a power-law rotation curve is not close to zero at R=2" ) return None # Tests for cold population, flat rotation curve: A = 0.5 def test_dehnendf_cold_flat_oortA(): df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) assert numpy.fabs(df.oortA(1.0) - 0.5 * 1.0 / 1.0) < 10.0**-3.0, ( "Oort A of cold dehnendf in a flat rotation curve is not close to expected at R=1" ) assert numpy.fabs(df.oortA(0.5) - 0.5 * 1.0 / 0.5) < 10.0**-3.0, ( "Oort A of cold dehnendf in a flat rotation curve is not close to expected at R=0.5" ) # one w/ Romberg assert numpy.fabs(df.oortA(2.0, romberg=True) - 0.5 * 1.0 / 2.0) < 10.0**-3.0, ( "Oort A of cold dehnendf in a flat rotation curve is not close to expected at R=2" ) return None # Tests for cold population, power-law rotation curve: A def test_dehnendf_cold_powerrise_oortA(): # Rising rotation curve beta = 0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert ( numpy.fabs(df.oortA(1.0) - 0.5 * 1.0 / 1.0 * (1.0 - beta)) < 10.0**-3.0 ), ( "Oort A of cold dehnendf in a power-law rotation curve is not close to expected at R=1" ) assert ( numpy.fabs(df.oortA(0.5) - 0.5 * (0.5) ** beta / 0.5 * (1.0 - beta)) < 10.0**-3.0 ), ( "Oort A of cold dehnendf in a power-law rotation curve is not close to expected at R=0.5" ) # one w/ Romberg assert ( numpy.fabs( df.oortA(2.0, romberg=True) - 0.5 * (2.0) ** beta / 2.0 * (1.0 - beta) ) < 10.0**-3.0 ), ( "Oort A of cold dehnendf in a power-law rotation curve is not close to expected at R=2" ) return None def test_dehnendf_cold_powerfall_oortA(): # Falling rotation curve beta = -0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert ( numpy.fabs(df.oortA(1.0) - 0.5 * 1.0 / 1.0 * (1.0 - beta)) < 10.0**-3.0 ), ( "Oort A of cold dehnendf in a power-law rotation curve is not close to expected at R=1" ) assert ( numpy.fabs(df.oortA(0.5) - 0.5 * (0.5) ** beta / 0.5 * (1.0 - beta)) < 10.0**-3.0 ), ( "Oort A of cold dehnendf in a power-law rotation curve is not close to expected at R=0.5" ) # One w/ Romberg if not _FEWERLONGINTEGRALS: assert ( numpy.fabs( df.oortA(2.0, romberg=True) - 0.5 * (2.0) ** beta / 2.0 * (1.0 - beta) ) < 10.0**-3.0 ), ( "Oort A of cold dehnendf in a power-law rotation curve is not close to expected at R=2" ) return None # Tests for cold population, flat rotation curve: B = -0.5 def test_dehnendf_cold_flat_oortB(): df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) assert numpy.fabs(df.oortB(1.0) + 0.5 * 1.0 / 1.0) < 10.0**-3.0, ( "Oort B of cold dehnendf in a flat rotation curve is not close to expected at R=1" ) assert numpy.fabs(df.oortB(0.5) + 0.5 * 1.0 / 0.5) < 10.0**-3.0, ( "Oort B of cold dehnendf in a flat rotation curve is not close to expected at R=0.5" ) assert numpy.fabs(df.oortB(2.0) + 0.5 * 1.0 / 2.0) < 10.0**-3.0, ( "Oort B of cold dehnendf in a flat rotation curve is not close to expected at R=2" ) return None # Tests for cold population, power-law rotation curve: B def test_dehnendf_cold_powerrise_oortB(): # Rising rotation curve beta = 0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert ( numpy.fabs(df.oortB(1.0) + 0.5 * 1.0 / 1.0 * (1.0 + beta)) < 10.0**-3.0 ), ( "Oort B of cold dehnendf in a power-law rotation curve is not close to expected at R=1" ) assert ( numpy.fabs(df.oortB(0.5) + 0.5 * (0.5) ** beta / 0.5 * (1.0 + beta)) < 10.0**-3.0 ), ( "Oort B of cold dehnendf in a power-law rotation curve is not close to expected at R=0.5" ) if not _FEWERLONGINTEGRALS: assert ( numpy.fabs(df.oortB(2.0) + 0.5 * (2.0) ** beta / 2.0 * (1.0 + beta)) < 10.0**-3.0 ), ( "Oort B of cold dehnendf in a power-law rotation curve is not close to expected at R=2" ) return None def test_dehnendf_cold_powerfall_oortB(): # Falling rotation curve beta = -0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert ( numpy.fabs(df.oortB(1.0) + 0.5 * 1.0 / 1.0 * (1.0 + beta)) < 10.0**-3.0 ), ( "Oort B of cold dehnendf in a power-law rotation curve is not close to expected at R=1" ) assert ( numpy.fabs(df.oortB(0.5) + 0.5 * (0.5) ** beta / 0.5 * (1.0 + beta)) < 10.0**-3.0 ), ( "Oort B of cold dehnendf in a power-law rotation curve is not close to expected at R=0.5" ) if not _FEWERLONGINTEGRALS: assert ( numpy.fabs(df.oortB(2.0) + 0.5 * (2.0) ** beta / 2.0 * (1.0 + beta)) < 10.0**-3.0 ), ( "Oort B of cold dehnendf in a power-law rotation curve is not close to expected at R=2" ) return None # Tests for cold population, flat rotation curve: C = 0 def test_dehnendf_cold_flat_oortC(): df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) assert numpy.fabs(df.oortC(1.0)) < 10.0**-3.0, ( "Oort C of cold dehnendf in a flat rotation curve is not close to expected at R=1" ) assert numpy.fabs(df.oortC(0.5)) < 10.0**-3.0, ( "Oort C of cold dehnendf in a flat rotation curve is not close to expected at R=0.5" ) assert numpy.fabs(df.oortC(2.0)) < 10.0**-3.0, ( "Oort C of cold dehnendf in a flat rotation curve is not close to expected at R=2" ) return None # Tests for cold population, power-law rotation curve: C def test_dehnendf_cold_powerrise_oortC(): # Rising rotation curve beta = 0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.oortC(1.0)) < 10.0**-3.0, ( "Oort C of cold dehnendf in a power-law rotation curve is not close to expected at R=1" ) assert numpy.fabs(df.oortC(0.5)) < 10.0**-3.0, ( "Oort C of cold dehnendf in a power-law rotation curve is not close to expected at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.oortC(2.0)) < 10.0**-3.0, ( "Oort C of cold dehnendf in a power-law rotation curve is not close to expected at R=2" ) return None def test_dehnendf_cold_powerfall_oortC(): # Falling rotation curve beta = -0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.oortC(1.0)) < 10.0**-3.0, ( "Oort C of cold dehnendf in a power-law rotation curve is not close to expected at R=1" ) assert numpy.fabs(df.oortC(0.5)) < 10.0**-3.0, ( "Oort C of cold dehnendf in a power-law rotation curve is not close to expected at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.oortC(2.0)) < 10.0**-3.0, ( "Oort C of cold dehnendf in a power-law rotation curve is not close to expected at R=2" ) return None # Tests for cold population, flat rotation curve: K = 0 def test_dehnendf_cold_flat_oortK(): df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) assert numpy.fabs(df.oortK(1.0)) < 10.0**-3.0, ( "Oort K of cold dehnendf in a flat rotation curve is not close to expected at R=1" ) assert numpy.fabs(df.oortK(0.5)) < 10.0**-3.0, ( "Oort K of cold dehnendf in a flat rotation curve is not close to expected at R=0.5" ) assert numpy.fabs(df.oortK(2.0)) < 10.0**-3.0, ( "Oort K of cold dehnendf in a flat rotation curve is not close to expected at R=2" ) return None # Tests for cold population, power-law rotation curve: K def test_dehnendf_cold_powerrise_oortK(): # Rising rotation curve beta = 0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.oortK(1.0)) < 10.0**-3.0, ( "Oort K of cold dehnendf in a power-law rotation curve is not close to expected at R=1" ) assert numpy.fabs(df.oortK(0.5)) < 10.0**-3.0, ( "Oort K of cold dehnendf in a power-law rotation curve is not close to expected at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.oortK(2.0)) < 10.0**-3.0, ( "Oort K of cold dehnendf in a power-law rotation curve is not close to expected at R=2" ) return None def test_dehnendf_cold_powerfall_oortK(): # Falling rotation curve beta = -0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.oortK(1.0)) < 10.0**-3.0, ( "Oort K of cold dehnendf in a power-law rotation curve is not close to expected at R=1" ) assert numpy.fabs(df.oortK(0.5)) < 10.0**-3.0, ( "Oort K of cold dehnendf in a power-law rotation curve is not close to expected at R=0.5" ) if not _FEWERLONGINTEGRALS: assert numpy.fabs(df.oortK(2.0)) < 10.0**-3.0, ( "Oort K of cold dehnendf in a power-law rotation curve is not close to expected at R=2" ) return None # Tests for cold population, flat rotation curve: sigma_R^2 / sigma_T^2 = kappa^2 / Omega^2 def test_dehnendf_cold_flat_srst(): df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) assert numpy.fabs(df.sigmaR2(1.0) / df.sigmaT2(1.0) - 2.0) < 10.0**-2.0, ( "sigma_R^2 / sigma_T^2 of cool dehnendf in a flat rotation curve is not close to expected at R=1" ) assert numpy.fabs(df.sigmaR2(0.5) / df.sigmaT2(0.5) - 2.0) < 10.0**-2.0, ( "sigma_R^2 / sigma_T^2 of cool dehnendf in a flat rotation curve is not close to expected at R=1" ) assert numpy.fabs(df.sigmaR2(2.0) / df.sigmaT2(2.0) - 2.0) < 10.0**-2.0, ( "sigma_R^2 / sigma_T^2 of cool dehnendf in a flat rotation curve is not close to expected at R=1" ) return None # Tests for cold population, power-law rotation curve: sigma_R^2 / sigma_T^2 = kappa^2 / Omega^2 def test_dehnendf_cold_powerrise_srst(): # Rising rotation curve beta = 0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) assert ( numpy.fabs(df.sigmaR2(1.0) / df.sigmaT2(1.0) - 2.0 / (1.0 + beta)) < 10.0**-2.0 ), ( "sigma_R^2 / sigma_T^2 of cool dehnendf in a flat rotation curve is not close to expected at R=1" ) assert ( numpy.fabs(df.sigmaR2(0.5) / df.sigmaT2(0.5) - 2.0 / (1.0 + beta)) < 10.0**-2.0 ), ( "sigma_R^2 / sigma_T^2 of cool dehnendf in a flat rotation curve is not close to expected at R=1" ) assert ( numpy.fabs(df.sigmaR2(2.0) / df.sigmaT2(2.0) - 2.0 / (1.0 + beta)) < 10.0**-2.0 ), ( "sigma_R^2 / sigma_T^2 of cool dehnendf in a flat rotation curve is not close to expected at R=1" ) return None def test_dehnendf_cold_powerfall_srst(): # Falling rotation curve beta = -0.2 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=beta, correct=False ) assert ( numpy.fabs(df.sigmaR2(1.0) / df.sigmaT2(1.0) - 2.0 / (1.0 + beta)) < 10.0**-2.0 ), ( "sigma_R^2 / sigma_T^2 of cool dehnendf in a flat rotation curve is not close to expected at R=1" ) assert ( numpy.fabs(df.sigmaR2(0.5) / df.sigmaT2(0.5) - 2.0 / (1.0 + beta)) < 10.0**-2.0 ), ( "sigma_R^2 / sigma_T^2 of cool dehnendf in a flat rotation curve is not close to expected at R=1" ) assert ( numpy.fabs(df.sigmaR2(2.0) / df.sigmaT2(2.0) - 2.0 / (1.0 + beta)) < 10.0**-2.0 ), ( "sigma_R^2 / sigma_T^2 of cool dehnendf in a flat rotation curve is not close to expected at R=1" ) return None def test_targetSigma2(): beta = 0.0 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.1), beta=beta, correct=False ) assert numpy.fabs(df.targetSigma2(1.0) - 0.1**2.0) < 10.0**-8.0, ( "targetSigma2 for dehnendf does not agree with input" ) assert ( numpy.fabs(df.targetSigma2(0.3) - 0.1**2.0 * numpy.exp(-(0.3 - 1.0) / 0.5)) < 10.0**-8.0 ), "targetSigma2 for dehnendf does not agree with input" assert ( numpy.fabs( df.targetSigma2(3.0, log=True) - numpy.log(0.1) * 2.0 + (3.0 - 1.0) / 0.5 ) < 10.0**-8.0 ), "targetSigma2 for dehnendf does not agree with input" return None def test_targetSurfacemass(): beta = 0.0 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.1), beta=beta, correct=False ) assert ( numpy.fabs(df.targetSurfacemass(1.0) - numpy.exp(-1.0 / 0.3333333333333333)) < 10.0**-8.0 ), "targetSigma2 for dehnendf does not agree with input" assert ( numpy.fabs(df.targetSurfacemass(0.3) - numpy.exp(-0.3 / 0.3333333333333333)) < 10.0**-8.0 ), "targetSigma2 for dehnendf does not agree with input" assert ( numpy.fabs(df.targetSurfacemass(3.0, log=True) + 3.0 / 0.3333333333333333) < 10.0**-8.0 ), "targetSigma2 for dehnendf does not agree with input" return None def test_targetSurfacemassLOS(): beta = 0.0 df = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.1), beta=beta, correct=False ) # Some easy directions in l assert ( numpy.fabs( df.targetSurfacemassLOS(0.2, l=0.0, deg=True) - 0.2 * numpy.exp(-0.8 / 0.3333333333333333) ) < 10.0**-8.0 ), "targetSigma2 for dehnendf does not agree with input" assert ( numpy.fabs( df.targetSurfacemassLOS(0.2, l=180.0, deg=True) - 0.2 * numpy.exp(-1.2 / 0.3333333333333333) ) < 10.0**-8.0 ), "targetSigma2 for dehnendf does not agree with input" assert ( numpy.fabs( df.targetSurfacemassLOS(0.2, l=numpy.pi, deg=False) - 0.2 * numpy.exp(-1.2 / 0.3333333333333333) ) < 10.0**-8.0 ), "targetSigma2 for dehnendf does not agree with input" assert ( numpy.fabs( df.targetSurfacemassLOS(0.2, l=numpy.pi / 2.0, log=True, deg=False) - numpy.log(0.2) + numpy.sqrt(1.0 + 0.2**2.0 - 2.0 * 0.2 * numpy.cos(numpy.pi / 2.0)) / 0.3333333333333333 ) < 10.0**-8.0 ), "targetSigma2 for dehnendf does not agree with input" return None def test_cold_surfacemass(): dfc = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) assert ( numpy.fabs( numpy.log(dfc.surfacemass(0.9)) - numpy.log(dfc.targetSurfacemass(0.9)) ) < 0.01 ), ( "True surfacemass deviates more from target surfacemass for cold Dehnen DF than expected" ) assert ( numpy.fabs( numpy.log(dfc.surfacemass(0.5)) - numpy.log(dfc.targetSurfacemass(0.5)) ) < 0.01 ), ( "True surfacemass deviates more from target surfacemass for cold Dehnen DF than expected" ) assert ( numpy.fabs( numpy.log(dfc.surfacemass(2.0)) - numpy.log(dfc.targetSurfacemass(2.0)) ) < 0.01 ), ( "True surfacemass deviates more from target surfacemass for cold Dehnen DF than expected" ) return None def test_surfacemass(): dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) assert ( numpy.fabs( numpy.log(dfc.surfacemass(0.9)) - numpy.log(dfc.targetSurfacemass(0.9)) ) < 0.05 ), ( "True surfacemass deviates more from target surfacemass for Dehnen DF with documentation-example parameters than expected" ) assert ( numpy.fabs( numpy.log(dfc.surfacemass(0.05)) - numpy.log(dfc.targetSurfacemass(0.05)) ) < 0.5 ), ( "True surfacemass deviates more from target surfacemass for Dehnen DF with documentation-example parameters than expected" ) assert ( numpy.fabs(numpy.log(dfc.surfacemass(4.0, romberg=True, relative=True))) < 0.05 ), ( "True surfacemass deviates more from target surfacemass for Dehnen DF with documentation-example parameters than expected" ) return None def test_cold_sigma2surfacemass(): dfc = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) assert ( numpy.fabs( numpy.log(dfc.sigma2surfacemass(0.9)) - numpy.log(dfc.targetSigma2(0.9) * dfc.targetSurfacemass(0.9)) ) < 0.01 ), ( "True surfacemass deviates more from target surfacemass for cold Dehnen DF than expected" ) assert ( numpy.fabs( numpy.log(dfc.sigma2surfacemass(0.5)) - numpy.log(dfc.targetSigma2(0.5) * dfc.targetSurfacemass(0.5)) ) < 0.01 ), ( "True surfacemass deviates more from target surfacemass for cold Dehnen DF than expected" ) assert ( numpy.fabs( numpy.log(dfc.sigma2surfacemass(2.0)) - numpy.log(dfc.targetSigma2(2.0) * dfc.targetSurfacemass(2.0)) ) < 0.01 ), ( "True surfacemass deviates more from target surfacemass for cold Dehnen DF than expected" ) return None def test_sigma2surfacemass(): dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) assert ( numpy.fabs( numpy.log(dfc.sigma2surfacemass(0.9)) - numpy.log(dfc.targetSigma2(0.9) * dfc.targetSurfacemass(0.9)) ) < 0.05 ), ( "True surfacemass deviates more from target surfacemass for Dehnen DF with documentation-example parameters than expected" ) assert ( numpy.fabs( numpy.log(dfc.sigma2surfacemass(0.3)) - numpy.log(dfc.targetSigma2(0.3) * dfc.targetSurfacemass(0.3)) ) < 0.2 ), ( "True surfacemass deviates more from target surfacemass for Dehnen DF with documentation-example parameters than expected" ) assert ( numpy.fabs(numpy.log(dfc.sigma2surfacemass(3.0, relative=True, romberg=True))) < 0.1 ), ( "True surfacemass deviates more from target surfacemass for Dehnen DF with documentation-example parameters than expected" ) return None def test_vmomentsurfacemass(): # Test that vmomentsurfacemass gives reasonable results dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.4)) assert ( numpy.fabs(dfc.vmomentsurfacemass(0.9, 0.0, 0.0) - dfc.surfacemass(0.9)) < 10.0**-8.0 ), "vmomentsurfacemass with (n,m) = (0,0) is not equal to surfacemass" assert ( numpy.fabs( dfc.vmomentsurfacemass(0.9, 0.0, 0.0, relative=True) - dfc.surfacemass(0.9) / dfc.targetSurfacemass(0.9) ) < 10.0**-8.0 ), ( "vmomentsurfacemass with (n,m) = (0,0) and relative=True is not equal to surfacemass/targetSurfacemass" ) assert ( numpy.fabs(dfc.vmomentsurfacemass(0.9, 2.0, 0.0) - dfc.sigma2surfacemass(0.9)) < 10.0**-8.0 ), "vmomentsurfacemass with (n,m) = (2,0) is not equal to sigma2surfacemass" assert ( numpy.fabs(dfc.vmomentsurfacemass(0.9, 1.0, 1.0, romberg=True)) < 10.0**-8.0 ), ( "vmomentsurfacemass with (n,m) = (1.,1.) is not equal to zero (not automatically zero)" ) assert numpy.fabs(dfc.vmomentsurfacemass(0.9, 1, 1)) < 10.0**-8.0, ( "vmomentsurfacemass with (n,m) = (1,1) is not equal to zero" ) return None def test_vmomentsurfacemass_physical(): # Test that vmomentsurfacemass gives correct physical results from galpy.util import conversion dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) ro, vo = 7.0, 230.0 assert ( numpy.fabs( dfc.vmomentsurfacemass(0.9, 0.0, 0.0, use_physical=True, ro=ro, vo=vo) - dfc.vmomentsurfacemass(0.9, 0.0, 0.0) * conversion.surfdens_in_msolpc2(vo, ro) ) < 10.0**-8.0 ), "vmomentsurfacemass with (n,m) = (0,0) is not equal to surfacemass" assert ( numpy.fabs( dfc.vmomentsurfacemass(0.9, 1.0, 0.0, use_physical=True, ro=ro, vo=vo) - dfc.vmomentsurfacemass(0.9, 1.0, 0.0) * vo * conversion.surfdens_in_msolpc2(vo, ro) ) < 10.0**-8.0 ), "vmomentsurfacemass with (n,m) = (0,0) is not equal to surfacemass" assert ( numpy.fabs( dfc.vmomentsurfacemass(0.9, 1.0, 2.0, use_physical=True, ro=ro, vo=vo) - dfc.vmomentsurfacemass(0.9, 1.0, 2.0) * vo**3.0 * conversion.surfdens_in_msolpc2(vo, ro) ) < 10.0**-8.0 ), "vmomentsurfacemass with (n,m) = (0,0) is not equal to surfacemass" return None def test_cold_surfacemassLOS(): dfc = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.01), beta=0.0, correct=False ) assert ( numpy.fabs( numpy.log(dfc.surfacemassLOS(0.1, 0.0, target=False)) - numpy.log(0.1 * dfc.targetSurfacemass(0.9)) ) < 0.01 ), ( "True surfacemassLOS deviates more from target surfacemassLOS for cold Dehnen DF than expected" ) assert ( numpy.fabs( numpy.log( dfc.surfacemassLOS( numpy.cos(numpy.pi / 6.0), numpy.pi / 6.0, target=False, deg=False ) ) - numpy.log( numpy.cos(numpy.pi / 6.0) * dfc.targetSurfacemass(numpy.cos(numpy.pi / 3.0)) ) ) < 0.01 ), ( "True surfacemassLOS deviates more from target surfacemassLOS for cold Dehnen DF than expected" ) assert ( numpy.fabs( numpy.log( dfc.surfacemassLOS( numpy.cos(numpy.pi / 3.0), numpy.pi / 3.0, deg=False, target=True ) ) - numpy.log( numpy.cos(numpy.pi / 3.0) * dfc.targetSurfacemass(numpy.cos(numpy.pi / 6.0)) ) ) < 0.01 ), ( "True surfacemassLOS deviates more from target surfacemassLOS for cold Dehnen DF than expected" ) assert ( numpy.fabs( numpy.log( dfc.surfacemassLOS( numpy.cos(numpy.pi / 3.0), numpy.pi / 3.0, deg=False, relative=True, target=True, ) ) - numpy.log(numpy.cos(numpy.pi / 3.0)) ) < 0.01 ), ( "True surfacemassLOS deviates more from target surfacemassLOS for cold Dehnen DF than expected" ) return None def test_warm_surfacemassLOS(): dfc = dehnendf( profileParams=(0.3333333333333333, 1.0, 0.1), beta=0.0, correct=False ) assert ( numpy.fabs( numpy.log(dfc.surfacemassLOS(0.1, 0.0, target=False)) - numpy.log(0.1 * dfc.surfacemass(0.9)) ) < 10.0**-6.0 ), "surfacemassLOS deviates more from surfacemass for warm Dehnen DF than expected" assert ( numpy.fabs( numpy.log( dfc.surfacemassLOS( numpy.cos(numpy.pi / 6.0), numpy.pi / 6.0, target=False, deg=False ) ) - numpy.log( numpy.cos(numpy.pi / 6.0) * dfc.surfacemass(numpy.cos(numpy.pi / 3.0)) ) ) < 0.01 ), ( "surfacemassLOS deviates more from target surfacemass for warm Dehnen DF than expected" ) assert ( numpy.fabs( numpy.log( dfc.surfacemassLOS( numpy.cos(numpy.pi / 3.0), numpy.pi / 3.0, deg=False, target=True ) ) - numpy.log( numpy.cos(numpy.pi / 3.0) * dfc.targetSurfacemass(numpy.cos(numpy.pi / 6.0)) ) ) < 0.01 ), ( "surfacemassLOS w/ target deviates more from target surfacemassLOS for warm Dehnen DF than expected" ) assert ( numpy.fabs( numpy.log( dfc.surfacemassLOS( numpy.cos(numpy.pi / 3.0), numpy.pi / 3.0, deg=False, relative=True, target=True, ) ) - numpy.log(numpy.cos(numpy.pi / 3.0)) ) < 0.01 ), ( "surfacemassLOS w/ target deviates more from target surfacemass for warm Dehnen DF than expected" ) return None def test_dehnendf_call_sanity(): # Sanity checking of dehnendf's call function dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) meanvt = dfc.meanvT(0.7) assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, 0.0, meanvt / 2.0]) ), "dehnendf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, 0.0, meanvt * 2.0]) ), "dehnendf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, -0.1, meanvt]) ), "dehnendf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, 0.1, meanvt]) ), "dehnendf does not peak near (vR,vT) = (0,meanvT)" return None def test_shudf_call_sanity_flat(): # Sanity checking of shudf's call function dfc = shudf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) meanvt = dfc.meanvT(0.7) assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, 0.0, meanvt / 2.0]) ), "shudf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, 0.0, meanvt * 2.0]) ), "shudf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, -0.1, meanvt]) ), "shudf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, 0.1, meanvt]) ), "shudf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, -0.1])) == 0.0, ( "shudf not zero for counter-rotating orbits" ) return None def test_shudf_call_sanity_powerfall(): # Sanity checking of shudf's call function dfc = shudf(beta=-0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) meanvt = dfc.meanvT(0.7) assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, 0.0, meanvt / 2.0]) ), "shudf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, 0.0, meanvt * 2.0]) ), "shudf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, -0.1, meanvt]) ), "shudf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, 0.1, meanvt]) ), "shudf does not peak near (vR,vT) = (0,meanvT)" return None def test_shudf_call_sanity_powerrise(): # Sanity checking of shudf's call function dfc = shudf(beta=0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) meanvt = dfc.meanvT(0.7, nsigma=3.0) assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, 0.0, meanvt / 2.0]) ), "shudf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, 0.0, meanvt * 2.0]) ), "shudf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, -0.1, meanvt]) ), "shudf does not peak near (vR,vT) = (0,meanvT)" assert dfc(numpy.array([0.7, 0.0, meanvt])) > dfc( numpy.array([0.7, 0.1, meanvt]) ), "shudf does not peak near (vR,vT) = (0,meanvT)" return None def test_call_diffinputs(): from galpy.orbit import Orbit dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) R, vR, vT, phi = 0.8, 0.4, 1.1, 2.0 to = Orbit([R, vR, vT, phi]) tao = Orbit([R, vR, vT]) # R,vR,vT,phi vs R,vR,vT assert ( numpy.fabs(dfc(numpy.array([R, vR, vT, phi])) - dfc(numpy.array([R, vR, vT]))) < 10.0**-10.0 ), "diskdf __call__ w/ array R,vR,vT,phi neq w/ array R,vR,vT" # orbit vs R,vR,vT assert numpy.fabs(dfc(to) - dfc(numpy.array([R, vR, vT]))) < 10.0**-10.0, ( "diskdf __call__ w/ orbit neq w/ array R,vR,vT" ) # axi orbit vs R,vR,vT assert numpy.fabs(dfc(tao) - dfc(numpy.array([R, vR, vT]))) < 10.0**-10.0, ( "diskdf __call__ w/ axi orbit neq w/ array R,vR,vT" ) # orbit w/ t vs R,vR,vT assert numpy.fabs(dfc(to, 0.0) - dfc(numpy.array([R, vR, vT]))) < 10.0**-10.0, ( "diskdf __call__ w/ orbit and t neq w/ array R,vR,vT" ) # axi orbit w/ t vs R,vR,vT assert numpy.fabs(dfc(tao, 0.0) - dfc(numpy.array([R, vR, vT]))) < 10.0**-10.0, ( "diskdf __call__ w/ axi orbit and t neq w/ array R,vR,vT" ) # list of orbit vs R,vR,vT assert numpy.fabs(dfc([to]) - dfc(numpy.array([R, vR, vT]))) < 10.0**-10.0, ( "diskdf __call__ w/ list of orbit neq w/ array R,vR,vT" ) # E,L vs R,vR,vT assert ( numpy.fabs( dfc(numpy.log(R) + vR**2.0 / 2.0 + vT**2.0 / 2.0, R * vT) - dfc(numpy.array([R, vR, vT])) ) < 10.0**-10.0 ), "diskdf __call__ w/ E,L and t neq w/ array R,vR,vT" return None def test_call_marginalizevperp(): from galpy.orbit import Orbit dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.4)) # l=0 R, vR = 1.8, 0.4 vts = numpy.linspace(0.0, 1.5, 51) pvts = numpy.array([dfc(numpy.array([R, vR, vt])) for vt in vts]) assert ( numpy.fabs( numpy.sum(pvts) * (vts[1] - vts[0]) - dfc(Orbit([R, vR, 0.0, 0.0]), marginalizeVperp=True) ) < 10.0**-4.0 ), "diskdf call w/ marginalizeVperp does not work" # Another l=0, where va > sigmaR1 R, vR = 1.25, 0.4 vts = numpy.linspace(0.0, 1.5, 51) pvts = numpy.array([dfc(numpy.array([R, vR, vt])) for vt in vts]) assert ( numpy.fabs( numpy.sum(pvts) * (vts[1] - vts[0]) - dfc(Orbit([R, vR, 0.0, 0.0]), marginalizeVperp=True) ) < 10.0**-4.0 ), "diskdf call w/ marginalizeVperp does not work" # l=270 R, vT = numpy.sin(numpy.pi / 6.0), 0.7 # l=30 degree vrs = numpy.linspace(-2.0, 2.0, 101) pvrs = numpy.array([dfc(numpy.array([R, vr, vT])) for vr in vrs]) assert ( numpy.fabs( numpy.sum(pvrs) * (vrs[1] - vrs[0]) - dfc(Orbit([R, 0.0, vT, -numpy.pi / 3.0]), marginalizeVperp=True, nsigma=4) ) < 10.0**-4.0 ), "diskdf call w/ marginalizeVperp does not work" return None def test_call_marginalizevlos(): from galpy.orbit import Orbit dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.4)) # l=0 R, vT = 0.8, 0.7 vrs = numpy.linspace(-2.0, 2.0, 101) pvrs = numpy.array([dfc(numpy.array([R, vr, vT])) for vr in vrs]) assert ( numpy.fabs( numpy.sum(pvrs) * (vrs[1] - vrs[0]) - dfc(Orbit([R, 0.0, vT, 0.0]), marginalizeVlos=True) ) < 10.0**-4.0 ), "diskdf call w/ marginalizeVlos does not work" # l=270 R, vR = numpy.sin(numpy.pi / 6.0), 0.4 # l=30 degree vts = numpy.linspace(-2.5, 2.5, 101) pvts = numpy.array([dfc(numpy.array([R, vR, vt])) for vt in vts]) assert ( numpy.fabs( numpy.sum(pvts) * (vts[1] - vts[0]) - dfc(Orbit([R, vR, 0.0, -numpy.pi / 3.0]), marginalizeVlos=True, nsigma=4) ) < 10.0**-4.0 ), "diskdf call w/ marginalizeVlos does not work" return None def test_dehnendf_dlnfdR_flat(): dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) dR = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 Rn = R + dR dR = Rn - R # representable number dlnf = ( numpy.log(dfc(numpy.array([R + dR, vR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dR assert numpy.fabs(dlnf - dfc._dlnfdR(R, vR, vT)) < 10.0**-6.0, ( "dehnendf's dlnfdR does not work" ) return None def test_dehnendf_dlnfdR_powerfall(): dfc = dehnendf(beta=-0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) dR = 10**-6.0 R, vR, vT = 0.8, 0.1, 0.9 Rn = R + dR dR = Rn - R # representable number dlnf = ( numpy.log(dfc(numpy.array([R + dR, vR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dR assert numpy.fabs(dlnf - dfc._dlnfdR(R, vR, vT)) < 10.0**-6.0, ( "dehnendf's dlnfdR does not work" ) return None def test_dehnendf_dlnfdR_powerrise(): dfc = dehnendf(beta=0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) dR = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 Rn = R + dR dR = Rn - R # representable number dlnf = ( numpy.log(dfc(numpy.array([R + dR, vR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dR assert numpy.fabs(dlnf - dfc._dlnfdR(R, vR, vT)) < 10.0**-6.0, ( "dehnendf's dlnfdR does not work" ) return None def test_dehnendf_dlnfdvR_flat(): dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) dvR = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 vRn = vR + dvR dvR = vRn - vR # representable number dlnf = ( numpy.log(dfc(numpy.array([R, vR + dvR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvR assert numpy.fabs(dlnf - dfc._dlnfdvR(R, vR, vT)) < 10.0**-6.0, ( "dehnendf's dlnfdvR does not work" ) return None def test_dehnendf_dlnfdvR_powerfall(): dfc = dehnendf(beta=-0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) dvR = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 vRn = vR + dvR dvR = vRn - vR # representable number dlnf = ( numpy.log(dfc(numpy.array([R, vR + dvR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvR assert numpy.fabs(dlnf - dfc._dlnfdvR(R, vR, vT)) < 10.0**-6.0, ( "dehnendf's dlnfdvR does not work" ) return None def test_dehnendf_dlnfdvR_powerrise(): dfc = dehnendf(beta=0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) dvR = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 vRn = vR + dvR dvR = vRn - vR # representable number dlnf = ( numpy.log(dfc(numpy.array([R, vR + dvR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvR assert numpy.fabs(dlnf - dfc._dlnfdvR(R, vR, vT)) < 10.0**-6.0, ( "dehnendf's dlnfdvR does not work" ) return None def test_dehnendf_dlnfdvT_flat(): dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) dvT = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 vTn = vT + dvT dvT = vTn - vT # representable number dlnf = ( numpy.log(dfc(numpy.array([R, vR, vT + dvT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvT assert numpy.fabs(dlnf - dfc._dlnfdvT(R, vR, vT)) < 10.0**-6.0, ( "dehnendf's dlnfdvT does not work" ) return None def test_dehnendf_dlnfdvT_powerfall(): dfc = dehnendf(beta=-0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) dvT = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 vTn = vT + dvT dvT = vTn - vT # representable number dlnf = ( numpy.log(dfc(numpy.array([R, vR, vT + dvT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvT assert numpy.fabs(dlnf - dfc._dlnfdvT(R, vR, vT)) < 10.0**-6.0, ( "dehnendf's dlnfdvT does not work" ) return None def test_dehnendf_dlnfdvT_powerrise(): dfc = dehnendf(beta=0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) dvT = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 vTn = vT + dvT dvT = vTn - vT # representable number dlnf = ( numpy.log(dfc(numpy.array([R, vR, vT + dvT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvT assert numpy.fabs(dlnf - dfc._dlnfdvT(R, vR, vT)) < 10.0**-6.0, ( "dehnendf's dlnfdvT does not work" ) return None def test_dehnendf_dlnfdRe_flat(): dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Calculate dlndfdRe w/ the chain rule; first calculate dR dR = 10**-6.0 R, vR, vT = 0.8, 0.1, 0.9 Rn = R + dR dR = Rn - R # representable number dlnfdR = ( numpy.log(dfc(numpy.array([R + dR, vR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dR E = vR**2.0 / 2.0 + vT**2.0 / 2.0 + numpy.log(R) RE = numpy.exp(E - 0.5) dE = vR**2.0 / 2.0 + vT**2.0 / 2.0 + numpy.log(R + dR) dRE = numpy.exp(dE - 0.5) dRedR = (dRE - RE) / dR # dvR dvR = 10**-6.0 vRn = vR + dvR dvR = vRn - vR # representable number dlnfdvR = ( numpy.log(dfc(numpy.array([R, vR + dvR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvR dE = (vR + dvR) ** 2.0 / 2.0 + vT**2.0 / 2.0 + numpy.log(R) dRE = numpy.exp(dE - 0.5) dRedvR = (dRE - RE) / dvR # dvT dvT = 10**-6.0 vTn = vT + dvT dvT = vTn - vT # representable number dlnfdvT = ( numpy.log(dfc(numpy.array([R, vR, vT + dvT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvT dE = vR**2.0 / 2.0 + (vT + dvT) ** 2.0 / 2.0 + numpy.log(R) dRE = numpy.exp(dE - 0.5) dRedvT = (dRE - RE) / dvT # Calculate dR/dRe etc. from matrix inversion dRvRvTdRe = numpy.linalg.inv( numpy.array([[dRedR, dRedvR, dRedvT], [vT, 0.0, R], [0.0, 1.0, 0.0]]) ) dlnf = ( dlnfdR * dRvRvTdRe[0, 0] + dlnfdvR * dRvRvTdRe[1, 0] + dlnfdvT * dRvRvTdRe[2, 0] ) assert numpy.fabs(dlnf - dfc._dlnfdRe(R, vR, vT)) < 10.0**-5.0, ( "dehnendf's dlnfdRe does not work" ) return None def test_dehnendf_dlnfdRe_powerfall(): beta = -0.2 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Calculate dlndfdRe w/ the chain rule; first calculate dR dR = 10**-6.0 R, vR, vT = 0.8, 0.1, 0.9 Rn = R + dR dR = Rn - R # representable number dlnfdR = ( numpy.log(dfc(numpy.array([R + dR, vR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dR E = vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / beta * R ** (2.0 * beta) RE = (2.0 * E / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dE = vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / beta * (R + dR) ** (2.0 * beta) dRE = (2.0 * dE / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dRedR = (dRE - RE) / dR # dvR dvR = 10**-6.0 vRn = vR + dvR dvR = vRn - vR # representable number dlnfdvR = ( numpy.log(dfc(numpy.array([R, vR + dvR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvR dE = (vR + dvR) ** 2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / beta * R ** (2.0 * beta) dRE = (2.0 * dE / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dRedvR = (dRE - RE) / dvR # dvT dvT = 10**-6.0 vTn = vT + dvT dvT = vTn - vT # representable number dlnfdvT = ( numpy.log(dfc(numpy.array([R, vR, vT + dvT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvT dE = vR**2.0 / 2.0 + (vT + dvT) ** 2.0 / 2.0 + 1.0 / 2.0 / beta * R ** (2.0 * beta) dRE = (2.0 * dE / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dRedvT = (dRE - RE) / dvT # Calculate dR/dRe etc. from matrix inversion dRvRvTdRe = numpy.linalg.inv( numpy.array([[dRedR, dRedvR, dRedvT], [vT, 0.0, R], [0.0, 1.0, 0.0]]) ) dlnf = ( dlnfdR * dRvRvTdRe[0, 0] + dlnfdvR * dRvRvTdRe[1, 0] + dlnfdvT * dRvRvTdRe[2, 0] ) assert numpy.fabs(dlnf - dfc._dlnfdRe(R, vR, vT)) < 10.0**-5.0, ( "dehnendf's dlnfdRe does not work" ) return None def test_dehnendf_dlnfdRe_powerrise(): beta = 0.2 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Calculate dlndfdRe w/ the chain rule; first calculate dR dR = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 Rn = R + dR dR = Rn - R # representable number dlnfdR = ( numpy.log(dfc(numpy.array([R + dR, vR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dR E = vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / beta * R ** (2.0 * beta) RE = (2.0 * E / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dE = vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / beta * (R + dR) ** (2.0 * beta) dRE = (2.0 * dE / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dRedR = (dRE - RE) / dR # dvR dvR = 10**-8.0 vRn = vR + dvR dvR = vRn - vR # representable number dlnfdvR = ( numpy.log(dfc(numpy.array([R, vR + dvR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvR dE = (vR + dvR) ** 2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / beta * R ** (2.0 * beta) dRE = (2.0 * dE / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dRedvR = (dRE - RE) / dvR # dvT dvT = 10**-8.0 vTn = vT + dvT dvT = vTn - vT # representable number dlnfdvT = ( numpy.log(dfc(numpy.array([R, vR, vT + dvT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvT dE = vR**2.0 / 2.0 + (vT + dvT) ** 2.0 / 2.0 + 1.0 / 2.0 / beta * R ** (2.0 * beta) dRE = (2.0 * dE / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dRedvT = (dRE - RE) / dvT # Calculate dR/dRe etc. from matrix inversion dRvRvTdRe = numpy.linalg.inv( numpy.array([[dRedR, dRedvR, dRedvT], [vT, 0.0, R], [0.0, 1.0, 0.0]]) ) dlnf = ( dlnfdR * dRvRvTdRe[0, 0] + dlnfdvR * dRvRvTdRe[1, 0] + dlnfdvT * dRvRvTdRe[2, 0] ) assert numpy.fabs(dlnf - dfc._dlnfdRe(R, vR, vT)) < 10.0**-5.0, ( "dehnendf's dlnfdRe does not work" ) return None def test_dehnendf_dlnfdl_flat(): dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Calculate dlndfdl w/ the chain rule; first calculate dR dR = 10**-6.0 R, vR, vT = 0.8, 0.1, 0.9 Rn = R + dR dR = Rn - R # representable number dlnfdR = ( numpy.log(dfc(numpy.array([R + dR, vR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dR E = vR**2.0 / 2.0 + vT**2.0 / 2.0 + numpy.log(R) RE = numpy.exp(E - 0.5) dE = vR**2.0 / 2.0 + vT**2.0 / 2.0 + numpy.log(R + dR) dRE = numpy.exp(dE - 0.5) dRedR = (dRE - RE) / dR # dvR dvR = 10**-6.0 vRn = vR + dvR dvR = vRn - vR # representable number dlnfdvR = ( numpy.log(dfc(numpy.array([R, vR + dvR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvR dE = (vR + dvR) ** 2.0 / 2.0 + vT**2.0 / 2.0 + numpy.log(R) dRE = numpy.exp(dE - 0.5) dRedvR = (dRE - RE) / dvR # dvT dvT = 10**-6.0 vTn = vT + dvT dvT = vTn - vT # representable number dlnfdvT = ( numpy.log(dfc(numpy.array([R, vR, vT + dvT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvT dE = vR**2.0 / 2.0 + (vT + dvT) ** 2.0 / 2.0 + numpy.log(R) dRE = numpy.exp(dE - 0.5) dRedvT = (dRE - RE) / dvT # Calculate dR/dl etc. from matrix inversion dRvRvTdl = numpy.linalg.inv( numpy.array([[dRedR, dRedvR, dRedvT], [vT, 0.0, R], [0.0, 1.0, 0.0]]) ) dlnf = dlnfdR * dRvRvTdl[0, 1] + dlnfdvR * dRvRvTdl[1, 1] + dlnfdvT * dRvRvTdl[2, 1] assert numpy.fabs(dlnf - dfc._dlnfdl(R, vR, vT)) < 10.0**-5.0, ( "dehnendf's dlnfdl does not work" ) return None def test_dehnendf_dlnfdl_powerfall(): beta = -0.2 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Calculate dlndfdl w/ the chain rule; first calculate dR dR = 10**-6.0 R, vR, vT = 0.8, 0.1, 0.9 Rn = R + dR dR = Rn - R # representable number dlnfdR = ( numpy.log(dfc(numpy.array([R + dR, vR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dR E = vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / beta * R ** (2.0 * beta) RE = (2.0 * E / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dE = vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / beta * (R + dR) ** (2.0 * beta) dRE = (2.0 * dE / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dRedR = (dRE - RE) / dR # dvR dvR = 10**-6.0 vRn = vR + dvR dvR = vRn - vR # representable number dlnfdvR = ( numpy.log(dfc(numpy.array([R, vR + dvR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvR dE = (vR + dvR) ** 2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / beta * R ** (2.0 * beta) dRE = (2.0 * dE / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dRedvR = (dRE - RE) / dvR # dvT dvT = 10**-6.0 vTn = vT + dvT dvT = vTn - vT # representable number dlnfdvT = ( numpy.log(dfc(numpy.array([R, vR, vT + dvT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvT dE = vR**2.0 / 2.0 + (vT + dvT) ** 2.0 / 2.0 + 1.0 / 2.0 / beta * R ** (2.0 * beta) dRE = (2.0 * dE / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dRedvT = (dRE - RE) / dvT # Calculate dR/dl etc. from matrix inversion dRvRvTdl = numpy.linalg.inv( numpy.array([[dRedR, dRedvR, dRedvT], [vT, 0.0, R], [0.0, 1.0, 0.0]]) ) dlnf = dlnfdR * dRvRvTdl[0, 1] + dlnfdvR * dRvRvTdl[1, 1] + dlnfdvT * dRvRvTdl[2, 1] assert numpy.fabs(dlnf - dfc._dlnfdl(R, vR, vT)) < 10.0**-5.0, ( "dehnendf's dlnfdl does not work" ) return None def test_dehnendf_dlnfdl_powerrise(): beta = 0.2 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Calculate dlndfdl w/ the chain rule; first calculate dR dR = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 Rn = R + dR dR = Rn - R # representable number dlnfdR = ( numpy.log(dfc(numpy.array([R + dR, vR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dR E = vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / beta * R ** (2.0 * beta) RE = (2.0 * E / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dE = vR**2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / beta * (R + dR) ** (2.0 * beta) dRE = (2.0 * dE / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dRedR = (dRE - RE) / dR # dvR dvR = 10**-8.0 vRn = vR + dvR dvR = vRn - vR # representable number dlnfdvR = ( numpy.log(dfc(numpy.array([R, vR + dvR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvR dE = (vR + dvR) ** 2.0 / 2.0 + vT**2.0 / 2.0 + 1.0 / 2.0 / beta * R ** (2.0 * beta) dRE = (2.0 * dE / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dRedvR = (dRE - RE) / dvR # dvT dvT = 10**-8.0 vTn = vT + dvT dvT = vTn - vT # representable number dlnfdvT = ( numpy.log(dfc(numpy.array([R, vR, vT + dvT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvT dE = vR**2.0 / 2.0 + (vT + dvT) ** 2.0 / 2.0 + 1.0 / 2.0 / beta * R ** (2.0 * beta) dRE = (2.0 * dE / (1.0 + 1.0 / beta)) ** (1.0 / 2.0 / beta) dRedvT = (dRE - RE) / dvT # Calculate dR/dl etc. from matrix inversion dRvRvTdl = numpy.linalg.inv( numpy.array([[dRedR, dRedvR, dRedvT], [vT, 0.0, R], [0.0, 1.0, 0.0]]) ) dlnf = dlnfdR * dRvRvTdl[0, 1] + dlnfdvR * dRvRvTdl[1, 1] + dlnfdvT * dRvRvTdl[2, 1] assert numpy.fabs(dlnf - dfc._dlnfdl(R, vR, vT)) < 10.0**-5.0, ( "dehnendf's dlnfdl does not work" ) return None def test_shudf_dlnfdR_flat(): dfc = shudf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) dR = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 Rn = R + dR dR = Rn - R # representable number dlnf = ( numpy.log(dfc(numpy.array([R + dR, vR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dR assert numpy.fabs(dlnf - dfc._dlnfdR(R, vR, vT)) < 10.0**-6.0, ( "shudf's dlnfdR does not work" ) return None def test_shudf_dlnfdR_powerfall(): dfc = shudf(beta=-0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) dR = 10**-6.0 R, vR, vT = 0.8, 0.1, 0.9 Rn = R + dR dR = Rn - R # representable number # print((dfc._dlnfdR(R+dR,vR,vT)-dfc._dlnfdR(R,vR,vT))/dR) dlnf = ( numpy.log(dfc(numpy.array([R + dR, vR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dR assert numpy.fabs(dlnf - dfc._dlnfdR(R, vR, vT)) < 10.0**-6.0, ( "shudf's dlnfdR does not work" ) return None def test_shudf_dlnfdR_powerrise(): dfc = shudf(beta=0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) dR = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 Rn = R + dR dR = Rn - R # representable number dlnf = ( numpy.log(dfc(numpy.array([R + dR, vR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dR assert numpy.fabs(dlnf - dfc._dlnfdR(R, vR, vT)) < 10.0**-6.0, ( "shudf's dlnfdR does not work" ) return None def test_shudf_dlnfdvR_flat(): dfc = shudf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) dvR = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 vRn = vR + dvR dvR = vRn - vR # representable number dlnf = ( numpy.log(dfc(numpy.array([R, vR + dvR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvR assert numpy.fabs(dlnf - dfc._dlnfdvR(R, vR, vT)) < 10.0**-6.0, ( "shudf's dlnfdvR does not work" ) return None def test_shudf_dlnfdvR_powerfall(): dfc = shudf(beta=-0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) dvR = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 vRn = vR + dvR dvR = vRn - vR # representable number dlnf = ( numpy.log(dfc(numpy.array([R, vR + dvR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvR assert numpy.fabs(dlnf - dfc._dlnfdvR(R, vR, vT)) < 10.0**-6.0, ( "shudf's dlnfdvR does not work" ) return None def test_shudf_dlnfdvR_powerrise(): dfc = shudf(beta=0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) dvR = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 vRn = vR + dvR dvR = vRn - vR # representable number dlnf = ( numpy.log(dfc(numpy.array([R, vR + dvR, vT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvR assert numpy.fabs(dlnf - dfc._dlnfdvR(R, vR, vT)) < 10.0**-6.0, ( "shudf's dlnfdvR does not work" ) return None def test_shudf_dlnfdvT_flat(): dfc = shudf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2)) dvT = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 vTn = vT + dvT dvT = vTn - vT # representable number dlnf = ( numpy.log(dfc(numpy.array([R, vR, vT + dvT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvT assert numpy.fabs(dlnf - dfc._dlnfdvT(R, vR, vT)) < 10.0**-6.0, ( "shudf's dlnfdvT does not work" ) return None def test_shudf_dlnfdvT_powerfall(): dfc = shudf(beta=-0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) dvT = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 vTn = vT + dvT dvT = vTn - vT # representable number dlnf = ( numpy.log(dfc(numpy.array([R, vR, vT + dvT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvT assert numpy.fabs(dlnf - dfc._dlnfdvT(R, vR, vT)) < 10.0**-6.0, ( "shudf's dlnfdvT does not work" ) return None def test_shudf_dlnfdvT_powerrise(): dfc = shudf(beta=0.2, profileParams=(1.0 / 4.0, 1.0, 0.2)) dvT = 10**-8.0 R, vR, vT = 0.8, 0.1, 0.9 vTn = vT + dvT dvT = vTn - vT # representable number dlnf = ( numpy.log(dfc(numpy.array([R, vR, vT + dvT]))) - numpy.log(dfc(numpy.array([R, vR, vT]))) ) / dvT assert numpy.fabs(dlnf - dfc._dlnfdvT(R, vR, vT)) < 10.0**-6.0, ( "shudf's dlnfdvT does not work" ) return None def test_estimatemeanvR(): beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) vrp8 = dfc.meanvR(0.8) assert numpy.fabs(dfc._estimatemeanvR(0.8) - vrp8) < 0.02, ( "_estimatemeanvR does not agree with meanvR to the expected level" ) return None def test_asymmetricdrift_flat(): beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) vtp8 = dfc.meanvT(0.8) assert numpy.fabs(dfc.asymmetricdrift(0.8) - 1.0 + vtp8) < 0.02, ( "asymmetricdrift does not agree with meanvT for flat rotation curve to the expected level" ) assert numpy.fabs(dfc.asymmetricdrift(1.2) - 1.0 + dfc.meanvT(1.2)) < 0.02, ( "asymmetricdrift does not agree with meanvT for flat rotation curve to the expected level" ) # also test _estimatemeanvT assert numpy.fabs(dfc._estimatemeanvT(0.8) - vtp8) < 0.02, ( "_estimatemeanvT does not agree with meanvT for flat rotation curve to the expected level" ) return None def test_asymmetricdrift_powerfall(): beta = -0.2 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) assert numpy.fabs(dfc.asymmetricdrift(0.8) - 0.8**beta + dfc.meanvT(0.8)) < 0.02, ( "asymmetricdrift does not agree with meanvT for flat rotation curve to the expected level" ) assert numpy.fabs(dfc.asymmetricdrift(1.2) - 1.2**beta + dfc.meanvT(1.2)) < 0.02, ( "asymmetricdrift does not agree with meanvT for flat rotation curve to the expected level" ) return None def test_asymmetricdrift_powerrise(): beta = 0.2 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) assert numpy.fabs(dfc.asymmetricdrift(0.8) - 0.8**beta + dfc.meanvT(0.8)) < 0.02, ( "asymmetricdrift does not agree with meanvT for flat rotation curve to the expected level" ) assert numpy.fabs(dfc.asymmetricdrift(1.2) - 1.2**beta + dfc.meanvT(1.2)) < 0.02, ( "asymmetricdrift does not agree with meanvT for flat rotation curve to the expected level" ) return None def test_estimateSigmaR2(): beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) assert numpy.fabs(dfc._estimateSigmaR2(0.8) / dfc.targetSigma2(0.8) - 1.0) < 0.02, ( "_estimateSigmaR2 does not agree with targetSigma2 to the expected level" ) return None def test_estimateSigmaT2(): beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.02)) assert ( numpy.fabs(dfc._estimateSigmaT2(0.8) / dfc.targetSigma2(0.8) * 2.0 - 1.0) < 0.02 ), "_estimateSigmaT2 does not agree with targetSigma2 to the expected level" assert ( numpy.fabs( dfc._estimateSigmaT2(0.8, log=True) - numpy.log(dfc.targetSigma2(0.8)) + numpy.log(2.0) ) < 0.02 ), "_estimateSigmaT2 does not agree with targetSigma2 to the expected level" return None def test_vmomentsurfacedensity_deriv(): # Quick test that the phi derivative is zero beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.02)) assert numpy.fabs(dfc.vmomentsurfacemass(0.9, 0, 0, deriv="phi")) < 10.0**-6.0, ( "surfacemass phi derivative is not zero" ) return None def test_ELtowRRapRperi_flat(): beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) Rc = 0.8 Lc = Rc Ec = numpy.log(Rc) + Lc**2.0 / 2.0 / Rc**2.0 + 0.01**2.0 / 2.0 wr, rap, rperi = dfc._ELtowRRapRperi(Ec, Lc) assert numpy.fabs(wr - numpy.sqrt(2.0) / Rc) < 10.0**-3.0, ( "diskdf's _ELtowRRapRperi's radial frequency for close to circular orbit is wrong" ) return None def test_ELtowRRapRperi_powerfall(): beta = -0.2 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) Rc = 0.8 Lc = Rc * Rc**beta Ec = ( 1.0 / 2.0 / beta * Rc ** (2.0 * beta) + Lc**2.0 / 2.0 / Rc**2.0 + 0.01**2.0 / 2.0 ) gamma = numpy.sqrt(2.0 / (1.0 + beta)) wr, rap, rperi = dfc._ELtowRRapRperi(Ec, Lc) assert numpy.fabs(wr - 2.0 * Rc ** (beta - 1.0) / gamma) < 10.0**-3.0, ( "diskdf's _ELtowRRapRperi's radial frequency for close to circular orbit is wrong" ) return None def test_ELtowRRapRperi_powerrise(): beta = 0.2 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) Rc = 0.8 Lc = Rc * Rc**beta Ec = ( 1.0 / 2.0 / beta * Rc ** (2.0 * beta) + Lc**2.0 / 2.0 / Rc**2.0 + 0.01**2.0 / 2.0 ) gamma = numpy.sqrt(2.0 / (1.0 + beta)) wr, rap, rperi = dfc._ELtowRRapRperi(Ec, Lc) assert numpy.fabs(wr - 2.0 * Rc ** (beta - 1.0) / gamma) < 10.0**-3.0, ( "diskdf's _ELtowRRapRperi's radial frequency for close to circular orbit is wrong" ) return None def test_sampledSurfacemassLOS_target(): numpy.random.seed(1) beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Sample a large number of points, then check some moments against the analytic distribution ds = dfc.sampledSurfacemassLOS(numpy.pi / 4.0, n=10000, target=True) xds = numpy.linspace(0.001, 4.0, 201) pds = numpy.array([dfc.surfacemassLOS(d, 45.0, deg=True, target=True) for d in xds]) md = numpy.sum(xds * pds) / numpy.sum(pds) sd = numpy.sqrt(numpy.sum(xds**2.0 * pds) / numpy.sum(pds) - md**2.0) assert numpy.fabs(numpy.mean(ds) - md) < 10.0**-2.0, ( "mean of surfacemassLOS for target surfacemass is not equal to the mean of the samples" ) assert numpy.fabs(numpy.std(ds) - sd) < 10.0**-2.0, ( "stddev of surfacemassLOS for target surfacemass is not equal to the mean of the samples" ) assert numpy.fabs(skew_samples(ds) - skew_pdist(xds, pds)) < 10.0**-1, ( "skew of surfacemassLOS for target surfacemass is not equal to the mean of the samples" ) return None def test_sampledSurfacemassLOS(): numpy.random.seed(1) beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Sample a large number of points, then check some moments against the analytic distribution ds = dfc.sampledSurfacemassLOS(numpy.pi / 4.0, n=10000, target=False) xds = numpy.linspace(0.001, 4.0, 101) # check against target, bc that's easy to calculate pds = numpy.array([dfc.surfacemassLOS(d, 45.0, deg=True, target=True) for d in xds]) md = numpy.sum(xds * pds) / numpy.sum(pds) sd = numpy.sqrt(numpy.sum(xds**2.0 * pds) / numpy.sum(pds) - md**2.0) assert numpy.fabs(numpy.mean(ds) - md) < 10.0**-2.0, ( "mean of surfacemassLOS surfacemass is not equal to the mean of the samples" ) assert numpy.fabs(numpy.std(ds) - sd) < 10.0**-2.0, ( "stddev of surfacemassLOS surfacemass is not equal to the mean of the samples" ) assert numpy.fabs(skew_samples(ds) - skew_pdist(xds, pds)) < 10.0**-1, ( "skew of surfacemassLOS surfacemass is not equal to the mean of the samples" ) return None def test_sampleVRVT_target_flat(): numpy.random.seed(1) beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Sample a large number of points, then check some moments against the analytic distribution vrvt = dfc.sampleVRVT(0.7, n=500, target=True) assert numpy.fabs(numpy.mean(vrvt[:, 0])) < 0.05, ( "mean vr of vrvt samples is not zero" ) assert numpy.fabs(numpy.mean(vrvt[:, 1]) - dfc.meanvT(0.7)) < 10.0**-2.0, ( "mean vt of vrvt samples is not equal to numerical calculation" ) assert ( numpy.fabs(numpy.std(vrvt[:, 0]) - numpy.sqrt(dfc.sigmaR2(0.7))) < 10.0**-1.5 ), "std dev vr of vrvt samples is not equal to the expected valueo" assert ( numpy.fabs(numpy.std(vrvt[:, 1]) - numpy.sqrt(dfc.sigmaT2(0.7))) < 10.0**-1.5 ), "std dev vr of vrvt samples is not equal to the expected valueo" return None def test_sampleVRVT_flat(): numpy.random.seed(1) beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Sample a large number of points, then check some moments against the analytic distribution vrvt = dfc.sampleVRVT(0.7, n=500, target=False) assert numpy.fabs(numpy.mean(vrvt[:, 0])) < 0.05, ( "mean vr of vrvt samples is not zero" ) assert numpy.fabs(numpy.mean(vrvt[:, 1]) - dfc.meanvT(0.7)) < 10.0**-2.0, ( "mean vt of vrvt samples is not equal to numerical calculation" ) assert ( numpy.fabs(numpy.std(vrvt[:, 0]) - numpy.sqrt(dfc.sigmaR2(0.7))) < 10.0**-1.5 ), "std dev vr of vrvt samples is not equal to the expected valueo" assert ( numpy.fabs(numpy.std(vrvt[:, 1]) - numpy.sqrt(dfc.sigmaT2(0.7))) < 10.0**-1.5 ), "std dev vr of vrvt samples is not equal to the expected valueo" return None def test_sampleVRVT_target_powerfall(): numpy.random.seed(1) beta = -0.2 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Sample a large number of points, then check some moments against the analytic distribution vrvt = dfc.sampleVRVT(0.7, n=500, target=True) assert numpy.fabs(numpy.mean(vrvt[:, 0])) < 0.05, ( "mean vr of vrvt samples is not zero" ) assert numpy.fabs(numpy.mean(vrvt[:, 1]) - dfc.meanvT(0.7)) < 10.0**-2.0, ( "mean vt of vrvt samples is not equal to numerical calculation" ) assert ( numpy.fabs(numpy.std(vrvt[:, 0]) - numpy.sqrt(dfc.sigmaR2(0.7))) < 10.0**-1.5 ), "std dev vr of vrvt samples is not equal to the expected valueo" assert ( numpy.fabs(numpy.std(vrvt[:, 1]) - numpy.sqrt(dfc.sigmaT2(0.7))) < 10.0**-1.5 ), "std dev vr of vrvt samples is not equal to the expected valueo" return None def test_sampleLOS_target(): numpy.random.seed(1) beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Sample a large number of points, then check some moments against the analytic distribution os = dfc.sampleLOS( numpy.pi / 4.0, n=1000, targetSurfmass=True, targetSigma2=True, deg=False ) ds = numpy.array([o.dist(ro=1.0, obs=[1.0, 0.0, 0.0]) for o in os]) xds = numpy.linspace(0.001, 4.0, 201) pds = numpy.array([dfc.surfacemassLOS(d, 45.0, deg=True, target=True) for d in xds]) md = numpy.sum(xds * pds) / numpy.sum(pds) sd = numpy.sqrt(numpy.sum(xds**2.0 * pds) / numpy.sum(pds) - md**2.0) assert numpy.fabs(numpy.mean(ds) - md) < 10.0**-2.0, ( "mean of distance in sampleLOS for target surfacemass is not equal to the mean of the distribution" ) assert numpy.fabs(numpy.std(ds) - sd) < 10.0**-1.0, ( "stddev of distance in sampleLOS for target surfacemass is not equal to the mean of the distribution" ) assert numpy.fabs(skew_samples(ds) - skew_pdist(xds, pds)) < 0.3, ( "skew of distance in sampleLOS for target surfacemass is not equal to the mean of the distribution" ) return None def test_sampleLOS(): numpy.random.seed(1) beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Sample a large number of points, then check some moments against the analytic distribution os = dfc.sampleLOS(45.0, n=1000, targetSurfmass=False, deg=True) ds = numpy.array([o.dist(ro=1.0, obs=[1.0, 0.0, 0.0]) for o in os]) xds = numpy.linspace(0.001, 4.0, 101) # check against target, bc that's easy to calculate pds = numpy.array([dfc.surfacemassLOS(d, 45.0, deg=True, target=True) for d in xds]) md = numpy.sum(xds * pds) / numpy.sum(pds) sd = numpy.sqrt(numpy.sum(xds**2.0 * pds) / numpy.sum(pds) - md**2.0) assert numpy.fabs(numpy.mean(ds) - md) < 10.0**-2.0, ( "mean of ds of sampleLOS is not equal to the mean of the distribution" ) assert numpy.fabs(numpy.std(ds) - sd) < 0.05, ( "stddev of ds of sampleLOS is not equal to the mean of the distribution" ) assert numpy.fabs(skew_samples(ds) - skew_pdist(xds, pds)) < 0.3, ( "skew of ds of sampleLOS is not equal to the mean of the distribution" ) return None def test_dehnendf_sample_sampleLOS(): # Test that the samples returned through sample with los are the same as those returned with sampleLOS beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Sample a large number of points, then check some moments against the analytic distribution numpy.random.seed(1) os = dfc.sampleLOS(45.0, n=2, targetSurfmass=False, deg=True) rs = numpy.array([o.R() for o in os]) vrs = numpy.array([o.vR() for o in os]) vts = numpy.array([o.vT() for o in os]) numpy.random.seed(1) os2 = dfc.sample(los=45.0, n=2, targetSurfmass=False, losdeg=True) rs2 = numpy.array([o.R() for o in os2]) vrs2 = numpy.array([o.vR() for o in os2]) vts2 = numpy.array([o.vT() for o in os2]) assert numpy.all(numpy.fabs(rs - rs2) < 10.0**-10.0), ( "Samples returned from dehnendf.sample with los set are not the same as those returned with sampleLOS" ) assert numpy.all(numpy.fabs(vrs - vrs2) < 10.0**-10.0), ( "Samples returned from dehnendf.sample with los set are not the same as those returned with sampleLOS" ) assert numpy.all(numpy.fabs(vts - vts2) < 10.0**-10.0), ( "Samples returned from dehnendf.sample with los set are not the same as those returned with sampleLOS" ) return None def test_shudf_sample_sampleLOS(): # Test that the samples returned through sample with los are the same as those returned with sampleLOS beta = 0.0 dfc = shudf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) # Sample a large number of points, then check some moments against the analytic distribution numpy.random.seed(1) os = dfc.sampleLOS(45.0, n=2, targetSurfmass=False, deg=True) rs = numpy.array([o.R() for o in os]) vrs = numpy.array([o.vR() for o in os]) vts = numpy.array([o.vT() for o in os]) numpy.random.seed(1) os2 = dfc.sample(los=45.0, n=2, targetSurfmass=False, losdeg=True) rs2 = numpy.array([o.R() for o in os2]) vrs2 = numpy.array([o.vR() for o in os2]) vts2 = numpy.array([o.vT() for o in os2]) assert numpy.all(numpy.fabs(rs - rs2) < 10.0**-10.0), ( "Samples returned from dehnendf.sample with los set are not the same as those returned with sampleLOS" ) assert numpy.all(numpy.fabs(vrs - vrs2) < 10.0**-10.0), ( "Samples returned from dehnendf.sample with los set are not the same as those returned with sampleLOS" ) assert numpy.all(numpy.fabs(vts - vts2) < 10.0**-10.0), ( "Samples returned from dehnendf.sample with los set are not the same as those returned with sampleLOS" ) return None def test_dehnendf_sample_flat_returnROrbit(): beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) numpy.random.seed(1) os = dfc.sample(n=300, returnROrbit=True) # Test the spatial distribution rs = numpy.array([o.R() for o in os]) assert numpy.fabs(numpy.mean(rs) - 0.5) < 0.05, ( "mean R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(rs) - numpy.sqrt(2.0) / 4.0) < 0.03, ( "stddev R of sampled points does not agree with that of the input surface profile" ) # Test the velocity distribution vrs = numpy.array([o.vR() for o in os]) assert numpy.fabs(numpy.mean(vrs)) < 0.05, ( "mean vR of sampled points does not agree with that of the input surface profile (i.e., it is not zero)" ) vts = numpy.array([o.vT() for o in os]) dvts = numpy.array( [vt - r**beta + dfc.asymmetricdrift(r) for (r, vt) in zip(rs, vts)] ) assert numpy.fabs(numpy.mean(dvts)) < 0.1, ( "mean vT of sampled points does not agree with an estimate based on asymmetric drift" ) return None def test_dehnendf_sample_flat_returnROrbit_rrange(): beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) numpy.random.seed(1) os = dfc.sample(n=100, returnROrbit=True, rrange=[0.0, 1.0]) # Test the spatial distribution rs = numpy.array([o.R() for o in os]) assert numpy.fabs(numpy.mean(rs) - 0.419352) < 0.05, ( "mean R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(rs) - 0.240852) < 0.05, ( "stddev R of sampled points does not agree with that of the input surface profile" ) # Test the velocity distribution vrs = numpy.array([o.vR() for o in os]) assert numpy.fabs(numpy.mean(vrs)) < 0.075, ( "mean vR of sampled points does not agree with that of the input surface profile (i.e., it is not zero)" ) vts = numpy.array([o.vT() for o in os]) dvts = numpy.array( [vt - r**beta + dfc.asymmetricdrift(r) for (r, vt) in zip(rs, vts)] ) assert numpy.fabs(numpy.mean(dvts)) < 0.1, ( "mean vT of sampled points does not agree with an estimate based on asymmetric drift" ) return None def test_dehnendf_sample_powerrise_returnROrbit(): beta = 0.2 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) numpy.random.seed(1) os = dfc.sample(n=300, returnROrbit=True) # Test the spatial distribution rs = numpy.array([o.R() for o in os]) assert numpy.fabs(numpy.mean(rs) - 0.5) < 0.1, ( "mean R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(rs) - numpy.sqrt(2.0) / 4.0) < 0.06, ( "stddev R of sampled points does not agree with that of the input surface profile" ) # Test the velocity distribution vrs = numpy.array([o.vR() for o in os]) assert numpy.fabs(numpy.mean(vrs)) < 0.05, ( "mean vR of sampled points does not agree with that of the input surface profile (i.e., it is not zero)" ) vts = numpy.array([o.vT() for o in os]) dvts = numpy.array( [vt - r**beta + dfc.asymmetricdrift(r) for (r, vt) in zip(rs, vts)] ) assert numpy.fabs(numpy.mean(dvts)) < 0.2, ( "mean vT of sampled points does not agree with an estimate based on asymmetric drift" ) return None def test_dehnendf_sample_flat_returnOrbit(): beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) numpy.random.seed(1) os = dfc.sample(n=100, returnOrbit=True) # Test the spatial distribution rs = numpy.array([o.R() for o in os]) phis = numpy.array([o.phi() for o in os]) assert numpy.fabs(numpy.mean(rs) - 0.5) < 0.05, ( "mean R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.mean(phis) - numpy.pi) < 0.2, ( "mean phi of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(rs) - numpy.sqrt(2.0) / 4.0) < 0.03, ( "stddev R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(phis) - numpy.pi / numpy.sqrt(3.0)) < 0.1, ( "stddev phi of sampled points does not agree with that of the input surface profile" ) # Test the velocity distribution vrs = numpy.array([o.vR() for o in os]) assert numpy.fabs(numpy.mean(vrs)) < 0.05, ( "mean vR of sampled points does not agree with that of the input surface profile (i.e., it is not zero)" ) vts = numpy.array([o.vT() for o in os]) dvts = numpy.array( [vt - r**beta + dfc.asymmetricdrift(r) for (r, vt) in zip(rs, vts)] ) assert numpy.fabs(numpy.mean(dvts)) < 0.1, ( "mean vT of sampled points does not agree with an estimate based on asymmetric drift" ) return None def test_dehnendf_sample_flat_EL(): beta = 0.0 dfc = dehnendf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) numpy.random.seed(1) EL = dfc.sample(n=50, returnROrbit=False, returnOrbit=False) E = [el[0] for el in EL] L = [el[1] for el in EL] # radii of circular orbits with this energy, these should follow an exponential rs = numpy.array([numpy.exp(e - 0.5) for e in E]) assert numpy.fabs(numpy.mean(rs) - 0.5) < 0.05, ( "mean R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(rs) - numpy.sqrt(2.0) / 4.0) < 0.03, ( "stddev R of sampled points does not agree with that of the input surface profile" ) # BOVY: Could use another test return None def test_shudf_sample_flat_returnROrbit(): beta = 0.0 dfc = shudf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) numpy.random.seed(1) os = dfc.sample(n=50, returnROrbit=True) # Test the spatial distribution rs = numpy.array([o.R() for o in os]) assert numpy.fabs(numpy.mean(rs) - 0.5) < 0.1, ( "mean R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(rs) - numpy.sqrt(2.0) / 4.0) < 0.1, ( "stddev R of sampled points does not agree with that of the input surface profile" ) # Test the velocity distribution vrs = numpy.array([o.vR() for o in os]) assert numpy.fabs(numpy.mean(vrs)) < 0.05, ( "mean vR of sampled points does not agree with that of the input surface profile (i.e., it is not zero)" ) vts = numpy.array([o.vT() for o in os]) dvts = numpy.array( [vt - r**beta + dfc.asymmetricdrift(r) for (r, vt) in zip(rs, vts)] ) assert numpy.fabs(numpy.mean(dvts)) < 0.1, ( "mean vT of sampled points does not agree with an estimate based on asymmetric drift" ) return None def test_shudf_sample_flat_returnROrbit_rrange(): beta = 0.0 dfc = shudf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) numpy.random.seed(1) os = dfc.sample(n=100, returnROrbit=True, rrange=[0.0, 1.0]) # Test the spatial distribution rs = numpy.array([o.R() for o in os]) assert numpy.fabs(numpy.mean(rs) - 0.419352) < 0.05, ( "mean R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(rs) - 0.240852) < 0.05, ( "stddev R of sampled points does not agree with that of the input surface profile" ) # Test the velocity distribution vrs = numpy.array([o.vR() for o in os]) assert numpy.fabs(numpy.mean(vrs)) < 0.075, ( "mean vR of sampled points does not agree with that of the input surface profile (i.e., it is not zero)" ) vts = numpy.array([o.vT() for o in os]) dvts = numpy.array( [vt - r**beta + dfc.asymmetricdrift(r) for (r, vt) in zip(rs, vts)] ) assert numpy.fabs(numpy.mean(dvts)) < 0.13, ( "mean vT of sampled points does not agree with an estimate based on asymmetric drift" ) return None def test_shudf_sample_powerrise_returnROrbit(): beta = 0.2 dfc = shudf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) numpy.random.seed(1) os = dfc.sample(n=100, returnROrbit=True) # Test the spatial distribution rs = numpy.array([o.R() for o in os]) assert numpy.fabs(numpy.mean(rs) - 0.5) < 0.1, ( "mean R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(rs) - numpy.sqrt(2.0) / 4.0) < 0.06, ( "stddev R of sampled points does not agree with that of the input surface profile" ) # Test the velocity distribution vrs = numpy.array([o.vR() for o in os]) assert numpy.fabs(numpy.mean(vrs)) < 0.05, ( "mean vR of sampled points does not agree with that of the input surface profile (i.e., it is not zero)" ) vts = numpy.array([o.vT() for o in os]) dvts = numpy.array( [vt - r**beta + dfc.asymmetricdrift(r) for (r, vt) in zip(rs, vts)] ) assert numpy.fabs(numpy.mean(dvts)) < 0.2, ( "mean vT of sampled points does not agree with an estimate based on asymmetric drift" ) return None def test_shudf_sample_flat_returnOrbit(): beta = 0.0 dfc = shudf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) numpy.random.seed(1) os = dfc.sample(n=100, returnOrbit=True) # Test the spatial distribution rs = numpy.array([o.R() for o in os]) phis = numpy.array([o.phi() for o in os]) assert numpy.fabs(numpy.mean(rs) - 0.5) < 0.05, ( "mean R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.mean(phis) - numpy.pi) < 0.2, ( "mean phi of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(rs) - numpy.sqrt(2.0) / 4.0) < 0.03, ( "stddev R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(phis) - numpy.pi / numpy.sqrt(3.0)) < 0.2, ( "stddev phi of sampled points does not agree with that of the input surface profile" ) # Test the velocity distribution vrs = numpy.array([o.vR() for o in os]) assert numpy.fabs(numpy.mean(vrs)) < 0.05, ( "mean vR of sampled points does not agree with that of the input surface profile (i.e., it is not zero)" ) vts = numpy.array([o.vT() for o in os]) dvts = numpy.array( [vt - r**beta + dfc.asymmetricdrift(r) for (r, vt) in zip(rs, vts)] ) assert numpy.fabs(numpy.mean(dvts)) < 0.1, ( "mean vT of sampled points does not agree with an estimate based on asymmetric drift" ) return None def test_shudf_sample_flat_EL(): beta = 0.0 dfc = shudf(beta=beta, profileParams=(1.0 / 4.0, 1.0, 0.2)) numpy.random.seed(1) EL = dfc.sample(n=50, returnROrbit=False, returnOrbit=False) E = [el[0] for el in EL] L = [el[1] for el in EL] # radii of circular orbits with this angular momentum, these should follow an exponential rs = numpy.array(L) assert numpy.fabs(numpy.mean(rs) - 0.5) < 0.05, ( "mean R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(rs) - numpy.sqrt(2.0) / 4.0) < 0.03, ( "stddev R of sampled points does not agree with that of the input surface profile" ) # BOVY: Could use another test return None def test_schwarzschild_vs_shu_flat(): # Schwarzschild DF should be ~~ Shu for small sigma, test w/ flat rotcurve dfs = shudf(profileParams=(0.3333333333333333, 1.0, 0.05), beta=0.0, correct=False) dfw = schwarzschilddf( profileParams=(0.3333333333333333, 1.0, 0.05), beta=0.0, correct=False ) assert numpy.fabs(dfs.meanvT(0.97) - dfw.meanvT(0.97)) < 10.0**-3.0, ( "Shu and Schwarschild DF differ more than expected for small sigma" ) assert numpy.fabs(dfs.oortA(0.97) - dfw.oortA(0.97)) < 10.0**-2.9, ( "Shu and Schwarschild DF differ more than expected for small sigma" ) return None def test_schwarzschild_vs_shu_powerfall(): # Schwarzschild DF should be ~~ Shu for small sigma, test w/ flat rotcurve beta = -0.2 dfs = shudf(profileParams=(0.3333333333333333, 1.0, 0.05), beta=beta, correct=False) dfw = schwarzschilddf( profileParams=(0.3333333333333333, 1.0, 0.05), beta=beta, correct=False ) assert numpy.fabs(dfs.meanvT(0.97) - dfw.meanvT(0.97)) < 10.0**-3.0, ( "Shu and Schwarschild DF differ more than expected for small sigma" ) assert numpy.fabs(dfs.oortA(0.97) - dfw.oortA(0.97)) < 10.0**-3.0, ( "Shu and Schwarschild DF differ more than expected for small sigma" ) return None def test_schwarzschild_vs_shu_powerrise(): # Schwarzschild DF should be ~~ Shu for small sigma, test w/ flat rotcurve beta = 0.2 dfs = shudf(profileParams=(0.3333333333333333, 1.0, 0.05), beta=beta, correct=False) dfw = schwarzschilddf( profileParams=(0.3333333333333333, 1.0, 0.05), beta=beta, correct=False ) assert numpy.fabs(dfs.meanvT(0.97) - dfw.meanvT(0.97)) < 10.0**-3.0, ( "Shu and Schwarschild DF differ more than expected for small sigma" ) assert numpy.fabs(dfs.oortA(0.97) - dfw.oortA(0.97)) < 10.0**-2.8, ( "Shu and Schwarschild DF differ more than expected for small sigma" ) return None ############################################################################### # Tests of DFcorrection ############################################################################### def test_dehnendf_flat_DFcorrection_setup(): global ddf_correct_flat global ddf_correct2_flat ddf_correct_flat = dehnendf( beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2), correct=True, niter=1, npoints=21, savedir=".", ) ddf_correct2_flat = dehnendf( beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2), correct=True, niter=2, npoints=21, savedir=".", ) return None def test_dehnendf_flat_DFcorrection_mag(): # Test that the call is not too different from before tcorr = ddf_correct2_flat._corr.correct(1.1, log=True) assert numpy.fabs(tcorr[0]) < 0.15, "dehnendf correction is larger than expected" assert numpy.fabs(tcorr[1]) < 0.1, "dehnendf correction is larger than expected" # small R tcorr = numpy.log(ddf_correct2_flat._corr.correct(10.0**-12.0)) assert numpy.fabs(tcorr[0]) < 0.4, "dehnendf correction is larger than expected" assert numpy.fabs(tcorr[1]) < 1.0, "dehnendf correction is larger than expected" # large R tcorr = numpy.log(ddf_correct2_flat._corr.correct(12.0)) assert numpy.fabs(tcorr[0]) < 0.01, "dehnendf correction is larger than expected" assert numpy.fabs(tcorr[1]) < 0.01, "dehnendf correction is larger than expected" # small R, array tcorr = numpy.log(ddf_correct2_flat._corr.correct(10.0**-12.0 * numpy.ones(2))) assert numpy.all(numpy.fabs(tcorr[0]) < 0.4), ( "dehnendf correction is larger than expected" ) assert numpy.all(numpy.fabs(tcorr[1]) < 1.0), ( "dehnendf correction is larger than expected" ) # large R tcorr = numpy.log(ddf_correct2_flat._corr.correct(12.0 * numpy.ones(2))) assert numpy.all(numpy.fabs(tcorr[0]) < 0.01), ( "dehnendf correction is larger than expected" ) assert numpy.all(numpy.fabs(tcorr[1]) < 0.01), ( "dehnendf correction is larger than expected" ) # large R, log tcorr = ddf_correct2_flat._corr.correct(12.0 * numpy.ones(2), log=True) assert numpy.all(numpy.fabs(tcorr[0]) < 0.01), ( "dehnendf correction is larger than expected" ) assert numpy.all(numpy.fabs(tcorr[1]) < 0.01), ( "dehnendf correction is larger than expected" ) return None def test_dehnendf_flat_DFcorrection_deriv_mag(): # Test that the derivative behaves as expected tcorr = ddf_correct2_flat._corr.derivLogcorrect(2.0) assert numpy.fabs(tcorr[0]) < 0.1, ( "dehnendf correction derivative is larger than expected" ) assert numpy.fabs(tcorr[1]) < 0.1, ( "dehnendf correction derivative is larger than expected" ) # small R, derivative should be very large tcorr = ddf_correct2_flat._corr.derivLogcorrect(10.0**-12.0) assert numpy.fabs(tcorr[0]) > 1.0, ( "dehnendf correction derivative is smaller than expected" ) assert numpy.fabs(tcorr[1]) > 1.0, ( "dehnendf correction derivative is larger than expected" ) # large R tcorr = ddf_correct2_flat._corr.derivLogcorrect(12.0) assert numpy.fabs(tcorr[0]) < 0.01, ( "dehnendf correction derivative is larger than expected" ) assert numpy.fabs(tcorr[1]) < 0.01, ( "dehnendf correction derivative is larger than expected" ) return None def test_dehnendf_flat_DFcorrection_surfacemass(): # Test that the surfacemass is better than before dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2), correct=False) diff_uncorr = numpy.fabs( numpy.log(dfc.surfacemass(0.8)) - numpy.log(dfc.targetSurfacemass(0.8)) ) diff_corr = numpy.fabs( numpy.log(ddf_correct_flat.surfacemass(0.8)) - numpy.log(dfc.targetSurfacemass(0.8)) ) diff_corr2 = numpy.fabs( numpy.log(ddf_correct2_flat.surfacemass(0.8)) - numpy.log(dfc.targetSurfacemass(0.8)) ) assert diff_corr < diff_uncorr, ( "surfacemass w/ corrected dehnenDF is does not agree better with target than with uncorrected dehnenDF" ) assert diff_corr2 < diff_corr, ( "surfacemass w/ corrected dehnenDF w/ 2 iterations is does not agree better with target than with 1 iteration" ) return None def test_dehnendf_flat_DFcorrection_sigmaR2(): # Test that the sigmaR2 is better than before dfc = dehnendf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2), correct=False) diff_uncorr = numpy.fabs( numpy.log(dfc.sigmaR2(0.8)) - numpy.log(dfc.targetSigma2(0.8)) ) diff_corr = numpy.fabs( numpy.log(ddf_correct_flat.sigmaR2(0.8)) - numpy.log(dfc.targetSigma2(0.8)) ) diff_corr2 = numpy.fabs( numpy.log(ddf_correct2_flat.sigmaR2(0.8)) - numpy.log(dfc.targetSigma2(0.8)) ) assert diff_corr < diff_uncorr, ( "sigmaR2 w/ corrected dehnenDF is does not agree better with target than with uncorrected dehnenDF" ) assert diff_corr2 < diff_corr, ( "sigmaR2 w/ corrected dehnenDF w/ 2 iterations is does not agree better with target than with 1 iteration" ) return None def test_dehnendf_flat_DFcorrection_reload(): # Test that the corrections aren't re-calculated if they were saved import time start = time.time() reddf = dehnendf( beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2), correct=True, niter=1, npoints=21, savedir=".", ) assert time.time() - start < 1.0, ( "Setup w/ correct=True, but already computed corrections takes too long" ) return None def test_dehnendf_flat_DFcorrection_cleanup(): # This should run quickly dfc = dehnendf( beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2), correct=True, niter=1, npoints=21, savedir=".", ) try: os.remove(dfc._corr._createSavefilename(1)) except: raise AssertionError("removing DFcorrection's savefile did not work") try: os.remove(dfc._corr._createSavefilename(2)) except: raise AssertionError("removing DFcorrection's savefile did not work") return None def test_DFcorrection_setup(): # Test that the keywords are setup correctly and that exceptions are raised dfc = dehnendf( beta=0.1, profileParams=(1.0 / 3.0, 1.0, 0.2), correct=True, rmax=4.0, niter=2, npoints=5, interp_k=3, savedir=".", ) assert numpy.fabs(dfc._corr._rmax - 4.0) < 10.0**-10.0, ( "rmax not set up correctly in DFcorrection" ) assert numpy.fabs(dfc._corr._npoints - 5) < 10.0**-10.0, ( "npoints not set up correctly in DFcorrection" ) assert numpy.fabs(dfc._corr._interp_k - 3) < 10.0**-10.0, ( "interp_k not set up correctly in DFcorrection" ) assert numpy.fabs(dfc._corr._beta - 0.1) < 10.0**-10.0, ( "beta not set up correctly in DFcorrection" ) # setup w/ corrections corrs = dfc._corr._corrections dfc = dehnendf( beta=0.1, profileParams=(1.0 / 3.0, 1.0, 0.2), correct=True, rmax=4.0, niter=2, interp_k=3, savedir=".", corrections=corrs, ) assert numpy.all(numpy.fabs(corrs - dfc._corr._corrections) < 10.0**-10.0), ( "DFcorrection initialized w/ corrections does not work properly" ) # If corrections.shape[0] neq npoints, should raise error from galpy.df.diskdf import DFcorrectionError try: dfc = dehnendf( beta=0.1, profileParams=(1.0 / 3.0, 1.0, 0.2), correct=True, rmax=4.0, niter=2, npoints=6, interp_k=3, savedir=".", corrections=corrs, ) except DFcorrectionError: pass else: raise AssertionError( "DFcorrection setup with corrections.shape[0] neq npoints did not raise DFcorrectionError" ) # rm savefile dfc = dehnendf( beta=0.1, profileParams=(1.0 / 3.0, 1.0, 0.2), correct=True, rmax=4.0, niter=2, npoints=5, interp_k=3, savedir=".", ) try: os.remove(dfc._corr._createSavefilename(2)) except: raise AssertionError("removing DFcorrection's savefile did not work") # Also explicitly setup a DFcorrection, to test for other stuff from galpy.df import DFcorrection from galpy.df.diskdf import DFcorrectionError # Should raise DFcorrectionError bc surfaceSigmaProfile is not set try: dfc = DFcorrection(npoints=2, niter=2, rmax=4.0, beta=-0.1, interp_k=3) except DFcorrectionError as e: print(e) else: raise AssertionError( "DFcorrection setup with no surfaceSigmaProfile set did not raise DFcorrectionError" ) # Now w/ surfaceSigmaProfile to test default dftype from galpy.df import expSurfaceSigmaProfile essp = expSurfaceSigmaProfile(params=(0.25, 0.75, 0.1)) dfc = DFcorrection( npoints=5, niter=1, rmax=4.0, surfaceSigmaProfile=essp, interp_k=3 ) assert issubclass(dfc._dftype, dehnendf), ( "DFcorrection w/ no dftype set does not default to dehnendf" ) assert numpy.fabs(dfc._beta) < 10.0**-10.0, ( "DFcorrection w/ no beta does not default to zero" ) try: os.remove(dfc._createSavefilename(1)) except: raise AssertionError("removing DFcorrection's savefile did not work") return None def test_dehnendf_sample_flat_returnROrbit_wcorrections(): beta = 0.0 dfc = ddf_correct2_flat numpy.random.seed(1) os = dfc.sample(n=100, returnROrbit=True) # Test the spatial distribution rs = numpy.array([o.R() for o in os]) assert numpy.fabs(numpy.mean(rs) - 0.5) < 0.05, ( "mean R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(rs) - numpy.sqrt(2.0) / 4.0) < 0.03, ( "stddev R of sampled points does not agree with that of the input surface profile" ) # Test the velocity distribution vrs = numpy.array([o.vR() for o in os]) assert numpy.fabs(numpy.mean(vrs)) < 0.1, ( "mean vR of sampled points does not agree with that of the input surface profile (i.e., it is not zero)" ) vts = numpy.array([o.vT() for o in os]) dvts = numpy.array( [vt - r**beta + dfc.asymmetricdrift(r) for (r, vt) in zip(rs, vts)] ) assert numpy.fabs(numpy.mean(dvts)) < 0.1, ( "mean vT of sampled points does not agree with an estimate based on asymmetric drift" ) return None def test_shudf_flat_DFcorrection_setup(): global sdf_correct_flat sdf_correct_flat = shudf( beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2), correct=True, niter=1, npoints=21, savedir=".", ) return None def test_shudf_flat_DFcorrection_surfacemass(): # Test that the surfacemass is better than before dfc = shudf(beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2), correct=False) diff_uncorr = numpy.fabs( numpy.log(dfc.surfacemass(0.8)) - numpy.log(dfc.targetSurfacemass(0.8)) ) diff_corr = numpy.fabs( numpy.log(sdf_correct_flat.surfacemass(0.8)) - numpy.log(dfc.targetSurfacemass(0.8)) ) assert diff_corr < diff_uncorr, ( "surfacemass w/ corrected shuDF is does not agree better with target than with uncorrected shuDF" ) return None def test_shudf_sample_flat_returnROrbit_wcorrections(): beta = 0.0 dfc = sdf_correct_flat numpy.random.seed(1) os = dfc.sample(n=100, returnROrbit=True) # Test the spatial distribution rs = numpy.array([o.R() for o in os]) assert numpy.fabs(numpy.mean(rs) - 0.5) < 0.05, ( "mean R of sampled points does not agree with that of the input surface profile" ) assert numpy.fabs(numpy.std(rs) - numpy.sqrt(2.0) / 4.0) < 0.035, ( "stddev R of sampled points does not agree with that of the input surface profile" ) # Test the velocity distribution vrs = numpy.array([o.vR() for o in os]) assert numpy.fabs(numpy.mean(vrs)) < 0.05, ( "mean vR of sampled points does not agree with that of the input surface profile (i.e., it is not zero)" ) vts = numpy.array([o.vT() for o in os]) dvts = numpy.array( [vt - r**beta + dfc.asymmetricdrift(r) for (r, vt) in zip(rs, vts)] ) assert numpy.fabs(numpy.mean(dvts)) < 0.1, ( "mean vT of sampled points does not agree with an estimate based on asymmetric drift" ) return None def test_shudf_flat_DFcorrection_cleanup(): # This should run quickly dfc = shudf( beta=0.0, profileParams=(1.0 / 4.0, 1.0, 0.2), correct=True, niter=1, npoints=21, savedir=".", ) try: os.remove(dfc._corr._createSavefilename(1)) except: raise AssertionError("removing DFcorrection's savefile did not work") return None def test_axipotential(): from galpy.df.diskdf import _RMIN, axipotential assert ( numpy.fabs(axipotential(numpy.array([0.5]), beta=0.0) - numpy.log(0.5)) < 10.0**-8 ), "axipotential w/ beta=0.0 does not work as expected" assert ( numpy.fabs(axipotential(numpy.array([0.5]), beta=0.2) - 1.0 / 0.4 * 0.5**0.4) < 10.0**-8 ), "axipotential w/ beta=0.2 does not work as expected" assert ( numpy.fabs(axipotential(numpy.array([0.5]), beta=-0.2) + 1.0 / 0.4 * 0.5**-0.4) < 10.0**-8 ), "axipotential w/ beta=0.2 does not work as expected" # special case of R=0 should go to _RMIN assert ( numpy.fabs(axipotential(numpy.array([0.0]), beta=0.0) - numpy.log(_RMIN)) < 10.0**-8 ), "axipotential w/ beta=0.0 does not work as expected" return None def test_dlToRphi(): from galpy.df.diskdf import _dlToRphi R, theta = _dlToRphi(1.0, 0.0) assert numpy.fabs(R) < 10.0**-3.0, ( "_dlToRphi close to center does not behave properly" ) assert numpy.fabs(theta % numpy.pi) < 10.0**-3.0, ( "_dlToRphi close to center does not behave properly" ) return None def skew_samples(s): m1 = numpy.mean(s) m2 = numpy.mean((s - m1) ** 2.0) m3 = numpy.mean((s - m1) ** 3.0) return m3 / m2**1.5 def skew_pdist(s, ps): norm = numpy.sum(ps) m1 = numpy.sum(s * ps) / norm m2 = numpy.sum((s - m1) ** 2.0 * ps) / norm m3 = numpy.sum((s - m1) ** 3.0 * ps) / norm return m3 / m2**1.5 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_dynamfric.py0000644000175100001660000003627514761352023017061 0ustar00runnerdocker# Tests of dynamical friction implementation import sys import pytest PY3 = sys.version > "3" import numpy from galpy import potential from galpy.util import galpyWarning def test_ChandrasekharDynamicalFrictionForce_constLambda(): # Test that the ChandrasekharDynamicalFrictionForce with constant Lambda # agrees with analytical solutions for circular orbits: # assuming that a mass remains on a circular orbit in an isothermal halo # with velocity dispersion sigma and for constant Lambda: # r_final^2 - r_initial^2 = -0.604 ln(Lambda) GM/sigma t # (e.g., B&T08, p. 648) from galpy.orbit import Orbit from galpy.util import conversion ro, vo = 8.0, 220.0 # Parameters GMs = 10.0**9.0 / conversion.mass_in_msol(vo, ro) const_lnLambda = 7.0 r_init = 2.0 dt = 2.0 / conversion.time_in_Gyr(vo, ro) # Compute lp = potential.LogarithmicHaloPotential(normalize=1.0, q=1.0) cdfc = potential.ChandrasekharDynamicalFrictionForce( GMs=GMs, const_lnLambda=const_lnLambda, dens=lp ) # don't provide sigmar, so it gets computed using galpy.df.jeans o = Orbit([r_init, 0.0, 1.0, 0.0, 0.0, 0.0]) ts = numpy.linspace(0.0, dt, 1001) o.integrate(ts, [lp, cdfc], method="odeint") r_pred = numpy.sqrt( o.r() ** 2.0 - 0.604 * const_lnLambda * GMs * numpy.sqrt(2.0) * dt ) assert numpy.fabs(r_pred - o.r(ts[-1])) < 0.01, ( "ChandrasekharDynamicalFrictionForce with constant lnLambda for circular orbits does not agree with analytical prediction" ) return None def test_ChandrasekharDynamicalFrictionForce_varLambda(): # Test that dynamical friction with variable Lambda for small r ranges # gives ~ the same result as using a constant Lambda that is the mean of # the variable lambda # Also tests that giving an axisymmetric list of potentials for the # density works from galpy.orbit import Orbit from galpy.util import conversion ro, vo = 8.0, 220.0 # Parameters GMs = 10.0**9.0 / conversion.mass_in_msol(vo, ro) r_init = 3.0 dt = 2.0 / conversion.time_in_Gyr(vo, ro) # Compute evolution with variable ln Lambda cdf = potential.ChandrasekharDynamicalFrictionForce( GMs=GMs, rhm=0.125, dens=potential.MWPotential2014, sigmar=lambda r: 1.0 / numpy.sqrt(2.0), ) o = Orbit([r_init, 0.0, 1.0, 0.0, 0.0, 0.0]) ts = numpy.linspace(0.0, dt, 1001) o.integrate(ts, [potential.MWPotential2014, cdf], method="odeint") lnLs = numpy.array( [ cdf.lnLambda(r, v) for (r, v) in zip( o.r(ts), numpy.sqrt(o.vx(ts) ** 2.0 + o.vy(ts) ** 2.0 + o.vz(ts) ** 2.0) ) ] ) cdfc = potential.ChandrasekharDynamicalFrictionForce( GMs=GMs, rhm=0.125, const_lnLambda=numpy.mean(lnLs), dens=potential.MWPotential2014, sigmar=lambda r: 1.0 / numpy.sqrt(2.0), ) oc = o() oc.integrate(ts, [potential.MWPotential2014, cdfc], method="odeint") assert numpy.fabs(oc.r(ts[-1]) - o.r(ts[-1])) < 0.05, ( "ChandrasekharDynamicalFrictionForce with variable lnLambda for a short radial range is not close to the calculation using a constant lnLambda" ) return None def test_ChandrasekharDynamicalFrictionForce_evaloutsideminrmaxr(): # Test that dynamical friction returns the expected force when evaluating # outside of the [minr,maxr] range over which sigmar is interpolated: # 0 at r < minr # using sigmar(r) for r > maxr from galpy.util import conversion ro, vo = 8.0, 220.0 # Parameters GMs = 10.0**9.0 / conversion.mass_in_msol(vo, ro) # Compute evolution with variable ln Lambda sigmar = lambda r: 1.0 / r cdf = potential.ChandrasekharDynamicalFrictionForce( GMs=GMs, rhm=0.125, dens=potential.MWPotential2014, sigmar=sigmar, minr=0.5, maxr=2.0, ) # cdf 2 for checking r > maxr of cdf cdf2 = potential.ChandrasekharDynamicalFrictionForce( GMs=GMs, rhm=0.125, dens=potential.MWPotential2014, sigmar=sigmar, minr=0.5, maxr=4.0, ) v = [0.1, 0.0, 0.0] # r < minr assert numpy.fabs(cdf.Rforce(0.1, 0.0, v=v)) < 1e-16, ( "potential.ChandrasekharDynamicalFrictionForce at r < minr not equal to zero" ) assert numpy.fabs(cdf.zforce(0.1, 0.0, v=v)) < 1e-16, ( "potential.ChandrasekharDynamicalFrictionForce at r < minr not equal to zero" ) # r > maxr assert numpy.fabs(cdf.Rforce(3.0, 0.0, v=v) - cdf2.Rforce(3.0, 0.0, v=v)) < 1e-10, ( "potential.ChandrasekharDynamicalFrictionForce at r > maxr not as expected" ) assert numpy.fabs(cdf.zforce(3.0, 0.0, v=v) - cdf2.zforce(3.0, 0.0, v=v)) < 1e-10, ( "potential.ChandrasekharDynamicalFrictionForce at r > maxr not as expected" ) return None def test_ChandrasekharDynamicalFrictionForce_pickling(): # Test that ChandrasekharDynamicalFrictionForce objects can/cannot be # pickled as expected import pickle from galpy.util import conversion ro, vo = 8.0, 220.0 # Parameters GMs = 10.0**9.0 / conversion.mass_in_msol(vo, ro) # sigmar internally computed, should be able to be pickled # Compute evolution with variable ln Lambda cdf = potential.ChandrasekharDynamicalFrictionForce( GMs=GMs, rhm=0.125, dens=potential.MWPotential2014, minr=0.5, maxr=2.0 ) pickled = pickle.dumps(cdf) cdfu = pickle.loads(pickled) # Test a few values assert ( numpy.fabs( cdf.Rforce(1.0, 0.2, v=[1.0, 1.0, 0.0]) - cdfu.Rforce(1.0, 0.2, v=[1.0, 1.0, 0.0]) ) < 1e-10 ), ( "Pickling of ChandrasekharDynamicalFrictionForce object does not work as expected" ) assert ( numpy.fabs( cdf.zforce(2.0, -0.2, v=[1.0, 1.0, 0.0]) - cdfu.zforce(2.0, -0.2, v=[1.0, 1.0, 0.0]) ) < 1e-10 ), ( "Pickling of ChandrasekharDynamicalFrictionForce object does not work as expected" ) # Not providing dens = Logarithmic should also work cdf = potential.ChandrasekharDynamicalFrictionForce( GMs=GMs, rhm=0.125, minr=0.5, maxr=2.0 ) pickled = pickle.dumps(cdf) cdfu = pickle.loads(pickled) # Test a few values assert ( numpy.fabs( cdf.Rforce(1.0, 0.2, v=[1.0, 1.0, 0.0]) - cdfu.Rforce(1.0, 0.2, v=[1.0, 1.0, 0.0]) ) < 1e-10 ), ( "Pickling of ChandrasekharDynamicalFrictionForce object does not work as expected" ) assert ( numpy.fabs( cdf.zforce(2.0, -0.2, v=[1.0, 1.0, 0.0]) - cdfu.zforce(2.0, -0.2, v=[1.0, 1.0, 0.0]) ) < 1e-10 ), ( "Pickling of ChandrasekharDynamicalFrictionForce object does not work as expected" ) # Providing sigmar as a lambda function gives AttributeError sigmar = lambda r: 1.0 / r cdf = potential.ChandrasekharDynamicalFrictionForce( GMs=GMs, rhm=0.125, dens=potential.MWPotential2014, sigmar=sigmar, minr=0.5, maxr=2.0, ) if PY3: with pytest.raises(AttributeError) as excinfo: pickled = pickle.dumps(cdf) else: with pytest.raises(pickle.PicklingError) as excinfo: pickled = pickle.dumps(cdf) return None # Test whether dynamical friction in C works (compare to Python, which is # tested below; put here because a test of many potentials) def test_dynamfric_c(): import copy from galpy.orbit import Orbit from galpy.potential.mwpotentials import McMillan17 from galpy.potential.Potential import _check_c # Basic parameters for the test times = numpy.linspace(0.0, -100.0, 1001) # ~3 Gyr at the Solar circle integrator = "dop853_c" py_integrator = "dop853" # Define all of the potentials (by hand, because need reasonable setup) MWPotential3021 = copy.deepcopy(potential.MWPotential2014) MWPotential3021[2] *= 1.5 # Increase mass by 50% pots = [ potential.LogarithmicHaloPotential(normalize=1), potential.LogarithmicHaloPotential(normalize=1.3, q=0.9, b=0.7), # nonaxi potential.NFWPotential(normalize=1.0, a=1.5), potential.MiyamotoNagaiPotential(normalize=0.02, a=10.0, b=10.0), potential.MiyamotoNagaiPotential(normalize=0.6, a=0.0, b=3.0), # special case potential.PowerSphericalPotential(alpha=2.3, normalize=2.0), potential.DehnenSphericalPotential(normalize=4.0, alpha=1.2), potential.DehnenCoreSphericalPotential(normalize=4.0), potential.HernquistPotential(normalize=1.0, a=3.5), potential.JaffePotential(normalize=1.0, a=20.5), potential.DoubleExponentialDiskPotential(normalize=0.2, hr=3.0, hz=0.6), potential.FlattenedPowerPotential(normalize=3.0), potential.FlattenedPowerPotential(normalize=3.0, alpha=0), # special case potential.IsochronePotential(normalize=2.0), potential.PowerSphericalPotentialwCutoff(normalize=0.3, rc=10.0), potential.PlummerPotential(normalize=0.6, b=3.0), potential.PseudoIsothermalPotential(normalize=0.1, a=3.0), potential.BurkertPotential(normalize=0.2, a=2.5), potential.TriaxialHernquistPotential(normalize=1.0, a=3.5, b=0.8, c=0.9), potential.TriaxialNFWPotential(normalize=1.0, a=1.5, b=0.8, c=0.9), potential.TriaxialJaffePotential(normalize=1.0, a=20.5, b=0.8, c=1.4), potential.PerfectEllipsoidPotential(normalize=0.3, a=3.0, b=0.7, c=1.5), potential.PerfectEllipsoidPotential( normalize=0.3, a=3.0, b=0.7, c=1.5, pa=3.0, zvec=[0.0, 1.0, 0.0] ), # rotated potential.HomogeneousSpherePotential( normalize=0.02, R=82.0 / 8 ), # make sure to go to dens = 0 part, potential.interpSphericalPotential( rforce=potential.HomogeneousSpherePotential(normalize=0.02, R=82.0 / 8.0), rgrid=numpy.linspace(0.0, 82.0 / 8.0, 201), ), potential.TriaxialGaussianPotential( normalize=0.03, sigma=4.0, b=0.8, c=1.5, pa=3.0, zvec=[1.0, 0.0, 0.0] ), potential.SCFPotential( Acos=numpy.array([[[1.0]]]), normalize=1.0, a=3.5, # same as Hernquist ), potential.SCFPotential( Acos=numpy.array([[[1.0, 0.0], [0.3, 0.0]]]), # nonaxi Asin=numpy.array([[[0.0, 0.0], [1e-1, 0.0]]]), normalize=1.0, a=3.5, ), MWPotential3021, McMillan17, # SCF + DiskSCF ] # tolerances in log10 tol = {} tol["default"] = -7.0 # Following are a little more difficult tol["DoubleExponentialDiskPotential"] = -4.5 tol["TriaxialHernquistPotential"] = -6.0 tol["TriaxialNFWPotential"] = -6.0 tol["TriaxialJaffePotential"] = -6.0 tol["MWPotential3021"] = -6.0 tol["HomogeneousSpherePotential"] = -6.0 tol["interpSphericalPotential"] = -6.0 # == HomogeneousSpherePotential tol["McMillan17"] = -6.0 for p in pots: if not _check_c(p, dens=True): continue # dynamfric not in C! pname = type(p).__name__ if pname == "list": if ( isinstance(p[0], potential.PowerSphericalPotentialwCutoff) and len(p) > 1 and isinstance(p[1], potential.MiyamotoNagaiPotential) and len(p) > 2 and isinstance(p[2], potential.NFWPotential) ): pname = "MWPotential3021" # Must be! else: pname = "McMillan17" # print(pname) if pname in list(tol.keys()): ttol = tol[pname] else: ttol = tol["default"] # Setup orbit, ~ LMC o = Orbit( [5.13200034, 1.08033051, 0.23323391, -3.48068653, 0.94950884, -1.54626091] ) # Setup dynamical friction object if pname == "McMillan17": cdf = potential.ChandrasekharDynamicalFrictionForce( GMs=0.5553870441722593, rhm=5.0 / 8.0, dens=p, maxr=500.0 / 8, nr=101 ) ttimes = numpy.linspace(0.0, -30.0, 1001) # ~1 Gyr at the Solar circle else: cdf = potential.ChandrasekharDynamicalFrictionForce( GMs=0.5553870441722593, rhm=5.0 / 8.0, dens=p, maxr=500.0 / 8, nr=201 ) ttimes = times # Integrate in C o.integrate(ttimes, p + cdf, method=integrator) # Integrate in Python op = o() op.integrate(ttimes, p + cdf, method=py_integrator) # Compare r (most important) assert numpy.amax(numpy.fabs(o.r(ttimes) - op.r(ttimes))) < 10**ttol, ( f"Dynamical friction in C does not agree with dynamical friction in Python for potential {pname}" ) return None # Test that r < minr in ChandrasekharDynamFric works properly def test_dynamfric_c_minr(): from galpy.orbit import Orbit times = numpy.linspace(0.0, -100.0, 1001) # ~3 Gyr at the Solar circle integrator = "dop853_c" pot = potential.LogarithmicHaloPotential(normalize=1) # Setup orbit, ~ LMC o = Orbit( [5.13200034, 1.08033051, 0.23323391, -3.48068653, 0.94950884, -1.54626091] ) # Setup dynamical friction object, with minr = 130 st always 0 for this orbit cdf = potential.ChandrasekharDynamicalFrictionForce( GMs=0.5553870441722593, rhm=5.0 / 8.0, dens=pot, minr=130.0 / 8.0, maxr=500.0 / 8, ) # Integrate in C with dynamical friction o.integrate(times, pot + cdf, method=integrator) # Integrate in C without dynamical friction op = o() op.integrate(times, pot, method=integrator) # Compare r (most important) assert numpy.amax(numpy.fabs(o.r(times) - op.r(times))) < 10**-8.0, ( "Dynamical friction in C does not properly use minr" ) return None # Test that when an orbit reaches r < minr, a warning is raised to alert the user def test_dynamfric_c_minr_warning(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 100.0, 1001) # ~3 Gyr at the Solar circle integrator = "dop853_c" pot = potential.LogarithmicHaloPotential(normalize=1) # Setup orbit o = Orbit() # Setup dynamical friction object, with minr = 1, should thus reach it cdf = potential.ChandrasekharDynamicalFrictionForce( GMs=0.5553870441722593, rhm=5.0 / 8.0, dens=pot, minr=1.0 ) # Integrate, should raise warning with pytest.warns(galpyWarning) as record: o.integrate(times, pot + cdf, method=integrator) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "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)" ) assert raisedWarning, ( "Integrating an orbit that goes to r < minr with dynamical friction should have raised a warning, but didn't" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_evolveddiskdf.py0000644000175100001660000013147314761352023017732 0ustar00runnerdocker# Tests of the evolveddiskdf module import numpy from galpy.df import dehnendf, evolveddiskdf from galpy.potential import ( EllipticalDiskPotential, LogarithmicHaloPotential, SteadyLogSpiralPotential, ) _GRIDPOINTS = 31 # globals to save the results from previous calculations to be reused, pre-setting them allows one to skip tests _maxi_surfacemass = 0.0672746475968 _maxi_meanvr = -0.000517132979969 _maxi_meanvt = 0.913328340109 _maxi_sigmar2 = 0.0457686414529 _maxi_sigmat2 = 0.0268245643697 _maxi_sigmart = -0.000541204894097 def test_mildnonaxi_meanvr_grid(): # Test that for a close to axisymmetric potential, the mean vr is close to zero idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvr, grid = edf.meanvR( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(mvr) < 0.001, ( "meanvR of evolveddiskdf for axisymmetric potential is not equal to zero" ) mvr = edf.meanvR(0.9, phi=0.2, integrate_method="rk6_c", grid=grid) assert numpy.fabs(mvr) < 0.001, ( "meanvR of evolveddiskdf for axisymmetric potential is not equal to zero when calculated with pre-computed grid" ) # Pre-compute surfmass and use it, first test that grid is properly returned when given smass, ngrid = edf.vmomentsurfacemass( 0.9, 0, 0, phi=0.2, integrate_method="rk6_c", grid=grid, gridpoints=_GRIDPOINTS, returnGrid=True, ) assert ngrid == grid, ( "grid returned by vmomentsurfacemass w/ grid input is not the same as the input" ) # Pre-compute surfmass and use it nsmass = edf.vmomentsurfacemass( 0.9, 0, 0, phi=0.2 * 180.0 / numpy.pi, integrate_method="rk6_c", grid=True, gridpoints=_GRIDPOINTS, deg=True, ) assert numpy.fabs(smass - nsmass) < 0.001, ( "surfacemass computed w/ and w/o returnGrid are not the same" ) mvr = edf.meanvR( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, surfacemass=smass ) assert numpy.fabs(mvr) < 0.001, ( "meanvR of evolveddiskdf for axisymmetric potential is not equal to zero when calculated with pre-computed grid and surfacemass" ) global _maxi_meanvr _maxi_meanvr = mvr global _maxi_surfacemass _maxi_surfacemass = smass return None def test_mildnonaxi_meanvr_direct(): # Test that for an axisymmetric potential, the mean vr is close to zero # We do this for an axisymmetric potential, bc otherwise it takes too long idf = dehnendf(beta=0.0) pot = [LogarithmicHaloPotential(normalize=1.0)] edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvr = edf.meanvR(0.9, phi=0.2, integrate_method="rk6_c", grid=False) assert numpy.fabs(mvr) < 0.001, ( "meanvR of evolveddiskdf for axisymmetric potential is not equal to zero when calculated directly" ) return None def test_mildnonaxi_meanvr_grid_tlist(): # Test that for a close to axisymmetric potential, the mean vr is close to zero idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvr, grid = edf.meanvR( 0.9, t=[0.0, -2.5, -5.0, -7.5, -10.0], phi=0.2, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.all(numpy.fabs(mvr) < 0.003), ( "meanvR of evolveddiskdf for axisymmetric potential is not equal to zero for list of times" ) mvr = edf.meanvR( 0.9, t=[0.0, -2.5, -5.0, -7.5, -10.0], phi=0.2, integrate_method="rk6_c", grid=grid, ) assert numpy.all(numpy.fabs(mvr) < 0.003), ( "meanvR of evolveddiskdf for axisymmetric potential is not equal to zero when calculated with pre-computed grid for list of times" ) return None def test_mildnonaxi_meanvt_grid(): # Test that for a close to axisymmetric potential, the mean vt is close to that of the initial DF idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvt, grid = edf.meanvT( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005, ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf" ) mvt = edf.meanvT( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005, ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf when calculated with pre-computed grid" ) global _maxi_meanvt _maxi_meanvt = mvt return None def test_mildnonaxi_meanvt_hierarchgrid(): # Test that for a close to axisymmetric potential, the mean vt is close to that of the initial DF idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvt, grid = edf.meanvT( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, hierarchgrid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005, ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf when using hierarchgrid" ) mvt = edf.meanvT( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, gridpoints=_GRIDPOINTS ) assert numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005, ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf when calculated with pre-computed grid when using hierarchgrid" ) # Also test that the hierarchgrid is properly returned smass, ngrid = edf.vmomentsurfacemass( 0.9, 0, 0, phi=0.2, integrate_method="rk6_c", grid=grid, gridpoints=_GRIDPOINTS, returnGrid=True, ) assert ngrid == grid, ( "hierarchical grid returned by vmomentsurfacemass w/ grid input is not the same as the input" ) nsmass = edf.vmomentsurfacemass( 0.9, 0, 0, phi=0.2, integrate_method="rk6_c", grid=True, hierarchgrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(smass - nsmass) < 0.001, ( "surfacemass computed w/ and w/o returnGrid are not the same" ) return None def test_mildnonaxi_meanvt_hierarchgrid_tlist(): # Test that for a close to axisymmetric potential, the mean vt is close to that of the initial DF idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvt, grid = edf.meanvT( 0.9, t=[0.0, -2.5, -5.0, -7.5, -10.0], phi=0.2, integrate_method="rk6_c", grid=True, hierarchgrid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.all(numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005), ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf when using hierarchgrid and tlist" ) mvt = edf.meanvT( 0.9, t=[0.0, -2.5, -5.0, -7.5, -10.0], phi=0.2, integrate_method="rk6_c", grid=grid, gridpoints=_GRIDPOINTS, ) assert numpy.all(numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005), ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf when calculated with pre-computed grid when using hierarchgrid and tlist" ) return None def test_mildnonaxi_meanvt_hierarchgrid_zerolevels(): # Test that for a close to axisymmetric potential, the mean vt is close to that of the initial DF idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvt, grid = edf.meanvT( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, hierarchgrid=True, nlevels=0, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005, ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf when using hierarchgrid" ) mvt = edf.meanvT( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, gridpoints=_GRIDPOINTS ) assert numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005, ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf when calculated with pre-computed grid when using hierarchgrid" ) return None def test_mildnonaxi_meanvt_hierarchgrid_tlist_zerolevels(): # Test that for a close to axisymmetric potential, the mean vt is close to that of the initial DF idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvt, grid = edf.meanvT( 0.9, t=[0.0, -2.5, -5.0, -7.5, -10.0], phi=0.2, integrate_method="rk6_c", grid=True, hierarchgrid=True, nlevels=0, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.all(numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005), ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf when using hierarchgrid and tlist" ) mvt = edf.meanvT( 0.9, t=[0.0, -2.5, -5.0, -7.5, -10.0], phi=0.2, integrate_method="rk6_c", grid=grid, gridpoints=_GRIDPOINTS, ) assert numpy.all(numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005), ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf when calculated with pre-computed grid when using hierarchgrid and tlist" ) return None def test_mildnonaxi_meanvt_grid_rmEstimates(): # Test vmomentsurfacemass w/o having the _estimateX functions in the initial DF class fakeDehnen(dehnendf): # class that removes the _estimate functions def __init__(self, *args, **kwargs): dehnendf.__init__(self, *args, **kwargs) _estimatemeanvR = property() _estimatemeanvT = property() _estimateSigmaR2 = property() _estimateSigmaT2 = property() idf = fakeDehnen(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvt, grid = edf.meanvT( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005, ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf" ) return None def test_mildnonaxi_meanvt_direct(): # Test that for a close to axisymmetric potential, the mean vt is close to that of the initial DF # We do this for an axisymmetric potential, bc otherwise it takes too long idf = dehnendf(beta=0.0) pot = [LogarithmicHaloPotential(normalize=1.0)] edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvt = edf.meanvT(0.9, phi=0.2, integrate_method="rk6_c", grid=False) assert numpy.fabs(mvt - idf.meanvT(0.9)) < 0.001, ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf when using direct integration" ) return None def test_mildnonaxi_sigmar2_grid(): # Test that for a close to axisymmetric potential, the sigmaR2 is close to the value of the initial DF idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) sr2, grid = edf.sigmaR2( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) isr2 = idf.sigmaR2(0.9) assert numpy.fabs(numpy.log(sr2) - numpy.log(isr2)) < 0.025, ( "sigmar2 of evolveddiskdf for axisymmetric potential is not equal to that of initial DF" ) sr2 = edf.sigmaR2( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, gridpoints=_GRIDPOINTS ) assert numpy.fabs(numpy.log(sr2) - numpy.log(isr2)) < 0.025, ( "sigmar2 of evolveddiskdf for axisymmetric potential is not equal to that of initial DF when calculated with pre-computed grid" ) sr2 = edf.sigmaR2( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, meanvR=_maxi_meanvr, surfacemass=_maxi_surfacemass, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(numpy.log(sr2) - numpy.log(isr2)) < 0.025, ( "sigmar2 of evolveddiskdf for axisymmetric potential is not equal to that of initial DF when calculated with pre-computed grid and meanvR,surfacemass" ) global _maxi_sigmar2 _maxi_sigmar2 = sr2 return None def test_mildnonaxi_sigmar2_direct(): # Test that for an axisymmetric potential, the sigmaR2 is close to the value of the initial DF # We do this for an axisymmetric potential, bc otherwise it takes too long idf = dehnendf(beta=0.0) pot = [LogarithmicHaloPotential(normalize=1.0)] edf = evolveddiskdf(idf, pot=pot, to=-10.0) sr2 = edf.sigmaR2(0.9, phi=0.2, integrate_method="rk6_c", grid=False) isr2 = idf.sigmaR2(0.9) assert numpy.fabs(numpy.log(sr2) - numpy.log(isr2)) < 0.025, ( "sigmar2 of evolveddiskdf for axisymmetric potential is not equal to that of initial DF when calculated directly" ) return None def test_mildnonaxi_sigmat2_grid(): # Test that for a close to axisymmetric potential, the sigmaR2 is close to the value of the initial DF idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) st2, grid = edf.sigmaT2( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) ist2 = idf.sigmaT2(0.9) assert numpy.fabs(numpy.log(st2) - numpy.log(ist2)) < 0.025, ( "sigmat2 of evolveddiskdf for axisymmetric potential is not equal to that of initial DF" ) st2 = edf.sigmaT2( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, gridpoints=_GRIDPOINTS ) assert numpy.fabs(numpy.log(st2) - numpy.log(ist2)) < 0.025, ( "sigmat2 of evolveddiskdf for axisymmetric potential is not equal to that of initial DF when calculated with pre-computed grid" ) st2 = edf.sigmaT2( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, meanvT=_maxi_meanvt, surfacemass=_maxi_surfacemass, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(numpy.log(st2) - numpy.log(ist2)) < 0.025, ( "sigmat2 of evolveddiskdf for axisymmetric potential is not equal to that of initial DF when calculated with pre-computed grid and meanvR,surfacemass" ) global _maxi_sigmat2 _maxi_sigmat2 = st2 return None def test_mildnonaxi_sigmat2_direct(): # Test that for an axisymmetric potential, the sigmaT2 is close to the value of the initial DF # We do this for an axisymmetric potential, bc otherwise it takes too long idf = dehnendf(beta=0.0) pot = [LogarithmicHaloPotential(normalize=1.0)] edf = evolveddiskdf(idf, pot=pot, to=-10.0) st2 = edf.sigmaT2(0.9, phi=0.2, integrate_method="rk6_c", grid=False) ist2 = idf.sigmaT2(0.9) assert numpy.fabs(numpy.log(st2) - numpy.log(ist2)) < 0.025, ( "sigmat2 of evolveddiskdf for axisymmetric potential is not equal to that of initial DF when calculated directly" ) return None def test_mildnonaxi_sigmart_grid(): # Test that for a close to axisymmetric potential, the sigmaR2 is close to zero idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) srt, grid = edf.sigmaRT( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(srt) < 0.01, ( "sigmart of evolveddiskdf for axisymmetric potential is not equal to zero" ) srt = edf.sigmaRT( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, gridpoints=_GRIDPOINTS ) assert numpy.fabs(srt) < 0.01, ( "sigmart of evolveddiskdf for axisymmetric potential is not equal zero when calculated with pre-computed grid" ) srt = edf.sigmaRT( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, meanvR=_maxi_meanvr, meanvT=_maxi_meanvt, surfacemass=_maxi_surfacemass, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(srt) < 0.01, ( "sigmart of evolveddiskdf for axisymmetric potential is not equal to zero when calculated with pre-computed grid and meanvR,surfacemass" ) global _maxi_sigmart _maxi_sigmart = srt return None def test_mildnonaxi_sigmart_direct(): # Test that for an axisymmetric potential, the sigmaRT is close zero # We do this for an axisymmetric potential, bc otherwise it takes too long idf = dehnendf(beta=0.0) pot = [LogarithmicHaloPotential(normalize=1.0)] edf = evolveddiskdf(idf, pot=pot, to=-10.0) srt = edf.sigmaRT(0.9, phi=0.2, integrate_method="rk6_c", grid=False) assert numpy.fabs(srt) < 0.01, ( "sigmart of evolveddiskdf for axisymmetric potential is not equal to zero when calculated directly" ) return None def test_mildnonaxi_vertexdev_grid(): # Test that for a close to axisymmetric potential, the vertex deviation is close to zero idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) vdev, grid = edf.vertexdev( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(vdev) < 2.0 / 180.0 * numpy.pi, ( "vertexdev of evolveddiskdf for axisymmetric potential is not close to zero" ) # 2 is pretty big, but the weak spiral creates that vdev = edf.vertexdev( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, gridpoints=_GRIDPOINTS ) assert numpy.fabs(vdev) < 2.0 / 180.0 * numpy.pi, ( "vertexdev of evolveddiskdf for axisymmetric potential is not equal zero when calculated with pre-computed grid" ) vdev = edf.vertexdev( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, sigmaR2=_maxi_sigmar2, sigmaT2=_maxi_sigmat2, sigmaRT=_maxi_sigmart, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(vdev) < 2.0 / 180.0 * numpy.pi, ( "sigmart of evolveddiskdf for axisymmetric potential is not equal to zero when calculated with pre-computed sigmaR2,sigmaT2,sigmaRT" ) return None def test_mildnonaxi_vertexdev_direct(): # Test that for an axisymmetric potential, the vertex deviation is close zero # We do this for an axisymmetric potential, bc otherwise it takes too long idf = dehnendf(beta=0.0) pot = [LogarithmicHaloPotential(normalize=1.0)] edf = evolveddiskdf(idf, pot=pot, to=-10.0) vdev = edf.vertexdev(0.9, phi=0.2, integrate_method="rk6_c", grid=False) assert numpy.fabs(vdev) < 0.01 / 180.0 * numpy.pi, ( "vertexdev of evolveddiskdf for axisymmetric potential is not equal to zero when calculated directly" ) return None def test_mildnonaxi_oortA_grid(): # Test that for a close to axisymmetric potential, the oortA is close to the value of the initial DF idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(twophio=0.001), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) oa, grid, dgridR, dgridphi = edf.oortA( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, derivRGrid=True, derivphiGrid=True, returnGrids=True, gridpoints=_GRIDPOINTS, derivGridpoints=_GRIDPOINTS, ) ioa = idf.oortA(0.9) assert numpy.fabs(oa - ioa) < 0.005, ( "oortA of evolveddiskdf for axisymmetric potential is not equal to that of initial DF" ) oa = edf.oortA( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, derivRGrid=dgridR, derivphiGrid=dgridphi, gridpoints=_GRIDPOINTS, derivGridpoints=_GRIDPOINTS, ) assert numpy.fabs(oa - ioa) < 0.005, ( "oortA of evolveddiskdf for axisymmetric potential is not equal to that of initial DF when calculated with pre-computed grid" ) return None def test_mildnonaxi_oortA_grid_tlist(): # Test that for a close to axisymmetric potential, the oortA is close to the value of the initial DF idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(twophio=0.001), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) oa, grid, dgridR, dgridphi = edf.oortA( 0.9, t=[0.0, -2.5, -5.0, -7.5, -10.0], phi=0.2, integrate_method="rk6_c", grid=True, derivRGrid=True, derivphiGrid=True, returnGrids=True, gridpoints=_GRIDPOINTS, derivGridpoints=_GRIDPOINTS, ) ioa = idf.oortA(0.9) assert numpy.all(numpy.fabs(oa - ioa) < 0.005), ( "oortA of evolveddiskdf for axisymmetric potential is not equal to that of initial DF" ) oa = edf.oortA( 0.9, t=[0.0, -2.5, -5.0, -7.5, -10.0], phi=0.2, integrate_method="rk6_c", grid=grid, derivRGrid=dgridR, derivphiGrid=dgridphi, gridpoints=_GRIDPOINTS, derivGridpoints=_GRIDPOINTS, ) assert numpy.all(numpy.fabs(oa - ioa) < 0.005), ( "oortA of evolveddiskdf for axisymmetric potential is not equal to that of initial DF when calculated with pre-computed grid" ) return None def test_mildnonaxi_oortB_grid(): # Test that for a close to axisymmetric potential, the oortB is close to the value of the initial DF idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(twophio=0.001), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) ob, grid, dgridR, dgridphi = edf.oortB( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, derivRGrid=True, derivphiGrid=True, returnGrids=True, gridpoints=_GRIDPOINTS, derivGridpoints=_GRIDPOINTS, ) iob = idf.oortB(0.9) assert numpy.fabs(ob - iob) < 0.005, ( "oortB of evolveddiskdf for axisymmetric potential is not equal to that of initial DF" ) ob = edf.oortB( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, derivRGrid=dgridR, derivphiGrid=dgridphi, gridpoints=_GRIDPOINTS, derivGridpoints=_GRIDPOINTS, ) assert numpy.fabs(ob - iob) < 0.005, ( "oortB of evolveddiskdf for axisymmetric potential is not equal to that of initial DF when calculated with pre-computed grid" ) return None def test_mildnonaxi_oortC_grid(): # Test that for a close to axisymmetric potential, the oortC is close to zero idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(twophio=0.001), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) oc, grid, dgridR, dgridphi = edf.oortC( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, derivRGrid=True, derivphiGrid=True, returnGrids=True, gridpoints=_GRIDPOINTS, derivGridpoints=_GRIDPOINTS, ) assert numpy.fabs(oc) < 0.005, ( "oortC of evolveddiskdf for axisymmetric potential is not equal to that of initial DF" ) oc = edf.oortC( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, derivRGrid=dgridR, derivphiGrid=dgridphi, gridpoints=_GRIDPOINTS, derivGridpoints=_GRIDPOINTS, ) assert numpy.fabs(oc) < 0.005, ( "oortC of evolveddiskdf for axisymmetric potential is not equal to that of initial DF when calculated with pre-computed grid" ) return None def test_mildnonaxi_oortK_grid(): # Test that for a close to axisymmetric potential, the oortK is close to zero idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(twophio=0.001), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) ok, grid, dgridR, dgridphi = edf.oortK( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, derivRGrid=True, derivphiGrid=True, returnGrids=True, gridpoints=_GRIDPOINTS, derivGridpoints=_GRIDPOINTS, ) assert numpy.fabs(ok) < 0.005, ( "oortK of evolveddiskdf for axisymmetric potential is not equal to that of initial DF" ) ok = edf.oortK( 0.9, phi=0.2, integrate_method="rk6_c", grid=grid, derivRGrid=dgridR, derivphiGrid=dgridphi, gridpoints=_GRIDPOINTS, derivGridpoints=_GRIDPOINTS, ) assert numpy.fabs(ok) < 0.005, ( "oortK of evolveddiskdf for axisymmetric potential is not equal to that of initial DF when calculated with pre-computed grid" ) return None # Some special cases def test_mildnonaxi_meanvt_grid_tlist_onet(): # Test that for a close to axisymmetric potential, the mean vt is close to that of the initial DF, for a list consisting of a single time idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvt, grid = edf.meanvT( 0.9, t=[0.0], phi=0.2, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005, ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf" ) mvt = edf.meanvT( 0.9, t=[0.0], phi=0.2, integrate_method="rk6_c", grid=grid, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(mvt - idf.meanvT(0.9)) < 0.005, ( "meanvT of evolveddiskdf for axisymmetric potential is not equal to that of the initial dehnendf when calculated with pre-computed grid" ) global _maxi_meanvt _maxi_meanvt = mvt return None def test_mildnonaxi_meanvt_direct_tlist(): # Shouldn't work idf = dehnendf(beta=0.0) pot = [LogarithmicHaloPotential(normalize=1.0)] edf = evolveddiskdf(idf, pot=pot, to=-10.0) try: edf.meanvT( 0.9, t=[0.0, -2.5, -5.0, -7.5, -10.0], phi=0.2, integrate_method="rk6_c", grid=False, ) except OSError: pass else: raise AssertionError( "direct evolveddiskdf calculation of meanvT w/ list of times did not raise IOError" ) return None # Tests with significant nonaxi, but cold def test_elliptical_cold_vr(): # Test that the radial velocity for the elliptical disk behaves as analytically expected idf = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.0125)) cp = 0.05 pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(cp=cp, sp=0.0, p=0.0, tform=-150.0, tsteady=125.0), ] edf = evolveddiskdf(idf, pot=pot, to=-150.0) # Should be cp mvr, grid = edf.meanvR( 0.9, phi=-numpy.pi / 4.0, integrate_method="rk6_c", grid=True, nsigma=7.0, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(mvr - cp) < 10.0**-4.0, ( "Cold elliptical disk does not agree with analytical calculation for vr" ) # Should be 0 mvr, grid = edf.meanvR( 0.9, phi=0.0, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(mvr) < 10.0**-4.0, ( "Cold elliptical disk does not agree with analytical calculation for vr" ) return None def test_elliptical_cold_vt(): # Test that the rotational velocity for the elliptical disk behaves as analytically expected idf = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.0125)) cp = 0.05 pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(cp=cp, sp=0.0, p=0.0, tform=-150.0, tsteady=125.0), ] edf = evolveddiskdf(idf, pot=pot, to=-150.0) # Should be 1. mvt, grid = edf.meanvT( 0.9, phi=-numpy.pi / 4.0, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(mvt - 1.0) < 10.0**-3.0, ( "Cold elliptical disk does not agree with analytical calculation for vt" ) # Should be 1.-cp mvt, grid = edf.meanvT( 0.9, phi=0.0, integrate_method="rk6_c", grid=True, nsigma=7.0, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(mvt - 1.0 + cp) < 10.0**-3.0, ( "Cold elliptical disk does not agree with analytical calculation for vt" ) return None def test_elliptical_cold_vertexdev(): # Test that the vertex deviations for the elliptical disk behaves as analytically expected idf = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.0125)) cp = 0.05 pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(cp=cp, sp=0.0, p=0.0, tform=-150.0, tsteady=125.0), ] edf = evolveddiskdf(idf, pot=pot, to=-150.0) # Should be -2cp in radians vdev, grid = edf.vertexdev( 0.9, phi=-numpy.pi / 4.0, integrate_method="rk6_c", grid=True, nsigma=7.0, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(vdev + 2.0 * cp) < 10.0**-3.0, ( "Cold elliptical disk does not agree with analytical calculation for vertexdev" ) # Should be 0 vdev, grid = edf.vertexdev( 0.9, phi=0.0, integrate_method="rk6_c", grid=True, nsigma=7.0, returnGrid=True, gridpoints=_GRIDPOINTS, ) assert numpy.fabs(vdev) < 10.0**-2.0 / 180.0 * numpy.pi, ( "Cold elliptical disk does not agree with analytical calculation for vertexdev" ) return None def test_elliptical_cold_oortABCK_position1(): # Test that the Oort functions A, B, C, and K for the elliptical disk behaves as analytically expected idf = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.0125)) cp = 0.05 pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(cp=cp, sp=0.0, p=0.0, tform=-150.0, tsteady=125.0), ] edf = evolveddiskdf(idf, pot=pot, to=-150.0) # Should be 0.5/0.9 oorta, grid, gridr, gridp = edf.oortA( 0.9, phi=-numpy.pi / 4.0, integrate_method="rk6_c", grid=True, nsigma=7.0, derivRGrid=True, derivphiGrid=True, returnGrids=True, gridpoints=51, derivGridpoints=51, ) assert numpy.fabs(oorta - 0.5 / 0.9) < 10.0**-3.0, ( "Cold elliptical disk does not agree with analytical calculation for oortA" ) # Also check other Oort constants # Should be -0.5/0.9 oortb = edf.oortB( 0.9, phi=-numpy.pi / 4.0, integrate_method="rk6_c", grid=grid, nsigma=7.0, derivRGrid=gridr, derivphiGrid=gridp, ) assert numpy.fabs(oortb + 0.5 / 0.9) < 10.0**-3.0, ( "Cold elliptical disk does not agree with analytical calculation for oortB" ) # Should be cp/2 oortc = edf.oortC( 0.9, phi=-numpy.pi / 4.0, integrate_method="rk6_c", grid=grid, nsigma=7.0, derivRGrid=gridr, derivphiGrid=gridp, ) assert numpy.fabs(oortc - cp / 2.0) < 10.0**-2.2, ( "Cold elliptical disk does not agree with analytical calculation for oortC" ) # Should be -cp/2 oortk = edf.oortK( 0.9, phi=-numpy.pi / 4.0, integrate_method="rk6_c", grid=grid, nsigma=7.0, derivRGrid=gridr, derivphiGrid=gridp, ) assert numpy.fabs(oortk + cp / 2.0) < 10.0**-2.2, ( "Cold elliptical disk does not agree with analytical calculation for oortK" ) return None def test_elliptical_cold_oortABCK_position2(): # Test that the Oort functions A, B, C, and K for the elliptical disk behaves as analytically expected idf = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.0125)) cp = 0.05 pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(cp=cp, sp=0.0, p=0.0, tform=-150.0, tsteady=125.0), ] edf = evolveddiskdf(idf, pot=pot, to=-150.0) # Should be 0.5/0.9+cp/2 oorta, grid, gridr, gridp = edf.oortA( 0.9, phi=0.0, integrate_method="rk6_c", grid=True, nsigma=7.0, derivRGrid=True, derivphiGrid=True, returnGrids=True, gridpoints=51, derivGridpoints=51, ) assert numpy.fabs(oorta - cp / 2.0 - 0.5 / 0.9) < 10.0**-2.2, ( "Cold elliptical disk does not agree with analytical calculation for oortA" ) # Should be -cp/2-0.5/0.9 oortb = edf.oortB( 0.9, phi=0.0, integrate_method="rk6_c", grid=grid, nsigma=7.0, derivRGrid=gridr, derivphiGrid=gridp, ) assert numpy.fabs(oortb + cp / 2.0 + 0.5 / 0.9) < 10.0**-2.2, ( "Cold elliptical disk does not agree with analytical calculation for oortB" ) # Should be 0 oortc = edf.oortC( 0.9, phi=0.0, integrate_method="rk6_c", grid=grid, nsigma=7.0, derivRGrid=gridr, derivphiGrid=gridp, ) assert numpy.fabs(oortc) < 10.0**-3.0, ( "Cold elliptical disk does not agree with analytical calculation for oortC" ) # Should be 0 oortk = edf.oortK( 0.9, phi=0.0, integrate_method="rk6_c", grid=grid, nsigma=7.0, derivRGrid=gridr, derivphiGrid=gridp, ) assert numpy.fabs(oortk) < 10.0**-3.0, ( "Cold elliptical disk does not agree with analytical calculation for oortK" ) return None def test_call_special(): from galpy.orbit import Orbit idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(twophio=0.001), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) o = Orbit([0.9, 0.1, 1.1, 2.0]) # call w/ and w/o explicit t assert numpy.fabs(numpy.log(edf(o, 0.0)) - numpy.log(edf(o))) < 10.0**-10.0, ( "edf.__call__ w/ explicit t=0. and w/o t do not give the same answer" ) # call must get Orbit, otherwise error try: edf(0.9, 0.1, 1.1, 2.0) except OSError: pass else: raise AssertionError("edf.__call__ w/o Orbit input did not raise IOError") # Call w/ list, but just to assert numpy.fabs(numpy.log(edf(o, [-10.0])) - numpy.log(idf(o))) < 10.0**-10.0, ( "edf.__call__ w/ tlist set to [to] did not return initial DF" ) # Call w/ just to assert numpy.fabs(numpy.log(edf(o, -10.0)) - numpy.log(idf(o))) < 10.0**-10.0, ( "edf.__call__ w/ tlist set to [to] did not return initial DF" ) # also w/ log assert numpy.fabs(edf(o, [-10.0], log=True) - numpy.log(idf(o))) < 10.0**-10.0, ( "edf.__call__ w/ tlist set to [to] did not return initial DF (log)" ) assert numpy.fabs(edf(o, -10.0, log=True) - numpy.log(idf(o))) < 10.0**-10.0, ( "edf.__call__ w/ tlist set to [to] did not return initial DF (log)" ) # Tests w/ odeint: tlist, one NaN codeint = edf( o, [0.0, -2.5, -5.0, -7.5, -10.0], integrate_method="odeint", log=True ) crk6c = edf(o, [0.0, -2.5, -5.0, -7.5, -10.0], integrate_method="rk6_c", log=True) assert numpy.all(numpy.fabs(codeint - crk6c) < 10.0**-4.0), ( "edf.__call__ w/ odeint and tlist does not give the same result as w/ rk6_c" ) # Crazy orbit w/ tlist crk6c = edf( Orbit([3.0, 1.0, -1.0, 2.0]), [0.0], integrate_method="odeint", log=True ) assert crk6c < -20.0, "crazy orbit does not have DF equal to zero" # deriv w/ odeint codeint = edf( o, [0.0, -2.5, -5.0, -7.5, -10.0], integrate_method="odeint", deriv="R" ) crk6c = edf(o, [0.0, -2.5, -5.0, -7.5, -10.0], integrate_method="rk6_c", deriv="R") assert numpy.all(numpy.fabs(codeint - crk6c) < 10.0**-4.0), ( "edf.__call__ w/ odeint and tlist does not give the same result as w/ rk6_c (deriv=R)" ) # deriv w/ len(tlist)=1 crk6c = edf(o, [0.0], integrate_method="rk6_c", deriv="R") crk6c2 = edf(o, 0.0, integrate_method="rk6_c", deriv="R") assert numpy.all(numpy.fabs(crk6c - crk6c2) < 10.0**-4.0), ( "edf.__call__ w/ tlist consisting of one time and just a scalar time do not agree" ) # Call w/ just to and deriv assert ( numpy.fabs( edf(o, -10.0, deriv="R") - idf(o) * idf._dlnfdR(o.vxvv[0, 0], o.vxvv[0, 1], o.vxvv[0, 2]) ) < 10.0**-10.0 ), "edf.__call__ w/ to did not return initial DF (deriv=R)" assert numpy.fabs(edf(o, -10.0, deriv="phi")) < 10.0**-10.0, ( "edf.__call__ w/ to did not return initial DF (deriv=phi)" ) # Call w/ just one t and odeint codeint = edf(o, 0, integrate_method="odeint", log=True) crk6c = edf(o, 0.0, integrate_method="rk6_c", log=True) assert numpy.fabs(codeint - crk6c) < 10.0**-4.0, ( "edf.__call__ w/ odeint and tlist does not give the same result as w/ rk6_c" ) # Call w/ just one t and fallback to odeint # turn off C edf._pot[0].hasC = False edf._pot[0].hasC_dxdv = False codeint = edf(o, 0, integrate_method="dopr54_c", log=True) assert numpy.fabs(codeint - crk6c) < 10.0**-4.0, ( "edf.__call__ w/ odeint and tlist does not give the same result as w/ rk6_c" ) # Call w/ just one t and fallback to leaprog cleapfrog = edf(o, 0, integrate_method="leapfrog_c", log=True) assert numpy.fabs(cleapfrog - crk6c) < 10.0**-4.0, ( "edf.__call__ w/ odeint and tlist does not give the same result as w/ rk6_c" ) # Call w/ just one t agrees whether or not t is list cleapfrog_list = edf(o, [-2.5], integrate_method="leapfrog_c", log=True) cleapfrog_scal = edf(o, -2.5, integrate_method="leapfrog_c", log=True) assert numpy.fabs(cleapfrog_list - cleapfrog_scal) < 10.0**-4.0, ( "edf.__call__ w/ single t scalar or tlist does not give the same result" ) # Radial orbit o = Orbit([1.0, -1.0, 0.0, 0.0]) assert numpy.fabs(edf(o, 0.0)) < 10.0**-10.0, ( "edf.__call__ w/ radial orbit does not return zero" ) def test_call_marginalizevperp(): from galpy.orbit import Orbit idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(twophio=0.001), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot[0], to=-10.0) # one with just one potential # l=0 R, phi, vR = 0.8, 0.0, 0.4 vts = numpy.linspace(0.0, 1.5, 51) pvts = numpy.array( [edf(Orbit([R, vR, vt, phi]), integrate_method="rk6_c") for vt in vts] ) assert ( numpy.fabs( numpy.sum(pvts) * (vts[1] - vts[0]) - edf( Orbit([R, vR, 0.0, phi]), marginalizeVperp=True, integrate_method="rk6_c", ) ) < 10.0**-3.5 ), "evolveddiskdf call w/ marginalizeVperp does not work" # l=270 edf = evolveddiskdf(idf, pot=pot, to=-10.0) R, phi, vT = numpy.sin(numpy.pi / 6.0), -numpy.pi / 3.0, 0.7 # l=30 degree vrs = numpy.linspace(-1.0, 1.0, 101) pvrs = numpy.array( [edf(Orbit([R, vr, vT, phi]), integrate_method="rk6_c") for vr in vrs] ) assert ( numpy.fabs( numpy.log(numpy.sum(pvrs) * (vrs[1] - vrs[0])) - edf( Orbit([R, 0.0, vT, phi]), marginalizeVperp=True, integrate_method="rk6_c", log=True, nsigma=4, ) ) < 10.0**-2.5 ), "evolveddiskdf call w/ marginalizeVperp does not work" return None def test_call_marginalizevlos(): from galpy.orbit import Orbit idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), EllipticalDiskPotential(twophio=0.001), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot[0], to=-10.0) # one with just one potential # l=0 R, phi, vT = 0.8, 0.0, 0.7 vrs = numpy.linspace(-1.0, 1.0, 101) pvrs = numpy.array( [edf(Orbit([R, vr, vT, phi]), integrate_method="rk6_c") for vr in vrs] ) assert ( numpy.fabs( numpy.log(numpy.sum(pvrs) * (vrs[1] - vrs[0])) - edf( Orbit([R, 0.0, vT, phi]), marginalizeVlos=True, integrate_method="rk6_c", log=True, ) ) < 10.0**-4.0 ), "diskdf call w/ marginalizeVlos does not work" # l=270, this DF has some issues, but it suffices to test the mechanics of the code edf = evolveddiskdf(idf, pot=pot, to=-10.0) R, phi, vR = numpy.sin(numpy.pi / 6.0), -numpy.pi / 3.0, 0.4 # l=30 degree vts = numpy.linspace(0.3, 1.5, 101) pvts = numpy.array( [edf(Orbit([R, vR, vt, phi]), integrate_method="rk6_c") for vt in vts] ) assert ( numpy.fabs( numpy.sum(pvts) * (vts[1] - vts[0]) - edf( Orbit([R, vR, 0.0, phi]), marginalizeVlos=True, integrate_method="rk6_c", nsigma=4, ) ) < 10.0**-3.5 ), "diskdf call w/ marginalizeVlos does not work" return None def test_plot_grid(): idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvr, grid = edf.meanvR( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) grid.plot() # w/ list of times mvr, grid = edf.meanvR( 0.9, t=[0.0, -2.5, -5.0, -7.5, -10.0], phi=0.2, integrate_method="rk6_c", grid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) grid.plot(1) return None def test_plot_hierarchgrid(): idf = dehnendf(beta=0.0) pot = [ LogarithmicHaloPotential(normalize=1.0), SteadyLogSpiralPotential(A=-0.005, omegas=0.2), ] # very mild non-axi edf = evolveddiskdf(idf, pot=pot, to=-10.0) mvr, grid = edf.meanvR( 0.9, phi=0.2, integrate_method="rk6_c", grid=True, hierarchgrid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) grid.plot() # w/ list of times mvr, grid = edf.meanvR( 0.9, t=[0.0, -2.5, -5.0, -7.5, -10.0], phi=0.2, integrate_method="rk6_c", grid=True, hierarchgrid=True, returnGrid=True, gridpoints=_GRIDPOINTS, ) grid.plot(1) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_galpypaper.py0000644000175100001660000007405114761352023017243 0ustar00runnerdocker"""Test that all of the examples in the galpy paper run isort:skip_file """ import os import numpy import pytest def test_overview(): from galpy.potential import NFWPotential np = NFWPotential(normalize=1.0) from galpy.orbit import Orbit o = Orbit(vxvv=[1.0, 0.1, 1.1, 0.1, 0.02, 0.0]) from galpy.actionAngle import actionAngleSpherical aA = actionAngleSpherical(pot=np) js = aA(o) assert numpy.fabs((js[0] - 0.00980542) / js[0]) < 10.0**-3.0, ( "Action calculation in the overview section has changed" ) assert numpy.fabs((js[1] - 1.1) / js[0]) < 10.0**-3.0, ( "Action calculation in the overview section has changed" ) assert numpy.fabs((js[2] - 0.00553155) / js[0]) < 10.0**-3.0, ( "Action calculation in the overview section has changed" ) from galpy.df import quasiisothermaldf qdf = quasiisothermaldf(1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=np, aA=aA) assert numpy.fabs((qdf(o) - 61.57476085) / 61.57476085) < 10.0**-3.0, ( "qdf calculation in the overview section has changed" ) return None def test_import(): import galpy import galpy.potential import galpy.orbit import galpy.actionAngle import galpy.df import galpy.util return None def test_units(): # Import changed because of bovy_conversion --> conversion name change from galpy.util import conversion print(conversion.force_in_pcMyr2(220.0, 8.0)) # pc/Myr^2 assert ( numpy.fabs(conversion.force_in_pcMyr2(220.0, 8.0) - 6.32793804994) < 10.0**-4.0 ), "unit conversion has changed" print(conversion.dens_in_msolpc3(220.0, 8.0)) # Msolar/pc^3 # Loosen tolerances including mass bc of 0.025% change in Msun in astropyv2 assert ( numpy.fabs( (conversion.dens_in_msolpc3(220.0, 8.0) - 0.175790330079) / 0.175790330079 ) < 0.0003 ), "unit conversion has changed" print(conversion.surfdens_in_msolpc2(220.0, 8.0)) # Msolar/pc^2 assert ( numpy.fabs( (conversion.surfdens_in_msolpc2(220.0, 8.0) - 1406.32264063) / 1406.32264063 ) < 0.0003 ), "unit conversion has changed" print(conversion.mass_in_1010msol(220.0, 8.0)) # 10^10 Msolar assert ( numpy.fabs( (conversion.mass_in_1010msol(220.0, 8.0) - 9.00046490005) / 9.00046490005 ) < 0.0003 ), "unit conversion has changed" print(conversion.freq_in_Gyr(220.0, 8.0)) # 1/Gyr assert ( numpy.fabs(conversion.freq_in_Gyr(220.0, 8.0) - 28.1245845523) < 10.0**-4.0 ), "unit conversion has changed" print(conversion.time_in_Gyr(220.0, 8.0)) # Gyr assert ( numpy.fabs(conversion.time_in_Gyr(220.0, 8.0) - 0.0355560807712) < 10.0**-4.0 ), "unit conversion has changed" return None def test_potmethods(): from galpy.potential import DoubleExponentialDiskPotential dp = DoubleExponentialDiskPotential(normalize=1.0, hr=3.0 / 8.0, hz=0.3 / 8.0) dp(1.0, 0.1) # The potential itself at R=1., z=0.1 assert numpy.fabs(dp(1.0, 0.1) + 1.1037196286636572) < 10.0**-4.0, ( "potmethods has changed" ) dp.Rforce(1.0, 0.1) # The radial force assert numpy.fabs(dp.Rforce(1.0, 0.1) + 0.9147659436328015) < 10.0**-4.0, ( "potmethods has changed" ) dp.zforce(1.0, 0.1) # The vertical force assert numpy.fabs(dp.zforce(1.0, 0.1) + 0.50056789703079607) < 10.0**-4.0, ( "potmethods has changed" ) dp.R2deriv(1.0, 0.1) # The second radial derivative # Loosened tolerance, because new (more precise) calculation differs by 3e-4 assert numpy.fabs(dp.R2deriv(1.0, 0.1) + 1.0189440730205248) < 3 * 10.0**-4.0, ( "potmethods has changed" ) dp.z2deriv(1.0, 0.1) # The second vertical derivative # Loosened tolerance, because new (more precise) calculation differs by 4e-4 assert numpy.fabs(dp.z2deriv(1.0, 0.1) - 1.0648350937842703) < 4 * 10.0**-4.0, ( "potmethods has changed" ) dp.Rzderiv(1.0, 0.1) # The mixed radial,vertical derivative assert numpy.fabs(dp.Rzderiv(1.0, 0.1) + 1.1872449759212851) < 10.0**-4.0, ( "potmethods has changed" ) dp.dens(1.0, 0.1) # The density assert numpy.fabs(dp.dens(1.0, 0.1) - 0.076502355610946121) < 10.0**-4.0, ( "potmethods has changed" ) dp.dens(1.0, 0.1, forcepoisson=True) # Using Poisson's eqn. assert ( numpy.fabs(dp.dens(1.0, 0.1, forcepoisson=True) - 0.076446652249682681) < 10.0**-4.0 ), "potmethods has changed" dp.mass(1.0, 0.1) # The mass assert numpy.fabs(dp.mass(1.0, 0.1) - 0.7281629803939751) < 10.0**-4.0, ( "potmethods has changed" ) dp.vcirc(1.0) # The circular velocity at R=1. assert numpy.fabs(dp.vcirc(1.0) - 1.0) < 10.0**-4.0, ( "potmethods has changed" ) # By definition, because of normalize=1. dp.omegac(1.0) # The rotational frequency assert numpy.fabs(dp.omegac(1.0) - 1.0) < 10.0**-4.0, ( "potmethods has changed" ) # Also because of normalize=1. dp.epifreq(1.0) # The epicycle frequency # Loosened tolerance, because new (more precise) calculation differs by 1e-3 assert numpy.fabs(dp.epifreq(1.0) - 1.3301123099210266) < 2 * 10.0**-3.0, ( "potmethods has changed" ) dp.verticalfreq(1.0) # The vertical frequency # Loosened tolerance, because new (more precise) calculation differs by 1e-3 assert numpy.fabs(dp.verticalfreq(1.0) - 3.7510872575640293) < 10.0**-3.0, ( "potmethods has changed" ) dp.flattening(1.0, 0.1) # The flattening (see caption) assert numpy.fabs(dp.flattening(1.0, 0.1) - 0.42748757564198159) < 10.0**-4.0, ( "potmethods has changed" ) dp.lindbladR(1.75, m="corotation") # co-rotation resonance assert ( numpy.fabs(dp.lindbladR(1.75, m="corotation") - 0.540985051273488) < 10.0**-4.0 ), "potmethods has changed" return None from galpy.potential import Potential def smoothInterp(t, dt, tform): """Smooth interpolation in time, following Dehnen (2000)""" if t < tform: smooth = 0.0 elif t > (tform + dt): smooth = 1.0 else: xi = 2.0 * (t - tform) / dt - 1.0 smooth = 3.0 / 16.0 * xi**5.0 - 5.0 / 8 * xi**3.0 + 15.0 / 16.0 * xi + 0.5 return smooth class TimeInterpPotential(Potential): """Potential that smoothly interpolates in time between two static potentials""" def __init__(self, pot1, pot2, dt=100.0, tform=50.0): """pot1= potential for t < tform, pot2= potential for t > tform+dt, dt: time over which to turn on pot2, tform: time at which the interpolation is switched on""" Potential.__init__(self, amp=1.0) self._pot1 = pot1 self._pot2 = pot2 self._tform = tform self._dt = dt return None def _Rforce(self, R, z, phi=0.0, t=0.0): smooth = smoothInterp(t, self._dt, self._tform) return (1.0 - smooth) * self._pot1.Rforce(R, z) + smooth * self._pot2.Rforce( R, z ) def _zforce(self, R, z, phi=0.0, t=0.0): smooth = smoothInterp(t, self._dt, self._tform) return (1.0 - smooth) * self._pot1.zforce(R, z) + smooth * self._pot2.zforce( R, z ) def test_TimeInterpPotential(): # Just to check that the code above has run properly from galpy.potential import LogarithmicHaloPotential, MiyamotoNagaiPotential lp = LogarithmicHaloPotential(normalize=1.0) mp = MiyamotoNagaiPotential(normalize=1.0) tip = TimeInterpPotential(lp, mp) assert ( numpy.fabs(tip.Rforce(1.0, 0.1, t=10.0) - lp.Rforce(1.0, 0.1)) < 10.0**-8.0 ), "TimeInterPotential does not work as expected" assert ( numpy.fabs(tip.Rforce(1.0, 0.1, t=200.0) - mp.Rforce(1.0, 0.1)) < 10.0**-8.0 ), "TimeInterPotential does not work as expected" return None @pytest.mark.skip(reason="Test does not work correctly") def test_potentialAPIChange_warning(): # Test that a warning is displayed about the API change for evaluatePotentials etc. functions from what is given in the galpy paper # Turn warnings into errors to test for them import warnings from galpy.util import galpyWarning with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) import galpy.potential raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "A major change in versions > 1.1 is that all galpy.potential functions and methods take the potential as the first argument; previously methods such as evaluatePotentials, evaluateDensities, etc. would be called with (R,z,Pot), now they are called as (Pot,R,z) for greater consistency across the codebase" ) if raisedWarning: break assert raisedWarning, ( "Importing galpy.potential does not raise warning about evaluatePotentials API change" ) return None def test_orbitint(): import numpy from galpy.potential import MWPotential2014 from galpy.potential import evaluatePotentials as evalPot from galpy.orbit import Orbit E, Lz = -1.25, 0.6 o1 = Orbit( [ 0.8, 0.0, Lz / 0.8, 0.0, numpy.sqrt( 2.0 * (E - evalPot(MWPotential2014, 0.8, 0.0) - (Lz / 0.8) ** 2.0 / 2.0) ), 0.0, ] ) ts = numpy.linspace(0.0, 100.0, 2001) o1.integrate(ts, MWPotential2014) o1.plot(xrange=[0.3, 1.0], yrange=[-0.2, 0.2], color="k") o2 = Orbit( [ 0.8, 0.3, Lz / 0.8, 0.0, numpy.sqrt( 2.0 * ( E - evalPot(MWPotential2014, 0.8, 0.0) - (Lz / 0.8) ** 2.0 / 2.0 - 0.3**2.0 / 2.0 ) ), 0.0, ] ) o2.integrate(ts, MWPotential2014) o2.plot(xrange=[0.3, 1.0], yrange=[-0.2, 0.2], color="k") return None def test_orbmethods(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 # 8/17/2019: added explicit z=0.025, because that was the default at the # time of the galpy paper, but the default has been changed o = Orbit([0.8, 0.3, 0.75, 0.0, 0.2, 0.0], zo=0.025) # setup R,vR,vT,z,vz,phi times = numpy.linspace(0.0, 10.0, 1001) # Output times o.integrate(times, MWPotential2014) # Integrate o.E() # Energy assert numpy.fabs(o.E() + 1.2547650648697966) < 10.0**-5.0, ( "Orbit method does not work as expected" ) o.L() # Angular momentum assert numpy.all( numpy.fabs(o.L() - numpy.array([[0.0, -0.16, 0.6]])) < 10.0**-5.0 ), "Orbit method does not work as expected" o.Jacobi(OmegaP=0.65) # Jacobi integral E-OmegaP Lz assert ( numpy.fabs(o.Jacobi(OmegaP=0.65) - numpy.array([-1.64476506])) < 10.0**-5.0 ), "Orbit method does not work as expected" o.ER(times[-1]), o.Ez(times[-1]) # Rad. and vert. E at end assert numpy.fabs(o.ER(times[-1]) + 1.27601734263047) < 10.0**-5.0, ( "Orbit method does not work as expected" ) assert numpy.fabs(o.Ez(times[-1]) - 0.021252201847851909) < 10.0**-5.0, ( "Orbit method does not work as expected" ) o.rperi(), o.rap(), o.zmax() # Peri-/apocenter r, max. |z| assert numpy.fabs(o.rperi() - 0.44231993168097) < 10.0**-5.0, ( "Orbit method does not work as expected" ) assert numpy.fabs(o.rap() - 0.87769030382105) < 10.0**-5.0, ( "Orbit method does not work as expected" ) assert numpy.fabs(o.zmax() - 0.077452357289016) < 10.0**-5.0, ( "Orbit method does not work as expected" ) o.e() # eccentricity (rap-rperi)/(rap+rperi) assert numpy.fabs(o.e() - 0.32982348199330563) < 10.0**-5.0, ( "Orbit method does not work as expected" ) o.R(2.0, ro=8.0) # Cylindrical radius at time 2. in kpc assert numpy.fabs(o.R(2.0, ro=8.0) - 3.5470772876920007) < 10.0**-3.0, ( "Orbit method does not work as expected" ) o.vR(5.0, vo=220.0) # Cyl. rad. velocity at time 5. in km/s assert numpy.fabs(o.vR(5.0, vo=220.0) - 45.202530965094553) < 10.0**-3.0, ( "Orbit method does not work as expected" ) o.ra(1.0), o.dec(1.0) # RA and Dec at t=1. (default settings) # 5/12/2016: test weakened, because improved galcen<->heliocen # transformation has changed these, but still close assert numpy.fabs(o.ra(1.0) - numpy.array([288.19277])) < 10.0**-1.0, ( "Orbit method does not work as expected" ) assert numpy.fabs(o.dec(1.0) - numpy.array([18.98069155])) < 10.0**-1.0, ( "Orbit method does not work as expected" ) o.jr(type="adiabatic"), o.jz() # R/z actions (ad. approx.) assert numpy.fabs(o.jr(type="adiabatic") - 0.05285302231137586) < 10.0**-3.0, ( "Orbit method does not work as expected" ) assert numpy.fabs(o.jz() - 0.006637988850751242) < 10.0**-3.0, ( "Orbit method does not work as expected" ) # Rad. period w/ Staeckel approximation w/ focal length 0.5, o.Tr(type="staeckel", delta=0.5, ro=8.0, vo=220.0) # in Gyr assert ( numpy.fabs( o.Tr(type="staeckel", delta=0.5, ro=8.0, vo=220.0) - 0.1039467864018446 ) < 10.0**-3.0 ), "Orbit method does not work as expected" o.plot(d1="R", d2="z") # Plot the orbit in (R,z) o.plot3d() # Plot the orbit in 3D, w/ default [x,y,z] return None def test_orbsetup(): from galpy.orbit import Orbit o = Orbit( [25.0, 10.0, 2.0, 5.0, -2.0, 50.0], radec=True, ro=8.0, vo=220.0, solarmotion=[-11.1, 25.0, 7.25], ) return None def test_surfacesection(): # Preliminary code import numpy from galpy.potential import MWPotential2014 from galpy.potential import evaluatePotentials as evalPot from galpy.orbit import Orbit E, Lz = -1.25, 0.6 o1 = Orbit( [ 0.8, 0.0, Lz / 0.8, 0.0, numpy.sqrt( 2.0 * (E - evalPot(MWPotential2014, 0.8, 0.0) - (Lz / 0.8) ** 2.0 / 2.0) ), 0.0, ] ) ts = numpy.linspace(0.0, 100.0, 2001) o1.integrate(ts, MWPotential2014) o2 = Orbit( [ 0.8, 0.3, Lz / 0.8, 0.0, numpy.sqrt( 2.0 * ( E - evalPot(MWPotential2014, 0.8, 0.0) - (Lz / 0.8) ** 2.0 / 2.0 - 0.3**2.0 / 2.0 ) ), 0.0, ] ) o2.integrate(ts, MWPotential2014) def surface_section(Rs, zs, vRs): # Find points where the orbit crosses z from - to + shiftzs = numpy.roll(zs, -1) indx = (zs[:-1] < 0.0) * (shiftzs[:-1] > 0.0) return (Rs[:-1][indx], vRs[:-1][indx]) # Calculate and plot the surface of section ts = numpy.linspace(0.0, 1000.0, 20001) # long integration o1.integrate(ts, MWPotential2014) o2.integrate(ts, MWPotential2014) sect1Rs, sect1vRs = surface_section(o1.R(ts), o1.z(ts), o1.vR(ts)) sect2Rs, sect2vRs = surface_section(o2.R(ts), o2.z(ts), o2.vR(ts)) from matplotlib.pyplot import plot, xlim, ylim plot(sect1Rs, sect1vRs, "bo", mec="none") xlim(0.3, 1.0) ylim(-0.69, 0.69) plot(sect2Rs, sect2vRs, "yo", mec="none") return None def test_adinvariance(): from galpy.potential import IsochronePotential from galpy.orbit import Orbit from galpy.actionAngle import actionAngleIsochrone # Initialize two different IsochronePotentials ip1 = IsochronePotential(normalize=1.0, b=1.0) ip2 = IsochronePotential(normalize=0.5, b=1.0) # Use TimeInterpPotential to interpolate smoothly tip = TimeInterpPotential(ip1, ip2, dt=100.0, tform=50.0) # Integrate: 1) Orbit in the first isochrone potential o1 = Orbit([1.0, 0.1, 1.1, 0.0, 0.1, 0.0]) ts = numpy.linspace(0.0, 50.0, 1001) o1.integrate(ts, tip) o1.plot(d1="x", d2="y", xrange=[-1.6, 1.6], yrange=[-1.6, 1.6], color="b") # 2) Orbit in the transition o2 = o1(ts[-1]) # Last time step => initial time step ts2 = numpy.linspace(50.0, 150.0, 1001) o2.integrate(ts2, tip) o2.plot(d1="x", d2="y", overplot=True, color="g") # 3) Orbit in the second isochrone potential o3 = o2(ts2[-1]) ts3 = numpy.linspace(150.0, 200.0, 1001) o3.integrate(ts3, tip) o3.plot(d1="x", d2="y", overplot=True, color="r") # Now we calculate energy, maximum height, and mean radius print(o1.E(pot=ip1), (o1.rperi() + o1.rap()) / 2, o1.zmax()) assert numpy.fabs(o1.E(pot=ip1) + 2.79921356237) < 10.0**-4.0, ( "Energy in the adiabatic invariance test is different" ) assert numpy.fabs((o1.rperi() + o1.rap()) / 2 - 1.07854158141) < 10.0**-4.0, ( "mean radius in the adiabatic invariance test is different" ) assert numpy.fabs(o1.zmax() - 0.106331362938) < 10.0**-4.0, ( "zmax in the adiabatic invariance test is different" ) print(o3.E(pot=ip2), (o3.rperi() + o3.rap()) / 2, o3.zmax()) assert numpy.fabs(o3.E(pot=ip2) + 1.19677002624) < 10.0**-4.0, ( "Energy in the adiabatic invariance test is different" ) assert numpy.fabs((o3.rperi() + o3.rap()) / 2 - 1.39962036137) < 10.0**-4.0, ( "mean radius in the adiabatic invariance test is different" ) assert numpy.fabs(o3.zmax() - 0.138364269321) < 10.0**-4.0, ( "zmax in the adiabatic invariance test is different" ) # The orbit has clearly moved to larger radii, # the actions are however conserved from beginning to end aAI1 = actionAngleIsochrone(ip=ip1) print(aAI1(o1)) js = aAI1(o1) assert numpy.fabs(js[0] - numpy.array([0.00773779])) < 10.0**-4.0, ( "action in the adiabatic invariance test is different" ) assert numpy.fabs(js[1] - numpy.array([1.1])) < 10.0**-4.0, ( "action in the adiabatic invariance test is different" ) assert numpy.fabs(js[2] - numpy.array([0.0045361])) < 10.0**-4.0, ( "action in the adiabatic invariance test is different" ) aAI2 = actionAngleIsochrone(ip=ip2) print(aAI2(o3)) js = aAI2(o3) assert numpy.fabs(js[0] - numpy.array([0.00773812])) < 10.0**-4.0, ( "action in the adiabatic invariance test is different" ) assert numpy.fabs(js[1] - numpy.array([1.1])) < 10.0**-4.0, ( "action in the adiabatic invariance test is different" ) assert numpy.fabs(js[2] - numpy.array([0.0045361])) < 10.0**-4.0, ( "action in the adiabatic invariance test is different" ) return None def test_diskdf(): from galpy.df import dehnendf # Init. dehnendf w/ flat rot., hr=1/3, hs=1, and sr(1)=0.2 df = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.2)) # Same, w/ correction factors to scale profiles dfc = dehnendf( beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.2), correct=True, niter=20 ) if True: # Log. diff. between scale and DF surf. dens. numpy.log(df.surfacemass(0.5) / df.targetSurfacemass(0.5)) assert ( numpy.fabs( numpy.log(df.surfacemass(0.5) / df.targetSurfacemass(0.5)) + 0.056954077791649592 ) < 10.0**-4.0 ), "diskdf does not behave as expected" # Same for corrected DF numpy.log(dfc.surfacemass(0.5) / dfc.targetSurfacemass(0.5)) assert ( numpy.fabs( numpy.log(dfc.surfacemass(0.5) / dfc.targetSurfacemass(0.5)) + 4.1440377205802041e-06 ) < 10.0**-4.0 ), "diskdf does not behave as expected" # Log. diff between scale and DF sr numpy.log(df.sigmaR2(0.5) / df.targetSigma2(0.5)) assert ( numpy.fabs( numpy.log(df.sigmaR2(0.5) / df.targetSigma2(0.5)) + 0.12786083001363127 ) < 10.0**-4.0 ), "diskdf does not behave as expected" # Same for corrected DF numpy.log(dfc.sigmaR2(0.5) / dfc.targetSigma2(0.5)) assert ( numpy.fabs( numpy.log(dfc.sigmaR2(0.5) / dfc.targetSigma2(0.5)) + 6.8065001252214986e-06 ) < 10.0**-4.0 ), "diskdf does not behave as expected" # Evaluate DF w/ R,vR,vT df(numpy.array([0.9, 0.1, 0.8])) assert ( numpy.fabs( df(numpy.array([0.9, 0.1, 0.8])) - numpy.array(0.1740247246180417) ) < 10.0**-4.0 ), "diskdf does not behave as expected" # Evaluate corrected DF w/ Orbit instance from galpy.orbit import Orbit dfc(Orbit([0.9, 0.1, 0.8])) assert ( numpy.fabs(dfc(Orbit([0.9, 0.1, 0.8])) - numpy.array(0.16834863725552207)) < 10.0**-4.0 ), "diskdf does not behave as expected" # Calculate the mean velocities df.meanvR(0.9), df.meanvT(0.9) assert numpy.fabs(df.meanvR(0.9)) < 10.0**-4.0, ( "diskdf does not behave as expected" ) assert numpy.fabs(df.meanvT(0.9) - 0.91144428051168291) < 10.0**-4.0, ( "diskdf does not behave as expected" ) # Calculate the velocity dispersions numpy.sqrt(dfc.sigmaR2(0.9)), numpy.sqrt(dfc.sigmaT2(0.9)) assert ( numpy.fabs(numpy.sqrt(dfc.sigmaR2(0.9)) - 0.22103383792719539) < 10.0**-4.0 ), "diskdf does not behave as expected" assert ( numpy.fabs(numpy.sqrt(dfc.sigmaT2(0.9)) - 0.17613725303902811) < 10.0**-4.0 ), "diskdf does not behave as expected" # Calculate the skew of the velocity distribution df.skewvR(0.9), df.skewvT(0.9) assert numpy.fabs(df.skewvR(0.9)) < 10.0**-4.0, ( "diskdf does not behave as expected" ) assert numpy.fabs(df.skewvT(0.9) + 0.47331638366025863) < 10.0**-4.0, ( "diskdf does not behave as expected" ) # Calculate the kurtosis of the velocity distribution df.kurtosisvR(0.9), df.kurtosisvT(0.9) assert numpy.fabs(df.kurtosisvR(0.9) + 0.13561300880237059) < 10.0**-4.0, ( "diskdf does not behave as expected" ) assert numpy.fabs(df.kurtosisvT(0.9) - 0.12612702099300721) < 10.0**-4.0, ( "diskdf does not behave as expected" ) # Calculate a higher-order moment of the velocity DF df.vmomentsurfacemass(1.0, 6.0, 2.0) / df.surfacemass(1.0) assert ( numpy.fabs( df.vmomentsurfacemass(1.0, 6.0, 2.0) / df.surfacemass(1.0) - 0.00048953492205559054 ) < 10.0**-4.0 ), "diskdf does not behave as expected" # Calculate the Oort functions dfc.oortA(1.0), dfc.oortB(1.0), dfc.oortC(1.0), dfc.oortK(1.0) assert numpy.fabs(dfc.oortA(1.0) - 0.40958989067012197) < 10.0**-4.0, ( "diskdf does not behave as expected" ) assert numpy.fabs(dfc.oortB(1.0) + 0.49396172114486514) < 10.0**-4.0, ( "diskdf does not behave as expected" ) assert numpy.fabs(dfc.oortC(1.0)) < 10.0**-4.0, ( "diskdf does not behave as expected" ) assert numpy.fabs(dfc.oortK(1.0)) < 10.0**-4.0, ( "diskdf does not behave as expected" ) # Sample Orbits from the DF, returns list of Orbits numpy.random.seed(1) os = dfc.sample(n=100, returnOrbit=True, nphi=1) # check that these have the right mean radius = 2hr=2/3 rs = numpy.array([o.R() for o in os]) assert numpy.fabs(numpy.mean(rs) - 2.0 / 3.0) < 0.1 # Sample vR and vT at given R, check their mean vrvt = dfc.sampleVRVT(0.7, n=500, target=True) vt = vrvt[:, 1] assert numpy.fabs(numpy.mean(vrvt[:, 0])) < 0.05 assert numpy.fabs(numpy.mean(vt) - dfc.meanvT(0.7)) < 0.01 # Sample Orbits along a given line-of-sight os = dfc.sampleLOS(45.0, n=1000) return None def test_oort(): from galpy.df import dehnendf df = dehnendf(beta=0.0, correct=True, niter=20, profileParams=(1.0 / 3.0, 1.0, 0.1)) va = 1.0 - df.meanvT(1.0) # asymmetric drift A = df.oortA(1.0) B = df.oortB(1.0) return None def test_qdf(): from galpy.df import quasiisothermaldf from galpy.potential import MWPotential2014 from galpy.actionAngle import actionAngleStaeckel # Setup actionAngle instance for action calcs aAS = actionAngleStaeckel(pot=MWPotential2014, delta=0.45, c=True) # Quasi-iso df w/ hr=1/3, hsr/z=1, sr(1)=0.2, sz(1)=0.1 df = quasiisothermaldf(1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, aA=aAS, pot=MWPotential2014) # Evaluate DF w/ R,vR,vT,z,vz df(0.9, 0.1, 0.8, 0.05, 0.02) assert ( numpy.fabs(df(0.9, 0.1, 0.8, 0.05, 0.02) - numpy.array([123.57158928])) < 10.0**-4.0 ), "qdf does not behave as expected" # Evaluate DF w/ Orbit instance, return ln from galpy.orbit import Orbit df(Orbit([0.9, 0.1, 0.8, 0.05, 0.02]), log=True) assert ( numpy.fabs( df(Orbit([0.9, 0.1, 0.8, 0.05, 0.02]), log=True) - numpy.array([4.81682066]) ) < 10.0**-4.0 ), "qdf does not behave as expected" # Evaluate DF marginalized over vz df.pvRvT(0.1, 0.9, 0.9, 0.05) assert ( numpy.fabs(df.pvRvT(0.1, 0.9, 0.9, 0.05) - 2.0 * 23.273310451852243) < 10.0**-4.0 ), "qdf does not behave as expected" # NOTE: The pvRvT() function has changed with respect to the version used in Bovy (2015). # As of January 2018, a prefactor of 2 has been added (=nsigma/2 with default nsigma=4), # to account for the correct Gauss-Legendre integration normalization. # Evaluate DF marginalized over vR,vT df.pvz(0.02, 0.9, 0.05) assert numpy.fabs(df.pvz(0.02, 0.9, 0.05) - 50.949586235238172) < 10.0**-4.0, ( "qdf does not behave as expected" ) # Calculate the density df.density(0.9, 0.05) assert numpy.fabs(df.density(0.9, 0.05) - 12.73725936526167) < 10.0**-4.0, ( "qdf does not behave as expected" ) # Estimate the DF's actual density scale length at z=0 df.estimate_hr(0.9, 0.0) assert numpy.fabs(df.estimate_hr(0.9, 0.0) - 0.322420336223) < 10.0**-2.0, ( "qdf does not behave as expected" ) # Estimate the DF's actual surface-density scale length df.estimate_hr(0.9, None) assert numpy.fabs(df.estimate_hr(0.9, None) - 0.38059909132766462) < 10.0**-4.0, ( "qdf does not behave as expected" ) # Estimate the DF's density scale height df.estimate_hz(0.9, 0.02) assert numpy.fabs(df.estimate_hz(0.9, 0.02) - 0.064836202345657207) < 10.0**-4.0, ( "qdf does not behave as expected" ) # Calculate the mean velocities ( df.meanvR(0.9, 0.05), df.meanvT(0.9, 0.05), ) df.meanvz(0.9, 0.05) assert numpy.fabs(df.meanvR(0.9, 0.05) - 3.8432265354618213e-18) < 10.0**-4.0, ( "qdf does not behave as expected" ) assert numpy.fabs(df.meanvT(0.9, 0.05) - 0.90840425173325279) < 10.0**-4.0, ( "qdf does not behave as expected" ) assert numpy.fabs(df.meanvz(0.9, 0.05) + 4.3579787517991084e-19) < 10.0**-4.0, ( "qdf does not behave as expected" ) # Calculate the velocity dispersions from numpy import sqrt sqrt(df.sigmaR2(0.9, 0.05)), sqrt(df.sigmaz2(0.9, 0.05)) assert numpy.fabs(sqrt(df.sigmaR2(0.9, 0.05)) - 0.22695537077102387) < 10.0**-4.0, ( "qdf does not behave as expected" ) assert ( numpy.fabs(sqrt(df.sigmaz2(0.9, 0.05)) - 0.094215523962105044) < 10.0**-4.0 ), "qdf does not behave as expected" # Calculate the tilt of the velocity ellipsoid # 2017/10-28: CHANGED bc tilt now returns angle in rad, no longer in deg df.tilt(0.9, 0.05) assert ( numpy.fabs(df.tilt(0.9, 0.05) - 2.5166061974413765 / 180.0 * numpy.pi) < 10.0**-4.0 ), "qdf does not behave as expected" # Calculate a higher-order moment of the velocity DF df.vmomentdensity(0.9, 0.05, 6.0, 2.0, 2.0, gl=True) assert ( numpy.fabs( df.vmomentdensity(0.9, 0.05, 6.0, 2.0, 2.0, gl=True) - 0.0001591100892366438 ) < 10.0**-4.0 ), "qdf does not behave as expected" # Sample velocities at given R,z, check mean numpy.random.seed(1) vs = df.sampleV(0.9, 0.05, n=500) mvt = numpy.mean(vs[:, 1]) assert numpy.fabs(numpy.mean(vs[:, 0])) < 0.05 # vR assert numpy.fabs(mvt - df.meanvT(0.9, 0.05)) < 0.01 # vT assert numpy.fabs(numpy.mean(vs[:, 2])) < 0.05 # vz return None def test_coords(): from galpy.util import coords ra, dec, dist = 161.0, 50.0, 8.5 pmra, pmdec, vlos = -6.8, -10.0, -115.0 # Convert to Galactic and then to rect. Galactic ll, bb = coords.radec_to_lb(ra, dec, degree=True) pmll, pmbb = coords.pmrapmdec_to_pmllpmbb(pmra, pmdec, ra, dec, degree=True) X, Y, Z = coords.lbd_to_XYZ(ll, bb, dist, degree=True) vX, vY, vZ = coords.vrpmllpmbb_to_vxvyvz(vlos, pmll, pmbb, X, Y, Z, XYZ=True) # Convert to cylindrical Galactocentric # Assuming Sun's distance to GC is (8,0.025) in (R,z) R, phi, z = coords.XYZ_to_galcencyl(X, Y, Z, Xsun=8.0, Zsun=0.025) vR, vT, vz = coords.vxvyvz_to_galcencyl( vX, vY, vZ, R, phi, Z, vsun=[-10.1, 244.0, 6.7], galcen=True ) # 5/12/2016: test weakened, because improved galcen<->heliocen # transformation has changed these, but still close print(R, phi, z, vR, vT, vz) assert numpy.fabs(R - 12.51328515156942) < 10.0**-1.0, ( "Coordinate transformation has changed" ) assert numpy.fabs(phi - 0.12177409073433249) < 10.0**-1.0, ( "Coordinate transformation has changed" ) assert numpy.fabs(z - 7.1241282354856228) < 10.0**-1.0, ( "Coordinate transformation has changed" ) assert numpy.fabs(vR - 78.961682923035966) < 10.0**-1.0, ( "Coordinate transformation has changed" ) assert numpy.fabs(vT + 241.49247772351964) < 10.0**-1.0, ( "Coordinate transformation has changed" ) assert numpy.fabs(vz + 102.83965442188689) < 10.0**-1.0, ( "Coordinate transformation has changed" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_import.py0000644000175100001660000000076314761352023016410 0ustar00runnerdocker###################TEST WHETHER THE PACKAGE CAN BE IMPORTED#################### def test_top_import(): import galpy def test_orbit_import(): import galpy.orbit def test_potential_import(): import galpy.potential def test_actionAngle_import(): import galpy.actionAngle def test_df_import(): import galpy.df def test_util_import(): import galpy.util import galpy.util.conversion import galpy.util.coords import galpy.util.multi import galpy.util.plot ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_interp_potential.py0000644000175100001660000017636214761352023020467 0ustar00runnerdockerimport numpy from galpy import potential def test_errors(): # Test that when we set up an interpRZPotential w/ another interpRZPotential, we get an error rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 11), zgrid=(0.0, 0.2, 11), logR=False, interpPot=True, zsym=True, ) try: rzpot2 = potential.interpRZPotential( RZPot=rzpot, rgrid=(0.01, 2.0, 11), zgrid=(0.0, 0.2, 11), logR=False, interpPot=True, zsym=True, ) except potential.PotentialError: pass else: raise AssertionError( "Setting up an interpRZPotential w/ another interpRZPotential did not raise PotentialError" ) def test_interpolation_potential(): # Test the interpolation of the potential rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpPot=True, zsym=True, ) # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21) zs = numpy.linspace(-0.2, 0.2, 41) for r in rs: for z in zs: assert ( numpy.fabs( ( rzpot(r, z) - potential.evaluatePotentials(potential.MWPotential, r, z) ) / potential.evaluatePotentials(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation w/ interpRZPotential fails at (R,z) = ({r:g},{z:g})" ) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) for r in rs: for z in zs: assert ( numpy.fabs( ( rzpot(r, z) - potential.evaluatePotentials(potential.MWPotential, r, z) ) / potential.evaluatePotentials(potential.MWPotential, r, z) ) < 10.0**-6.0 ), ( f"RZPot interpolation w/ interpRZPotential fails at (R,z) = ({r:g},{z:g})" ) # Test all at the same time to use vector evaluation mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot(mr, mz) - potential.evaluatePotentials(potential.MWPotential, mr, mz) ) / potential.evaluatePotentials(potential.MWPotential, mr, mz) ) < 10.0**-6.0 ), "RZPot interpolation w/ interpRZPotential fails for vector input" # Test the interpolation of the potential, now with logR rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 201), logR=True, zgrid=(0.0, 0.2, 101), interpPot=True, zsym=True, ) rs = numpy.linspace(0.01, 20.0, 20) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot(mr, mz) - potential.evaluatePotentials(potential.MWPotential, mr, mz) ) / potential.evaluatePotentials(potential.MWPotential, mr, mz) ) < 10.0**-6.0 ), "RZPot interpolation w/ interpRZPotential fails for vector input, w/ logR" # Test the interpolation of the potential, w/o zsym rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(-0.2, 0.2, 101), logR=False, interpPot=True, zsym=False, ) rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot(mr, mz) - potential.evaluatePotentials(potential.MWPotential, mr, mz) ) / potential.evaluatePotentials(potential.MWPotential, mr, mz) ) < 2.0 * 10.0**-6.0 ), "RZPot interpolation w/ interpRZPotential fails for vector input, w/o zsym" # Test the interpolation of the potential, w/o zsym and with logR rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 201), logR=True, zgrid=(-0.2, 0.2, 101), interpPot=True, zsym=False, ) rs = numpy.linspace(0.01, 20.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot(mr, mz) - potential.evaluatePotentials(potential.MWPotential, mr, mz) ) / potential.evaluatePotentials(potential.MWPotential, mr, mz) ) < 2.0 * 10.0**-6.0 ), ( "RZPot interpolation w/ interpRZPotential fails for vector input w/o zsym and w/ logR" ) return None def test_interpolation_potential_diffinputs(): # Test the interpolation of the potential for different inputs: combination of vector and scalar (we've already done both scalars and both vectors above) rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpPot=True, zsym=True, ) # Test all at the same time to use vector evaluation rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) # R vector, z scalar assert numpy.all( numpy.fabs( ( rzpot(rs, zs[10]) - potential.evaluatePotentials( potential.MWPotential, rs, zs[10] * numpy.ones(len(rs)) ) ) / potential.evaluatePotentials( potential.MWPotential, rs, zs[10] * numpy.ones(len(rs)) ) ) < 10.0**-6.0 ), "RZPot interpolation w/ interpRZPotential fails for vector R and scalar Z" # R scalar, z vector assert numpy.all( numpy.fabs( ( rzpot(rs[10], zs) - potential.evaluatePotentials( potential.MWPotential, rs[10] * numpy.ones(len(zs)), zs ) ) / potential.evaluatePotentials( potential.MWPotential, rs[10] * numpy.ones(len(zs)), zs ) ) < 10.0**-6.0 ), "RZPot interpolation w/ interpRZPotential fails for vector R and scalar Z" return None def test_interpolation_potential_c(): # Test the interpolation of the potential rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 151), zgrid=(0.0, 0.2, 151), logR=False, interpPot=True, enable_c=True, zsym=True, ) # Test within the grid, using vector evaluation rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot(mr, mz) - potential.evaluatePotentials(potential.MWPotential, mr, mz) ) / potential.evaluatePotentials(potential.MWPotential, mr, mz) ) < 10.0**-6.0 ), "RZPot interpolation w/ interpRZPotential fails for vector input, using C" # now w/o zsym rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 151), zgrid=(-0.2, 0.2, 301), logR=False, interpPot=True, enable_c=True, zsym=False, ) assert numpy.all( numpy.fabs( ( rzpot(mr, mz) - potential.evaluatePotentials(potential.MWPotential, mr, mz) ) / potential.evaluatePotentials(potential.MWPotential, mr, mz) ) < 2.0 * 10.0**-6.0 ), ( "RZPot interpolation w/ interpRZPotential fails for vector input, using C, w/o zsym" ) # now with logR rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 251), logR=True, zgrid=(0.0, 0.2, 151), interpPot=True, enable_c=True, zsym=True, ) rs = numpy.linspace(0.01, 10.0, 20) # don't go too far zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot(mr, mz) - potential.evaluatePotentials(potential.MWPotential, mr, mz) ) / potential.evaluatePotentials(potential.MWPotential, mr, mz) ) < 10.0**-6.0 ), ( "RZPot interpolation w/ interpRZPotential fails for vector input, using C, w/ logR" ) # now with logR and w/o zsym rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 251), logR=True, zgrid=(-0.2, 0.2, 301), interpPot=True, enable_c=True, zsym=False, ) rs = numpy.linspace(0.01, 10.0, 20) # don't go too far zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot(mr, mz) - potential.evaluatePotentials(potential.MWPotential, mr, mz) ) / potential.evaluatePotentials(potential.MWPotential, mr, mz) ) < 2.0 * 10.0**-6.0 ), ( "RZPot interpolation w/ interpRZPotential fails for vector input, using C, w/ logR, and w/o zsym" ) return None def test_interpolation_potential_diffinputs_c(): # Test the interpolation of the potential for different inputs: combination of vector and scalar (we've already done both scalars and both vectors above) rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 151), zgrid=(0.0, 0.2, 151), logR=False, interpPot=True, zsym=True, enable_c=True, ) # Test all at the same time to use vector evaluation rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) # R vector, z scalar assert numpy.all( numpy.fabs( ( rzpot(rs, zs[10]) - potential.evaluatePotentials( potential.MWPotential, rs, zs[10] * numpy.ones(len(rs)) ) ) / potential.evaluatePotentials( potential.MWPotential, rs, zs[10] * numpy.ones(len(rs)), ) ) < 10.0**-6.0 ), "RZPot interpolation w/ interpRZPotential fails for vector R and scalar Z" # R scalar, z vector assert numpy.all( numpy.fabs( ( rzpot(rs[10], zs) - potential.evaluatePotentials( potential.MWPotential, rs[10] * numpy.ones(len(zs)), zs ) ) / potential.evaluatePotentials( potential.MWPotential, rs[10] * numpy.ones(len(zs)), zs ) ) < 10.0**-6.0 ), "RZPot interpolation w/ interpRZPotential fails for vector R and scalar Z" return None def test_interpolation_potential_c_vdiffgridsizes(): # Test the interpolation of the potential rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 271), zgrid=(0.0, 0.2, 162), logR=False, interpPot=True, enable_c=True, zsym=True, ) # Test within the grid, using vector evaluation rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot(mr, mz) - potential.evaluatePotentials(potential.MWPotential, mr, mz) ) / potential.evaluatePotentials(potential.MWPotential, mr, mz) ) < 10.0**-6.0 ), "RZPot interpolation w/ interpRZPotential fails for vector input, using C" return None def test_interpolation_potential_use_c(): # Test the interpolation of the potential, using C to calculate the grid rzpot_c = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpPot=True, zsym=True, use_c=False, ) rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpPot=True, zsym=True, use_c=True, ) assert numpy.all(numpy.fabs(rzpot._potGrid - rzpot_c._potGrid) < 10.0**-14.0), ( "Potential interpolation grid calculated with use_c does not agree with that calculated in python" ) return None # Test evaluation outside the grid def test_interpolation_potential_outsidegrid(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpPot=True, zsym=False, ) rs = [0.005, 2.5] zs = [-0.1, 0.3] for r in rs: for z in zs: assert ( numpy.fabs( ( rzpot(r, z) - potential.evaluatePotentials(potential.MWPotential, r, z) ) / potential.evaluatePotentials(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation w/ interpRZPotential fails outside the grid at (R,z) = ({r:g},{z:g})" ) return None # Test evaluation outside the grid in C def test_interpolation_potential_outsidegrid_c(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpPot=True, zsym=False, enable_c=True, ) rs = [0.005, 2.5] zs = [-0.1, 0.3] for r in rs: for z in zs: assert ( numpy.fabs( ( rzpot(r, z) - potential.evaluatePotentials(potential.MWPotential, r, z) ) / potential.evaluatePotentials(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation w/ interpRZPotential fails outside the grid at (R,z) = ({r:g},{z:g})" ) return None def test_interpolation_potential_notinterpolated(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpPot=False, zsym=True, ) rs = [0.5, 1.5] zs = [0.075, 0.15] for r in rs: for z in zs: assert ( numpy.fabs( ( rzpot(r, z) - potential.evaluatePotentials(potential.MWPotential, r, z) ) / potential.evaluatePotentials(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation w/ interpRZPotential fails when the potential was not interpolated at (R,z) = ({r:g},{z:g})" ) return None # Test Rforce and zforce def test_interpolation_potential_force(): # Test the interpolation of the potential rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), zgrid=(0.0, 0.2, 201), logR=False, interpRforce=True, interpzforce=True, zsym=True, ) # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21) zs = numpy.linspace(-0.2, 0.2, 41) for r in rs: for z in zs: assert ( numpy.fabs( ( rzpot.Rforce(r, z) - potential.evaluateRforces(potential.MWPotential, r, z) ) / potential.evaluateRforces(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation of Rforce w/ interpRZPotential fails at (R,z) = ({r:g},{z:g})" ) assert ( numpy.fabs( ( rzpot.zforce(r, z) - potential.evaluatezforces(potential.MWPotential, r, z) ) / potential.evaluateRforces(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation of zforce w/ interpRZPotential fails at (R,z) = ({r:g},{z:g})" ) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) for r in rs: for z in zs: rforcediff = numpy.fabs( ( rzpot.Rforce(r, z) - potential.evaluateRforces(potential.MWPotential, r, z) ) / potential.evaluateRforces(potential.MWPotential, r, z) ) assert rforcediff < 10.0**-5.0, ( f"RZPot interpolation of Rforce w/ interpRZPotential fails at (R,z) = ({r:g},{z:g}) by {rforcediff:g}" ) zforcediff = numpy.fabs( ( rzpot.zforce(r, z) - potential.evaluatezforces(potential.MWPotential, r, z) ) / potential.evaluatezforces(potential.MWPotential, r, z) ) assert zforcediff < 5.0 * 10.0**-5.0, ( f"RZPot interpolation of zforce w/ interpRZPotential fails at (R,z) = ({r:g},{z:g}) by {zforcediff:g}" ) # Test all at the same time to use vector evaluation mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.Rforce(mr, mz) - potential.evaluateRforces(potential.MWPotential, mr, mz) ) / potential.evaluateRforces(potential.MWPotential, mr, mz) ) < 10.0**-5.0 ), "RZPot interpolation of Rforce w/ interpRZPotential fails for vector input" assert numpy.all( numpy.fabs( ( rzpot.zforce(mr, mz) - potential.evaluatezforces(potential.MWPotential, mr, mz) ) / potential.evaluatezforces(potential.MWPotential, mr, mz) ) < 10.0**-5.0 ), "RZPot interpolation of zforce w/ interpRZPotential fails for vector input" # Test the interpolation of the potential, now with logR rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 201), logR=True, zgrid=(0.0, 0.2, 201), interpRforce=True, interpzforce=True, zsym=True, ) rs = numpy.linspace(0.01, 20.0, 20) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.Rforce(mr, mz) - potential.evaluateRforces(potential.MWPotential, mr, mz) ) / potential.evaluateRforces(potential.MWPotential, mr, mz) ) < 10.0**-5.0 ), ( "RZPot interpolation of Rforce w/ interpRZPotential fails for vector input, w/ logR" ) assert numpy.all( numpy.fabs( ( rzpot.zforce(mr, mz) - potential.evaluatezforces(potential.MWPotential, mr, mz) ) / potential.evaluatezforces(potential.MWPotential, mr, mz) ) < 10.0**-5.0 ), ( "RZPot interpolation of zforce w/ interpRZPotential fails for vector input, w/ logR" ) # Test the interpolation of the potential, w/o zsym rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), zgrid=(-0.2, 0.2, 301), logR=False, interpRforce=True, interpzforce=True, zsym=False, ) rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.Rforce(mr, mz) - potential.evaluateRforces(potential.MWPotential, mr, mz) ) / potential.evaluateRforces(potential.MWPotential, mr, mz) ) < 4.0 * 10.0**-5.0 ), ( "RZPot interpolation of Rforce w/ interpRZPotential fails for vector input, w/o zsym" ) assert numpy.all( numpy.fabs( ( rzpot.zforce(mr, mz) - potential.evaluatezforces(potential.MWPotential, mr, mz) ) / potential.evaluatezforces(potential.MWPotential, mr, mz) ) < 4.0 * 10.0**-5.0 ), ( "RZPot interpolation of zforce w/ interpRZPotential fails for vector input, w/o zsym" ) # Test the interpolation of the potential, w/o zsym and with logR rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 201), logR=True, zgrid=(-0.2, 0.2, 201), interpRforce=True, interpzforce=True, zsym=False, ) rs = numpy.linspace(0.01, 20.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.Rforce(mr, mz) - potential.evaluateRforces(potential.MWPotential, mr, mz) ) / potential.evaluateRforces(potential.MWPotential, mr, mz) ) < 2.0 * 10.0**-5.0 ), ( "RZPot interpolation of Rforce w/ interpRZPotential fails for vector input w/o zsym and w/ logR" ) assert numpy.all( numpy.fabs( ( rzpot.zforce(mr, mz) - potential.evaluatezforces(potential.MWPotential, mr, mz) ) / potential.evaluatezforces(potential.MWPotential, mr, mz) ) < 2.0 * 10.0**-5.0 ), ( "RZPot interpolation of zforce w/ interpRZPotential fails for vector input w/o zsym and w/ logR" ) return None def test_interpolation_potential_force_diffinputs(): # Test the interpolation of the potential for different inputs: combination of vector and scalar (we've already done both scalars and both vectors above) rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), zgrid=(0.0, 0.2, 201), logR=False, interpRforce=True, interpzforce=True, zsym=True, ) # Test all at the same time to use vector evaluation rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) # R vector, z scalar assert numpy.all( numpy.fabs( ( rzpot.Rforce(rs, zs[10]) - potential.evaluateRforces( potential.MWPotential, rs, zs[10] * numpy.ones(len(rs)) ) ) / potential.evaluateRforces( potential.MWPotential, rs, zs[10] * numpy.ones(len(rs)) ) ) < 10.0**-5.0 ), ( "RZPot interpolation of of Rforce w/ interpRZPotential fails for vector R and scalar Z" ) assert numpy.all( numpy.fabs( ( rzpot.zforce(rs, zs[10]) - potential.evaluatezforces( potential.MWPotential, rs, zs[10] * numpy.ones(len(rs)) ) ) / potential.evaluatezforces( potential.MWPotential, rs, zs[10] * numpy.ones(len(rs)) ) ) < 10.0**-5.0 ), ( "RZPot interpolation of of zforce w/ interpRZPotential fails for vector R and scalar Z" ) # R scalar, z vector assert numpy.all( numpy.fabs( ( rzpot.Rforce(rs[10], zs) - potential.evaluateRforces( potential.MWPotential, rs[10] * numpy.ones(len(zs)), zs ) ) / potential.evaluateRforces( potential.MWPotential, rs[10] * numpy.ones(len(zs)), zs ) ) < 10.0**-6.0 ), "RZPot interpolation w/ interpRZPotential fails for vector R and scalar Z" assert numpy.all( numpy.fabs( ( rzpot.zforce(rs[10], zs) - potential.evaluatezforces( potential.MWPotential, rs[10] * numpy.ones(len(zs)), zs ) ) / potential.evaluatezforces( potential.MWPotential, rs[10] * numpy.ones(len(zs)), zs ) ) < 10.0**-6.0 ), "RZPot interpolation w/ interpRZPotential fails for vector R and scalar Z" return None # Test Rforce in C def test_interpolation_potential_force_c(): # Test the interpolation of the potential rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 251), zgrid=(0.0, 0.2, 251), logR=False, interpRforce=True, interpzforce=True, enable_c=True, zsym=True, ) # Test within the grid, using vector evaluation rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.Rforce(mr, mz) - potential.evaluateRforces(potential.MWPotential, mr, mz) ) / potential.evaluateRforces(potential.MWPotential, mr, mz) ) < 10.0**-5.0 ), ( "RZPot interpolation of Rforce w/ interpRZPotential fails for vector input, using C" ) assert numpy.all( numpy.fabs( ( rzpot.zforce(mr, mz) - potential.evaluatezforces(potential.MWPotential, mr, mz) ) / potential.evaluatezforces(potential.MWPotential, mr, mz) ) < 2.0 * 10.0**-5.0 ), ( "RZPot interpolation of zforce w/ interpRZPotential fails for vector input, using C" ) # now w/o zsym rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 251), zgrid=(-0.2, 0.2, 351), logR=False, interpRforce=True, interpzforce=True, enable_c=True, zsym=False, ) assert numpy.all( numpy.fabs( ( rzpot.Rforce(mr, mz) - potential.evaluateRforces( potential.MWPotential, mr, mz, ) ) / potential.evaluateRforces(potential.MWPotential, mr, mz) ) < 2.0 * 10.0**-5.0 ), ( "RZPot interpolation of Rforce w/ interpRZPotential fails for vector input, using C, w/o zsym" ) assert numpy.all( numpy.fabs( ( rzpot.zforce(mr, mz) - potential.evaluatezforces(potential.MWPotential, mr, mz) ) / potential.evaluatezforces(potential.MWPotential, mr, mz) ) < 2.0 * 10.0**-5.0 ), ( "RZPot interpolation of zforce w/ interpRZPotential fails for vector input, using C, w/o zsym" ) # now with logR rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 351), logR=True, zgrid=(0.0, 0.2, 251), interpRforce=True, interpzforce=True, enable_c=True, zsym=True, ) rs = numpy.linspace(0.01, 10.0, 20) # don't go too far zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.Rforce(mr, mz) - potential.evaluateRforces(potential.MWPotential, mr, mz) ) / potential.evaluateRforces(potential.MWPotential, mr, mz) ) < 10.0**-5.0 ), ( "RZPot interpolation Rforcew/ interpRZPotential fails for vector input, using C, w/ logR" ) assert numpy.all( numpy.fabs( ( rzpot.zforce(mr, mz) - potential.evaluatezforces(potential.MWPotential, mr, mz) ) / potential.evaluatezforces(potential.MWPotential, mr, mz) ) < 10.0**-5.0 ), ( "RZPot interpolation zforcew/ interpRZPotential fails for vector input, using C, w/ logR" ) # now with logR and w/o zsym rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 351), logR=True, zgrid=(-0.2, 0.2, 351), interpRforce=True, interpzforce=True, enable_c=True, zsym=False, ) rs = numpy.linspace(0.01, 10.0, 20) # don't go too far zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.Rforce(mr, mz) - potential.evaluateRforces(potential.MWPotential, mr, mz) ) / potential.evaluateRforces(potential.MWPotential, mr, mz) ) < 2.0 * 10.0**-5.0 ), ( "RZPot interpolation of Rforce w/ interpRZPotential fails for vector input, using C, w/ logR, and w/o zsym" ) assert numpy.all( numpy.fabs( ( rzpot.zforce(mr, mz) - potential.evaluatezforces(potential.MWPotential, mr, mz) ) / potential.evaluatezforces(potential.MWPotential, mr, mz) ) < 2.0 * 10.0**-5.0 ), ( "RZPot interpolation of zforce w/ interpRZPotential fails for vector input, using C, w/ logR, and w/o zsym" ) return None def test_interpolation_potential_force_c_vdiffgridsizes(): # Test the interpolation of the potential rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 391), zgrid=(0.0, 0.2, 262), logR=False, interpPot=True, enable_c=True, zsym=True, ) # Test within the grid, using vector evaluation rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.Rforce(mr, mz) - potential.evaluateRforces(potential.MWPotential, mr, mz) ) / potential.evaluateRforces(potential.MWPotential, mr, mz) ) < 10.0**-6.0 ), ( "RZPot interpolation of Rforce w/ interpRZPotential fails for vector input, using C" ) assert numpy.all( numpy.fabs( ( rzpot.zforce(mr, mz) - potential.evaluatezforces(potential.MWPotential, mr, mz) ) / potential.evaluatezforces(potential.MWPotential, mr, mz) ) < 10.0**-6.0 ), ( "RZPot interpolation of zforce w/ interpRZPotential fails for vector input, using C" ) return None def test_interpolation_potential_force_use_c(): # Test the interpolation of the potential, using C to calculate the grid rzpot_c = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpRforce=True, interpzforce=True, zsym=True, use_c=False, ) rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpRforce=True, interpzforce=True, zsym=True, use_c=True, ) assert numpy.all( numpy.fabs(rzpot._rforceGrid - rzpot_c._rforceGrid) < 10.0**-13.0 ), ( f"Potential interpolation grid of Rforce calculated with use_c does not agree with that calculated in python, max diff = {numpy.amax(numpy.fabs(rzpot._rforceGrid - rzpot_c._rforceGrid))}" ) assert numpy.all( numpy.fabs(rzpot._zforceGrid - rzpot_c._zforceGrid) < 10.0**-13.0 ), ( f"Potential interpolation grid of zforce calculated with use_c does not agree with that calculated in python, max diff = {numpy.amax(numpy.fabs(rzpot._zforceGrid - rzpot_c._zforceGrid))}" ) return None # Test evaluation outside the grid def test_interpolation_potential_force_outsidegrid(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpRforce=True, interpzforce=True, zsym=False, ) rs = [0.005, 2.5] zs = [-0.1, 0.3] for r in rs: for z in zs: assert ( numpy.fabs( ( rzpot.Rforce(r, z) - potential.evaluateRforces(potential.MWPotential, r, z) ) / potential.evaluateRforces(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation of Rforce w/ interpRZPotential fails outside the grid at (R,z) = ({r:g},{z:g})" ) assert ( numpy.fabs( ( rzpot.zforce(r, z) - potential.evaluatezforces(potential.MWPotential, r, z) ) / potential.evaluatezforces(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation of zforce w/ interpRZPotential fails outside the grid at (R,z) = ({r:g},{z:g})" ) return None # Test evaluation outside the grid in C def test_interpolation_potential_force_outsidegrid_c(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpRforce=True, interpzforce=True, zsym=False, enable_c=True, ) rs = [0.005, 2.5] zs = [-0.1, 0.3] for r in rs: for z in zs: assert ( numpy.fabs( ( rzpot.Rforce(r, z) - potential.evaluateRforces(potential.MWPotential, r, z) ) / potential.evaluateRforces(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation of Rforce w/ interpRZPotential fails outside the grid at (R,z) = ({r:g},{z:g})" ) assert ( numpy.fabs( ( rzpot.zforce(r, z) - potential.evaluatezforces(potential.MWPotential, r, z) ) / potential.evaluatezforces(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation of zforce w/ interpRZPotential fails outside the grid at (R,z) = ({r:g},{z:g})" ) return None def test_interpolation_potential_force_notinterpolated(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpRforce=False, interpzforce=False, zsym=True, ) rs = [0.5, 1.5] zs = [0.075, 0.15] for r in rs: for z in zs: assert ( numpy.fabs( ( rzpot.Rforce(r, z) - potential.evaluateRforces(potential.MWPotential, r, z) ) / potential.evaluateRforces(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation of Rforce w/ interpRZPotential fails when the potential was not interpolated at (R,z) = ({r:g},{z:g})" ) assert ( numpy.fabs( ( rzpot.zforce(r, z) - potential.evaluatezforces(potential.MWPotential, r, z) ) / potential.evaluatezforces(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation of zforce w/ interpRZPotential fails when the potential was not interpolated at (R,z) = ({r:g},{z:g})" ) return None # Test RZderiv, taken from the origPot, so quite trivial def test_interpolation_potential_rzderiv(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, zsym=True, ) # Test all at the same time to use vector evaluation rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.Rzderiv(mr, mz) - potential.evaluateRzderivs(potential.MWPotential, mr, mz) ) / potential.evaluateRzderivs(potential.MWPotential, mr, mz) ) < 10.0**-10.0 ), ( "RZPot interpolation of Rzderiv (which is not an interpolation at all) w/ interpRZPotential fails for vector input" ) return None # Test density def test_interpolation_potential_dens(): # Test the interpolation of the potential rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), zgrid=(0.0, 0.2, 201), logR=False, interpDens=True, zsym=True, ) # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21) zs = numpy.linspace(-0.2, 0.2, 41) for r in rs: for z in zs: densdiff = numpy.fabs( ( rzpot.dens(r, z) - potential.evaluateDensities(potential.MWPotential, r, z) ) / potential.evaluateDensities(potential.MWPotential, r, z) ) assert densdiff < 10.0**-10.0, ( f"RZPot interpolation of density of density w/ interpRZPotential fails at (R,z) = ({r:g},{z:g}) by {densdiff:g}" ) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) for r in rs: for z in zs: densdiff = numpy.fabs( ( rzpot.dens(r, z) - potential.evaluateDensities(potential.MWPotential, r, z) ) / potential.evaluateDensities(potential.MWPotential, r, z) ) assert densdiff < 4.0 * 10.0**-6.0, ( f"RZPot interpolation of density w/ interpRZPotential fails at (R,z) = ({r:g},{z:g}) by {densdiff:g}" ) # Test all at the same time to use vector evaluation mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.dens(mr, mz) - potential.evaluateDensities(potential.MWPotential, mr, mz) ) / potential.evaluateDensities(potential.MWPotential, mr, mz) ) < 4.0 * 10.0**-6.0 ), "RZPot interpolation of density w/ interpRZPotential fails for vector input" # Test the interpolation of the potential, now with logR rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 251), logR=True, zgrid=(0.0, 0.2, 201), interpDens=True, zsym=True, ) rs = numpy.linspace(0.01, 20.0, 20) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.dens(mr, mz) - potential.evaluateDensities(potential.MWPotential, mr, mz) ) / potential.evaluateDensities(potential.MWPotential, mr, mz) ) < 4.0 * 10.0**-6.0 ), ( "RZPot interpolation of density w/ interpRZPotential fails for vector input, w/ logR" ) # Test the interpolation of the potential, w/o zsym rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), zgrid=(-0.2, 0.2, 251), logR=False, interpDens=True, zsym=False, ) rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.dens(mr, mz) - potential.evaluateDensities(potential.MWPotential, mr, mz) ) / potential.evaluateDensities(potential.MWPotential, mr, mz) ) < 4.0 * 10.0**-6.0 ), ( "RZPot interpolation of density w/ interpRZPotential fails for vector input, w/o zsym" ) # Test the interpolation of the potential, w/o zsym and with logR rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 251), logR=True, zgrid=(-0.2, 0.2, 201), interpDens=True, zsym=False, ) rs = numpy.linspace(0.01, 20.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs( ( rzpot.dens(mr, mz) - potential.evaluateDensities(potential.MWPotential, mr, mz) ) / potential.evaluateDensities(potential.MWPotential, mr, mz) ) < 4.0 * 10.0**-6.0 ), ( "RZPot interpolation of density w/ interpRZPotential fails for vector input w/o zsym and w/ logR" ) return None def test_interpolation_potential_dens_diffinputs(): # Test the interpolation of the potential for different inputs: combination of vector and scalar (we've already done both scalars and both vectors above) rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), zgrid=(0.0, 0.2, 201), logR=False, interpDens=True, zsym=True, ) # Test all at the same time to use vector evaluation rs = numpy.linspace(0.01, 2.0, 20) zs = numpy.linspace(-0.2, 0.2, 40) # R vector, z scalar assert numpy.all( numpy.fabs( ( rzpot.dens(rs, zs[10]) - potential.evaluateDensities( potential.MWPotential, rs, zs[10] * numpy.ones(len(rs)) ) ) / potential.evaluateDensities( potential.MWPotential, rs, zs[10] * numpy.ones(len(rs)) ) ) < 4.0 * 10.0**-6.0 ), ( "RZPot interpolation of the density w/ interpRZPotential fails for vector R and scalar Z" ) # R scalar, z vector assert numpy.all( numpy.fabs( ( rzpot.dens(rs[10], zs) - potential.evaluateDensities( potential.MWPotential, rs[10] * numpy.ones(len(zs)), zs ) ) / potential.evaluateDensities( potential.MWPotential, rs[10] * numpy.ones(len(zs)), zs ) ) < 4.0 * 10.0**-6.0 ), ( "RZPot interpolation of the density w/ interpRZPotential fails for vector R and scalar Z" ) return None # Test evaluation outside the grid def test_interpolation_potential_dens_outsidegrid(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpDens=True, zsym=False, ) rs = [0.005, 2.5] zs = [-0.1, 0.3] for r in rs: for z in zs: assert ( numpy.fabs( ( rzpot.dens(r, z) - potential.evaluateDensities(potential.MWPotential, r, z) ) / potential.evaluateDensities(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation of the density w/ interpRZPotential fails outside the grid at (R,z) = ({r:g},{z:g})" ) return None def test_interpolation_potential_density_notinterpolated(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpDens=False, zsym=True, ) rs = [0.5, 1.5] zs = [0.075, 0.15] for r in rs: for z in zs: assert ( numpy.fabs( ( rzpot.dens(r, z) - potential.evaluateDensities(potential.MWPotential, r, z) ) / potential.evaluateDensities(potential.MWPotential, r, z) ) < 10.0**-10.0 ), ( f"RZPot interpolation of the density w/ interpRZPotential fails when the potential was not interpolated at (R,z) = ({r:g},{z:g})" ) return None # Test the circular velocity def test_interpolation_potential_vcirc(): # Test the interpolation of the potential rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpvcirc=True, zsym=True, ) # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21) for r in rs: assert ( numpy.fabs( (rzpot.vcirc(r) - potential.vcirc(potential.MWPotential, r)) / potential.vcirc(potential.MWPotential, r) ) < 10.0**-10.0 ), "RZPot interpolation of vcirc w/ interpRZPotential fails at R = %g" % (r) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 20) for r in rs: vcdiff = numpy.fabs( (rzpot.vcirc(r) - potential.vcirc(potential.MWPotential, r)) / potential.vcirc(potential.MWPotential, r) ) assert vcdiff < 10.0**-6.0, ( f"RZPot interpolation of vcirc w/ interpRZPotential fails at R = {r:g} by {vcdiff:g}" ) # Test all at the same time to use vector evaluation assert numpy.all( numpy.fabs( (rzpot.vcirc(rs) - potential.vcirc(potential.MWPotential, rs)) / potential.vcirc(potential.MWPotential, rs) ) < 10.0**-6.0 ), "RZPot interpolation of vcirc w/ interpRZPotential fails for vector input" # Test the interpolation of the potential, now with logR rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 201), logR=True, interpvcirc=True, zsym=True, ) rs = numpy.linspace(0.01, 20.0, 20) assert numpy.all( numpy.fabs( (rzpot.vcirc(rs) - potential.vcirc(potential.MWPotential, rs)) / potential.vcirc(potential.MWPotential, rs) ) < 10.0**-6.0 ), ( "RZPot interpolation of vcirc w/ interpRZPotential fails for vector input, w/ logR" ) # Test the interpolation of the potential, with numcores rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpvcirc=True, numcores=1, zsym=True, ) rs = numpy.linspace(0.01, 2.0, 20) assert numpy.all( numpy.fabs( (rzpot.vcirc(rs) - potential.vcirc(potential.MWPotential, rs)) / potential.vcirc(potential.MWPotential, rs) ) < 10.0**-6.0 ), "RZPot interpolation of vcirc w/ interpRZPotential fails for vector input" return None # Test evaluation outside the grid def test_interpolation_potential_vcirc_outsidegrid(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpvcirc=True, zsym=False, ) rs = [0.005, 2.5] for r in rs: vcdiff = numpy.fabs( (rzpot.vcirc(r) - potential.vcirc(potential.MWPotential, r)) / potential.vcirc(potential.MWPotential, r) ) assert vcdiff < 10.0**-10.0, ( f"RZPot interpolation w/ interpRZPotential fails outside the grid at R = {r:g} by {vcdiff:g}" ) return None def test_interpolation_potential_vcirc_notinterpolated(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpvcirc=False, zsym=True, ) rs = [0.5, 1.5] for r in rs: vcdiff = numpy.fabs( (rzpot.vcirc(r) - potential.vcirc(potential.MWPotential, r)) / potential.vcirc(potential.MWPotential, r) ) assert vcdiff < 10.0**-10.0, ( f"RZPot interpolation w/ interpRZPotential fails when the potential was not interpolated at R = {r:g} by {vcdiff:g}" ) return None # Test dvcircdR def test_interpolation_potential_dvcircdR(): # Test the interpolation of the potential rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpdvcircdr=True, zsym=True, ) # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21) for r in rs: assert ( numpy.fabs( (rzpot.dvcircdR(r) - potential.dvcircdR(potential.MWPotential, r)) / potential.dvcircdR(potential.MWPotential, r) ) < 10.0**-10.0 ), "RZPot interpolation of dvcircdR w/ interpRZPotential fails at R = %g" % (r) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 20) for r in rs: dvcdrdiff = numpy.fabs( (rzpot.dvcircdR(r) - potential.dvcircdR(potential.MWPotential, r)) / potential.dvcircdR(potential.MWPotential, r) ) assert dvcdrdiff < 10.0**-5.0, ( f"RZPot interpolation of dvcircdR w/ interpRZPotential fails at R = {r:g} by {dvcdrdiff:g}" ) # Test all at the same time to use vector evaluation assert numpy.all( numpy.fabs( (rzpot.dvcircdR(rs) - potential.dvcircdR(potential.MWPotential, rs)) / potential.dvcircdR(potential.MWPotential, rs) ) < 10.0**-5.0 ), "RZPot interpolation of dvcircdR w/ interpRZPotential fails for vector input" # Test the interpolation of the potential, now with logR rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 201), logR=True, interpdvcircdr=True, zsym=True, ) rs = numpy.linspace(0.01, 20.0, 20) assert numpy.all( numpy.fabs( (rzpot.dvcircdR(rs) - potential.dvcircdR(potential.MWPotential, rs)) / potential.dvcircdR(potential.MWPotential, rs) ) < 10.0**-5.0 ), ( "RZPot interpolation of dvcircdR w/ interpRZPotential fails for vector input, w/ logR" ) # Test the interpolation of the potential, with numcores rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpdvcircdr=True, numcores=1, zsym=True, ) rs = numpy.linspace(0.01, 2.0, 20) assert numpy.all( numpy.fabs( (rzpot.dvcircdR(rs) - potential.dvcircdR(potential.MWPotential, rs)) / potential.dvcircdR(potential.MWPotential, rs) ) < 10.0**-5.0 ), "RZPot interpolation of dvcircdR w/ interpRZPotential fails for vector input" return None # Test evaluation outside the grid def test_interpolation_potential_dvcircdR_outsidegrid(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpdvcircdr=True, zsym=False, ) rs = [0.005, 2.5] for r in rs: dvcdrdiff = numpy.fabs( (rzpot.dvcircdR(r) - potential.dvcircdR(potential.MWPotential, r)) / potential.dvcircdR(potential.MWPotential, r) ) assert dvcdrdiff < 10.0**-10.0, ( f"RZPot interpolation w/ interpRZPotential fails outside the grid at R = {r:g} by {dvcdrdiff:g}" ) return None def test_interpolation_potential_dvcircdR_notinterpolated(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpdvcircdr=False, zsym=True, ) rs = [0.5, 1.5] for r in rs: dvcdrdiff = numpy.fabs( (rzpot.dvcircdR(r) - potential.dvcircdR(potential.MWPotential, r)) / potential.dvcircdR(potential.MWPotential, r) ) assert dvcdrdiff < 10.0**-10.0, ( f"RZPot interpolation w/ interpRZPotential fails when the potential was not interpolated at R = {r:g} by {dvcdrdiff:g}" ) return None # Test epifreq def test_interpolation_potential_epifreq(): # Test the interpolation of the potential rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpepifreq=True, zsym=True, ) # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21) for r in rs: assert ( numpy.fabs( (rzpot.epifreq(r) - potential.epifreq(potential.MWPotential, r)) / potential.epifreq(potential.MWPotential, r) ) < 10.0**-10.0 ), "RZPot interpolation of epifreq w/ interpRZPotential fails at R = %g" % (r) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 20) for r in rs: epidiff = numpy.fabs( (rzpot.epifreq(r) - potential.epifreq(potential.MWPotential, r)) / potential.epifreq(potential.MWPotential, r) ) assert epidiff < 10.0**-5.0, ( f"RZPot interpolation of epifreq w/ interpRZPotential fails at R = {r:g} by {epidiff:g}" ) # Test all at the same time to use vector evaluation assert numpy.all( numpy.fabs( (rzpot.epifreq(rs) - potential.epifreq(potential.MWPotential, rs)) / potential.epifreq(potential.MWPotential, rs) ) < 10.0**-5.0 ), "RZPot interpolation of epifreq w/ interpRZPotential fails for vector input" # Test the interpolation of the potential, now with logR rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 201), logR=True, interpepifreq=True, zsym=True, ) rs = numpy.linspace(0.01, 20.0, 20) assert numpy.all( numpy.fabs( (rzpot.epifreq(rs) - potential.epifreq(potential.MWPotential, rs)) / potential.epifreq(potential.MWPotential, rs) ) < 10.0**-5.0 ), ( "RZPot interpolation of epifreq w/ interpRZPotential fails for vector input, w/ logR" ) # Test the interpolation of the potential, with numcores rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpepifreq=True, numcores=1, zsym=True, ) rs = numpy.linspace(0.01, 2.0, 20) assert numpy.all( numpy.fabs( (rzpot.epifreq(rs) - potential.epifreq(potential.MWPotential, rs)) / potential.epifreq(potential.MWPotential, rs) ) < 10.0**-5.0 ), "RZPot interpolation of epifreq w/ interpRZPotential fails for vector input" return None # Test epifreq setup when the number of r points is small def test_interpolation_potential_epifreq_smalln(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(1.0, 1.3, 3), logR=False, interpepifreq=True, zsym=False, ) rs = numpy.linspace(1.1, 1.2, 20) assert numpy.all( numpy.fabs( (rzpot.epifreq(rs) - potential.epifreq(potential.MWPotential, rs)) / potential.epifreq(potential.MWPotential, rs) ) < 10.0**-2.0 ), ( "RZPot interpolation of epifreq w/ interpRZPotential fails for vector input" ) # not as harsh, bc we don't have many points rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(1.0), numpy.log(1.3), 3), logR=True, interpepifreq=True, zsym=False, ) rs = numpy.linspace(1.1, 1.2, 20) assert numpy.all( numpy.fabs( (rzpot.epifreq(rs) - potential.epifreq(potential.MWPotential, rs)) / potential.epifreq(potential.MWPotential, rs) ) < 10.0**-2.0 ), ( "RZPot interpolation of epifreq w/ interpRZPotential fails for vector input" ) # not as harsh, bc we don't have many points return None # Test evaluation outside the grid def test_interpolation_potential_epifreq_outsidegrid(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpepifreq=True, zsym=False, ) rs = [0.005, 2.5] for r in rs: epidiff = numpy.fabs( (rzpot.epifreq(r) - potential.epifreq(potential.MWPotential, r)) / potential.epifreq(potential.MWPotential, r) ) assert epidiff < 10.0**-10.0, ( f"RZPot interpolation w/ interpRZPotential fails outside the grid at R = {r:g} by {epidiff:g}" ) return None def test_interpolation_potential_epifreq_notinterpolated(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpepifreq=False, zsym=True, ) rs = [0.5, 1.5] for r in rs: epidiff = numpy.fabs( (rzpot.epifreq(r) - potential.epifreq(potential.MWPotential, r)) / potential.epifreq(potential.MWPotential, r) ) assert epidiff < 10.0**-10.0, ( f"RZPot interpolation w/ interpRZPotential fails when the potential was not interpolated at R = {r:g} by {epidiff:g}" ) return None # Test verticalfreq def test_interpolation_potential_verticalfreq(): # Test the interpolation of the potential rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpverticalfreq=True, zsym=True, ) # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21) for r in rs: assert ( numpy.fabs( ( rzpot.verticalfreq(r) - potential.verticalfreq(potential.MWPotential, r) ) / potential.verticalfreq(potential.MWPotential, r) ) < 10.0**-10.0 ), ( "RZPot interpolation of verticalfreq w/ interpRZPotential fails at R = %g" % (r) ) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 20) for r in rs: vfdiff = numpy.fabs( (rzpot.verticalfreq(r) - potential.verticalfreq(potential.MWPotential, r)) / potential.verticalfreq(potential.MWPotential, r) ) assert vfdiff < 10.0**-5.0, ( f"RZPot interpolation of verticalfreq w/ interpRZPotential fails at R = {r:g} by {vfdiff:g}" ) # Test all at the same time to use vector evaluation assert numpy.all( numpy.fabs( (rzpot.verticalfreq(rs) - potential.verticalfreq(potential.MWPotential, rs)) / potential.verticalfreq(potential.MWPotential, rs) ) < 10.0**-5.0 ), "RZPot interpolation of verticalfreq w/ interpRZPotential fails for vector input" # Test the interpolation of the potential, now with logR rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(numpy.log(0.01), numpy.log(20.0), 201), logR=True, interpverticalfreq=True, zsym=True, ) rs = numpy.linspace(0.01, 20.0, 20) assert numpy.all( numpy.fabs( (rzpot.verticalfreq(rs) - potential.verticalfreq(potential.MWPotential, rs)) / potential.verticalfreq(potential.MWPotential, rs) ) < 10.0**-5.0 ), ( "RZPot interpolation of verticalfreq w/ interpRZPotential fails for vector input, w/ logR" ) # Test the interpolation of the potential, with numcores rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpverticalfreq=True, numcores=1, zsym=True, ) rs = numpy.linspace(0.01, 2.0, 20) assert numpy.all( numpy.fabs( (rzpot.verticalfreq(rs) - potential.verticalfreq(potential.MWPotential, rs)) / potential.verticalfreq(potential.MWPotential, rs) ) < 10.0**-5.0 ), "RZPot interpolation of verticalfreq w/ interpRZPotential fails for vector input" return None # Test evaluation outside the grid def test_interpolation_potential_verticalfreq_outsidegrid(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpverticalfreq=True, zsym=False, ) rs = [0.005, 2.5] for r in rs: vfdiff = numpy.fabs( (rzpot.verticalfreq(r) - potential.verticalfreq(potential.MWPotential, r)) / potential.verticalfreq(potential.MWPotential, r) ) assert vfdiff < 10.0**-10.0, ( f"RZPot interpolation w/ interpRZPotential fails outside the grid at R = {r:g} by {vfdiff:g}" ) return None def test_interpolation_potential_verticalfreq_notinterpolated(): rzpot = potential.interpRZPotential( RZPot=potential.MWPotential, rgrid=(0.01, 2.0, 201), logR=False, interpverticalfreq=False, zsym=True, ) rs = [0.5, 1.5] for r in rs: vfdiff = numpy.fabs( (rzpot.verticalfreq(r) - potential.verticalfreq(potential.MWPotential, r)) / potential.verticalfreq(potential.MWPotential, r) ) assert vfdiff < 10.0**-10.0, ( f"RZPot interpolation w/ interpRZPotential fails when the potential was not interpolated at R = {r:g} by {vfdiff:g}" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_jeans.py0000644000175100001660000002476714761352023016210 0ustar00runnerdocker# Tests of the galpy.df.jeans module: Jeans equations import numpy from galpy.df import jeans # Test sigmar: radial velocity dispersion from the spherical Jeans equation # For log halo, constant beta: sigma(r) = vc/sqrt(2.-2*beta) def test_sigmar_wlog_constbeta(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) rs = numpy.linspace(0.001, 5.0, 101) # beta = 0 --> sigma = vc/sqrt(2) assert numpy.all( numpy.fabs( numpy.array([jeans.sigmar(lp, r) for r in rs]) - 1.0 / numpy.sqrt(2.0) ) < 1e-10 ), ( "Radial sigma computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=0" ) # general beta --> sigma = vc/sqrt(2-2beta) beta = 0.5 assert numpy.all( numpy.fabs( numpy.array([jeans.sigmar(lp, r, beta=beta) for r in rs]) - 1.0 / numpy.sqrt(2.0 - 2.0 * beta) ) < 1e-10 ), ( "Radial sigma computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=0.5" ) beta = -0.5 assert numpy.all( numpy.fabs( numpy.array([jeans.sigmar(lp, r, beta=beta) for r in rs]) - 1.0 / numpy.sqrt(2.0 - 2.0 * beta) ) < 1e-10 ), ( "Radial sigma computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=-0.5" ) return None # Test sigmar: radial velocity dispersion from the spherical Jeans equation # For log halo, constant beta: sigma(r) = vc/sqrt(2.-2*beta) def test_sigmar_wlog_constbeta_diffdens_powerlaw(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) rs = numpy.linspace(0.001, 5.0, 101) # general beta and r^-gamma --> sigma = vc/sqrt(gamma-2beta) gamma, beta = 1.0, 0.0 assert numpy.all( numpy.fabs( numpy.array( [jeans.sigmar(lp, r, beta=beta, dens=lambda r: r**-gamma) for r in rs] ) - 1.0 / numpy.sqrt(gamma - 2.0 * beta) ) < 1e-10 ), ( "Radial sigma computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential, beta=0, and power-law density r^-1" ) gamma, beta = 3.0, 0.5 assert numpy.all( numpy.fabs( numpy.array( [jeans.sigmar(lp, r, beta=beta, dens=lambda r: r**-gamma) for r in rs] ) - 1.0 / numpy.sqrt(gamma - 2.0 * beta) ) < 1e-10 ), ( "Radial sigma computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential, beta=0.5, and power-law density r^-3" ) gamma, beta = 0.0, -0.5 assert numpy.all( numpy.fabs( numpy.array( [jeans.sigmar(lp, r, beta=beta, dens=lambda r: r**-gamma) for r in rs] ) - 1.0 / numpy.sqrt(gamma - 2.0 * beta) ) < 1e-10 ), ( "Radial sigma computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential, beta=-0.5, and power-law density r^0" ) return None def test_sigmar_wlog_constbeta_asbetafunc(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) rs = numpy.linspace(0.001, 5.0, 101) # beta = 0 --> sigma = vc/sqrt(2) assert numpy.all( numpy.fabs( numpy.array([jeans.sigmar(lp, r, beta=lambda x: 0.0) for r in rs]) - 1.0 / numpy.sqrt(2.0) ) < 1e-10 ), ( "Radial sigma computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=0" ) # general beta --> sigma = vc/sqrt(2-2beta) beta = lambda x: 0.5 assert numpy.all( numpy.fabs( numpy.array([jeans.sigmar(lp, r, beta=beta) for r in rs]) - 1.0 / numpy.sqrt(2.0 - 2.0 * beta(0)) ) < 1e-10 ), ( "Radial sigma computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=0.5" ) beta = lambda x: -0.5 assert numpy.all( numpy.fabs( numpy.array([jeans.sigmar(lp, r, beta=beta) for r in rs]) - 1.0 / numpy.sqrt(2.0 - 2.0 * beta(0)) ) < 1e-10 ), ( "Radial sigma computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=-0.5" ) return None def test_sigmar_wlog_linbeta(): # for log halo, dens ~ r^-gamma, and beta = -b x r --> # sigmar = vc sqrt( scipy.special.gamma(-gamma)*scipy.special.gammaincc(-gamma,2*b*r)/[(2*b*r)**-gamma*exp(-2*b*r)] from scipy import special from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) rs = numpy.linspace(0.001, 5.0, 101) gamma, b = -0.1, 3.0 assert numpy.all( numpy.fabs( numpy.array( [ jeans.sigmar(lp, r, beta=lambda x: -b * x, dens=lambda x: x**-gamma) - numpy.sqrt( special.gamma(-gamma) * special.gammaincc(-gamma, 2 * b * r) / ((2 * b * r) ** -gamma * numpy.exp(-2.0 * b * r)) ) for r in rs ] ) ) < 1e-10 ), ( "Radial sigma computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential, beta= -b*r, and dens ~ r^-gamma" ) gamma, b = -0.5, 4.0 assert numpy.all( numpy.fabs( numpy.array( [ jeans.sigmar(lp, r, beta=lambda x: -b * x, dens=lambda x: x**-gamma) - numpy.sqrt( special.gamma(-gamma) * special.gammaincc(-gamma, 2 * b * r) / ((2 * b * r) ** -gamma * numpy.exp(-2.0 * b * r)) ) for r in rs ] ) ) < 1e-10 ), ( "Radial sigma computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential, beta= -b*r, and dens ~ r^-gamma" ) return None # Test sigmalos: radial velocity dispersion from the spherical Jeans equation # For log halo, beta = 0: sigmalos(r) = vc/sqrt(2.) def test_sigmalos_wlog_zerobeta(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) rs = numpy.linspace(0.5, 2.0, 3) assert numpy.all( numpy.fabs( numpy.array([jeans.sigmalos(lp, r) for r in rs]) - 1.0 / numpy.sqrt(2.0) ) < 1e-8 ), ( "Radial sigma_los computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=0" ) # Also with pre-computed sigmar rs = numpy.linspace(0.5, 2.0, 11) # beta = 0 --> sigma = vc/sqrt(2) assert numpy.all( numpy.fabs( numpy.array( [jeans.sigmalos(lp, r, sigma_r=1.0 / numpy.sqrt(2.0)) for r in rs] ) - 1.0 / numpy.sqrt(2.0) ) < 1e-8 ), ( "Radial sigma_los computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=0" ) # Also with pre-computed, callable sigmar rs = numpy.linspace(0.5, 2.0, 11) # beta = 0 --> sigma = vc/sqrt(2) assert numpy.all( numpy.fabs( numpy.array( [ jeans.sigmalos(lp, r, sigma_r=lambda x: 1.0 / numpy.sqrt(2.0)) for r in rs ] ) - 1.0 / numpy.sqrt(2.0) ) < 1e-8 ), ( "Radial sigma_los computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=0" ) # Also with pre-computed, callable sigmar and dens given rs = numpy.linspace(0.5, 2.0, 11) # beta = 0 --> sigma = vc/sqrt(2) assert numpy.all( numpy.fabs( numpy.array( [ jeans.sigmalos( lp, r, dens=lambda x: x**-2, sigma_r=lambda x: 1.0 / numpy.sqrt(2.0), ) for r in rs ] ) - 1.0 / numpy.sqrt(2.0) ) < 1e-8 ), ( "Radial sigma_los computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=0" ) # Also with pre-computed, callable sigmar and dens,surfdens given as func rs = numpy.linspace(0.5, 2.0, 11) # beta = 0 --> sigma = vc/sqrt(2) assert numpy.all( numpy.fabs( numpy.array( [ jeans.sigmalos( lp, r, dens=lambda x: lp.dens(x, 0.0), surfdens=lambda x: lp.surfdens(x, numpy.inf), sigma_r=lambda x: 1.0 / numpy.sqrt(2.0), ) for r in rs ] ) - 1.0 / numpy.sqrt(2.0) ) < 1e-8 ), ( "Radial sigma_los computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=0" ) # Also with pre-computed, callable sigmar and dens,surfdens given (value) rs = numpy.linspace(0.5, 2.0, 11) # beta = 0 --> sigma = vc/sqrt(2) assert numpy.all( numpy.fabs( numpy.array( [ jeans.sigmalos( lp, r, dens=lambda x: lp.dens(x, 0.0), surfdens=lp.surfdens(r, numpy.inf), sigma_r=lambda x: 1.0 / numpy.sqrt(2.0), ) for r in rs ] ) - 1.0 / numpy.sqrt(2.0) ) < 1e-8 ), ( "Radial sigma_los computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=0" ) # Also with pre-computed sigmar and callable beta rs = numpy.linspace(0.5, 2.0, 11) # beta = 0 --> sigma = vc/sqrt(2) assert numpy.all( numpy.fabs( numpy.array( [ jeans.sigmalos( lp, r, sigma_r=1.0 / numpy.sqrt(2.0), beta=lambda x: 0.0 ) for r in rs ] ) - 1.0 / numpy.sqrt(2.0) ) < 1e-8 ), ( "Radial sigma_los computed w/ spherical Jeans equation incorrect for LogarithmicHaloPotential and beta=0" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_kuzminkutuzov.py0000644000175100001660000007035614761352023020070 0ustar00runnerdocker# _____import packages_____ import numpy import scipy from galpy.actionAngle import actionAngleStaeckel, estimateDeltaStaeckel from galpy.orbit import Orbit from galpy.potential import ( KuzminKutuzovStaeckelPotential, evaluateDensities, evaluatePotentials, evaluateRforces, ) from galpy.util import coords # Test whether circular velocity calculation works def test_vcirc(): # test the circular velocity of the KuzminKutuzovStaeckelPotential # using parameters from Batsleer & Dejonghe 1994, fig. 1-3 # and their formula eq. (10) # _____model parameters______ # surface ratios of disk and halo: ac_Ds = [50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 40.0, 40.0, 40.0] ac_Hs = [1.005, 1.005, 1.005, 1.01, 1.01, 1.01, 1.02, 1.02, 1.02] # disk contribution to total mass: ks = [0.05, 0.06, 0.07, 0.07, 0.1, 0.125, 0.1, 0.12, 0.125] # focal distance: Delta = 1.0 for ii in range(9): ac_D = ac_Ds[ii] ac_H = ac_Hs[ii] k = ks[ii] # _____setup potential_____ # first try, not normalized: V_D = KuzminKutuzovStaeckelPotential( amp=k, ac=ac_D, Delta=Delta, normalize=False ) V_H = KuzminKutuzovStaeckelPotential( amp=(1.0 - k), ac=ac_H, Delta=Delta, normalize=False ) pot = [V_D, V_H] # normalization according to Batsleer & Dejonghe 1994: V00 = evaluatePotentials(pot, 0.0, 0.0) # second try, normalized: V_D = KuzminKutuzovStaeckelPotential( amp=k / (-V00), ac=ac_D, Delta=Delta, normalize=False ) V_H = KuzminKutuzovStaeckelPotential( amp=(1.0 - k) / (-V00), ac=ac_H, Delta=Delta, normalize=False ) pot = [V_D, V_H] # _____calculate rotation curve_____ Rs = numpy.linspace(0.0, 20.0, 100) z = 0.0 vcirc_calc = numpy.sqrt(-Rs * evaluateRforces(pot, Rs, z)) # _____vcirc by Batsleer & Dejonghe eq. (10) (with proper Jacobian)_____ def vc2w(R): g_D = Delta**2 / (1.0 - ac_D**2) a_D = g_D - Delta**2 g_H = Delta**2 / (1.0 - ac_H**2) a_H = g_H - Delta**2 l = R**2 - a_D q = a_H - a_D termD = numpy.sqrt(l) * (numpy.sqrt(l) + numpy.sqrt(-g_D)) ** 2 termH = numpy.sqrt(l - q) * (numpy.sqrt(l - q) + numpy.sqrt(-g_D - q)) ** 2 return R**2 * (k / termD + (1.0 - k) / termH) vcirc_formula = numpy.sqrt(vc2w(Rs) / (-V00)) assert numpy.all(numpy.fabs(vcirc_calc - vcirc_formula) < 10**-8.0), ( "Calculated circular velocity for KuzminKutuzovStaeckelPotential " + "does not agree with eq. (10) (corrected by proper Jacobian) " + "by Batsleer & Dejonghe (1994)" ) return None # ----------------------------------------------------------------------------- # test whether the density calculation works def test_density(): # test the density calculation of the KuzminKutuzovStaeckelPotential # using parameters from Batsleer & Dejonghe 1994, tab. 2 # _____parameters_____ # table 2 in Batsleer & Dejonghe ac_D = [ 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 40.0, 40.0, 40.0, 40.0, 40.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 75.0, 75.0, 75.0, 75.0, 75.0, 75.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 150.0, 150.0, 150.0, 150.0, 150.0, 150.0, ] ac_H = [ 1.005, 1.005, 1.01, 1.01, 1.02, 1.02, 1.005, 1.005, 1.01, 1.01, 1.02, 1.005, 1.005, 1.01, 1.01, 1.02, 1.02, 1.005, 1.005, 1.01, 1.01, 1.02, 1.02, 1.005, 1.005, 1.01, 1.01, 1.02, 1.02, 1.005, 1.005, 1.01, 1.01, 1.02, 1.02, ] k = [ 0.05, 0.08, 0.075, 0.11, 0.105, 0.11, 0.05, 0.08, 0.075, 0.11, 0.11, 0.05, 0.07, 0.07, 0.125, 0.1, 0.125, 0.05, 0.065, 0.07, 0.125, 0.10, 0.125, 0.05, 0.065, 0.07, 0.125, 0.10, 0.125, 0.05, 0.065, 0.075, 0.125, 0.11, 0.125, ] Delta = [ 0.99, 1.01, 0.96, 0.99, 0.86, 0.88, 1.00, 1.01, 0.96, 0.99, 0.89, 1.05, 1.06, 1.00, 1.05, 0.91, 0.97, 0.98, 0.99, 0.94, 0.98, 0.85, 0.91, 1.06, 1.07, 1.01, 1.06, 0.94, 0.97, 1.06, 1.07, 0.98, 1.06, 0.94, 0.97, ] Mmin = [ 7.49, 6.17, 4.08, 3.70, 2.34, 2.36, 7.57, 6.16, 4.08, 2.64, 2.38, 8.05, 6.94, 4.37, 3.70, 2.48, 2.50, 7.37, 6.66, 4.05, 3.46, 2.33, 2.36, 8.14, 7.27, 4.42, 3.72, 2.56, 2.50, 8.14, 7.26, 4.17, 3.72, 2.51, 2.50, ] Mmax = [ 7.18, 6.12, 3.99, 3.69, 2.37, 2.40, 7.27, 6.11, 3.99, 2.66, 2.42, 7.76, 6.85, 4.26, 3.72, 2.51, 2.54, 7.07, 6.51, 3.95, 3.48, 2.36, 2.40, 7.85, 7.15, 4.30, 3.75, 2.58, 2.54, 7.85, 7.07, 4.08, 3.75, 2.53, 2.53, ] rhomin = [ 0.04, 0.05, 0.04, 0.04, 0.03, 0.03, 0.06, 0.06, 0.05, 0.04, 0.04, 0.07, 0.08, 0.06, 0.07, 0.04, 0.05, 0.08, 0.09, 0.07, 0.09, 0.05, 0.06, 0.12, 0.13, 0.09, 0.13, 0.07, 0.09, 0.16, 0.19, 0.12, 0.18, 0.10, 0.12, ] rhomax = [ 0.03, 0.03, 0.02, 0.03, 0.02, 0.02, 0.04, 0.04, 0.03, 0.03, 0.02, 0.05, 0.05, 0.04, 0.05, 0.03, 0.03, 0.05, 0.06, 0.04, 0.06, 0.03, 0.04, 0.07, 0.08, 0.06, 0.08, 0.04, 0.05, 0.09, 0.10, 0.07, 0.10, 0.06, 0.07, ] Sigmin = [ 58, 52, 52, 49, 39, 40, 58, 55, 51, 44, 40, 59, 54, 53, 49, 41, 42, 58, 55, 51, 48, 39, 40, 59, 55, 53, 49, 42, 42, 59, 55, 52, 49, 42, 42, ] Sigmax = [ 45, 41, 38, 37, 28, 28, 45, 32, 37, 32, 30, 46, 43, 40, 37, 30, 31, 45, 43, 38, 36, 28, 29, 46, 43, 40, 38, 31, 31, 46, 44, 39, 38, 30, 31, ] for ii in range(len(ac_D)): if ac_D[ii] == 40.0: continue # because I believe that there are typos in tab. 2 by Batsleer & Dejonghe... for jj in range(2): # _____parameters depending on solar position____ if jj == 0: Rsun = 7.5 zsun = 0.004 GM = Mmin[ii] # units: G = 1, M in 10^11 solar masses rho = rhomin[ii] Sig = Sigmin[ii] elif jj == 1: Rsun = 8.5 zsun = 0.02 GM = Mmax[ii] # units: G = 1, M in 10^11 solar masses rho = rhomax[ii] Sig = Sigmax[ii] outstr = ( "ac_D=" + str(ac_D[ii]) + ", ac_H=" + str(ac_H[ii]) + ", k=" + str(k[ii]) + ", Delta=" + str(Delta[ii]) + ", Mtot=" + str(GM) + "*10^11Msun, Rsun=" + str(Rsun) + "kpc, rho(Rsun,zsun)=" + str(rho) + "Msun/pc^3, Sig(Rsun,z<1.1kpc)=" + str(Sig) + "Msun/pc^2" ) # _____setup potential_____ amp_D = GM * k[ii] V_D = KuzminKutuzovStaeckelPotential( amp=amp_D, ac=ac_D[ii], Delta=Delta[ii], normalize=False ) amp_H = GM * (1.0 - k[ii]) V_H = KuzminKutuzovStaeckelPotential( amp=amp_H, ac=ac_H[ii], Delta=Delta[ii], normalize=False ) pot = [V_D, V_H] # _____local density_____ rho_calc = ( evaluateDensities(pot, Rsun, zsun) * 100.0 ) # units: [solar mass / pc^3] rho_calc = round(rho_calc, 2) # an error of 0.01 corresponds to the significant digit # given in the table, to which the density was rounded, # to be wrong by one. assert numpy.fabs(rho_calc - rho) <= 0.01 + 10.0**-8, ( "Calculated density %f for KuzminKutuzovStaeckelPotential " % rho_calc + "with model parameters:\n" + outstr + "\n" + "does not agree with value from tab. 2 " + "by Batsleer & Dejonghe (1994)" ) # _____surface density_____ Sig_calc, err = scipy.integrate.quad( lambda z: ( evaluateDensities(pot, Rsun, z / 1000.0) * 100.0 ), # units: [solar mass / pc^3] 0.0, 1100.0, ) # units: pc Sig_calc = round(2.0 * Sig_calc) # an error of 1 corresponds to the significant digit # given in the table, to which the surface density was rounded, # to be wrong by one. assert numpy.fabs(Sig_calc - Sig) <= 1.0, ( "Calculated surface density %f for KuzminKutuzovStaeckelPotential " % Sig_calc + "with model parameters:\n" + outstr + "\n" + "does not agree with value from tab. 2 " + "by Batsleer & Dejonghe (1994)" ) return None # ----------------------------------------------------------------------------- # test whether the orbit integration in C and Python are the same def test_orbitIntegrationC(): # _____initialize some KKSPot_____ Delta = 1.0 pot = KuzminKutuzovStaeckelPotential(ac=20.0, Delta=Delta, normalize=True) # _____initialize an orbit (twice)_____ vxvv = [1.0, 0.1, 1.1, 0.0, 0.1] o_P = Orbit(vxvv=vxvv) o_C = Orbit(vxvv=vxvv) # _____integrate the orbit with python and C_____ ts = numpy.linspace(0, 100, 101) o_P.integrate(ts, pot, method="leapfrog") # python o_C.integrate(ts, pot, method="leapfrog_c") # C for ii in range(5): exp3 = -1.7 if ii == 0: Python, CC, string, exp1, exp2 = o_P.R(ts), o_C.R(ts), "R", -5.0, -10.0 elif ii == 1: Python, CC, string, exp1, exp2 = o_P.z(ts), o_C.z(ts), "z", -3.25, -4.0 elif ii == 2: Python, CC, string, exp1, exp2 = o_P.vR(ts), o_C.vR(ts), "vR", -3.0, -10.0 elif ii == 3: Python, CC, string, exp1, exp2, exp3 = ( o_P.vz(ts), o_C.vz(ts), "vz", -3.0, -4.0, -1.3, ) elif ii == 4: Python, CC, string, exp1, exp2 = o_P.vT(ts), o_C.vT(ts), "vT", -5.0, -10.0 rel_diff = numpy.fabs((Python - CC) / CC) < 10.0**exp1 abs_diff = (numpy.fabs(Python - CC) < 10.0**exp2) * ( numpy.fabs(Python) < 10.0**exp3 ) assert numpy.all(rel_diff + abs_diff), ( "Orbit integration for " + string + " coordinate different in " + "C and Python implementation." ) return None # ----------------------------------------------------------------------------- # test whether this is really a Staeckel potential and the Delta is constant def test_estimateDelta(): # _____initialize some KKSPot_____ Delta = 1.0 pot = KuzminKutuzovStaeckelPotential(ac=20.0, Delta=Delta, normalize=True) # _____initialize an orbit (twice)_____ vxvv = [1.0, 0.1, 1.1, 0.01, 0.1] o = Orbit(vxvv=vxvv) # _____integrate the orbit with C_____ ts = numpy.linspace(0, 101, 100) o.integrate(ts, pot, method="leapfrog_c") # ____estimate Focal length Delta_____ # for each time step individually: deltas_estimate = numpy.zeros(len(ts)) for ii in range(len(ts)): deltas_estimate[ii] = estimateDeltaStaeckel(pot, o.R(ts[ii]), o.z(ts[ii])) assert numpy.all(numpy.fabs(deltas_estimate - Delta) < 10.0**-8), ( "Focal length Delta estimated along the orbit is not constant." ) # for all time steps together: delta_estimate = estimateDeltaStaeckel(pot, o.R(ts), o.z(ts)) assert numpy.fabs(delta_estimate - Delta) < 10.0**-8, ( "Focal length Delta estimated from the orbit is not the same as the input focal length." ) return None # ----------------------------------------------------------------------------- # test whether this is really a Staeckel potential and the Actions are conserved along the orbit def test_actionConservation(): # _____initialize some KKSPot_____ Delta = 1.0 pot = KuzminKutuzovStaeckelPotential(ac=20.0, Delta=Delta, normalize=True) # _____initialize an orbit (twice)_____ vxvv = [1.0, 0.1, 1.1, 0.01, 0.1] o = Orbit(vxvv=vxvv) # _____integrate the orbit with C_____ ts = numpy.linspace(0, 101, 100) o.integrate(ts, pot, method="leapfrog_c") # _____Setup ActionAngle object and calculate actions (Staeckel approximation)_____ aAS = actionAngleStaeckel(pot=pot, delta=Delta, c=True) jrs, lzs, jzs = aAS(o.R(ts), o.vR(ts), o.vT(ts), o.z(ts), o.vz(ts)) assert numpy.all(numpy.fabs(jrs - jrs[0]) < 10.0**-8.0), ( "Radial action is not conserved along orbit." ) assert numpy.all(numpy.fabs(lzs - lzs[0]) < 10.0**-8.0), ( "Angular momentum is not conserved along orbit." ) assert numpy.all(numpy.fabs(jzs - jzs[0]) < 10.0**-8.0), ( "Vertical action is not conserved along orbit." ) return None # ----------------------------------------------------------------------------- # test coordinate transformation def test_lambdanu_to_Rz(): # coordinate system: a = 3.0 g = 4.0 Delta = numpy.sqrt(g - a) ac = numpy.sqrt(a / g) # _____test float input (z=0)_____ # coordinate transformation: l, n = 2.0, -4.0 R, z = coords.lambdanu_to_Rz(l, n, ac=ac, Delta=Delta) # true values: R_true = numpy.sqrt((l + a) * (n + a) / (a - g)) z_true = numpy.sqrt((l + g) * (n + g) / (g - a)) # test: assert numpy.fabs(R - R_true) < 10.0**-10.0, ( "lambdanu_to_Rz conversion did not work as expected (R)" ) assert numpy.fabs(z - z_true) < 10.0**-10.0, ( "lambdanu_to_Rz conversion did not work as expected (z)" ) # _____Also test for arrays_____ # coordinate transformation: l = numpy.array([2.0, 10.0, 20.0, 0.0]) n = numpy.array([-4.0, -3.0, -3.5, -3.5]) R, z = coords.lambdanu_to_Rz(l, n, ac=ac, Delta=Delta) # true values: R_true = numpy.sqrt((l + a) * (n + a) / (a - g)) z_true = numpy.sqrt((l + g) * (n + g) / (g - a)) # test: rel_diff = numpy.fabs((R - R_true) / R_true) < 10.0**-8.0 abs_diff = (numpy.fabs(R - R_true) < 10.0**-6.0) * (numpy.fabs(R_true) < 10.0**-6.0) assert numpy.all(rel_diff + abs_diff), ( "lambdanu_to_Rz conversion did not work as expected (R array)" ) rel_diff = numpy.fabs((z - z_true) / z_true) < 10.0**-8.0 abs_diff = (numpy.fabs(z - z_true) < 10.0**-6.0) * (numpy.fabs(z_true) < 10.0**-6.0) assert numpy.all(rel_diff + abs_diff), ( "lambdanu_to_Rz conversion did not work as expected (z array)" ) return None def test_Rz_to_lambdanu(): # coordinate system: a = 3.0 g = 7.0 Delta = numpy.sqrt(g - a) ac = numpy.sqrt(a / g) # _____test float input (R=0)_____ # true values: l, n = 2.0, -3.0 # coordinate transformation: lt, nt = coords.Rz_to_lambdanu( *coords.lambdanu_to_Rz(l, n, ac=ac, Delta=Delta), ac=ac, Delta=Delta ) # test: assert numpy.fabs(lt - l) < 10.0**-10.0, ( "Rz_to_lambdanu conversion did not work as expected (l)" ) assert numpy.fabs(nt - n) < 10.0**-10.0, ( "Rz_to_lambdanu conversion did not work as expected (n)" ) # ___Also test for arrays___ l = numpy.array([2.0, 10.0, 20.0, 0.0]) n = numpy.array([-7.0, -3.0, -5.0, -5.0]) lt, nt = coords.Rz_to_lambdanu( *coords.lambdanu_to_Rz(l, n, ac=ac, Delta=Delta), ac=ac, Delta=Delta ) assert numpy.all(numpy.fabs(lt - l) < 10.0**-10.0), ( "Rz_to_lambdanu conversion did not work as expected (l array)" ) assert numpy.all(numpy.fabs(nt - n) < 10.0**-10.0), ( "Rz_to_lambdanu conversion did not work as expected (n array)" ) return None def test_Rz_to_lambdanu_r2lt0(): # Special case that leads to r2 just below zero # coordinate system: a = 3.0 g = 7.0 Delta = numpy.sqrt(g - a) ac = numpy.sqrt(a / g) # _____test float input (R=0)_____ # true values: l, n = 2.0, -3.0 + 10.0**-10.0 # coordinate transformation: lt, nt = coords.Rz_to_lambdanu( *coords.lambdanu_to_Rz(l, n, ac=ac, Delta=Delta), ac=ac, Delta=Delta ) # test: assert numpy.fabs(lt - l) < 10.0**-8.0, ( "Rz_to_lambdanu conversion did not work as expected (l)" ) assert numpy.fabs(nt - n) < 10.0**-8.0, ( "Rz_to_lambdanu conversion did not work as expected (n)" ) # ___Also test for arrays___ l = numpy.array([2.0, 10.0, 20.0, 0.0]) n = numpy.array([-7.0, -3.0 + 10.0**-10.0, -5.0, -5.0]) lt, nt = coords.Rz_to_lambdanu( *coords.lambdanu_to_Rz(l, n, ac=ac, Delta=Delta), ac=ac, Delta=Delta ) assert numpy.all(numpy.fabs(lt - l) < 10.0**-8.0), ( "Rz_to_lambdanu conversion did not work as expected (l array)" ) assert numpy.all(numpy.fabs(nt - n) < 10.0**-8.0), ( "Rz_to_lambdanu conversion did not work as expected (n array)" ) return None def test_Rz_to_lambdanu_jac(): # coordinate system: a = 3.0 g = 7.0 Delta = numpy.sqrt(g - a) ac = numpy.sqrt(a / g) # _____test float input (R=0)_____ R, z = 1.4, 0.1 dR = 10.0**-8.0 dz = 10.0**-8.0 # R derivatives tmp = R + dR dR = tmp - R num_deriv_lR = ( coords.Rz_to_lambdanu(R + dR, z, ac=ac, Delta=Delta)[0] - coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[0] ) / dR num_deriv_nR = ( coords.Rz_to_lambdanu(R + dR, z, ac=ac, Delta=Delta)[1] - coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[1] ) / dR # z derivatives tmp = z + dz dz = tmp - z num_deriv_lz = ( coords.Rz_to_lambdanu(R, z + dz, ac=ac, Delta=Delta)[0] - coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[0] ) / dR num_deriv_nz = ( coords.Rz_to_lambdanu(R, z + dz, ac=ac, Delta=Delta)[1] - coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[1] ) / dR jac = coords.Rz_to_lambdanu_jac(R, z, Delta=Delta) assert numpy.fabs(num_deriv_lR - jac[0, 0]) < 10.0**-6.0, ( "jacobian d((lambda,nu))/d((R,z)) fails for (dl/dR)" ) assert numpy.fabs(num_deriv_nR - jac[1, 0]) < 10.0**-6.0, ( "jacobian d((lambda,nu))/d((R,z)) fails for (dn/dR)" ) assert numpy.fabs(num_deriv_lz - jac[0, 1]) < 10.0**-6.0, ( "jacobian d((lambda,nu))/d((R,z)) fails for (dl/dz)" ) assert numpy.fabs(num_deriv_nz - jac[1, 1]) < 10.0**-6.0, ( "jacobian d((lambda,nu))/d((R,z)) fails for (dn/dz)" ) # ___Also test for arrays___ R = numpy.arange(1, 4) * 0.5 z = numpy.arange(1, 4) * 0.125 dR = 10.0**-8.0 dz = 10.0**-8.0 # R derivatives tmp = R + dR dR = tmp - R num_deriv_lR = ( coords.Rz_to_lambdanu(R + dR, z, ac=ac, Delta=Delta)[0] - coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[0] ) / dR num_deriv_nR = ( coords.Rz_to_lambdanu(R + dR, z, ac=ac, Delta=Delta)[1] - coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[1] ) / dR # z derivatives tmp = z + dz dz = tmp - z num_deriv_lz = ( coords.Rz_to_lambdanu(R, z + dz, ac=ac, Delta=Delta)[0] - coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[0] ) / dR num_deriv_nz = ( coords.Rz_to_lambdanu(R, z + dz, ac=ac, Delta=Delta)[1] - coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[1] ) / dR jac = coords.Rz_to_lambdanu_jac(R, z, Delta=Delta) assert numpy.all(numpy.fabs(num_deriv_lR - jac[0, 0]) < 10.0**-6.0), ( "jacobian d((lambda,nu))/d((R,z)) fails for (dl/dR)" ) assert numpy.all(numpy.fabs(num_deriv_nR - jac[1, 0]) < 10.0**-6.0), ( "jacobian d((lambda,nu))/d((R,z)) fails for (dn/dR)" ) assert numpy.all(numpy.fabs(num_deriv_lz - jac[0, 1]) < 10.0**-6.0), ( "jacobian d((lambda,nu))/d((R,z)) fails for (dl/dz)" ) assert numpy.all(numpy.fabs(num_deriv_nz - jac[1, 1]) < 10.0**-6.0), ( "jacobian d((lambda,nu))/d((R,z)) fails for (dn/dz)" ) return None def test_Rz_to_lambdanu_hess(): # coordinate system: a = 3.0 g = 7.0 Delta = numpy.sqrt(g - a) ac = numpy.sqrt(a / g) # _____test float input (R=0)_____ R, z = 1.4, 0.1 dR = 10.0**-5.0 dz = 10.0**-5.0 # R derivatives tmp = R + dR dR = tmp - R # z derivatives tmp = z + dz dz = tmp - z num_deriv_llRR = ( coords.Rz_to_lambdanu(R + dR, z, ac=ac, Delta=Delta)[0] - 2.0 * coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[0] + coords.Rz_to_lambdanu(R - dR, z, ac=ac, Delta=Delta)[0] ) / dR**2.0 num_deriv_nnRR = ( coords.Rz_to_lambdanu(R + dR, z, ac=ac, Delta=Delta)[1] - 2.0 * coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[1] + coords.Rz_to_lambdanu(R - dR, z, ac=ac, Delta=Delta)[1] ) / dR**2.0 num_deriv_llRz = ( ( coords.Rz_to_lambdanu(R + dR, z + dz, ac=ac, Delta=Delta)[0] - coords.Rz_to_lambdanu(R + dR, z - dz, ac=ac, Delta=Delta)[0] - coords.Rz_to_lambdanu(R - dR, z + dz, ac=ac, Delta=Delta)[0] + coords.Rz_to_lambdanu(R - dR, z - dz, ac=ac, Delta=Delta)[0] ) / dR**2.0 / 4.0 ) num_deriv_llzz = ( coords.Rz_to_lambdanu(R, z + dz, ac=ac, Delta=Delta)[0] - 2.0 * coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[0] + coords.Rz_to_lambdanu(R, z - dz, ac=ac, Delta=Delta)[0] ) / dz**2.0 num_deriv_nnzz = ( coords.Rz_to_lambdanu(R, z + dz, ac=ac, Delta=Delta)[1] - 2.0 * coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[1] + coords.Rz_to_lambdanu(R, z - dz, ac=ac, Delta=Delta)[1] ) / dz**2.0 num_deriv_nnRz = ( ( coords.Rz_to_lambdanu(R + dR, z + dz, ac=ac, Delta=Delta)[1] - coords.Rz_to_lambdanu(R + dR, z - dz, ac=ac, Delta=Delta)[1] - coords.Rz_to_lambdanu(R - dR, z + dz, ac=ac, Delta=Delta)[1] + coords.Rz_to_lambdanu(R - dR, z - dz, ac=ac, Delta=Delta)[1] ) / dR**2.0 / 4.0 ) hess = coords.Rz_to_lambdanu_hess(R, z, Delta=Delta) assert numpy.fabs(num_deriv_llRR - hess[0, 0, 0]) < 10.0**-4.0, ( "hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] fails for (dl/dR)" ) assert numpy.fabs(num_deriv_llRz - hess[0, 0, 1]) < 10.0**-4.0, ( "hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] fails for (dn/dR)" ) assert numpy.fabs(num_deriv_nnRR - hess[1, 0, 0]) < 10.0**-4.0, ( "hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] fails for (dn/dR)" ) assert numpy.fabs(num_deriv_llzz - hess[0, 1, 1]) < 10.0**-4.0, ( "hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] fails for (dl/dz)" ) assert numpy.fabs(num_deriv_nnRz - hess[1, 0, 1]) < 10.0**-4.0, ( "hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] fails for (dn/dz)" ) assert numpy.fabs(num_deriv_nnzz - hess[1, 1, 1]) < 10.0**-4.0, ( "hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] fails for (dn/dz)" ) # ___Also test for arrays___ R = numpy.arange(1, 4) * 0.5 z = numpy.arange(1, 4) * 0.125 # R derivatives tmp = R + dR dR = tmp - R # z derivatives tmp = z + dz dz = tmp - z dR = 10.0**-5.0 dz = 10.0**-5.0 num_deriv_llRR = ( coords.Rz_to_lambdanu(R + dR, z, ac=ac, Delta=Delta)[0] - 2.0 * coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[0] + coords.Rz_to_lambdanu(R - dR, z, ac=ac, Delta=Delta)[0] ) / dR**2.0 num_deriv_nnRR = ( coords.Rz_to_lambdanu(R + dR, z, ac=ac, Delta=Delta)[1] - 2.0 * coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[1] + coords.Rz_to_lambdanu(R - dR, z, ac=ac, Delta=Delta)[1] ) / dR**2.0 num_deriv_llRz = ( ( coords.Rz_to_lambdanu(R + dR, z + dz, ac=ac, Delta=Delta)[0] - coords.Rz_to_lambdanu(R + dR, z - dz, ac=ac, Delta=Delta)[0] - coords.Rz_to_lambdanu(R - dR, z + dz, ac=ac, Delta=Delta)[0] + coords.Rz_to_lambdanu(R - dR, z - dz, ac=ac, Delta=Delta)[0] ) / dR**2.0 / 4.0 ) num_deriv_llzz = ( coords.Rz_to_lambdanu(R, z + dz, ac=ac, Delta=Delta)[0] - 2.0 * coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[0] + coords.Rz_to_lambdanu(R, z - dz, ac=ac, Delta=Delta)[0] ) / dz**2.0 num_deriv_nnzz = ( coords.Rz_to_lambdanu(R, z + dz, ac=ac, Delta=Delta)[1] - 2.0 * coords.Rz_to_lambdanu(R, z, ac=ac, Delta=Delta)[1] + coords.Rz_to_lambdanu(R, z - dz, ac=ac, Delta=Delta)[1] ) / dz**2.0 num_deriv_nnRz = ( ( coords.Rz_to_lambdanu(R + dR, z + dz, ac=ac, Delta=Delta)[1] - coords.Rz_to_lambdanu(R + dR, z - dz, ac=ac, Delta=Delta)[1] - coords.Rz_to_lambdanu(R - dR, z + dz, ac=ac, Delta=Delta)[1] + coords.Rz_to_lambdanu(R - dR, z - dz, ac=ac, Delta=Delta)[1] ) / dR**2.0 / 4.0 ) hess = coords.Rz_to_lambdanu_hess(R, z, Delta=Delta) assert numpy.all(numpy.fabs(num_deriv_llRR - hess[0, 0, 0]) < 10.0**-4.0), ( "hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] fails for (dl/dR)" ) assert numpy.all(numpy.fabs(num_deriv_llRz - hess[0, 0, 1]) < 10.0**-4.0), ( "hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] fails for (dn/dR)" ) assert numpy.all(numpy.fabs(num_deriv_nnRR - hess[1, 0, 0]) < 10.0**-4.0), ( "hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] fails for (dn/dR)" ) assert numpy.all(numpy.fabs(num_deriv_llzz - hess[0, 1, 1]) < 10.0**-4.0), ( "hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] fails for (dl/dz)" ) assert numpy.all(numpy.fabs(num_deriv_nnRz - hess[1, 0, 1]) < 10.0**-4.0), ( "hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] fails for (dn/dz)" ) assert numpy.all(numpy.fabs(num_deriv_nnzz - hess[1, 1, 1]) < 10.0**-4.0), ( "hessian [d^2(lambda)/d(R,z)^2 , d^2(nu)/d(R,z)^2] fails for (dn/dz)" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_nemo.py0000644000175100001660000001553414761352023016036 0ustar00runnerdocker# Test consistency between galpy and NEMO import os import subprocess import numpy from galpy import potential from galpy.orbit import Orbit from galpy.util import conversion def test_nemo_MN3ExponentialDiskPotential(): mn = potential.MN3ExponentialDiskPotential(normalize=1.0, hr=0.5, hz=0.1) tmax = 3.0 vo, ro = 215.0, 8.75 o = Orbit([1.0, 0.1, 1.1, 0.3, 0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, mn, tmax, vo, ro) return None def test_nemo_MiyamotoNagaiPotential(): mp = potential.MiyamotoNagaiPotential(normalize=1.0, a=0.5, b=0.1) tmax = 4.0 vo, ro = 220.0, 8.0 o = Orbit([1.0, 0.1, 1.1, 0.3, 0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, mp, tmax, vo, ro) return None def test_nemo_NFWPotential(): np = potential.NFWPotential(normalize=1.0, a=3.0) tmax = 3.0 vo, ro = 200.0, 7.0 o = Orbit([1.0, 0.5, 1.3, 0.3, 0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, np, tmax, vo, ro) return None def test_nemo_HernquistPotential(): hp = potential.HernquistPotential(normalize=1.0, a=3.0) tmax = 3.0 vo, ro = 210.0, 7.5 o = Orbit([1.0, 0.25, 1.4, 0.3, -0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, hp, tmax, vo, ro) return None def test_nemo_PowerSphericalPotentialwCutoffPotential(): pp = potential.PowerSphericalPotentialwCutoff(normalize=1.0, alpha=1.0, rc=0.4) tmax = 2.0 vo, ro = 180.0, 9.0 o = Orbit([1.0, 0.03, 1.03, 0.2, 0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, pp, tmax, vo, ro) return None def test_nemo_LogarithmicHaloPotential(): lp = potential.LogarithmicHaloPotential(normalize=1.0) tmax = 2.0 vo, ro = 210.0, 8.5 o = Orbit([1.0, 0.1, 1.1, 0.3, 0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, lp, tmax, vo, ro, tol=0.03) return None def test_nemo_PlummerPotential(): pp = potential.PlummerPotential(normalize=1.0, b=2.0) tmax = 3.0 vo, ro = 213.0, 8.23 o = Orbit([1.0, 0.1, 1.1, 0.3, 0.1, 0.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, pp, tmax, vo, ro, tol=0.03) return None def test_nemo_MWPotential2014(): mp = potential.MWPotential2014 tmax = 3.5 vo, ro = 220.0, 8.0 o = Orbit([1.0, 0.1, 1.1, 0.2, 0.1, 1.4], ro=ro, vo=vo) run_orbitIntegration_comparison(o, mp, tmax, vo, ro, isList=True) return None def run_orbitIntegration_comparison(orb, pot, tmax, vo, ro, isList=False, tol=0.01): # Integrate in galpy ts = numpy.linspace(0.0, tmax / conversion.time_in_Gyr(vo, ro), 1001) orb.integrate(ts, pot) # Now setup a NEMO snapshot in the correct units ([x] = kpc, [v] = kpc/Gyr) numpy.savetxt( "orb.dat", numpy.array( [ [ 10.0**-6.0, orb.x(), orb.y(), orb.z(), orb.vx(use_physical=False) * conversion.velocity_in_kpcGyr(vo, ro), orb.vy(use_physical=False) * conversion.velocity_in_kpcGyr(vo, ro), orb.vz(use_physical=False) * conversion.velocity_in_kpcGyr(vo, ro), ] ] ), ) # Now convert to NEMO format try: convert_to_nemo("orb.dat", "orb.nemo") finally: os.remove("orb.dat") # Integrate with gyrfalcON try: if isList: integrate_gyrfalcon( "orb.nemo", "orb_evol.nemo", tmax, potential.nemo_accname(pot), potential.nemo_accpars(pot, vo, ro), ) else: integrate_gyrfalcon( "orb.nemo", "orb_evol.nemo", tmax, pot.nemo_accname(), pot.nemo_accpars(vo, ro), ) finally: os.remove("orb.nemo") os.remove("gyrfalcON.log") # Convert back to ascii try: convert_from_nemo("orb_evol.nemo", "orb_evol.dat") finally: os.remove("orb_evol.nemo") # Read and compare try: nemodata = numpy.loadtxt("orb_evol.dat", comments="#") xdiff = numpy.fabs((nemodata[-1, 1] - orb.x(ts[-1])) / nemodata[-1, 1]) ydiff = numpy.fabs((nemodata[-1, 2] - orb.y(ts[-1])) / nemodata[-1, 2]) zdiff = numpy.fabs((nemodata[-1, 3] - orb.z(ts[-1])) / nemodata[-1, 3]) vxdiff = numpy.fabs( ( nemodata[-1, 4] - orb.vx(ts[-1], use_physical=False) * conversion.velocity_in_kpcGyr(vo, ro) ) / nemodata[-1, 4] ) vydiff = numpy.fabs( ( nemodata[-1, 5] - orb.vy(ts[-1], use_physical=False) * conversion.velocity_in_kpcGyr(vo, ro) ) / nemodata[-1, 5] ) vzdiff = numpy.fabs( ( nemodata[-1, 6] - orb.vz(ts[-1], use_physical=False) * conversion.velocity_in_kpcGyr(vo, ro) ) / nemodata[-1, 6] ) assert xdiff < tol, ( "galpy and NEMO gyrfalcON orbit integration inconsistent for x by %g" % xdiff ) assert ydiff < tol, ( "galpy and NEMO gyrfalcON orbit integration inconsistent for y by %g" % ydiff ) assert zdiff < tol, ( "galpy and NEMO gyrfalcON orbit integration inconsistent for z by %g" % zdiff ) assert vxdiff < tol, ( "galpy and NEMO gyrfalcON orbit integration inconsistent for vx by %g" % vxdiff ) assert vydiff < tol, ( "galpy and NEMO gyrfalcON orbit integration inconsistent for vy by %g" % vydiff ) assert vzdiff < tol, ( "galpy and NEMO gyrfalcON orbit integration inconsistent for vz by %g" % vzdiff ) finally: os.remove("orb_evol.dat") return None def convert_to_nemo(infile, outfile): subprocess.check_call( ["a2s", "in=%s" % infile, "out=%s" % outfile, "N=1", "read=mxv"] ) def convert_from_nemo(infile, outfile): subprocess.check_call(["s2a", "in=%s" % infile, "out=%s" % outfile]) def integrate_gyrfalcon(infile, outfile, tmax, nemo_accname, nemo_accpars): """Integrate a snapshot in infile until tmax in Gyr, save to outfile""" with open("gyrfalcON.log", "w") as f: subprocess.check_call( [ "gyrfalcON", "in=%s" % infile, "out=%s" % outfile, "tstop=%g" % tmax, "eps=0.0015", "step=0.01", "kmax=10", "Nlev=8", "fac=0.01", "accname=%s" % nemo_accname, "accpars=%s" % nemo_accpars, ], stdout=f, ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_noninertial.py0000644000175100001660000034751214761352023017426 0ustar00runnerdocker# Tests of integrating orbits in non-inertial frames import numpy import pytest from galpy import potential from galpy.orbit import Orbit from galpy.util import coords def test_lsrframe_scalaromegaz(): # Test that integrating an orbit in the LSR frame is equivalent to # normal orbit integration lp = potential.LogarithmicHaloPotential(normalize=1.0) omega = lp.omegac(1.0) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp framepot = potential.NonInertialFrameForce(Omega=omega) dp_frame = potential.DehnenBarPotential(omegab=1.8 - omega, rb=0.5, Af=0.03) diskframepot = lp + dp_frame + framepot # Now integrate the orbit of the Sun in both the inertial and the lsr frame def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.z(), o.vz(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.R(ts) * numpy.cos(o.phi(ts) - omega * ts) o_ys = o.R(ts) * numpy.sin(o.phi(ts) - omega * ts) op_xs = op.x(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in the rotating LSR frame does not agree with the equivalent orbit in the inertial frame for integration method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in the rotating LSR frame does not agree with the equivalent orbit in the inertial frame for integration method {method}" ) check_orbit(method="odeint", tol=1e-6) check_orbit(method="dop853_c", tol=1e-9) return None def test_lsrframe_scalaromegaz_2d(): # Test that integrating an orbit in the LSR frame is equivalent to # normal orbit integration in 2D lp = potential.LogarithmicHaloPotential(normalize=1.0) omega = lp.omegac(1.0) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp framepot = potential.NonInertialFrameForce(Omega=omega) dp_frame = potential.DehnenBarPotential(omegab=1.8 - omega, rb=0.5, Af=0.03) diskframepot = lp + dp_frame + framepot # Now integrate the orbit of the Sun in both the inertial and the lsr frame def check_orbit(method="odeint", tol=1e-9): o = Orbit().toPlanar() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.R(ts) * numpy.cos(o.phi(ts) - omega * ts) o_ys = o.R(ts) * numpy.sin(o.phi(ts) - omega * ts) op_xs = op.x(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in the rotating LSR frame does not agree with the equivalent orbit in the inertial frame for integration method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in the rotating LSR frame does not agree with the equivalent orbit in the inertial frame for integration method {method}" ) check_orbit(method="odeint", tol=1e-6) check_orbit(method="dop853_c", tol=1e-9) return None def test_lsrframe_vecomegaz(): # Test that integrating an orbit in the LSR frame is equivalent to # normal orbit integration lp = potential.LogarithmicHaloPotential(normalize=1.0) omega = lp.omegac(1.0) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp framepot = potential.NonInertialFrameForce(Omega=numpy.array([0.0, 0.0, omega])) dp_frame = potential.DehnenBarPotential(omegab=1.8 - omega, rb=0.5, Af=0.03) diskframepot = lp + dp_frame + framepot # Now integrate the orbit of the Sun in both the inertial and the lsr frame def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.z(), o.vz(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.R(ts) * numpy.cos(o.phi(ts) - omega * ts) o_ys = o.R(ts) * numpy.sin(o.phi(ts) - omega * ts) op_xs = op.x(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in the rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in the rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-6) check_orbit(method="dop853_c", tol=1e-9) return None def test_lsrframe_vecomegaz_2d(): # Test that integrating an orbit in the LSR frame is equivalent to # normal orbit integration in 2D lp = potential.LogarithmicHaloPotential(normalize=1.0) omega = lp.omegac(1.0) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp framepot = potential.NonInertialFrameForce(Omega=numpy.array([0.0, 0.0, omega])) dp_frame = potential.DehnenBarPotential(omegab=1.8 - omega, rb=0.5, Af=0.03) diskframepot = lp + dp_frame + framepot # Now integrate the orbit of the Sun in both the inertial and the lsr frame def check_orbit(method="odeint", tol=1e-9): o = Orbit().toPlanar() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.R(ts) * numpy.cos(o.phi(ts) - omega * ts) o_ys = o.R(ts) * numpy.sin(o.phi(ts) - omega * ts) op_xs = op.x(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in the rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in the rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-6) check_orbit(method="dop853_c", tol=1e-9) return None def test_accellsrframe_scalaromegaz(): # Test that integrating an orbit in an LSR frame that is accelerating # is equivalent to normal orbit integration lp = potential.LogarithmicHaloPotential(normalize=1.0) omega = lp.omegac(1.0) omegadot = 0.02 diskpot = lp framepot = potential.NonInertialFrameForce(Omega=omega, Omegadot=omegadot) diskframepot = lp + framepot # Now integrate the orbit of the Sun in both the inertial and the lsr frame def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.z(), o.vz(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.R(ts) * numpy.cos(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) o_ys = o.R(ts) * numpy.sin(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) op_xs = op.x(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-6) check_orbit(method="dop853_c", tol=1e-9) return None def test_accellsrframe_scalaromegaz_2d(): # Test that integrating an orbit in an LSR frame that is accelerating # is equivalent to normal orbit integration in 2D lp = potential.LogarithmicHaloPotential(normalize=1.0) omega = lp.omegac(1.0) omegadot = 0.02 diskpot = lp framepot = potential.NonInertialFrameForce(Omega=omega, Omegadot=omegadot) diskframepot = lp + framepot # Now integrate the orbit of the Sun in both the inertial and the lsr frame def check_orbit(method="odeint", tol=1e-9): o = Orbit().toPlanar() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.R(ts) * numpy.cos(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) o_ys = o.R(ts) * numpy.sin(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) op_xs = op.x(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-6) check_orbit(method="dop853_c", tol=1e-9) return None def test_accellsrframe_vecomegaz(): # Test that integrating an orbit in an LSR frame that is accelerating # is equivalent to normal orbit integration lp = potential.LogarithmicHaloPotential(normalize=1.0) omega = lp.omegac(1.0) omegadot = 0.02 diskpot = lp framepot = potential.NonInertialFrameForce( Omega=numpy.array([0.0, 0.0, omega]), Omegadot=numpy.array([0.0, 0.0, omegadot]) ) diskframepot = lp + framepot # Now integrate the orbit of the Sun in both the inertial and the lsr frame def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.z(), o.vz(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.R(ts) * numpy.cos(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) o_ys = o.R(ts) * numpy.sin(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) op_xs = op.x(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-6) check_orbit(method="dop853_c", tol=1e-9) return None def test_accellsrframe_vecomegaz_2d(): # Test that integrating an orbit in an LSR frame that is accelerating # is equivalent to normal orbit integration in 2D lp = potential.LogarithmicHaloPotential(normalize=1.0) omega = lp.omegac(1.0) omegadot = 0.02 diskpot = lp framepot = potential.NonInertialFrameForce( Omega=numpy.array([0.0, 0.0, omega]), Omegadot=numpy.array([0.0, 0.0, omegadot]) ) diskframepot = lp + framepot # Now integrate the orbit of the Sun in both the inertial and the lsr frame def check_orbit(method="odeint", tol=1e-9): o = Orbit().toPlanar() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.R(ts) * numpy.cos(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) o_ys = o.R(ts) * numpy.sin(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) op_xs = op.x(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-6) check_orbit(method="dop853_c", tol=1e-9) return None def test_accellsrframe_funcomegaz(): # Test that integrating an orbit in an LSR frame that is accelerating # is equivalent to normal orbit integration lp = potential.LogarithmicHaloPotential(normalize=1.0) omega = lp.omegac(1.0) omegadot = 0.02 omega_func = lambda t: lp.omegac(1.0) + 0.02 * t omegadot_func = lambda t: 0.02 diskpot = lp framepot = potential.NonInertialFrameForce(Omega=omega_func, Omegadot=omegadot_func) diskframepot = lp + framepot # Now integrate the orbit of the Sun in both the inertial and the lsr frame def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.z(), o.vz(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.R(ts) * numpy.cos(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) o_ys = o.R(ts) * numpy.sin(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) op_xs = op.x(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-6) check_orbit(method="dop853_c", tol=1e-9) return None def test_accellsrframe_funcomegaz_2d(): # Test that integrating an orbit in an LSR frame that is accelerating # is equivalent to normal orbit integration lp = potential.LogarithmicHaloPotential(normalize=1.0) omega = lp.omegac(1.0) omegadot = 0.02 omega_func = lambda t: lp.omegac(1.0) + 0.02 * t omegadot_func = lambda t: 0.02 diskpot = lp framepot = potential.NonInertialFrameForce(Omega=omega_func, Omegadot=omegadot_func) diskframepot = lp + framepot # Now integrate the orbit of the Sun in both the inertial and the lsr frame def check_orbit(method="odeint", tol=1e-9): o = Orbit().toPlanar() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.R(ts) * numpy.cos(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) o_ys = o.R(ts) * numpy.sin(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) op_xs = op.x(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-6) check_orbit(method="dop853_c", tol=1e-9) return None def test_accellsrframe_vecfuncomegaz(): # Test that integrating an orbit in an LSR frame that is accelerating # is equivalent to normal orbit integration lp = potential.LogarithmicHaloPotential(normalize=1.0) omega = lp.omegac(1.0) omegadot = 0.02 omega_func = [lambda t: 0.0, lambda t: 0.0, lambda t: lp.omegac(1.0) + 0.02 * t] omegadot_func = [lambda t: 0.0, lambda t: 0.0, lambda t: 0.02] diskpot = lp framepot = potential.NonInertialFrameForce(Omega=omega_func, Omegadot=omegadot_func) diskframepot = lp + framepot # Now integrate the orbit of the Sun in both the inertial and the lsr frame def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.z(), o.vz(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.R(ts) * numpy.cos(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) o_ys = o.R(ts) * numpy.sin(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) op_xs = op.x(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-6) check_orbit(method="dop853_c", tol=1e-9) return None def test_accellsrframe_vecfuncomegaz_2D(): # Test that integrating an orbit in an LSR frame that is accelerating # is equivalent to normal orbit integration lp = potential.LogarithmicHaloPotential(normalize=1.0) omega = lp.omegac(1.0) omegadot = 0.02 omega_func = [lambda t: 0.0, lambda t: 0.0, lambda t: lp.omegac(1.0) + 0.02 * t] omegadot_func = [lambda t: 0.0, lambda t: 0.0, lambda t: 0.02] diskpot = lp framepot = potential.NonInertialFrameForce(Omega=omega_func, Omegadot=omegadot_func) diskframepot = lp + framepot # Now integrate the orbit of the Sun in both the inertial and the lsr frame def check_orbit(method="odeint", tol=1e-9): o = Orbit().toPlanar() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.R(ts) * numpy.cos(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) o_ys = o.R(ts) * numpy.sin(o.phi(ts) - omega * ts - omegadot * ts**2.0 / 2.0) op_xs = op.x(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in the acceleratingly-rotating LSR frame does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-6) check_orbit(method="dop853_c", tol=1e-9) return None def test_arbitraryaxisrotation_nullpotential(): # Test that integrating an orbit in a frame rotating around an # arbitrary axis works # Start with a test where there is no potential, so a static # object should remain static np = potential.NullPotential() def check_orbit(zvec=[1.0, 0.0, 1.0], omega=1.3, method="odeint", tol=1e-9): # Set up the rotating frame's rotation matrix rotpot = potential.RotateAndTiltWrapperPotential(pot=np, zvec=zvec) rot = rotpot._rot # Now integrate an orbit in the inertial frame # and then as seen by the rotating observer o = Orbit([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]) o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, np, method=method) # Non-inertial frame # First compute initial condition Rp, phip, zp = rotate_and_omega( o.R(), o.z(), phi=o.phi(), t=0.0, rot=rot, omega=-omega ) vRp, vTp, vzp = rotate_and_omega_vec( o.vR(), o.vT(), o.vz(), o.R(), o.z(), phi=o.phi(), t=0.0, rot=rot, omega=-omega, ) op = Orbit([Rp, vRp, vTp, zp, vzp, phip]) op.integrate( ts, RotatingPotentialWrapperPotential(pot=np, rot=rot, omega=omega) + potential.NonInertialFrameForce( Omega=numpy.array(derive_noninert_omega(omega, rot=rot)) ), method=method, ) # Compare # Orbit in the inertial frame o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) o_vRs = o.vR(ts) o_vTs = o.vT(ts) o_vzs = o.vz(ts) # and that computed in the non-inertial frame converted back to inertial op_xs, op_ys, op_zs = [], [], [] op_vRs, op_vTs, op_vzs = [], [], [] for ii, t in enumerate(ts): xyz = rotate_and_omega( op.x(t), op.y(t), phi=op.z(t), t=t, rot=rot, omega=omega, rect=True ) op_xs.append(xyz[0]) op_ys.append(xyz[1]) op_zs.append(xyz[2]) vRTz = rotate_and_omega_vec( op.vR(t), op.vT(t), op.vz(t), op.R(t), op.z(t), phi=op.phi(t), t=t, rot=rot, omega=omega, ) op_vRs.append(vRTz[0]) op_vTs.append(vRTz[1]) op_vzs.append(vRTz[2]) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vRs - op_vRs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vTs - op_vTs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vzs - op_vzs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(zvec=[0.0, 1.0, 1.0], omega=1.3, method="odeint", tol=1e-5) check_orbit(zvec=[2.0, 0.0, 1.0], omega=-1.1, method="dop853", tol=1e-4) check_orbit( zvec=[2.0, 3.0, 1.0], omega=0.9, method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_arbitraryaxisrotation(): # Test that integrating an orbit in a frame rotating around an # arbitrary axis works lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp def check_orbit(zvec=[1.0, 0.0, 1.0], omega=1.3, method="odeint", tol=1e-9): # Set up the rotating frame's rotation matrix rotpot = potential.RotateAndTiltWrapperPotential(pot=diskpot, zvec=zvec) rot = rotpot._rot # Now integrate an orbit in the inertial frame # and then as seen by the rotating observer o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot, method=method) # Non-inertial frame # First compute initial condition Rp, phip, zp = rotate_and_omega( o.R(), o.z(), phi=o.phi(), t=0.0, rot=rot, omega=-omega ) vRp, vTp, vzp = rotate_and_omega_vec( o.vR(), o.vT(), o.vz(), o.R(), o.z(), phi=o.phi(), t=0.0, rot=rot, omega=-omega, ) op = Orbit([Rp, vRp, vTp, zp, vzp, phip]) op.integrate( ts, RotatingPotentialWrapperPotential(pot=diskpot, rot=rot, omega=omega) + potential.NonInertialFrameForce( Omega=numpy.array(derive_noninert_omega(omega, rot=rot)) ), method=method, ) # Compare # Orbit in the inertial frame o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) o_vRs = o.vR(ts) o_vTs = o.vT(ts) o_vzs = o.vz(ts) # and that computed in the non-inertial frame converted back to inertial op_xs, op_ys, op_zs = [], [], [] op_vRs, op_vTs, op_vzs = [], [], [] for ii, t in enumerate(ts): xyz = rotate_and_omega( op.x(t), op.y(t), phi=op.z(t), t=t, rot=rot, omega=omega, rect=True ) op_xs.append(xyz[0]) op_ys.append(xyz[1]) op_zs.append(xyz[2]) vRTz = rotate_and_omega_vec( op.vR(t), op.vT(t), op.vz(t), op.R(t), op.z(t), phi=op.phi(t), t=t, rot=rot, omega=omega, ) op_vRs.append(vRTz[0]) op_vTs.append(vRTz[1]) op_vzs.append(vRTz[2]) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vRs - op_vRs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vTs - op_vTs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vzs - op_vzs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(zvec=[0.0, 1.0, 1.0], omega=1.3, method="odeint", tol=1e-5) check_orbit(zvec=[2.0, 0.0, 1.0], omega=-1.1, method="dop853", tol=1e-4) check_orbit( zvec=[2.0, 3.0, 1.0], omega=0.9, method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_arbitraryaxisrotation_omegadot_nullpotential(): # Test that integrating an orbit in a frame rotating around an # arbitrary axis works, where the frame rotation is changing in time # Start with a test where there is no potential, so a static # object should remain static np = potential.NullPotential() def check_orbit( zvec=[1.0, 0.0, 1.0], omega=1.3, omegadot=0.1, method="odeint", tol=1e-9 ): # Set up the rotating frame's rotation matrix rotpot = potential.RotateAndTiltWrapperPotential(pot=np, zvec=zvec) rot = rotpot._rot # Now integrate an orbit in the inertial frame # and then as seen by the rotating observer o = Orbit([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]) o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, np, method=method) # Non-inertial frame # First compute initial condition, no need to specify omegadot, bc t=0 Rp, phip, zp = rotate_and_omega( o.R(), o.z(), phi=o.phi(), t=0.0, rot=rot, omega=-omega ) vRp, vTp, vzp = rotate_and_omega_vec( o.vR(), o.vT(), o.vz(), o.R(), o.z(), phi=o.phi(), t=0.0, rot=rot, omega=-omega, ) op = Orbit([Rp, vRp, vTp, zp, vzp, phip]) # Omegadot is just a scaled version of Omega op.integrate( ts, RotatingPotentialWrapperPotential( pot=np, rot=rot, omega=omega, omegadot=omegadot ) + potential.NonInertialFrameForce( Omega=numpy.array(derive_noninert_omega(omega, rot=rot)), Omegadot=numpy.array(derive_noninert_omega(omega, rot=rot)) * omegadot / omega, ), method=method, ) # Compare # Orbit in the inertial frame o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) o_vRs = o.vR(ts) o_vTs = o.vT(ts) o_vzs = o.vz(ts) # and that computed in the non-inertial frame converted back to inertial op_xs, op_ys, op_zs = [], [], [] op_vRs, op_vTs, op_vzs = [], [], [] for ii, t in enumerate(ts): xyz = rotate_and_omega( op.x(t), op.y(t), phi=op.z(t), t=t, rot=rot, omega=omega, omegadot=omegadot, rect=True, ) op_xs.append(xyz[0]) op_ys.append(xyz[1]) op_zs.append(xyz[2]) vRTz = rotate_and_omega_vec( op.vR(t), op.vT(t), op.vz(t), op.R(t), op.z(t), phi=op.phi(t), t=t, rot=rot, omega=omega, omegadot=omegadot, ) op_vRs.append(vRTz[0]) op_vTs.append(vRTz[1]) op_vzs.append(vRTz[2]) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vRs - op_vRs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vTs - op_vTs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vzs - op_vzs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(zvec=[0.0, 1.0, 1.0], omega=1.3, method="odeint", tol=1e-5) check_orbit(zvec=[2.0, 0.0, 1.0], omega=-1.1, method="dop853", tol=1e-4) check_orbit( zvec=[2.0, 3.0, 1.0], omega=0.9, method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_arbitraryaxisrotation_omegadot(): # Test that integrating an orbit in a frame rotating around an # arbitrary axis works, where the frame rotation is changing in time # Start with a test where there is no potential, so a static # object should remain static lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp def check_orbit( zvec=[1.0, 0.0, 1.0], omega=1.3, omegadot=0.03, method="odeint", tol=1e-9 ): # Set up the rotating frame's rotation matrix rotpot = potential.RotateAndTiltWrapperPotential(pot=diskpot, zvec=zvec) rot = rotpot._rot # Now integrate an orbit in the inertial frame # and then as seen by the rotating observer o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot, method=method) # Non-inertial frame # First compute initial condition, no need to specify omegadot, bc t=0 Rp, phip, zp = rotate_and_omega( o.R(), o.z(), phi=o.phi(), t=0.0, rot=rot, omega=-omega ) vRp, vTp, vzp = rotate_and_omega_vec( o.vR(), o.vT(), o.vz(), o.R(), o.z(), phi=o.phi(), t=0.0, rot=rot, omega=-omega, ) op = Orbit([Rp, vRp, vTp, zp, vzp, phip]) # Omegadot is just a scaled version of Omega op.integrate( ts, RotatingPotentialWrapperPotential( pot=diskpot, rot=rot, omega=omega, omegadot=omegadot ) + potential.NonInertialFrameForce( Omega=numpy.array(derive_noninert_omega(omega, rot=rot)), Omegadot=numpy.array(derive_noninert_omega(omega, rot=rot)) * omegadot / omega, ), method=method, ) # Compare # Orbit in the inertial frame o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) o_vRs = o.vR(ts) o_vTs = o.vT(ts) o_vzs = o.vz(ts) # and that computed in the non-inertial frame converted back to inertial op_xs, op_ys, op_zs = [], [], [] op_vRs, op_vTs, op_vzs = [], [], [] for ii, t in enumerate(ts): xyz = rotate_and_omega( op.x(t), op.y(t), phi=op.z(t), t=t, rot=rot, omega=omega, omegadot=omegadot, rect=True, ) op_xs.append(xyz[0]) op_ys.append(xyz[1]) op_zs.append(xyz[2]) vRTz = rotate_and_omega_vec( op.vR(t), op.vT(t), op.vz(t), op.R(t), op.z(t), phi=op.phi(t), t=t, rot=rot, omega=omega, omegadot=omegadot, ) op_vRs.append(vRTz[0]) op_vTs.append(vRTz[1]) op_vzs.append(vRTz[2]) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vRs - op_vRs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vTs - op_vTs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vzs - op_vzs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(zvec=[2.0, 0.0, 1.0], omega=-1.1, method="dop853", tol=1e-5) return None def test_arbitraryaxisrotation_omegafunc_nullpotential(): # Test that integrating an orbit in a frame rotating around an # arbitrary axis works, where the frame rotation is changing in time # Start with a test where there is no potential, so a static # object should remain static np = potential.NullPotential() def check_orbit( zvec=[1.0, 0.0, 1.0], omega=1.3, omegadot=0.1, omegadotdot=0.01, method="odeint", tol=1e-9, ): # Set up the rotating frame's rotation matrix rotpot = potential.RotateAndTiltWrapperPotential(pot=np, zvec=zvec) rot = rotpot._rot # Now integrate an orbit in the inertial frame # and then as seen by the rotating observer o = Orbit([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]) o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, np, method=method) # Non-inertial frame # First compute initial condition, no need to specify omegadot, bc t=0 Rp, phip, zp = rotate_and_omega( o.R(), o.z(), phi=o.phi(), t=0.0, rot=rot, omega=-omega ) vRp, vTp, vzp = rotate_and_omega_vec( o.vR(), o.vT(), o.vz(), o.R(), o.z(), phi=o.phi(), t=0.0, rot=rot, omega=-omega, ) op = Orbit([Rp, vRp, vTp, zp, vzp, phip]) # Omegadot is just a scaled version of Omega Omega = numpy.array(derive_noninert_omega(omega, rot=rot)) Omegadot = numpy.array(derive_noninert_omega(omega, rot=rot)) * omegadot / omega Omegadotdot = ( numpy.array(derive_noninert_omega(omega, rot=rot)) * omegadotdot / omega ) op.integrate( ts, RotatingPotentialWrapperPotential( pot=np, rot=rot, omega=omega, omegadot=omegadot, omegadotdot=omegadotdot ) + potential.NonInertialFrameForce( Omega=[ lambda t: Omega[0] + Omegadot[0] * t + Omegadotdot[0] * t**2.0 / 2.0, lambda t: Omega[1] + Omegadot[1] * t + Omegadotdot[1] * t**2.0 / 2.0, lambda t: Omega[2] + Omegadot[2] * t + Omegadotdot[2] * t**2.0 / 2.0, ], Omegadot=[ lambda t: Omegadot[0] + Omegadotdot[0] * t, lambda t: Omegadot[1] + Omegadotdot[1] * t, lambda t: Omegadot[2] + Omegadotdot[2] * t, ], ), method=method, ) # Compare # Orbit in the inertial frame o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) o_vRs = o.vR(ts) o_vTs = o.vT(ts) o_vzs = o.vz(ts) # and that computed in the non-inertial frame converted back to inertial op_xs, op_ys, op_zs = [], [], [] op_vRs, op_vTs, op_vzs = [], [], [] for ii, t in enumerate(ts): xyz = rotate_and_omega( op.x(t), op.y(t), phi=op.z(t), t=t, rot=rot, omega=omega, omegadot=omegadot, omegadotdot=omegadotdot, rect=True, ) op_xs.append(xyz[0]) op_ys.append(xyz[1]) op_zs.append(xyz[2]) vRTz = rotate_and_omega_vec( op.vR(t), op.vT(t), op.vz(t), op.R(t), op.z(t), phi=op.phi(t), t=t, rot=rot, omega=omega, omegadot=omegadot, omegadotdot=omegadotdot, ) op_vRs.append(vRTz[0]) op_vTs.append(vRTz[1]) op_vzs.append(vRTz[2]) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vRs - op_vRs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vTs - op_vTs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vzs - op_vzs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(zvec=[0.0, 1.0, 1.0], omega=1.3, method="odeint", tol=1e-5) check_orbit(zvec=[2.0, 0.0, 1.0], omega=-1.1, method="dop853", tol=1e-4) check_orbit( zvec=[2.0, 3.0, 1.0], omega=0.9, method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_arbitraryaxisrotation_omegafunc(): # Test that integrating an orbit in a frame rotating around an # arbitrary axis works, where the frame rotation is changing in time # Start with a test where there is no potential, so a static # object should remain static lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp def check_orbit( zvec=[1.0, 0.0, 1.0], omega=1.3, omegadot=0.03, omegadotdot=0.01, method="odeint", tol=1e-9, ): # Set up the rotating frame's rotation matrix rotpot = potential.RotateAndTiltWrapperPotential(pot=diskpot, zvec=zvec) rot = rotpot._rot # Now integrate an orbit in the inertial frame # and then as seen by the rotating observer o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot, method=method) # Non-inertial frame # First compute initial condition, no need to specify omegadot, bc t=0 Rp, phip, zp = rotate_and_omega( o.R(), o.z(), phi=o.phi(), t=0.0, rot=rot, omega=-omega ) vRp, vTp, vzp = rotate_and_omega_vec( o.vR(), o.vT(), o.vz(), o.R(), o.z(), phi=o.phi(), t=0.0, rot=rot, omega=-omega, ) op = Orbit([Rp, vRp, vTp, zp, vzp, phip]) # Omegadot is just a scaled version of Omega Omega = numpy.array(derive_noninert_omega(omega, rot=rot)) Omegadot = numpy.array(derive_noninert_omega(omega, rot=rot)) * omegadot / omega Omegadotdot = ( numpy.array(derive_noninert_omega(omega, rot=rot)) * omegadotdot / omega ) op.integrate( ts, RotatingPotentialWrapperPotential( pot=diskpot, rot=rot, omega=omega, omegadot=omegadot, omegadotdot=omegadotdot, ) + potential.NonInertialFrameForce( Omega=[ lambda t: Omega[0] + Omegadot[0] * t + Omegadotdot[0] * t**2.0 / 2.0, lambda t: Omega[1] + Omegadot[1] * t + Omegadotdot[1] * t**2.0 / 2.0, lambda t: Omega[2] + Omegadot[2] * t + Omegadotdot[2] * t**2.0 / 2.0, ], Omegadot=[ lambda t: Omegadot[0] + Omegadotdot[0] * t, lambda t: Omegadot[1] + Omegadotdot[1] * t, lambda t: Omegadot[2] + Omegadotdot[2] * t, ], ), method=method, ) # Compare # Orbit in the inertial frame o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) o_vRs = o.vR(ts) o_vTs = o.vT(ts) o_vzs = o.vz(ts) # and that computed in the non-inertial frame converted back to inertial op_xs, op_ys, op_zs = [], [], [] op_vRs, op_vTs, op_vzs = [], [], [] for ii, t in enumerate(ts): xyz = rotate_and_omega( op.x(t), op.y(t), phi=op.z(t), t=t, rot=rot, omega=omega, omegadot=omegadot, omegadotdot=omegadotdot, rect=True, ) op_xs.append(xyz[0]) op_ys.append(xyz[1]) op_zs.append(xyz[2]) vRTz = rotate_and_omega_vec( op.vR(t), op.vT(t), op.vz(t), op.R(t), op.z(t), phi=op.phi(t), t=t, rot=rot, omega=omega, omegadot=omegadot, omegadotdot=omegadotdot, ) op_vRs.append(vRTz[0]) op_vTs.append(vRTz[1]) op_vzs.append(vRTz[2]) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vRs - op_vRs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vTs - op_vTs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vzs - op_vzs)) < tol, ( f"Integrating an orbit in a rotating frame around an arbitrary axis does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(zvec=[2.0, 0.0, 1.0], omega=-1.1, method="dop853", tol=1e-5) return None def test_linacc_constantacc_z(): # Test that a linearly-accelerating frame along the z direction works # with a constant acceleration lp = potential.LogarithmicHaloPotential(normalize=1.0) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp az = 0.02 # Trick to use a non-numba version, by causing numba compilation # to fail. Fails because scipy special is not supported in base # numba from scipy import special intaz = ( lambda t: 0.02 * t**2.0 / 2.0 * (special.erf(t) + 2.0) / (special.erf(t) + 2.0) ) framepot = potential.NonInertialFrameForce(a0=[0.0, 0.0, az]) diskframepot = ( AcceleratingPotentialWrapperPotential( pot=diskpot, x0=[lambda t: 0.0, lambda t: 0.0, intaz] ) + framepot ) def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot, method=method) # Non-inertial frame op = o() op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) op_xs = op.x(ts) op_ys = op.y(ts) op_zs = op.z(ts) + intaz(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-9) check_orbit(method="dop853", tol=1e-9) check_orbit( method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_linacc_constantacc_x_2d(): # Test that a linearly-accelerating frame along the x direction works # with a constant acceleration in 2D lp = potential.LogarithmicHaloPotential(normalize=1.0) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp ax = 0.02 intax = lambda t: ax * t**2.0 / 2.0 framepot = potential.NonInertialFrameForce(a0=[ax, 0.0, 0.0]) diskframepot = ( AcceleratingPotentialWrapperPotential( pot=diskpot, x0=[intax, lambda t: 0.0, lambda t: 0.0] ) + framepot ) def check_orbit(method="odeint", tol=1e-9): o = Orbit().toPlanar() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot, method=method) # Non-inertial frame op = o() op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.x(ts) o_ys = o.y(ts) op_xs = op.x(ts) + intax(ts) op_ys = op.y(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-5) check_orbit(method="dop853", tol=1e-9) check_orbit( method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_linacc_constantacc_xyz(): # Test that a linearly-accelerating frame along the z direction works # with a constant acceleration lp = potential.LogarithmicHaloPotential(normalize=1.0) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp ax, ay, az = -0.03, 0.04, 0.02 inta = [ lambda t: -0.03 * t**2.0 / 2.0, lambda t: 0.04 * t**2.0 / 2.0, lambda t: 0.02 * t**2.0 / 2.0, ] framepot = potential.NonInertialFrameForce(a0=[ax, ay, az]) diskframepot = ( AcceleratingPotentialWrapperPotential(pot=diskpot, x0=inta) + framepot ) def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot, method=method) # Non-inertial frame op = o() op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) op_xs = op.x(ts) + inta[0](ts) op_ys = op.y(ts) + inta[1](ts) op_zs = op.z(ts) + inta[2](ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-5) check_orbit(method="dop853", tol=1e-9) check_orbit( method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_linacc_changingacc_z(): # Test that a linearly-accelerating frame along the z direction works # with a changing acceleration lp = potential.LogarithmicHaloPotential(normalize=1.0) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp az = lambda t: 0.02 + 0.03 * t / 20.0 intaz = lambda t: 0.02 * t**2.0 / 2.0 + 0.03 * t**3.0 / 6.0 / 20.0 framepot = potential.NonInertialFrameForce(a0=[lambda t: 0.0, lambda t: 0.0, az]) diskframepot = ( AcceleratingPotentialWrapperPotential( pot=diskpot, x0=[lambda t: 0.0, lambda t: 0.0, intaz] ) + framepot ) def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot, method=method) # Non-inertial frame op = o() op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) op_xs = op.x(ts) op_ys = op.y(ts) op_zs = op.z(ts) + intaz(ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-9) check_orbit(method="dop853", tol=1e-9) check_orbit( method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_linacc_changingacc_xyz(): # Test that a linearly-accelerating frame along the z direction works # with a changing acceleration lp = potential.LogarithmicHaloPotential(normalize=1.0) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp ax, ay, az = ( lambda t: -0.03 - 0.03 * t / 20.0, lambda t: 0.04 + 0.08 * t / 20.0, lambda t: 0.02 + 0.03 * t / 20.0, ) inta = [ lambda t: -0.03 * t**2.0 / 2.0 - 0.03 * t**3.0 / 6.0 / 20.0, lambda t: 0.04 * t**2.0 / 2.0 + 0.08 * t**3.0 / 6.0 / 20.0, lambda t: 0.02 * t**2.0 / 2.0 + 0.03 * t**3.0 / 6.0 / 20.0, ] framepot = potential.NonInertialFrameForce(a0=[ax, ay, az]) diskframepot = ( AcceleratingPotentialWrapperPotential(pot=diskpot, x0=inta) + framepot ) def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot, method=method) # Non-inertial frame op = o() op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) op_xs = op.x(ts) + inta[0](ts) op_ys = op.y(ts) + inta[1](ts) op_zs = op.z(ts) + inta[2](ts) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a linearly-accelerating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-5) check_orbit(method="dop853", tol=1e-9) check_orbit( method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_linacc_changingacc_xyz_accellsrframe_scalaromegaz(): # Test that a linearly-accelerating frame along the z direction works # with a changing acceleration, also combining it with changing # rotation around the z axis lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp x0 = [ lambda t: -0.03 * t**2.0 / 2.0 - 0.03 * t**3.0 / 6.0 / 20.0, lambda t: 0.04 * t**2.0 / 2.0 + 0.08 * t**3.0 / 6.0 / 20.0, lambda t: 0.02 * t**2.0 / 2.0 + 0.03 * t**3.0 / 6.0 / 20.0, ] v0 = [ lambda t: -0.03 * t - 0.03 * t**2.0 / 2.0 / 20.0, lambda t: 0.04 * t + 0.08 * t**2.0 / 2.0 / 20.0, lambda t: 0.02 * t + 0.03 * t**2.0 / 2.0 / 20.0, ] a0 = [ lambda t: -0.03 - 0.03 * t / 20.0, lambda t: 0.04 + 0.08 * t / 20.0, lambda t: 0.02 + 0.03 * t / 20.0, ] omega = lp.omegac(1.0) omegadot = 0.02 framepot = potential.NonInertialFrameForce( x0=x0, v0=v0, a0=a0, Omega=omega, Omegadot=omegadot ) diskframepot = ( AcceleratingPotentialWrapperPotential( pot=diskpot, x0=x0, omegaz=omega, omegazdot=omegadot ) + framepot ) def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot, method=method) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.z(), o.vz(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) o_vxs = o.vx(ts) o_vys = o.vy(ts) o_vzs = o.vz(ts) op_xs = op.x(ts) + x0[0](ts) op_ys = op.y(ts) + x0[1](ts) op_zs = op.z(ts) + x0[2](ts) Rp, phip, _ = coords.rect_to_cyl(op_xs, op_ys, op_zs) phip += omega * ts + omegadot * ts**2.0 / 2.0 op_xs, op_ys, _ = coords.cyl_to_rect(Rp, phip, op_zs) op_vxs = op.vx(ts) + v0[0](ts) op_vys = op.vy(ts) + v0[1](ts) op_vzs = op.vz(ts) + v0[2](ts) vRp, vTp, _ = coords.rect_to_cyl_vec( op_vxs, op_vys, op_vzs, op.x(ts) + x0[0](ts), op.y(ts) + x0[1](ts), op.z(ts) + x0[2](ts), ) vTp += omega * Rp + omegadot * ts * Rp op_vxs, op_vys, _ = coords.cyl_to_rect_vec(vRp, vTp, op_vzs, phi=phip) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vxs - op_vxs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vys - op_vys)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vzs - op_vzs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-5) check_orbit(method="dop853", tol=1e-9) check_orbit( method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_linacc_changingacc_xyz_accellsrframe_vecomegaz(): # Test that a linearly-accelerating frame along the z direction works # with a changing acceleration, also combining it with changing # rotation around the z axis lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp x0 = [ lambda t: -0.03 * t**2.0 / 2.0 - 0.03 * t**3.0 / 6.0 / 20.0, lambda t: 0.04 * t**2.0 / 2.0 + 0.08 * t**3.0 / 6.0 / 20.0, lambda t: 0.02 * t**2.0 / 2.0 + 0.03 * t**3.0 / 6.0 / 20.0, ] v0 = [ lambda t: -0.03 * t - 0.03 * t**2.0 / 2.0 / 20.0, lambda t: 0.04 * t + 0.08 * t**2.0 / 2.0 / 20.0, lambda t: 0.02 * t + 0.03 * t**2.0 / 2.0 / 20.0, ] a0 = [ lambda t: -0.03 - 0.03 * t / 20.0, lambda t: 0.04 + 0.08 * t / 20.0, lambda t: 0.02 + 0.03 * t / 20.0, ] omega = lp.omegac(1.0) omegadot = 0.02 framepot = potential.NonInertialFrameForce( x0=x0, v0=v0, a0=a0, Omega=numpy.array([0.0, 0.0, omega]), Omegadot=numpy.array([0.0, 0.0, omegadot]), ) diskframepot = ( AcceleratingPotentialWrapperPotential( pot=diskpot, x0=x0, omegaz=omega, omegazdot=omegadot ) + framepot ) def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot, method=method) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.z(), o.vz(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) o_vxs = o.vx(ts) o_vys = o.vy(ts) o_vzs = o.vz(ts) op_xs = op.x(ts) + x0[0](ts) op_ys = op.y(ts) + x0[1](ts) op_zs = op.z(ts) + x0[2](ts) Rp, phip, _ = coords.rect_to_cyl(op_xs, op_ys, op_zs) phip += omega * ts + omegadot * ts**2.0 / 2.0 op_xs, op_ys, _ = coords.cyl_to_rect(Rp, phip, op_zs) op_vxs = op.vx(ts) + v0[0](ts) op_vys = op.vy(ts) + v0[1](ts) op_vzs = op.vz(ts) + v0[2](ts) vRp, vTp, _ = coords.rect_to_cyl_vec( op_vxs, op_vys, op_vzs, op.x(ts) + x0[0](ts), op.y(ts) + x0[1](ts), op.z(ts) + x0[2](ts), ) vTp += omega * Rp + omegadot * ts * Rp op_vxs, op_vys, _ = coords.cyl_to_rect_vec(vRp, vTp, op_vzs, phi=phip) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vxs - op_vxs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vys - op_vys)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vzs - op_vzs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-5) check_orbit(method="dop853", tol=1e-9) check_orbit( method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_linacc_changingacc_xyz_accellsrframe_scalarfuncomegaz(): # Test that a linearly-accelerating frame along the z direction works # with a changing acceleration, also combining it with changing # rotation around the z axis lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp x0 = [ lambda t: -0.03 * t**2.0 / 2.0 - 0.03 * t**3.0 / 6.0 / 20.0, lambda t: 0.04 * t**2.0 / 2.0 + 0.08 * t**3.0 / 6.0 / 20.0, lambda t: 0.02 * t**2.0 / 2.0 + 0.03 * t**3.0 / 6.0 / 20.0, ] v0 = [ lambda t: -0.03 * t - 0.03 * t**2.0 / 2.0 / 20.0, lambda t: 0.04 * t + 0.08 * t**2.0 / 2.0 / 20.0, lambda t: 0.02 * t + 0.03 * t**2.0 / 2.0 / 20.0, ] a0 = [ lambda t: -0.03 - 0.03 * t / 20.0, lambda t: 0.04 + 0.08 * t / 20.0, lambda t: 0.02 + 0.03 * t / 20.0, ] omega = lp.omegac(1.0) omegadot = 0.1 omegadotdot = 0.01 omega_func = lambda t: omega + omegadot * t + omegadotdot * t**2.0 / 2.0 omegadot_func = lambda t: omegadot + omegadotdot * t framepot = potential.NonInertialFrameForce( x0=x0, v0=v0, a0=a0, Omega=omega_func, Omegadot=omegadot_func ) diskframepot = ( AcceleratingPotentialWrapperPotential( pot=diskpot, x0=x0, omegaz=omega, omegazdot=omegadot, omegazdotdot=omegadotdot, ) + framepot ) def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot, method=method) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.z(), o.vz(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) o_vxs = o.vx(ts) o_vys = o.vy(ts) o_vzs = o.vz(ts) op_xs = op.x(ts) + x0[0](ts) op_ys = op.y(ts) + x0[1](ts) op_zs = op.z(ts) + x0[2](ts) Rp, phip, _ = coords.rect_to_cyl(op_xs, op_ys, op_zs) phip += omega * ts + omegadot * ts**2.0 / 2.0 + omegadotdot * ts**3.0 / 6.0 op_xs, op_ys, _ = coords.cyl_to_rect(Rp, phip, op_zs) op_vxs = op.vx(ts) + v0[0](ts) op_vys = op.vy(ts) + v0[1](ts) op_vzs = op.vz(ts) + v0[2](ts) vRp, vTp, _ = coords.rect_to_cyl_vec( op_vxs, op_vys, op_vzs, op.x(ts) + x0[0](ts), op.y(ts) + x0[1](ts), op.z(ts) + x0[2](ts), ) vTp += omega * Rp + omegadot * ts * Rp + omegadotdot * ts**2.0 / 2.0 * Rp op_vxs, op_vys, _ = coords.cyl_to_rect_vec(vRp, vTp, op_vzs, phi=phip) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vxs - op_vxs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vys - op_vys)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vzs - op_vzs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-5) check_orbit(method="dop853", tol=1e-9) check_orbit( method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_linacc_changingacc_xyz_accellsrframe_funcomegaz(): # Test that a linearly-accelerating frame along the z direction works # with a changing acceleration, also combining it with changing # rotation around the z axis lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp x0 = [ lambda t: -0.03 * t**2.0 / 2.0 - 0.03 * t**3.0 / 6.0 / 20.0, lambda t: 0.04 * t**2.0 / 2.0 + 0.08 * t**3.0 / 6.0 / 20.0, lambda t: 0.02 * t**2.0 / 2.0 + 0.03 * t**3.0 / 6.0 / 20.0, ] v0 = [ lambda t: -0.03 * t - 0.03 * t**2.0 / 2.0 / 20.0, lambda t: 0.04 * t + 0.08 * t**2.0 / 2.0 / 20.0, lambda t: 0.02 * t + 0.03 * t**2.0 / 2.0 / 20.0, ] a0 = [ lambda t: -0.03 - 0.03 * t / 20.0, lambda t: 0.04 + 0.08 * t / 20.0, lambda t: 0.02 + 0.03 * t / 20.0, ] omega = lp.omegac(1.0) omegadot = 0.1 omegadotdot = 0.01 omega_func = [ lambda t: 0.0, lambda t: 0.0, lambda t: omega + omegadot * t + omegadotdot * t**2.0 / 2.0, ] omegadot_func = [lambda t: 0.0, lambda t: 0.0, lambda t: omegadot + omegadotdot * t] framepot = potential.NonInertialFrameForce( x0=x0, v0=v0, a0=a0, Omega=omega_func, Omegadot=omegadot_func ) diskframepot = ( AcceleratingPotentialWrapperPotential( pot=diskpot, x0=x0, omegaz=omega, omegazdot=omegadot, omegazdotdot=omegadotdot, ) + framepot ) def check_orbit(method="odeint", tol=1e-9): o = Orbit() o.turn_physical_off() # Inertial frame ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot, method=method) # Non-inertial frame op = Orbit([o.R(), o.vR(), o.vT() - omega * o.R(), o.z(), o.vz(), o.phi()]) op.integrate(ts, diskframepot, method=method) # Compare o_xs = o.x(ts) o_ys = o.y(ts) o_zs = o.z(ts) o_vxs = o.vx(ts) o_vys = o.vy(ts) o_vzs = o.vz(ts) op_xs = op.x(ts) + x0[0](ts) op_ys = op.y(ts) + x0[1](ts) op_zs = op.z(ts) + x0[2](ts) Rp, phip, _ = coords.rect_to_cyl(op_xs, op_ys, op_zs) phip += omega * ts + omegadot * ts**2.0 / 2.0 + omegadotdot * ts**3.0 / 6.0 op_xs, op_ys, _ = coords.cyl_to_rect(Rp, phip, op_zs) op_vxs = op.vx(ts) + v0[0](ts) op_vys = op.vy(ts) + v0[1](ts) op_vzs = op.vz(ts) + v0[2](ts) vRp, vTp, _ = coords.rect_to_cyl_vec( op_vxs, op_vys, op_vzs, op.x(ts) + x0[0](ts), op.y(ts) + x0[1](ts), op.z(ts) + x0[2](ts), ) vTp += omega * Rp + omegadot * ts * Rp + omegadotdot * ts**2.0 / 2.0 * Rp op_vxs, op_vys, _ = coords.cyl_to_rect_vec(vRp, vTp, op_vzs, phi=phip) assert numpy.amax(numpy.fabs(o_xs - op_xs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_ys - op_ys)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_zs - op_zs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vxs - op_vxs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vys - op_vys)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) assert numpy.amax(numpy.fabs(o_vzs - op_vzs)) < tol, ( f"Integrating an orbit in a linearly-accelerating, acceleratingly-rotating frame with constant acceleration does not agree with the equivalent orbit in the inertial frame for method {method}" ) check_orbit(method="odeint", tol=1e-5) check_orbit(method="dop853", tol=1e-9) check_orbit( method="dop853_c", tol=1e-5 ) # Lower tol, because diff integrators for inertial and non-inertial, bc wrapper not implemented in C return None def test_python_vs_c_arbitraryaxisrotation(): # Integrate an orbit in both Python and C to check that they match # We don't need to known the true answer here lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp def check_orbit( zvec=[1.0, 0.0, 1.0], omega=1.3, py_method="dop853", c_method="dop853_c", tol=1e-9, ): # Set up the rotating frame's rotation matrix rotpot = potential.RotateAndTiltWrapperPotential(pot=diskpot, zvec=zvec) rot = rotpot._rot # Now integrate an orbit in the rotating frame in Python o = Orbit() o.turn_physical_off() # Rotating frame in Python ts = numpy.linspace(0.0, 20.0, 1001) o.integrate( ts, diskpot + potential.NonInertialFrameForce( Omega=numpy.array(derive_noninert_omega(omega, rot=rot)) ), method=py_method, ) # In C op = o() op.integrate( ts, diskpot + potential.NonInertialFrameForce( Omega=numpy.array(derive_noninert_omega(omega, rot=rot)) ), method=c_method, ) assert numpy.amax(numpy.fabs(o.x(ts) - op.x(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.y(ts) - op.y(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.z(ts) - op.z(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vx(ts) - op.vx(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vy(ts) - op.vy(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vz(ts) - op.vz(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) return None check_orbit() return None def test_python_vs_c_arbitraryaxisrotation_omegadot(): # Integrate an orbit in both Python and C to check that they match # We don't need to known the true answer here lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp def check_orbit( zvec=[1.0, 0.0, 1.0], omega=1.3, omegadot=0.03, py_method="dop853", c_method="dop853_c", tol=1e-9, ): # Set up the rotating frame's rotation matrix rotpot = potential.RotateAndTiltWrapperPotential(pot=diskpot, zvec=zvec) rot = rotpot._rot # Now integrate an orbit in the rotating frame in Python o = Orbit() o.turn_physical_off() # Rotating frame in Python ts = numpy.linspace(0.0, 20.0, 1001) o.integrate( ts, diskpot + potential.NonInertialFrameForce( Omega=numpy.array(derive_noninert_omega(omega, rot=rot)), Omegadot=numpy.array(derive_noninert_omega(omega, rot=rot)) * omegadot / omega, ), method=py_method, ) # In C op = o() op.integrate( ts, diskpot + potential.NonInertialFrameForce( Omega=numpy.array(derive_noninert_omega(omega, rot=rot)), Omegadot=numpy.array(derive_noninert_omega(omega, rot=rot)) * omegadot / omega, ), method=c_method, ) # Compare assert numpy.amax(numpy.fabs(o.x(ts) - op.x(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.y(ts) - op.y(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.z(ts) - op.z(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vx(ts) - op.vx(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vy(ts) - op.vy(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vz(ts) - op.vz(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) return None check_orbit(tol=1e-8) return None def test_python_vs_c_arbitraryaxisrotation_funcomega(): # Integrate an orbit in both Python and C to check that they match # We don't need to known the true answer here lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp def check_orbit( zvec=[1.0, 0.0, 1.0], omega=1.3, omegadot=0.03, omegadotdot=0.01, py_method="dop853", c_method="dop853_c", tol=1e-9, ): # Set up the rotating frame's rotation matrix rotpot = potential.RotateAndTiltWrapperPotential(pot=diskpot, zvec=zvec) rot = rotpot._rot # Now integrate an orbit in the rotating frame in Python o = Orbit() o.turn_physical_off() # Rotating frame in Python ts = numpy.linspace(0.0, 20.0, 1001) Omega = numpy.array(derive_noninert_omega(omega, rot=rot)) Omegadot = numpy.array(derive_noninert_omega(omega, rot=rot)) * omegadot / omega Omegadotdot = ( numpy.array(derive_noninert_omega(omega, rot=rot)) * omegadotdot / omega ) o.integrate( ts, diskpot + potential.NonInertialFrameForce( Omega=[ lambda t: Omega[0] + Omegadot[0] * t + Omegadotdot[0] * t**2.0 / 2.0, lambda t: Omega[1] + Omegadot[1] * t + Omegadotdot[1] * t**2.0 / 2.0, lambda t: Omega[2] + Omegadot[2] * t + Omegadotdot[2] * t**2.0 / 2.0, ], Omegadot=[ lambda t: Omegadot[0] + Omegadotdot[0] * t, lambda t: Omegadot[1] + Omegadotdot[1] * t, lambda t: Omegadot[2] + Omegadotdot[2] * t, ], ), method=py_method, ) # In C op = o() op.integrate( ts, diskpot + potential.NonInertialFrameForce( Omega=[ lambda t: Omega[0] + Omegadot[0] * t + Omegadotdot[0] * t**2.0 / 2.0, lambda t: Omega[1] + Omegadot[1] * t + Omegadotdot[1] * t**2.0 / 2.0, lambda t: Omega[2] + Omegadot[2] * t + Omegadotdot[2] * t**2.0 / 2.0, ], Omegadot=[ lambda t: Omegadot[0] + Omegadotdot[0] * t, lambda t: Omegadot[1] + Omegadotdot[1] * t, lambda t: Omegadot[2] + Omegadotdot[2] * t, ], ), method=c_method, ) # Compare assert numpy.amax(numpy.fabs(o.x(ts) - op.x(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.y(ts) - op.y(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.z(ts) - op.z(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vx(ts) - op.vx(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vy(ts) - op.vy(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vz(ts) - op.vz(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) return None check_orbit(tol=1e-8) return None def test_python_vs_c_linacc_changingacc_xyz(): # Integrate an orbit in both Python and C to check that they match # We don't need to known the true answer here lp = potential.LogarithmicHaloPotential(normalize=1.0) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp ax, ay, az = ( lambda t: -0.03 - 0.03 * t / 20.0, lambda t: 0.04 + 0.08 * t / 20.0, lambda t: 0.02 + 0.03 * t / 20.0, ) inta = [ lambda t: -0.03 * t**2.0 / 2.0 - 0.03 * t**3.0 / 6.0 / 20.0, lambda t: 0.04 * t**2.0 / 2.0 + 0.08 * t**3.0 / 6.0 / 20.0, lambda t: 0.02 * t**2.0 / 2.0 + 0.03 * t**3.0 / 6.0 / 20.0, ] framepot = potential.NonInertialFrameForce(a0=[ax, ay, az]) def check_orbit(py_method="dop853", c_method="dop853_c", tol=1e-9): o = Orbit() o.turn_physical_off() # In Python ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot + framepot, method=py_method) # In C op = o() op.integrate(ts, diskpot + framepot, method=c_method) # Compare assert numpy.amax(numpy.fabs(o.x(ts) - op.x(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.y(ts) - op.y(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.z(ts) - op.z(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vx(ts) - op.vx(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vy(ts) - op.vy(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vz(ts) - op.vz(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) return None check_orbit(tol=1e-10) return None def test_python_vs_c_linacc_changingacc_xyz_accellsrframe_scalaromegaz(): # Integrate an orbit in both Python and C to check that they match # We don't need to known the true answer here lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp x0 = [ lambda t: -0.03 * t**2.0 / 2.0 - 0.03 * t**3.0 / 6.0 / 20.0, lambda t: 0.04 * t**2.0 / 2.0 + 0.08 * t**3.0 / 6.0 / 20.0, lambda t: 0.02 * t**2.0 / 2.0 + 0.03 * t**3.0 / 6.0 / 20.0, ] v0 = [ lambda t: -0.03 * t - 0.03 * t**2.0 / 2.0 / 20.0, lambda t: 0.04 * t + 0.08 * t**2.0 / 2.0 / 20.0, lambda t: 0.02 * t + 0.03 * t**2.0 / 2.0 / 20.0, ] a0 = [ lambda t: -0.03 - 0.03 * t / 20.0, lambda t: 0.04 + 0.08 * t / 20.0, lambda t: 0.02 + 0.03 * t / 20.0, ] omega = lp.omegac(1.0) omegadot = 0.02 framepot = potential.NonInertialFrameForce( x0=x0, v0=v0, a0=a0, Omega=omega, Omegadot=omegadot ) def check_orbit(py_method="dop853", c_method="dop853_c", tol=1e-9): # Now integrate an orbit in the rotating frame in Python o = Orbit() o.turn_physical_off() # Rotating frame in Python ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot + framepot, method=py_method) # In C op = o() op.integrate(ts, diskpot + framepot, method=c_method) assert numpy.amax(numpy.fabs(o.x(ts) - op.x(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.y(ts) - op.y(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.z(ts) - op.z(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vx(ts) - op.vx(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vy(ts) - op.vy(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vz(ts) - op.vz(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) return None check_orbit() return None def test_python_vs_c_linacc_changingacc_xyz_accellsrframe_scalaromegaz_2d(): # Integrate an orbit in both Python and C to check that they match in 2D # We don't need to known the true answer here lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp x0 = [ lambda t: -0.03 * t**2.0 / 2.0 - 0.03 * t**3.0 / 6.0 / 20.0, lambda t: 0.04 * t**2.0 / 2.0 + 0.08 * t**3.0 / 6.0 / 20.0, lambda t: 0.0, ] v0 = [ lambda t: -0.03 * t - 0.03 * t**2.0 / 2.0 / 20.0, lambda t: 0.04 * t + 0.08 * t**2.0 / 2.0 / 20.0, lambda t: 0.0, ] a0 = [ lambda t: -0.03 - 0.03 * t / 20.0, lambda t: 0.04 + 0.08 * t / 20.0, lambda t: 0.0, ] omega = lp.omegac(1.0) omegadot = 0.02 framepot = potential.NonInertialFrameForce( x0=x0, v0=v0, a0=a0, Omega=omega, Omegadot=omegadot ) def check_orbit(py_method="dop853", c_method="dop853_c", tol=1e-8): # Now integrate an orbit in the rotating frame in Python o = Orbit().toPlanar() o.turn_physical_off() # Rotating frame in Python ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot + framepot, method=py_method) # In C op = o() op.integrate(ts, diskpot + framepot, method=c_method) assert numpy.amax(numpy.fabs(o.x(ts) - op.x(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.y(ts) - op.y(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vx(ts) - op.vx(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vy(ts) - op.vy(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) return None check_orbit() return None def test_python_vs_c_linacc_changingacc_xyz_accellsrframe_vecomegaz(): # Integrate an orbit in both Python and C to check that they match # We don't need to known the true answer here lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp x0 = [ lambda t: -0.03 * t**2.0 / 2.0 - 0.03 * t**3.0 / 6.0 / 20.0, lambda t: 0.04 * t**2.0 / 2.0 + 0.08 * t**3.0 / 6.0 / 20.0, lambda t: 0.02 * t**2.0 / 2.0 + 0.03 * t**3.0 / 6.0 / 20.0, ] v0 = [ lambda t: -0.03 * t - 0.03 * t**2.0 / 2.0 / 20.0, lambda t: 0.04 * t + 0.08 * t**2.0 / 2.0 / 20.0, lambda t: 0.02 * t + 0.03 * t**2.0 / 2.0 / 20.0, ] a0 = [ lambda t: -0.03 - 0.03 * t / 20.0, lambda t: 0.04 + 0.08 * t / 20.0, lambda t: 0.02 + 0.03 * t / 20.0, ] omega = lp.omegac(1.0) omegadot = 0.02 framepot = potential.NonInertialFrameForce( x0=x0, v0=v0, a0=a0, Omega=numpy.array([0.0, 0.0, omega]), Omegadot=numpy.array([0.0, 0.0, omegadot]), ) def check_orbit(py_method="dop853", c_method="dop853_c", tol=1e-9): # Now integrate an orbit in the rotating frame in Python o = Orbit() o.turn_physical_off() # Rotating frame in Python ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot + framepot, method=py_method) # In C op = o() op.integrate(ts, diskpot + framepot, method=c_method) assert numpy.amax(numpy.fabs(o.x(ts) - op.x(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.y(ts) - op.y(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.z(ts) - op.z(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vx(ts) - op.vx(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vy(ts) - op.vy(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vz(ts) - op.vz(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) return None check_orbit() return None def test_python_vs_c_linacc_changingacc_xyz_accellsrframe_scalarfuncomegaz(): # Integrate an orbit in both Python and C to check that they match # We don't need to known the true answer here lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp x0 = [ lambda t: -0.03 * t**2.0 / 2.0 - 0.03 * t**3.0 / 6.0 / 20.0, lambda t: 0.04 * t**2.0 / 2.0 + 0.08 * t**3.0 / 6.0 / 20.0, lambda t: 0.02 * t**2.0 / 2.0 + 0.03 * t**3.0 / 6.0 / 20.0, ] v0 = [ lambda t: -0.03 * t - 0.03 * t**2.0 / 2.0 / 20.0, lambda t: 0.04 * t + 0.08 * t**2.0 / 2.0 / 20.0, lambda t: 0.02 * t + 0.03 * t**2.0 / 2.0 / 20.0, ] a0 = [ lambda t: -0.03 - 0.03 * t / 20.0, lambda t: 0.04 + 0.08 * t / 20.0, lambda t: 0.02 + 0.03 * t / 20.0, ] omega = lp.omegac(1.0) omegadot = 0.1 omegadotdot = 0.01 omega_func = lambda t: omega + omegadot * t + omegadotdot * t**2.0 / 2.0 omegadot_func = lambda t: omegadot + omegadotdot * t framepot = potential.NonInertialFrameForce( x0=x0, v0=v0, a0=a0, Omega=omega_func, Omegadot=omegadot_func ) def check_orbit(py_method="dop853", c_method="dop853_c", tol=1e-8): # Now integrate an orbit in the rotating frame in Python o = Orbit() o.turn_physical_off() # Rotating frame in Python ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot + framepot, method=py_method) # In C op = o() op.integrate(ts, diskpot + framepot, method=c_method) assert numpy.amax(numpy.fabs(o.x(ts) - op.x(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.y(ts) - op.y(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.z(ts) - op.z(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vx(ts) - op.vx(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vy(ts) - op.vy(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vz(ts) - op.vz(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) return None check_orbit() return None def test_python_vs_c_linacc_changingacc_xyz_accellsrframe_vecomegaz(): # Integrate an orbit in both Python and C to check that they match # We don't need to known the true answer here lp = potential.MiyamotoNagaiPotential(normalize=1.0, a=1.0, b=0.2) dp = potential.DehnenBarPotential(omegab=1.8, rb=0.5, Af=0.03) diskpot = lp + dp x0 = [ lambda t: -0.03 * t**2.0 / 2.0 - 0.03 * t**3.0 / 6.0 / 20.0, lambda t: 0.04 * t**2.0 / 2.0 + 0.08 * t**3.0 / 6.0 / 20.0, lambda t: 0.02 * t**2.0 / 2.0 + 0.03 * t**3.0 / 6.0 / 20.0, ] v0 = [ lambda t: -0.03 * t - 0.03 * t**2.0 / 2.0 / 20.0, lambda t: 0.04 * t + 0.08 * t**2.0 / 2.0 / 20.0, lambda t: 0.02 * t + 0.03 * t**2.0 / 2.0 / 20.0, ] a0 = [ lambda t: -0.03 - 0.03 * t / 20.0, lambda t: 0.04 + 0.08 * t / 20.0, lambda t: 0.02 + 0.03 * t / 20.0, ] omega = lp.omegac(1.0) omegadot = 0.1 omegadotdot = 0.01 omega_func = [ lambda t: 0.0, lambda t: 0.0, lambda t: omega + omegadot * t + omegadotdot * t**2.0 / 2.0, ] omegadot_func = [lambda t: 0.0, lambda t: 0.0, lambda t: omegadot + omegadotdot * t] framepot = potential.NonInertialFrameForce( x0=x0, v0=v0, a0=a0, Omega=omega_func, Omegadot=omegadot_func ) def check_orbit(py_method="dop853", c_method="dop853_c", tol=1e-8): # Now integrate an orbit in the rotating frame in Python o = Orbit() o.turn_physical_off() # Rotating frame in Python ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, diskpot + framepot, method=py_method) # In C op = o() op.integrate(ts, diskpot + framepot, method=c_method) assert numpy.amax(numpy.fabs(o.x(ts) - op.x(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.y(ts) - op.y(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.z(ts) - op.z(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vx(ts) - op.vx(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vy(ts) - op.vy(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) assert numpy.amax(numpy.fabs(o.vz(ts) - op.vz(ts))) < tol, ( f"Integrating an orbit in a rotating frame in Python does not agree with integrating the same orbit in C; using methods {py_method} and {c_method}" ) return None check_orbit() return None from galpy.potential.Potential import ( _evaluatephitorques, _evaluateRforces, _evaluatezforces, ) # Utility wrappers and other functions from galpy.potential.WrapperPotential import parentWrapperPotential class AcceleratingPotentialWrapperPotential(parentWrapperPotential): def __init__( self, amp=1.0, pot=None, x0=[lambda t: 0.0, lambda t: 0.0, lambda t: 0.0], omegaz=None, omegazdot=None, omegazdotdot=None, ro=None, vo=None, ): # x0 = accelerated origin self._x0 = x0 # we also allow for rotation around the z axis: # Omega(t) = omegaz + omegazdot * t + omegazdotdot * t^2 / 2 self._omegaz = omegaz self._omegazdot = omegazdot self._omegazdotdot = omegazdotdot def _Rforce(self, R, z, phi=0.0, t=0.0): Fxyz = self._force_xyz(R, z, phi=phi, t=t) return numpy.cos(phi) * Fxyz[0] + numpy.sin(phi) * Fxyz[1] def _phitorque(self, R, z, phi=0.0, t=0.0): Fxyz = self._force_xyz(R, z, phi=phi, t=t) return R * (-numpy.sin(phi) * Fxyz[0] + numpy.cos(phi) * Fxyz[1]) def _zforce(self, R, z, phi=0.0, t=0.0): return self._force_xyz(R, z, phi=phi, t=t)[2] def _force_xyz(self, R, z, phi=0.0, t=0.0): """Get the rectangular forces in the transformed frame""" x, y, _ = coords.cyl_to_rect(R, phi, z) xp = x + self._x0[0](t) yp = y + self._x0[1](t) zp = z + self._x0[2](t) Rp, phip, zp = coords.rect_to_cyl(xp, yp, zp) if not self._omegaz is None: phip += self._omegaz * t if not self._omegazdot is None: phip += self._omegazdot * t**2.0 / 2.0 if not self._omegazdotdot is None: phip += self._omegazdotdot * t**3.0 / 6.0 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 if not self._omegaz is None: rotphi = self._omegaz * t if not self._omegazdot is None: rotphi += self._omegazdot * t**2.0 / 2.0 if not self._omegazdotdot is None: rotphi += self._omegazdotdot * t**3.0 / 6.0 return numpy.dot( numpy.array( [ [numpy.cos(rotphi), numpy.sin(rotphi), 0.0], [-numpy.sin(rotphi), numpy.cos(rotphi), 0.0], [0.0, 0.0, 1.0], ] ), numpy.array([xforcep, yforcep, zforcep]), ) else: return numpy.array([xforcep, yforcep, zforcep]) # Functions and wrappers for rotation around an arbitrary axis # Rotation happens around an axis that is rotated by rot # So transformation from rotating to inertial is # rot.T x omega-rotation x rot def rotate_and_omega( R, z, phi=0.0, t=0.0, rot=None, omega=None, omegadot=None, omegadotdot=None, rect=False, ): # From the rotating frame to the inertial frame if rect: x, y, z = R, z, phi else: x, y, z = coords.cyl_to_rect(R, phi, z) xyzp = numpy.dot(rot, numpy.array([x, y, z])) Rp, phip, zp = coords.rect_to_cyl(xyzp[0], xyzp[1], xyzp[2]) phip += omega * t if not omegadot is None: phip += omegadot * t**2.0 / 2.0 if not omegadotdot is None: phip += omegadotdot * t**3.0 / 6.0 xp, yp, zp = coords.cyl_to_rect(Rp, phip, zp) xyz = numpy.dot(rot.T, numpy.array([xp, yp, zp])) if rect: R, phi, z = xyz[0], xyz[1], xyz[2] else: R, phi, z = coords.rect_to_cyl(xyz[0], xyz[1], xyz[2]) return R, phi, z def rotate_and_omega_vec( vR, vT, vz, R, z, phi=0.0, t=0.0, rot=None, omega=None, omegadot=None, omegadotdot=None, ): # From the rotating frame to the inertial frame, for vectors x, y, z = coords.cyl_to_rect(R, phi, z) vx, vy, vz = coords.cyl_to_rect_vec(vR, vT, vz, phi=phi) xyzp = numpy.dot(rot, numpy.array([x, y, z])) Rp, phip, zp = coords.rect_to_cyl(xyzp[0], xyzp[1], xyzp[2]) vxyzp = numpy.dot(rot, numpy.array([vx, vy, vz])) vRp, vTp, vzp = coords.rect_to_cyl_vec( vxyzp[0], vxyzp[1], vxyzp[2], xyzp[0], xyzp[1], xyzp[2] ) phip += omega * t vTp += omega * Rp if not omegadot is None: phip += omegadot * t**2.0 / 2.0 vTp += omegadot * t * Rp if not omegadotdot is None: phip += omegadotdot * t**3.0 / 6.0 vTp += omegadotdot * t**2.0 / 2.0 * Rp xp, yp, zp = coords.cyl_to_rect(Rp, phip, zp) vxp, vyp, vzp = coords.cyl_to_rect_vec(vRp, vTp, vzp, phi=phip) xyz = numpy.dot(rot.T, numpy.array([xp, yp, zp])) vxyz = numpy.dot(rot.T, numpy.array([vxp, vyp, vzp])) vR, vT, vz = coords.rect_to_cyl_vec( vxyz[0], vxyz[1], vxyz[2], xyz[0], xyz[1], xyz[2] ) return vR, vT, vz def derive_noninert_omega( omega, rot=None, x=1.0, y=2.0, z=3.0, x2=-1.0, y2=2.0, z2=-5.0, t=0.0 ): # Numerically compute Omega of the non-inertial frame # Need to use the rotation of two arbitrary (non-parallel) vectors # To fully describe the rotation # Then can solve for it # See https://en.wikipedia.org/wiki/Rotation_formalisms_in_three_dimensions#Conversion_formulae_for_derivatives # Let's begin # Compute d r / d t, numerically eps = 1e-6 r11 = rotate_and_omega(x, y, phi=z, t=t, rot=rot, omega=omega, rect=True) r12 = rotate_and_omega(x, y, phi=z, t=t + eps, rot=rot, omega=omega, rect=True) dr1dt = (numpy.array(r12) - numpy.array(r11)) / eps r21 = rotate_and_omega(x2, y2, phi=z2, t=t, rot=rot, omega=omega, rect=True) r22 = rotate_and_omega(x2, y2, phi=z2, t=t + eps, rot=rot, omega=omega, rect=True) dr2dt = (numpy.array(r22) - numpy.array(r21)) / eps # Solve # dxdt= -omegaz * y + omegay * z # dydt= omegaz * x - omegax * z # dzdt= -omegay * x + omegax * y # with the two given points. Use # dy1dt= omegaz * x1 - omegax * z1 # dy2dt= omegaz * x2 - omegax * z2 # dy1dt * x2 - dy2dt * x1 = -omegax * ( z1 * x2 - z2 * x1) # and # dx1dt= -omegaz * y1 + omegay * z1 # dx2dt= -omegaz * y2 + omegay * z2 # dx1dt * y2 - dx2dt * y1 = omegay * ( z1 * y2 - z2 * y1 ) # dx1dt * z2 - dx2dt * z1 = -omegaz * ( y1 * z2 - y2 * z1 ) omegax = -(dr1dt[1] * r21[0] - dr2dt[1] * r11[0]) / ( r11[2] * r21[0] - r21[2] * r11[0] ) omegay = (dr1dt[0] * r21[1] - dr2dt[0] * r11[1]) / ( r11[2] * r21[1] - r21[2] * r11[1] ) omegaz = -(dr1dt[0] * r21[2] - dr2dt[0] * r11[2]) / ( r11[1] * r21[2] - r21[1] * r11[2] ) return (omegax, omegay, omegaz) class RotatingPotentialWrapperPotential(parentWrapperPotential): def __init__( self, amp=1.0, pot=None, rot=None, omega=None, omegadot=None, omegadotdot=None, ro=None, vo=None, ): # Frame rotates as rot.T x omega-rotation x rot, see transformations above self._rot = rot self._omega = omega self._omegadot = omegadot self._omegadotdot = omegadotdot def _Rforce(self, R, z, phi=0.0, t=0.0): Fxyz = self._force_xyz(R, z, phi=phi, t=t) return numpy.cos(phi) * Fxyz[0] + numpy.sin(phi) * Fxyz[1] def _phitorque(self, R, z, phi=0.0, t=0.0): Fxyz = self._force_xyz(R, z, phi=phi, t=t) return R * (-numpy.sin(phi) * Fxyz[0] + numpy.cos(phi) * Fxyz[1]) def _zforce(self, R, z, phi=0.0, t=0.0): return self._force_xyz(R, z, phi=phi, t=t)[2] def _force_xyz(self, R, z, phi=0.0, t=0.0): """Get the rectangular forces in the transformed frame""" Rp, phip, zp = rotate_and_omega( R, z, phi=phi, t=t, rot=self._rot, omega=self._omega, omegadot=self._omegadot, omegadotdot=self._omegadotdot, ) 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 # Now figure out the inverse rotation matrix to rotate the forces # The way this is written, we effectively compute the transpose of the # rotation matrix, which is its inverse inv_rot = numpy.array( [ list( rotate_and_omega( 1.0, 0.0, phi=0.0, t=t, rot=self._rot, omega=self._omega, omegadot=self._omegadot, omegadotdot=self._omegadotdot, rect=True, ) ), list( rotate_and_omega( 0.0, 1.0, phi=0.0, t=t, rot=self._rot, omega=self._omega, omegadot=self._omegadot, omegadotdot=self._omegadotdot, rect=True, ) ), list( rotate_and_omega( 0.0, 0.0, phi=1.0, t=t, rot=self._rot, omega=self._omega, omegadot=self._omegadot, omegadotdot=self._omegadotdot, rect=True, ) ), ] ) return numpy.dot(inv_rot, numpy.array([xforcep, yforcep, zforcep])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_orbit.py0000644000175100001660000137201014761352023016213 0ustar00runnerdocker##############################TESTS ON ORBITS################################## import os import os.path import platform import sys import warnings WIN32 = platform.system() == "Windows" MACOS = platform.system() == "Darwin" import signal import subprocess import time import astropy import numpy import pytest PY2 = sys.version < "3" _APY3 = astropy.__version__ > "3" from test_actionAngle import reset_warning_registry from test_potential import ( BurkertPotentialNoC, NFWTwoPowerTriaxialPotential, altExpwholeDiskSCFPotential, expwholeDiskSCFPotential, fullyRotatedTriaxialNFWPotential, mockAdiabaticContractionMWP14WrapperPotential, mockCombLinearPotential, mockFlatCorotatingRotationSpiralArmsPotential, mockFlatCosmphiDiskPotential, mockFlatCosmphiDiskwBreakPotential, mockFlatDehnenBarPotential, mockFlatDehnenSmoothBarPotential, mockFlatEllipticalDiskPotential, mockFlatGaussianAmplitudeBarPotential, mockFlatLopsidedDiskPotential, mockFlatSolidBodyRotationPlanarSpiralArmsPotential, mockFlatSolidBodyRotationSpiralArmsPotential, mockFlatSpiralArmsPotential, mockFlatSteadyLogSpiralPotential, mockFlatTransientLogSpiralPotential, mockFlatTrulyCorotatingRotationSpiralArmsPotential, mockFlatTrulyGaussianAmplitudeBarPotential, mockInterpSphericalPotential, mockKuzminLikeWrapperPotential, mockMovingObjectLongIntPotential, mockRotatedAndTiltedMWP14WrapperPotential, mockRotatingFlatSpiralArmsPotential, mockSCFAxiDensity1Potential, mockSCFAxiDensity2Potential, mockSCFDensityPotential, mockSCFNFWPotential, mockSCFZeeuwPotential, mockSimpleLinearPotential, mockSlowFlatDecayingDehnenSmoothBarPotential, mockSlowFlatDehnenBarPotential, mockSlowFlatDehnenSmoothBarPotential, mockSlowFlatEllipticalDiskPotential, mockSlowFlatSteadyLogSpiralPotential, mockSpecialRotatingFlatSpiralArmsPotential, nestedListPotential, oblateHernquistPotential, oblateNFWPotential, prolateJaffePotential, prolateNFWPotential, sech2DiskSCFPotential, specialFlattenedPowerPotential, specialMiyamotoNagaiPotential, testlinearMWPotential, testMWPotential, testNullPotential, testorbitHenonHeilesPotential, testplanarMWPotential, triaxialLogarithmicHaloPotential, triaxialNFWPotential, ) from galpy import potential from galpy.potential.Potential import _check_c from galpy.util import galpyWarning from galpy.util.coords import _K _GHACTIONS = bool(os.getenv("GITHUB_ACTIONS")) if not _GHACTIONS: _QUICKTEST = True # Run a more limited set of tests else: _QUICKTEST = True # Also do this for GH Actions, bc otherwise it takes too long _NOLONGINTEGRATIONS = False # Don't show all warnings, to reduce log output warnings.simplefilter("always", galpyWarning) # Test whether the energy of simple orbits is conserved for different # integrators; tests 3D and 2D orbits, 1D orbits done separately below # Parametrized list of tests generated programmatically in conftest.py def test_energy_jacobi_conservation(pot, ttol, tjactol, firstTest): if _NOLONGINTEGRATIONS: return None # Basic parameters for the test times = numpy.linspace(0.0, 210.0, 5001) # ~7.5 Gyr at the Solar circle growtimes = numpy.linspace(0.0, 280.0, 5001) # for pots that grow slowly fasttimes = numpy.linspace(0.0, 14.0, 501) # ~0.5 Gyr at the Solar circle integrators = [ "dopr54_c", # first, because we do it for all potentials "odeint", # direct python solver "dop853", "dop853_c", "leapfrog", "leapfrog_c", "rk4_c", "rk6_c", "symplec4_c", "symplec6_c", "ias15_c", ] try: tclass = getattr(potential, pot) except AttributeError: tclass = getattr(sys.modules[__name__], pot) # if not pot == 'NFWPotential' and not pot == 'mockSlowFlatDecayingDehnenSmoothBarPotential': continue tp = tclass() if not hasattr(tp, "normalize"): return None # skip these tp.normalize(1.0) if hasattr(tp, "toPlanar"): ptp = tp.toPlanar() else: ptp = None for integrator in integrators: if integrator == "dopr54_c" and ( "Spiral" in pot or "Lopsided" in pot or "Dehnen" in pot or "Cosmphi" in pot ): ttimes = growtimes elif ( integrator == "dopr54_c" and not "MovingObject" in pot and not pot == "FerrersPotential" ): ttimes = times else: ttimes = fasttimes # First track azimuth o = setup_orbit_energy(tp, axi=False, henon="Henon" in pot) if isinstance(tp, testMWPotential): o.integrate(ttimes, tp._potlist, method=integrator) elif isinstance(tp, testplanarMWPotential): o.integrate(ttimes, tp._potlist, method=integrator) else: o.integrate(ttimes, tp, method=integrator) tEs = o.E(ttimes) # print(p, integrator, (numpy.std(tEs)/numpy.mean(tEs))**2.) if ( not "Bar" in pot and not "Spiral" in pot and not "MovingObject" in pot and not "Slow" in pot ): assert (numpy.std(tEs) / numpy.mean(tEs)) ** 2.0 < 10.0**ttol, ( "Energy conservation during the orbit integration fails for potential %s and integrator %s by %g" % (pot, integrator, (numpy.std(tEs) / numpy.mean(tEs))) ) # Jacobi if ( "Elliptical" in pot or "Lopsided" in pot or "DehnenSmoothBar" in pot or "SolidBodyRotation" in pot or "CorotatingRotation" in pot or "GaussianAmplitudeBar" in pot or pot == "mockMovingObjectLongIntPotential" or "Cosmphi" in pot or "triaxialLog" in pot or "RotatedAndTilted" in pot or "Henon" in pot ): tJacobis = o.Jacobi(ttimes, pot=tp) else: tJacobis = o.Jacobi(ttimes) # print(p, (numpy.std(tJacobis)/numpy.mean(tJacobis))**2.) assert (numpy.std(tJacobis) / numpy.mean(tJacobis)) ** 2.0 < 10.0**tjactol, ( "Jacobi integral conservation during the orbit integration fails for potential %s and integrator %s at the %g level" % (pot, integrator, (numpy.std(tJacobis) / numpy.mean(tJacobis)) ** 2.0) ) if firstTest or "testMWPotential" in pot: # Some basic checking of the energy and Jacobi functions assert (o.E(pot=None) - o.E(pot=tp)) ** 2.0 < 10.0**ttol, ( "Energy calculated with pot=None and pot=the Potential the orbit was integrated with do not agree" ) assert (o.E() - o.E(0.0)) ** 2.0 < 10.0**ttol, ( "Energy calculated with o.E() and o.E(0.) do not agree" ) assert (o.Jacobi(OmegaP=None) - o.Jacobi()) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with OmegaP=None is not equal to o.Jacobi" ) assert (o.Jacobi(pot=None) - o.Jacobi(pot=tp)) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with pot=None is not equal to o.Jacobi with pot=the Potential the orbit was integrated with do not agree" ) assert (o.Jacobi(pot=None) - o.Jacobi(pot=[tp])) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with pot=None is not equal to o.Jacobi with pot=[the Potential the orbit was integrated with] do not agree" ) if not tp.isNonAxi: assert (o.Jacobi(OmegaP=1.0) - o.Jacobi()) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with OmegaP=1. for axisymmetric potential is not equal to o.Jacobi (OmegaP=1 is the default for potentials without a pattern speed" ) assert ( o.Jacobi(OmegaP=[0.0, 0.0, 1.0]) - o.Jacobi(OmegaP=1.0) ) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with OmegaP=[0,0,1] for axisymmetric potential is not equal to o.Jacobi with OmegaP=1" ) assert ( o.Jacobi(OmegaP=numpy.array([0.0, 0.0, 1.0])) - o.Jacobi(OmegaP=1.0) ) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with OmegaP=[0,0,1] for axisymmetric potential is not equal to o.Jacobi with OmegaP=1" ) o = setup_orbit_energy(tp, axi=False, henon="Henon" in pot) try: o.E() except AttributeError: pass else: raise AssertionError( "o.E() before the orbit was integrated did not throw an AttributeError" ) if not isinstance(tp, potential.linearPotential): try: o.Jacobi() except AttributeError: pass else: raise AssertionError( "o.Jacobi() before the orbit was integrated did not throw an AttributeError" ) if "MovingObject" in pot: if _QUICKTEST and not ( ("NFW" in pot and not tp.isNonAxi and "SCF" not in pot) or "linearMWPotential" in pot or ("Burkert" in pot and not tp.hasC) ): break else: continue # AnyAxisymmetricRazorThinDiskPot has bad behavior for odeint and isn't really meant for orbit integration if "AnyAxisymmetricRazorThinDiskPotential" in pot: break # Now do axisymmetric if not tp.isNonAxi: o = setup_orbit_energy(tp, axi=True, henon="Henon" in pot) if isinstance(tp, testMWPotential) or isinstance(tp, testplanarMWPotential): o.integrate(ttimes, tp._potlist, method=integrator) else: o.integrate(ttimes, tp, method=integrator) tEs = o.E(ttimes) # print p, integrator, (numpy.std(tEs)/numpy.mean(tEs))**2. assert (numpy.std(tEs) / numpy.mean(tEs)) ** 2.0 < 10.0**ttol, ( "Energy conservation during the orbit integration fails for potential %s and integrator %s by %g" % (pot, integrator, (numpy.std(tEs) / numpy.mean(tEs)) ** 2.0) ) # Jacobi tJacobis = o.Jacobi(ttimes) assert ( numpy.std(tJacobis) / numpy.mean(tJacobis) ) ** 2.0 < 10.0**tjactol, ( "Jacobi integral conservation during the orbit integration fails for potential %s and integrator %s" % (pot, integrator) ) if firstTest or "MWPotential" in pot: # Some basic checking of the energy function assert (o.E(pot=None) - o.E(pot=tp)) ** 2.0 < 10.0**ttol, ( "Energy calculated with pot=None and pot=the Potential the orbit was integrated with do not agree" ) assert (o.E() - o.E(0.0)) ** 2.0 < 10.0**ttol, ( "Energy calculated with o.E() and o.E(0.) do not agree" ) assert (o.Jacobi(OmegaP=None) - o.Jacobi()) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with OmegaP=None is not equal to o.Jacobi" ) assert (o.Jacobi(pot=None) - o.Jacobi(pot=tp)) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with pot=None is not equal to o.Jacobi with pot=the Potential the orbit was integrated with do not agree" ) assert (o.Jacobi(pot=None) - o.Jacobi(pot=[tp])) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with pot=None is not equal to o.Jacobi with pot=the Potential the orbit was integrated with do not agree" ) assert (o.Jacobi(OmegaP=1.0) - o.Jacobi()) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with OmegaP=1. for axisymmetric potential is not equal to o.Jacobi (OmegaP=1 is the default for potentials without a pattern speed" ) o = setup_orbit_energy(tp, axi=True, henon="Henon" in pot) try: o.E() except AttributeError: pass else: raise AssertionError( "o.E() before the orbit was integrated did not throw an AttributeError" ) try: o.Jacobi() except AttributeError: pass else: raise AssertionError( "o.Jacobi() before the orbit was integrated did not throw an AttributeError" ) if ptp is None: if _QUICKTEST and not ( ("NFW" in pot and not tp.isNonAxi and "SCF" not in pot) or ("Burkert" in pot and not tp.hasC) ): break else: continue # Same for a planarPotential if pot == "mockRotatedAndTiltedMWP14WrapperPotential": break # print integrator if not ptp is None and not ptp.isNonAxi: o = setup_orbit_energy(ptp, axi=True) if isinstance(tp, testMWPotential) or isinstance(tp, testplanarMWPotential): o.integrate( ttimes, potential.toPlanarPotential(tp._potlist), method=integrator ) else: o.integrate(ttimes, ptp, method=integrator) tEs = o.E(ttimes) # print(p, integrator, (numpy.std(tEs)/numpy.mean(tEs))**2.) assert (numpy.std(tEs) / numpy.mean(tEs)) ** 2.0 < 10.0**ttol, ( "Energy conservation during the orbit integration fails for potential %s and integrator %s" % (pot, integrator) ) # Jacobi tJacobis = o.Jacobi(ttimes) assert ( numpy.std(tJacobis) / numpy.mean(tJacobis) ) ** 2.0 < 10.0**tjactol, ( "Jacobi integral conservation during the orbit integration fails for potential %s and integrator %s" % (pot, integrator) ) if firstTest or "MWPotential" in pot: # Some basic checking of the energy function assert (o.E(pot=None) - o.E(pot=ptp)) ** 2.0 < 10.0**ttol, ( "Energy calculated with pot=None and pot=the planarPotential the orbit was integrated with do not agree for planarPotential" ) assert (o.E(pot=None) - o.E(pot=tp)) ** 2.0 < 10.0**ttol, ( "Energy calculated with pot=None and pot=the Potential the orbit was integrated with do not agree for planarPotential" ) assert (o.E() - o.E(0.0)) ** 2.0 < 10.0**ttol, ( "Energy calculated with o.E() and o.E(0.) do not agree" ) assert (o.Jacobi(OmegaP=None) - o.Jacobi()) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with OmegaP=None is not equal to o.Jacobi" ) assert (o.Jacobi(pot=None) - o.Jacobi(pot=tp)) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with pot=None is not equal to o.Jacobi with pot=the Potential the orbit was integrated with do not agree" ) assert (o.Jacobi(pot=None) - o.Jacobi(pot=[tp])) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with pot=None is not equal to o.Jacobi with pot=the Potential the orbit was integrated with do not agree" ) assert (o.Jacobi(OmegaP=1.0) - o.Jacobi()) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with OmegaP=1. for axisymmetric potential is not equal to o.Jacobi (OmegaP=1 is the default for potentials without a pattern speed" ) o = setup_orbit_energy(ptp, axi=True) try: o.E() except AttributeError: pass else: raise AssertionError( "o.E() before the orbit was integrated did not throw an AttributeError" ) try: o.Jacobi() except AttributeError: pass else: raise AssertionError( "o.Jacobi() before the orbit was integrated did not throw an AttributeError" ) # Same for a planarPotential, track azimuth o = setup_orbit_energy(ptp, axi=False) if isinstance(tp, testMWPotential) or isinstance(tp, testplanarMWPotential): o.integrate( ttimes, potential.toPlanarPotential(tp._potlist), method=integrator ) else: o.integrate(ttimes, ptp, method=integrator) tEs = o.E(ttimes) # print(p, integrator, (numpy.std(tEs)/numpy.mean(tEs))**2.) if not "Bar" in pot and not "Spiral" in pot: assert (numpy.std(tEs) / numpy.mean(tEs)) ** 2.0 < 10.0**ttol, ( "Energy conservation during the orbit integration fails for potential %s and integrator %s by %g" % (pot, integrator, (numpy.std(tEs) / numpy.mean(tEs)) ** 2.0) ) # Jacobi if ( "DehnenSmoothBar" in pot or "SolidBodyRotation" in pot or "CorotatingRotation" in pot or "GaussianAmplitudeBar" in pot ): tJacobis = o.Jacobi(ttimes, pot=tp) else: tJacobis = o.Jacobi(ttimes) assert (numpy.std(tJacobis) / numpy.mean(tJacobis)) ** 2.0 < 10.0**tjactol, ( "Jacobi integral conservation during the orbit integration fails by %g for potential %s and integrator %s" % ((numpy.std(tJacobis) / numpy.mean(tJacobis)) ** 2.0, pot, integrator) ) if firstTest or "MWPotential" in pot: # Some basic checking of the energy function assert (o.E(pot=None) - o.E(pot=ptp)) ** 2.0 < 10.0**ttol, ( "Energy calculated with pot=None and pot=the planarPotential the orbit was integrated with do not agree for planarPotential" ) assert (o.E(pot=None) - o.E(pot=tp)) ** 2.0 < 10.0**ttol, ( "Energy calculated with pot=None and pot=the Potential the orbit was integrated with do not agree for planarPotential" ) assert (o.E() - o.E(0.0)) ** 2.0 < 10.0**ttol, ( "Energy calculated with o.E() and o.E(0.) do not agree" ) assert (o.Jacobi(OmegaP=None) - o.Jacobi()) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with OmegaP=None is not equal to o.Jacobi" ) assert (o.Jacobi(pot=None) - o.Jacobi(pot=tp)) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with pot=None is not equal to o.Jacobi with pot=the Potential the orbit was integrated with do not agree" ) assert (o.Jacobi(pot=None) - o.Jacobi(pot=[tp])) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with pot=None is not equal to o.Jacobi with pot=the Potential the orbit was integrated with do not agree" ) assert (o.Jacobi(OmegaP=1.0) - o.Jacobi()) ** 2.0 < 10.0**ttol, ( "o.Jacobi calculated with OmegaP=1. for axisymmetric potential is not equal to o.Jacobi (OmegaP=1 is the default for potentials without a pattern speed" ) o = setup_orbit_energy(ptp, axi=False) try: o.E() except AttributeError: pass else: raise AssertionError( "o.E() before the orbit was integrated did not throw an AttributeError" ) try: o.Jacobi() except AttributeError: pass else: raise AssertionError( "o.Jacobi() before the orbit was integrated did not throw an AttributeError" ) firstTest = False # Same for a planarPotential, but integrating w/ the potential directly, rather than the toPlanar instance; this tests that those potential attributes are passed to C correctly # print integrator if not ptp is None and not ptp.isNonAxi: o = setup_orbit_energy(ptp, axi=True) if isinstance(tp, testMWPotential) or isinstance(tp, testplanarMWPotential): o.integrate(ttimes, tp._potlist, method=integrator) else: o.integrate(ttimes, tp, method=integrator) tEs = o.E(ttimes) # print(p, integrator, (numpy.std(tEs)/numpy.mean(tEs))**2.) assert (numpy.std(tEs) / numpy.mean(tEs)) ** 2.0 < 10.0**ttol, ( "Energy conservation during the orbit integration fails for potential %s and integrator %s by %g" % (pot, integrator, (numpy.std(tEs) / numpy.mean(tEs)) ** 2.0) ) # Jacobi if ( "DehnenSmoothBar" in pot or "SolidBodyRotation" in pot or "CorotatingRotation" in pot or "GaussianAmplitudeBar" in pot ): tJacobis = o.Jacobi(ttimes, pot=tp) else: tJacobis = o.Jacobi(ttimes) assert ( numpy.std(tJacobis) / numpy.mean(tJacobis) ) ** 2.0 < 10.0**tjactol, ( "Jacobi integral conservation during the orbit integration fails for potential %s and integrator %s" % (pot, integrator) ) # Same for a planarPotential, track azimuth o = setup_orbit_energy(ptp, axi=False) if isinstance(tp, testMWPotential) or isinstance(tp, testplanarMWPotential): o.integrate(ttimes, tp._potlist, method=integrator) else: o.integrate(ttimes, tp, method=integrator) tEs = o.E(ttimes) # print p, integrator, (numpy.std(tEs)/numpy.mean(tEs))**2. if not "Bar" in pot and not "Spiral" in pot: assert (numpy.std(tEs) / numpy.mean(tEs)) ** 2.0 < 10.0**ttol, ( "Energy conservation during the orbit integration fails for potential %s and integrator %s" % (pot, integrator) ) # Jacobi if ( "DehnenSmoothBar" in pot or "SolidBodyRotation" in pot or "CorotatingRotation" in pot or "GaussianAmplitudeBar" in pot ): tJacobis = o.Jacobi(ttimes, pot=tp) else: tJacobis = o.Jacobi(ttimes) assert (numpy.std(tJacobis) / numpy.mean(tJacobis)) ** 2.0 < 10.0**tjactol, ( "Jacobi integral conservation during the orbit integration fails for potential %s and integrator %s" % (pot, integrator) ) if _QUICKTEST and not ( ("NFW" in pot and not tp.isNonAxi and "SCF" not in pot) or ("Burkert" in pot and not tp.hasC) ): break # raise AssertionError return None # Test whether the energy of 1D orbits is conserved for different integrators # Parametrized list of tests generated programmatically in conftest.py def test_energy_conservation_linear(pot, ttol, firstTest): if _NOLONGINTEGRATIONS: return None # Basic parameters for the test times = numpy.linspace(0.0, 210.0, 5001) # ~7.5 Gyr at the Solar circle growtimes = numpy.linspace(0.0, 280.0, 5001) # for pots that grow slowly fasttimes = numpy.linspace(0.0, 14.0, 501) # ~0.5 Gyr at the Solar circle integrators = [ "dopr54_c", # first, because we do it for all potentials "odeint", # direct python solver "dop853", "dop853_c", "leapfrog", "leapfrog_c", "rk4_c", "rk6_c", "symplec4_c", "symplec6_c", "ias15_c", ] # Setup instance of potential try: tclass = getattr(potential, pot) except AttributeError: tclass = getattr(sys.modules[__name__], pot) # if not p == 'NFWPotential' and not p == 'mockFlatGaussianAmplitudeBarPotential': continue tp = tclass() if hasattr(tp, "toVertical"): if not hasattr(tp, "normalize"): return None # skip these tp.normalize(1.0) tp = tp.toVertical(1.2, phi=0.3) elif isinstance(tp, potential.linearPotential): pass else: # not 3D --> 1D or 1D, so skip return None for integrator in integrators: if integrator == "dopr54_c" and ( "Spiral" in pot or "Lopsided" in pot or "Dehnen" in pot or "Cosmphi" in pot ): ttimes = growtimes elif ( integrator == "dopr54_c" and not "MovingObject" in pot and not pot == "FerrersPotential" and not pot == "AnyAxisymmetricRazorThinDiskPotential" ): ttimes = times else: ttimes = fasttimes # First track azimuth o = setup_orbit_energy(tp) if isinstance(tp, testMWPotential): o.integrate(ttimes, tp._potlist, method=integrator) elif isinstance(tp, testplanarMWPotential): o.integrate(ttimes, tp._potlist, method=integrator) elif isinstance(tp, testlinearMWPotential): o.integrate(ttimes, tp._potlist, method=integrator) else: o.integrate(ttimes, tp, method=integrator) tEs = o.E(ttimes) # print(p, integrator, (numpy.std(tEs)/numpy.mean(tEs))**2.) if ( not "Bar" in pot and not "Spiral" in pot and not "MovingObject" in pot and not "Slow" in pot ): assert (numpy.std(tEs) / numpy.mean(tEs)) ** 2.0 < 10.0**ttol, ( "Energy conservation during the orbit integration fails for potential %s and integrator %s by %g" % (pot, integrator, (numpy.std(tEs) / numpy.mean(tEs))) ) if firstTest or "testMWPotential" in pot or "linearMWPotential" in pot: # Some basic checking of the energy function assert (o.E(pot=None) - o.E(pot=tp)) ** 2.0 < 10.0**ttol, ( "Energy calculated with pot=None and pot=the Potential the orbit was integrated with do not agree" ) assert (o.E() - o.E(0.0)) ** 2.0 < 10.0**ttol, ( "Energy calculated with o.E() and o.E(0.) do not agree" ) o = setup_orbit_energy(tp, axi=False, henon="Henon" in pot) try: o.E() except AttributeError: pass else: raise AssertionError( "o.E() before the orbit was integrated did not throw an AttributeError" ) if _QUICKTEST and not ( pot == "NFWPotential" or ("Burkert" in pot and not tp.hasC) ): break return None # Test some long-term integrations for the symplectic integrators def test_energy_symplec_longterm(): if _NOLONGINTEGRATIONS: return None # Basic parameters for the test times = numpy.linspace(0.0, 10000.0, 100001) # ~360 Gyr at the Solar circle integrators = [ "leapfrog_c", # don't do leapfrog, because it takes too long "symplec4_c", "symplec6_c", ] # Only use KeplerPotential pots = ["KeplerPotential"] # tolerances in log10 tol = {} tol["default"] = -20.0 tol["leapfrog_c"] = -16.0 tol["leapfrog"] = -16.0 for p in pots: # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if not hasattr(tp, "normalize"): continue # skip these tp.normalize(1.0) for integrator in integrators: if integrator in list(tol.keys()): ttol = tol[integrator] else: ttol = tol["default"] o = setup_orbit_energy(tp) o.integrate(times, tp, method=integrator) tEs = o.E(times) # print p, integrator, (numpy.std(tEs)/numpy.mean(tEs))**2. # print p, ((numpy.mean(o.E(times[0:20]))-numpy.mean(o.E(times[-20:-1])))/numpy.mean(tEs))**2. assert (numpy.std(tEs) / numpy.mean(tEs)) ** 2.0 < 10.0**ttol, ( "Energy conservation during the orbit integration fails for potential %s and integrator %s by %.20f" % (p, integrator, (numpy.std(tEs) / numpy.mean(tEs)) ** 2) ) # Check whether there is a trend linfit = numpy.polyfit(times, tEs, 1) # print p assert linfit[0] ** 2.0 < 10.0**ttol, ( "Absence of secular trend in energy conservation fails for potential %s and symplectic integrator %s" % (p, integrator) ) # raise AssertionError return None def test_liouville_planar(): if _NOLONGINTEGRATIONS: return None # Basic parameters for the test times = numpy.linspace(0.0, 28.0, 1001) # ~1 Gyr at the Solar circle integrators = [ "dopr54_c", # first, because we do it for all potentials "dop853_c", "dop853", "odeint", # direct python solver "rk4_c", "rk6_c", ] # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("mockFlatEllipticalDiskPotential") pots.append("mockFlatLopsidedDiskPotential") pots.append("mockFlatCosmphiDiskPotential") pots.append("mockFlatCosmphiDiskwBreakPotential") pots.append("mockSlowFlatEllipticalDiskPotential") pots.append("mockFlatDehnenBarPotential") pots.append("mockFlatDehnenBarPotential") pots.append("mockSlowFlatDehnenBarPotential") pots.append("specialFlattenedPowerPotential") pots.append("BurkertPotentialNoC") pots.append("NFWTwoPowerTriaxialPotential") # for planar-from-full pots.append("mockSCFZeeuwPotential") pots.append("mockSCFNFWPotential") pots.append("mockSCFAxiDensity1Potential") pots.append("mockSCFAxiDensity2Potential") pots.append("mockSCFDensityPotential") pots.append("mockFlatSpiralArmsPotential") pots.append("mockRotatingFlatSpiralArmsPotential") pots.append("mockSpecialRotatingFlatSpiralArmsPotential") # pots.append('mockFlatSteadyLogSpiralPotential') # pots.append('mockFlatTransientLogSpiralPotential') pots.append("mockFlatDehnenSmoothBarPotential") pots.append("mockSlowFlatDehnenSmoothBarPotential") pots.append("mockSlowFlatDecayingDehnenSmoothBarPotential") pots.append("mockFlatSolidBodyRotationSpiralArmsPotential") pots.append("triaxialLogarithmicHaloPotential") pots.append("testorbitHenonHeilesPotential") pots.append("mockFlatTrulyCorotatingRotationSpiralArmsPotential") pots.append("mockFlatTrulyGaussianAmplitudeBarPotential") pots.append("nestedListPotential") pots.append("mockInterpSphericalPotential") pots.append("mockAdiabaticContractionMWP14WrapperPotential") pots.append("testNullPotential") pots.append("mockKuzminLikeWrapperPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] # rmpots.append('BurkertPotential') # Don't have C implementations of the relevant 2nd derivatives rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") # Doesn't have C at all rmpots.append("AnyAxisymmetricRazorThinDiskPotential") # rmpots.append('PowerSphericalPotentialwCutoff') # Doesn't have the R2deriv rmpots.append("TwoPowerSphericalPotential") rmpots.append("TwoPowerTriaxialPotential") rmpots.append("TriaxialHernquistPotential") rmpots.append("TriaxialJaffePotential") rmpots.append("SoftenedNeedleBarPotential") rmpots.append("DiskSCFPotential") rmpots.append("SphericalShellPotential") rmpots.append("RingPotential") rmpots.append("PerfectEllipsoidPotential") rmpots.append("TriaxialGaussianPotential") rmpots.append("PowerTriaxialPotential") for p in rmpots: pots.remove(p) # tolerances in log10 tol = {} tol["default"] = -8.0 tol["KeplerPotential"] = -7.0 # more difficult tol["NFWPotential"] = -6.0 # more difficult for rk4_c, only one that does this tol["TriaxialNFWPotential"] = -4.0 # more difficult tol["triaxialLogarithmicHaloPotential"] = -7.0 # more difficult tol["FerrersPotential"] = -2.0 tol["HomogeneousSpherePotential"] = -4.0 tol["KingPotential"] = -6.0 tol["mockInterpSphericalPotential"] = -4.0 # == HomogeneousSpherePotential tol["mockFlatCosmphiDiskwBreakPotential"] = -7.0 # more difficult tol["mockFlatTrulyCorotatingRotationSpiralArmsPotential"] = -5.0 # more difficult firstTest = True for p in pots: # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if not hasattr(tp, "normalize"): continue # skip these tp.normalize(1.0) # if not p == 'NFWPotential' and not p == 'mockSlowFlatDecayingDehnenSmoothBarPotential': continue if hasattr(tp, "toPlanar"): ptp = tp.toPlanar() for integrator in integrators: if p in list(tol.keys()): ttol = tol[p] else: ttol = tol["default"] if isinstance(tp, testMWPotential) or isinstance(tp, testplanarMWPotential): thasC = _check_c(tp._potlist) else: thasC = _check_c(tp) if (integrator == "odeint" or not thasC) and not p == "FerrersPotential": ttol = -4.0 if True: ttimes = times o = setup_orbit_liouville(ptp, axi=False, henon="Henon" in p) # Calculate the Jacobian d x / d x if hasattr(tp, "_potlist"): if isinstance(tp, testMWPotential): plist = potential.toPlanarPotential(tp._potlist) else: plist = tp._potlist o.integrate_dxdv( [1.0, 0.0, 0.0, 0.0], ttimes, plist, method=integrator, rectIn=True, rectOut=True, ) dx = o.getOrbit_dxdv()[-1, :] o.integrate_dxdv( [0.0, 1.0, 0.0, 0.0], ttimes, plist, method=integrator, rectIn=True, rectOut=True, ) dy = o.getOrbit_dxdv()[-1, :] o.integrate_dxdv( [0.0, 0.0, 1.0, 0.0], ttimes, plist, method=integrator, rectIn=True, rectOut=True, ) dvx = o.getOrbit_dxdv()[-1, :] o.integrate_dxdv( [0.0, 0.0, 0.0, 1.0], ttimes, plist, method=integrator, rectIn=True, rectOut=True, ) dvy = o.getOrbit_dxdv()[-1, :] else: o.integrate_dxdv( [1.0, 0.0, 0.0, 0.0], ttimes, ptp, method=integrator, rectIn=True, rectOut=True, ) dx = o.getOrbit_dxdv()[-1, :] o.integrate_dxdv( [0.0, 1.0, 0.0, 0.0], ttimes, ptp, method=integrator, rectIn=True, rectOut=True, ) dy = o.getOrbit_dxdv()[-1, :] o.integrate_dxdv( [0.0, 0.0, 1.0, 0.0], ttimes, ptp, method=integrator, rectIn=True, rectOut=True, ) dvx = o.getOrbit_dxdv()[-1, :] o.integrate_dxdv( [0.0, 0.0, 0.0, 1.0], ttimes, ptp, method=integrator, rectIn=True, rectOut=True, ) dvy = o.getOrbit_dxdv()[-1, :] tjac = numpy.linalg.det(numpy.array([dx, dy, dvx, dvy])) # print(p, integrator, numpy.fabs(tjac-1.),ttol) assert numpy.fabs(tjac - 1.0) < 10.0**ttol, ( f"Liouville theorem jacobian differs from one by {numpy.fabs(tjac - 1.0):g} for {p} and integrator {integrator}" ) if firstTest or ("Burkert" in p and not ptp.hasC): # Some one time tests # Test non-rectangular in- and output try: o.integrate_dxdv( [0.0, 0.0, 0.0, 1.0], ttimes, ptp, method="leapfrog", rectIn=True, rectOut=True, ) except ValueError: pass else: raise AssertionError( "integrate_dxdv with symplectic integrator should have raised ValueError, but didn't" ) firstTest = False if _QUICKTEST and not ( ("NFW" in p and not ptp.isNonAxi and "SCF" not in p) or ("Burkert" in p and not ptp.hasC) ): break return None # Test that integrating an orbit in MWPotential2014 using integrate_SOS conserves energy def test_integrate_SOS_3D(): pot = potential.MWPotential2014 o = setup_orbit_energy(pot, axi=True) psis = numpy.linspace(0.0, 20.0 * numpy.pi, 1001) for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: o.integrate_SOS(psis, pot, method=method) Es = o.E(o.t) assert (numpy.std(Es) / numpy.mean(Es)) ** 2.0 < 10.0**-10, ( f"Energy is not conserved by integrate_sos for method={method}" ) return None # Test that the 3D SOS function returns points with z=0, vz > 0 def test_SOS_3D(): pot = potential.MWPotential2014 o = setup_orbit_energy(pot) for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: o.SOS( pot, method=method, ncross=500 if "_c" in method else 20, force_map="rk" in method, ) zs = o.z(o.t) vzs = o.vz(o.t) assert (numpy.fabs(zs) < 10.0**-7.0).all(), ( f"z on SOS is not zero for integrate_sos for method={method}" ) assert (vzs > 0.0).all(), ( f"vz on SOS is not positive for integrate_sos for method={method}" ) return None # Test that the 3D bruteSOS function returns points with z=0, vz > 0 def test_bruteSOS_3D(): pot = potential.MWPotential2014 o = setup_orbit_energy(pot) for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: o.bruteSOS( numpy.linspace(0.0, 20.0 * numpy.pi, 100001), pot, method=method, force_map="rk" in method, ) zs = o.z(o.t) vzs = o.vz(o.t) assert (numpy.fabs(zs) < 10.0**-3.0).all(), ( f"z on SOS is not zero for bruteSOS for method={method}" ) assert (vzs > 0.0).all(), ( f"vz on SOS is not zero for bruteSOS for method={method}" ) return None # Test that integrating an orbit in MWPotential2014 using integrate_SOS conserves energy def test_integrate_SOS_2D(): pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() o = setup_orbit_energy(pot, axi=True) psis = numpy.linspace(0.0, 20.0 * numpy.pi, 1001) for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: for surface in ["x", "y"]: o.integrate_SOS(psis, pot, method=method) # default is surface='x' Es = o.E(o.t) assert (numpy.std(Es) / numpy.mean(Es)) ** 2.0 < 10.0**-10, ( f"Energy is not conserved by integrate_sos for method={method} and surface={surface}" ) return None # Test that the 2D SOS function returns points with x=0, vx > 0 when surface='x' def test_SOS_2Dx(): pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() o = setup_orbit_energy(pot) for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: o.SOS( pot, method=method, ncross=500 if "_c" in method else 20, force_map="rk" in method, surface="x", ) xs = o.x(o.t) vxs = o.vx(o.t) assert (numpy.fabs(xs) < 10.0**-6.0).all(), ( f"x on SOS is not zero for integrate_sos for method={method}" ) assert (vxs > 0.0).all(), ( f"vx on SOS is not positive for integrate_sos for method={method}" ) return None # Test that the 2D SOS function returns points with y=0, vy > 0 when surface='y' def test_SOS_2Dy(): pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() o = setup_orbit_energy(pot) for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: o.SOS( pot, method=method, ncross=500 if "_c" in method else 20, force_map="rk" in method, surface="y", ) ys = o.y(o.t) vys = o.vy(o.t) assert (numpy.fabs(ys) < 10.0**-7.0).all(), ( f"y on SOS is not zero for integrate_sos for method={method}" ) assert (vys > 0.0).all(), ( f"vy on SOS is not positive for integrate_sos for method={method}" ) return None # Test that the 2D SOS function returns points with x=0, vx > 0 when surface='x' def test_bruteSOS_2Dx(): pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() o = setup_orbit_energy(pot) for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: o.bruteSOS( numpy.linspace(0.0, 20.0 * numpy.pi, 100001), pot, method=method, force_map="rk" in method, surface="x", ) xs = o.x(o.t) vxs = o.vx(o.t) assert (numpy.fabs(xs) < 10.0**-3.0).all(), ( f"x on SOS is not zero for bruteSOS for method={method}" ) assert (vxs > 0.0).all(), ( f"vx on SOS is not zero for bruteSOS for method={method}" ) return None # Test that the 2D SOS function returns points with y=0, vy > 0 when surface='y' def test_bruteSOS_2Dy(): pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() o = setup_orbit_energy(pot) for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: o.bruteSOS( numpy.linspace(0.0, 20.0 * numpy.pi, 100001), pot, method=method, force_map="rk" in method, surface="y", ) ys = o.y(o.t) vys = o.vy(o.t) assert (numpy.fabs(ys) < 10.0**-3.0).all(), ( f"y on SOS is not zero for bruteSOS for method={method}" ) assert (vys > 0.0).all(), f"vy SOS is not zero for bruteSOS for method={method}" return None # Test that the SOS integration returns an error # when the orbit does not leave the surface def test_SOS_onsurfaceerror_3D(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.0, 0.0, 0.0]) with pytest.raises( RuntimeError, match="An orbit appears to be within the SOS surface. Refusing to perform specialized SOS integration, please use normal integration instead", ): o.SOS(potential.MWPotential2014) return None # Test that the SOS integration returns an error # when the orbit does not leave the surface def test_SOS_onsurfaceerror_2D(): from galpy.orbit import Orbit # An orbit considered in the book def orbit_xvxE(x, vx, E, pot=None): """Returns Orbit at (x,vx,y=0) with given E""" return Orbit( [ x, vx, numpy.sqrt( 2.0 * ( E - potential.evaluatePotentials(pot, x, 0.0, phi=0.0) - vx**2.0 / 2 ) ), 0.0, ] ) # Need the 2d zvc here (maybe should add to galpy?) def zvc(x, E, pot=None): """Returns the maximum v_x at this x and this energy: the zero-velocity curve""" return numpy.sqrt( 2.0 * (E - potential.evaluatePotentials(pot, x, 0.0, phi=0.0)) ) lp = potential.LogarithmicHaloPotential(normalize=True, b=0.9, core=0.2) E = -0.87 x = 0.204 # This orbit remains in the y=0 plane and psi therefore # remains zero, thus not increasing maxvx = zvc(x, E, pot=lp) o = orbit_xvxE(x, maxvx, E, pot=lp) with pytest.raises( RuntimeError, match="An orbit appears to be within the SOS surface. Refusing to perform specialized SOS integration, please use normal integration instead", ): o.SOS(lp, surface="y") return None # Test that the eccentricity of circular orbits is zero def test_eccentricity(): # return None # Basic parameters for the test times = numpy.linspace(0.0, 7.0, 251) # ~10 Gyr at the Solar circle integrators = [ "dopr54_c", # first, because we do it for all potentials "odeint", # direct python solver "dop853", "dop853_c", "leapfrog", "leapfrog_c", "rk4_c", "ias15_c", "rk6_c", "symplec4_c", "symplec6_c", ] # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("testMWPotential") pots.append("testplanarMWPotential") pots.append("mockInterpSphericalPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] rmpots.append("SphericalShellPotential") rmpots.append("RingPotential") if False: # _GHACTIONS: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) # tolerances in log10 tol = {} tol["default"] = -16.0 tol["DoubleExponentialDiskPotential"] = -6.0 # these are more difficult tol["NFWPotential"] = -12.0 # these are more difficult tol["TriaxialNFWPotential"] = -12.0 # these are more difficult firstTest = True for p in pots: # Setup instance of potential if p in list(tol.keys()): ttol = tol[p] else: ttol = tol["default"] try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "isNonAxi") and tp.isNonAxi: continue # skip, bc eccentricity of circ. =/= 0 if not hasattr(tp, "normalize"): continue # skip these tp.normalize(1.0) if hasattr(tp, "toPlanar"): ptp = tp.toPlanar() else: ptp = None for integrator in integrators: # First do axi o = setup_orbit_eccentricity(tp, axi=True) if firstTest: try: o.e() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.e() before the orbit was integrated did not throw an AttributeError" ) if isinstance(tp, testplanarMWPotential) or isinstance(tp, testMWPotential): o.integrate(times, tp._potlist, method=integrator) else: o.integrate(times, tp, method=integrator) tecc = o.e() # print p, integrator, tecc assert tecc**2.0 < 10.0**ttol, ( "Eccentricity of a circular orbit is not equal to zero by %g for potential %s and integrator %s" % (tecc**2.0, p, integrator) ) # add tracking azimuth o = setup_orbit_eccentricity(tp, axi=False) if firstTest: try: o.e() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.e() before the orbit was integrated did not throw an AttributeError" ) o.integrate(times, tp, method=integrator) tecc = o.e() # print p, integrator, tecc assert tecc**2.0 < 10.0**ttol, ( "Eccentricity of a circular orbit is not equal to zero for potential %s and integrator %s" % (p, integrator) ) if ptp is None: if _QUICKTEST and (not "NFW" in p or tp.isNonAxi): break # Same for a planarPotential # print integrator o = setup_orbit_eccentricity(ptp, axi=True) if firstTest: try: o.e() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.e() before the orbit was integrated did not throw an AttributeError" ) o.integrate(times, ptp, method=integrator) tecc = o.e() # print p, integrator, tecc assert tecc**2.0 < 10.0**ttol, ( "Eccentricity of a circular orbit is not equal to zero for potential %s and integrator %s" % (p, integrator) ) # Same for a planarPotential, track azimuth o = setup_orbit_eccentricity(ptp, axi=False) if firstTest: try: o.e() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.e() before the orbit was integrated did not throw an AttributeError" ) firstTest = True o.integrate(times, ptp, method=integrator) tecc = o.e() # print p, integrator, tecc assert tecc**2.0 < 10.0**ttol, ( "Eccentricity of a circular orbit is not equal to zero for potential %s and integrator %s" % (p, integrator) ) if _QUICKTEST and (not "NFW" in p or tp.isNonAxi): break # raise AssertionError return None # Test that the pericenter of orbits launched with vR=0 and vT > vc is the starting radius def test_pericenter(): # return None # Basic parameters for the test times = numpy.linspace(0.0, 7.0, 251) # ~10 Gyr at the Solar circle integrators = [ "dopr54_c", # first, because we do it for all potentials "odeint", # direct python solver "dop853", "dop853_c", "leapfrog", "leapfrog_c", "rk4_c", "rk6_c", "symplec4_c", "symplec6_c", "ias15_c", ] # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("testMWPotential") pots.append("testplanarMWPotential") pots.append("mockInterpSphericalPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] rmpots.append("SphericalShellPotential") rmpots.append("RingPotential") if False: # _GHACTIONS: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) # tolerances in log10 tol = {} tol["default"] = -16.0 # tol['DoubleExponentialDiskPotential']= -6. #these are more difficult # tol['NFWPotential']= -12. #these are more difficult firstTest = True for p in pots: # Setup instance of potential if p in list(tol.keys()): ttol = tol[p] else: ttol = tol["default"] try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "isNonAxi") and tp.isNonAxi: continue # skip, bc eccentricity of circ. =/= 0 if not hasattr(tp, "normalize"): continue # skip these tp.normalize(1.0) if hasattr(tp, "toPlanar"): ptp = tp.toPlanar() else: ptp = None for integrator in integrators: # First do axi o = setup_orbit_pericenter(tp, axi=True) if firstTest: try: o.rperi() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.rperi() before the orbit was integrated did not throw an AttributeError" ) if isinstance(tp, testplanarMWPotential) or isinstance(tp, testMWPotential): o.integrate(times, tp._potlist, method=integrator) else: o.integrate(times, tp, method=integrator) tperi = o.rperi() # print p, integrator, tperi assert (tperi - o.R()) ** 2.0 < 10.0**ttol, ( "Pericenter radius for an orbit launched with vR=0 and vT > Vc is not equal to the initial radius for potential %s and integrator %s" % (p, integrator) ) # add tracking azimuth o = setup_orbit_pericenter(tp, axi=False) if firstTest: try: o.rperi() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.rperi() before the orbit was integrated did not throw an AttributeError" ) o.integrate(times, tp, method=integrator) tperi = o.rperi() # print p, integrator, tperi assert (tperi - o.R()) ** 2.0 < 10.0**ttol, ( "Pericenter radius for an orbit launched with vR=0 and vT > Vc is not equal to the initial radius for potential %s and integrator %s" % (p, integrator) ) if ptp is None: if _QUICKTEST and (not "NFW" in p or tp.isNonAxi): break # Same for a planarPotential # print integrator o = setup_orbit_pericenter(ptp, axi=True) if firstTest: try: o.rperi() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.rperi() before the orbit was integrated did not throw an AttributeError" ) o.integrate(times, ptp, method=integrator) tperi = o.rperi() # print p, integrator, tperi assert (tperi - o.R()) ** 2.0 < 10.0**ttol, ( "Pericenter radius for an orbit launched with vR=0 and vT > Vc is not equal to the initial radius for potential %s and integrator %s" % (p, integrator) ) # Same for a planarPotential, track azimuth o = setup_orbit_pericenter(ptp, axi=False) if firstTest: try: o.rperi() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.rperi() before the orbit was integrated did not throw an AttributeError" ) firstTest = False o.integrate(times, ptp, method=integrator) tperi = o.rperi() # print p, integrator, tperi assert (tperi - o.R()) ** 2.0 < 10.0**ttol, ( "Pericenter radius for an orbit launched with vR=0 and vT > Vc is not equal to the initial radius for potential %s and integrator %s" % (p, integrator) ) if _QUICKTEST and (not "NFW" in p or tp.isNonAxi): break # raise AssertionError return None # Test that the apocenter of orbits launched with vR=0 and vT < vc is the starting radius def test_apocenter(): # return None # Basic parameters for the test times = numpy.linspace(0.0, 7.0, 251) # ~10 Gyr at the Solar circle integrators = [ "dopr54_c", # first, because we do it for all potentials "odeint", # direct python solver "dop853", "dop853_c", "leapfrog", "leapfrog_c", "rk4_c", "rk6_c", "symplec4_c", "symplec6_c", "ias15_c", ] # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("testMWPotential") pots.append("testplanarMWPotential") pots.append("mockInterpSphericalPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] rmpots.append("SphericalShellPotential") rmpots.append("RingPotential") if False: # _GHACTIONS: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) # tolerances in log10 tol = {} tol["default"] = -16.0 tol["FlattenedPowerPotential"] = -14.0 # these are more difficult # tol['DoubleExponentialDiskPotential']= -6. #these are more difficult # tol['NFWPotential']= -12. #these are more difficult firstTest = True for p in pots: # Setup instance of potential if p in list(tol.keys()): ttol = tol[p] else: ttol = tol["default"] try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "isNonAxi") and tp.isNonAxi: continue # skip, bc eccentricity of circ. =/= 0 if not hasattr(tp, "normalize"): continue # skip these tp.normalize(1.0) if hasattr(tp, "toPlanar"): ptp = tp.toPlanar() else: ptp = None for integrator in integrators: # First do axi o = setup_orbit_apocenter(tp, axi=True) if firstTest: try: o.rap() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.rap() before the orbit was integrated did not throw an AttributeError" ) if isinstance(tp, testplanarMWPotential) or isinstance(tp, testMWPotential): o.integrate(times, tp._potlist, method=integrator) else: o.integrate(times, tp, method=integrator) tapo = o.rap() # print p, integrator, tapo, (tapo-o.R())**2. assert (tapo - o.R()) ** 2.0 < 10.0**ttol, ( "Apocenter radius for an orbit launched with vR=0 and vT > Vc is not equal to the initial radius for potential %s and integrator %s" % (p, integrator) ) # add tracking azimuth o = setup_orbit_apocenter(tp, axi=False) if firstTest: try: o.rap() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.rap() before the orbit was integrated did not throw an AttributeError" ) o.integrate(times, tp, method=integrator) tapo = o.rap() # print p, integrator, tapo assert (tapo - o.R()) ** 2.0 < 10.0**ttol, ( "Apocenter radius for an orbit launched with vR=0 and vT > Vc is not equal to the initial radius for potential %s and integrator %s" % (p, integrator) ) if ptp is None: if _QUICKTEST and (not "NFW" in p or tp.isNonAxi): break # Same for a planarPotential # print integrator o = setup_orbit_apocenter(ptp, axi=True) if firstTest: try: o.rap() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.rap() before the orbit was integrated did not throw an AttributeError" ) o.integrate(times, ptp, method=integrator) tapo = o.rap() # print p, integrator, tapo assert (tapo - o.R()) ** 2.0 < 10.0**ttol, ( "Apocenter radius for an orbit launched with vR=0 and vT > Vc is not equal to the initial radius for potential %s and integrator %s" % (p, integrator) ) # Same for a planarPotential, track azimuth o = setup_orbit_apocenter(ptp, axi=False) if firstTest: try: o.rap() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.rap() before the orbit was integrated did not throw an AttributeError" ) firstTest = False o.integrate(times, ptp, method=integrator) tapo = o.rap() # print p, integrator, tapo assert (tapo - o.R()) ** 2.0 < 10.0**ttol, ( "Apocenter radius for an orbit launched with vR=0 and vT > Vc is not equal to the initial radius for potential %s and integrator %s" % (p, integrator) ) if _QUICKTEST and (not "NFW" in p or tp.isNonAxi): break # raise AssertionError return None # Test that the zmax of orbits launched with vz=0 is the starting height def test_zmax(): # return None # Basic parameters for the test times = numpy.linspace(0.0, 7.0, 251) # ~10 Gyr at the Solar circle integrators = [ "dopr54_c", # first, because we do it for all potentials "odeint", # direct python solver "dop853", "dop853_c", "leapfrog", "leapfrog_c", "rk4_c", "rk6_c", "symplec4_c", "symplec6_c", "ias15_c", ] # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("testMWPotential") pots.append("mockInterpSphericalPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] rmpots.append("SphericalShellPotential") rmpots.append("RingPotential") # No C and therefore annoying rmpots.append("AnyAxisymmetricRazorThinDiskPotential") if False: # _GHACTIONS: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) # tolerances in log10 tol = {} tol["default"] = -16.0 tol["RazorThinExponentialDiskPotential"] = -6.0 # these are more difficult tol["KuzminDiskPotential"] = -6.0 # these are more difficult # tol['DoubleExponentialDiskPotential']= -6. #these are more difficult firstTest = True for p in pots: # Setup instance of potential if p in list(tol.keys()): ttol = tol[p] else: ttol = tol["default"] try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "isNonAxi") and tp.isNonAxi: continue # skip, bc eccentricity of circ. =/= 0 if not hasattr(tp, "normalize"): continue # skip these tp.normalize(1.0) if hasattr(tp, "toPlanar"): ptp = tp.toPlanar() else: ptp = None for integrator in integrators: # First do axi o = setup_orbit_zmax(tp, axi=True) if firstTest: try: o.zmax() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.zmax() before the orbit was integrated did not throw an AttributeError" ) if isinstance(tp, testMWPotential): o.integrate(times, tp._potlist, method=integrator) else: o.integrate(times, tp, method=integrator) tzmax = o.zmax() # print p, integrator, tzmax assert (tzmax - o.z()) ** 2.0 < 10.0**ttol, ( "Zmax for an orbit launched with vR=0 and vT > Vc is not equal to the initial height for potential %s and integrator %s" % (p, integrator) ) # add tracking azimuth o = setup_orbit_zmax(tp, axi=False) if firstTest: try: o.zmax() # This should throw an AttributeError except AttributeError: pass else: raise AssertionError( "o.zmax() before the orbit was integrated did not throw an AttributeError" ) o.integrate(times, tp, method=integrator) tzmax = o.zmax() # print p, integrator, tzmax assert (tzmax - o.z()) ** 2.0 < 10.0**ttol, ( "Zmax for an orbit launched with vR=0 and vT > Vc is not equal to the initial height for potential %s and integrator %s" % (p, integrator) ) if firstTest: ptp = tp.toPlanar() o = setup_orbit_energy(ptp, axi=False) try: o.zmax() # This should throw an AttributeError, bc there is no zmax except AttributeError: pass else: raise AssertionError( "o.zmax() for a planarOrbit did not throw an AttributeError" ) o = setup_orbit_energy(ptp, axi=True) try: o.zmax() # This should throw an AttributeError, bc there is no zmax except AttributeError: pass else: raise AssertionError( "o.zmax() for a planarROrbit did not throw an AttributeError" ) if _QUICKTEST and (not "NFW" in p or tp.isNonAxi): break # raise AssertionError return None # Test that vR of circular orbits is always zero # Test the vT of circular orbits is always vc # Test that the eccentricity, apo-, and pericenters of orbits calculated analytically agrees with the numerical calculation def test_analytic_ecc_rperi_rap(): # Basic parameters for the test times = numpy.linspace(0.0, 20.0, 251) # ~10 Gyr at the Solar circle integrators = [ "dopr54_c", # first, because we do it for all potentials "odeint", # direct python solver "dop853", "dop853_c", "leapfrog", "leapfrog_c", "rk4_c", "rk6_c", "symplec4_c", "symplec6_c", "ias15_c", ] # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("testMWPotential") pots.append("testplanarMWPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] rmpots.append("SphericalShellPotential") rmpots.append("RingPotential") rmpots.append( "HomogeneousSpherePotential" ) # fails currently, because delta estimation gives a NaN due to a 0/0; delta should just be zero, but don't want to special-case # No C and therefore annoying rmpots.append("AnyAxisymmetricRazorThinDiskPotential") if False: # _GHACTIONS: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) # tolerances in log10 tol = {} tol["default"] = -10.0 tol["NFWPotential"] = -9.0 # these are more difficult tol["PlummerPotential"] = -9.0 # these are more difficult tol["DoubleExponentialDiskPotential"] = -6.0 # these are more difficult tol["RazorThinExponentialDiskPotential"] = -8.0 # these are more difficult tol["IsochronePotential"] = -6.0 # these are more difficult tol["DehnenSphericalPotential"] = -8.0 # these are more difficult tol["DehnenCoreSphericalPotential"] = -8.0 # these are more difficult tol["JaffePotential"] = -6.0 # these are more difficult tol["TriaxialHernquistPotential"] = -8.0 # these are more difficult tol["TriaxialJaffePotential"] = -8.0 # these are more difficult tol["TriaxialNFWPotential"] = -8.0 # these are more difficult tol["PowerSphericalPotential"] = -8.0 # these are more difficult tol["PowerSphericalPotentialwCutoff"] = -8.0 # these are more difficult tol["FlattenedPowerPotential"] = -8.0 # these are more difficult tol["KeplerPotential"] = -8.0 # these are more difficult tol["PseudoIsothermalPotential"] = -7.0 # these are more difficult tol["KuzminDiskPotential"] = -8.0 # these are more difficult tol["DiskSCFPotential"] = -8.0 # these are more difficult tol["PowerTriaxialPotential"] = -8.0 # these are more difficult for p in pots: # Setup instance of potential if p in list(tol.keys()): ttol = tol[p] else: ttol = tol["default"] if p == "MWPotential": tp = potential.MWPotential ptp = [ttp.toPlanar() for ttp in tp] else: try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "isNonAxi") and tp.isNonAxi: continue # skip, bc eccentricity of circ. =/= 0 if not hasattr(tp, "normalize"): continue # skip these tp.normalize(1.0) if hasattr(tp, "toPlanar"): ptp = tp.toPlanar() else: ptp = None for integrator in integrators: for ii in range(4): if ii == 0: # axi, full # First do axi o = setup_orbit_analytic(tp, axi=True) if isinstance(tp, testplanarMWPotential) or isinstance( tp, testMWPotential ): o.integrate(times, tp._potlist, method=integrator) else: o.integrate(times, tp, method=integrator) elif ii == 1: # track azimuth, full # First do axi o = setup_orbit_analytic(tp, axi=False) if isinstance(tp, testplanarMWPotential) or isinstance( tp, testMWPotential ): o.integrate(times, tp._potlist, method=integrator) else: o.integrate(times, tp, method=integrator) elif ii == 2: # axi, planar if ptp is None: continue # First do axi o = setup_orbit_analytic(ptp, axi=True) if isinstance(ptp, testplanarMWPotential) or isinstance( ptp, testMWPotential ): o.integrate(times, ptp._potlist, method=integrator) else: o.integrate(times, ptp, method=integrator) elif ii == 3: # track azimuth, full if ptp is None: continue # First do axi o = setup_orbit_analytic(ptp, axi=False) if isinstance(ptp, testplanarMWPotential) or isinstance( ptp, testMWPotential ): o.integrate(times, ptp._potlist, method=integrator) else: o.integrate(times, ptp, method=integrator) # Eccentricity tecc = o.e() if ii < 2 and ( p == "BurkertPotential" or "SCFPotential" in p or "FlattenedPower" in p or "RazorThinExponential" in p or "TwoPowerSpherical" in p ): # no Rzderiv currently tecc_analytic = o.e(analytic=True, type="adiabatic") else: tecc_analytic = o.e(analytic=True) # print p, integrator, tecc, tecc_analytic, (tecc-tecc_analytic)**2. assert (tecc - tecc_analytic) ** 2.0 < 10.0**ttol, ( "Analytically computed eccentricity does not agree with numerical estimate for potential %s and integrator %s, by %g" % (p, integrator, (tecc - tecc_analytic) ** 2.0) ) # Pericenter radius trperi = o.rperi() if ii < 2 and ( p == "BurkertPotential" or "SCFPotential" in p or "FlattenedPower" in p or "RazorThinExponential" in p or "TwoPowerSpherical" in p ): # no Rzderiv currently trperi_analytic = o.rperi(analytic=True, type="adiabatic") else: trperi_analytic = o.rperi(analytic=True) # print p, integrator, trperi, trperi_analytic, (trperi-trperi_analytic)**2. assert (trperi - trperi_analytic) ** 2.0 < 10.0**ttol, ( "Analytically computed pericenter radius does not agree with numerical estimate for potential %s and integrator %s" % (p, integrator) ) assert (o.rperi(ro=8.0) / 8.0 - trperi_analytic) ** 2.0 < 10.0**ttol, ( "Pericenter in physical coordinates does not agree with physical-scale times pericenter in normalized coordinates for potential %s and integrator %s" % (p, integrator) ) # Apocenter radius trap = o.rap() if ii < 2 and ( p == "BurkertPotential" or "SCFPotential" in p or "FlattenedPower" in p or "RazorThinExponential" in p or "TwoPowerSpherical" in p ): # no Rzderiv currently trap_analytic = o.rap(analytic=True, type="adiabatic") else: trap_analytic = o.rap(analytic=True) # print p, integrator, trap, trap_analytic, (trap-trap_analytic)**2. assert (trap - trap_analytic) ** 2.0 < 10.0**ttol, ( "Analytically computed apocenter radius does not agree with numerical estimate for potential %s and integrator %s by %g" % (p, integrator, (trap - trap_analytic) ** 2.0) ) assert (o.rap(ro=8.0) / 8.0 - trap_analytic) ** 2.0 < 10.0**ttol, ( "Apocenter in physical coordinates does not agree with physical-scale times apocenter in normalized coordinates for potential %s and integrator %s" % (p, integrator) ) # Do this also for an orbit starting at pericenter if ii == 0: # axi, full # First do axi o = setup_orbit_pericenter(tp, axi=True) o.integrate(times, tp, method=integrator) elif ii == 1: # track azimuth, full # First do axi o = setup_orbit_pericenter(tp, axi=False) o.integrate(times, tp, method=integrator) elif ii == 2: # axi, planar # First do axi o = setup_orbit_pericenter(ptp, axi=True) o.integrate(times, ptp, method=integrator) elif ii == 3: # track azimuth, full # First do axi o = setup_orbit_pericenter(ptp, axi=False) o.integrate(times, ptp, method=integrator) # Eccentricity tecc = o.e() if ii < 2 and ( p == "BurkertPotential" or "SCFPotential" in p or "FlattenedPower" in p or "RazorThinExponential" in p or "TwoPowerSpherical" in p ): # no Rzderiv currently tecc_analytic = o.e(analytic=True, type="adiabatic") else: tecc_analytic = o.e(analytic=True) # print p, integrator, tecc, tecc_analytic, (tecc-tecc_analytic)**2. assert (tecc - tecc_analytic) ** 2.0 < 10.0**ttol, ( "Analytically computed eccentricity does not agree with numerical estimate for potential %s and integrator %s" % (p, integrator) ) # Pericenter radius trperi = o.rperi() if ii < 2 and ( p == "BurkertPotential" or "SCFPotential" in p or "FlattenedPower" in p or "RazorThinExponential" in p or "TwoPowerSpherical" in p ): # no Rzderiv currently trperi_analytic = o.rperi(analytic=True, type="adiabatic") else: trperi_analytic = o.rperi(analytic=True) # print p, integrator, trperi, trperi_analytic, (trperi-trperi_analytic)**2. assert (trperi - trperi_analytic) ** 2.0 < 10.0**ttol, ( "Analytically computed pericenter radius does not agree with numerical estimate for potential %s and integrator %s" % (p, integrator) ) assert (o.rperi(ro=8.0) / 8.0 - trperi_analytic) ** 2.0 < 10.0**ttol, ( "Pericenter in physical coordinates does not agree with physical-scale times pericenter in normalized coordinates for potential %s and integrator %s" % (p, integrator) ) # Apocenter radius trap = o.rap() if ii < 2 and ( p == "BurkertPotential" or "SCFPotential" in p or "FlattenedPower" in p or "RazorThinExponential" in p or "TwoPowerSpherical" in p ): # no Rzderiv currently trap_analytic = o.rap(analytic=True, type="adiabatic") else: trap_analytic = o.rap(analytic=True) # print p, integrator, trap, trap_analytic, (trap-trap_analytic)**2. assert (trap - trap_analytic) ** 2.0 < 10.0**ttol, ( "Analytically computed apocenter radius does not agree with numerical estimate for potential %s and integrator %s by %g" % (p, integrator, (trap - trap_analytic)) ) assert (o.rap(ro=8.0) / 8.0 - trap_analytic) ** 2.0 < 10.0**ttol, ( "Apocenter in physical coordinates does not agree with physical-scale times apocenter in normalized coordinates for potential %s and integrator %s" % (p, integrator) ) # Do this also for an orbit starting at apocenter if ii == 0: # axi, full # First do axi o = setup_orbit_apocenter(tp, axi=True) o.integrate(times, tp, method=integrator) elif ii == 1: # track azimuth, full # First do axi o = setup_orbit_apocenter(tp, axi=False) o.integrate(times, tp, method=integrator) elif ii == 2: # axi, planar # First do axi o = setup_orbit_apocenter(ptp, axi=True) o.integrate(times, ptp, method=integrator) elif ii == 3: # track azimuth, full # First do axi o = setup_orbit_apocenter(ptp, axi=False) o.integrate(times, ptp, method=integrator) # Eccentricity tecc = o.e() if ii < 2 and ( p == "BurkertPotential" or "SCFPotential" in p or "FlattenedPower" in p or "RazorThinExponential" in p or "TwoPowerSpherical" in p ): # no Rzderiv currently tecc_analytic = o.e(analytic=True, type="adiabatic") else: tecc_analytic = o.e(analytic=True) # print p, integrator, tecc, tecc_analytic, (tecc-tecc_analytic)**2. assert (tecc - tecc_analytic) ** 2.0 < 10.0**ttol, ( "Analytically computed eccentricity does not agree with numerical estimate by %g for potential %s and integrator %s" % ((tecc - tecc_analytic) ** 2.0, p, integrator) ) # Pericenter radius trperi = o.rperi() if ii < 2 and ( p == "BurkertPotential" or "SCFPotential" in p or "FlattenedPower" in p or "RazorThinExponential" in p or "TwoPowerSpherical" in p ): # no Rzderiv currently trperi_analytic = o.rperi(analytic=True, type="adiabatic") else: trperi_analytic = o.rperi(analytic=True) # print p, integrator, trperi, trperi_analytic, (trperi-trperi_analytic)**2. assert (trperi - trperi_analytic) ** 2.0 < 10.0**ttol, ( "Analytically computed pericenter radius does not agree with numerical estimate for potential %s and integrator %s" % (p, integrator) ) assert (o.rperi(ro=8.0) / 8.0 - trperi_analytic) ** 2.0 < 10.0**ttol, ( "Pericenter in physical coordinates does not agree with physical-scale times pericenter in normalized coordinates for potential %s and integrator %s" % (p, integrator) ) # Apocenter radius trap = o.rap() if ii < 2 and ( p == "BurkertPotential" or "SCFPotential" in p or "FlattenedPower" in p or "RazorThinExponential" in p or "TwoPowerSpherical" in p ): # no Rzderiv currently trap_analytic = o.rap(analytic=True, type="adiabatic") else: trap_analytic = o.rap(analytic=True) # print p, integrator, trap, trap_analytic, (trap-trap_analytic)**2. assert (trap - trap_analytic) ** 2.0 < 10.0**ttol, ( "Analytically computed apocenter radius does not agree with numerical estimate for potential %s and integrator %s" % (p, integrator) ) assert (o.rap(ro=8.0) / 8.0 - trap_analytic) ** 2.0 < 10.0**ttol, ( "Apocenter in physical coordinates does not agree with physical-scale times apocenter in normalized coordinates for potential %s and integrator %s" % (p, integrator) ) if _QUICKTEST and (not "NFW" in p or tp.isNonAxi): break # raise AssertionError return None def test_orbit_rguiding(): from galpy.orbit import Orbit from galpy.potential import ( LogarithmicHaloPotential, MWPotential2014, TriaxialNFWPotential, rl, ) # For a single potential lp = LogarithmicHaloPotential(normalize=1.0) R, Lz = 1.0, 1.4 o = Orbit([R, 0.4, Lz / R, 0.0, 0.1, 0.0]) assert numpy.fabs(o.rguiding(pot=lp) - rl(lp, Lz)) < 1e-10, ( "Guiding center radius returned by Orbit interface rguiding is different from that returned by potential interface rl" ) # For a list of potentials R, Lz = 1.4, 0.9 o = Orbit([R, 0.4, Lz / R, 0.0, 0.1, 0.0]) assert ( numpy.fabs(o.rguiding(pot=MWPotential2014) - rl(MWPotential2014, Lz)) < 1e-10 ), ( "Guiding center radius returned by Orbit interface rguiding is different from that returned by potential interface rl" ) # For an orbit integrated in a non-axisymmetric potential, such that Lz varies np = TriaxialNFWPotential(amp=20.0, c=0.8, b=0.7) npaxi = TriaxialNFWPotential(amp=20.0, c=0.8) R, Lz = 1.2, 2.4 o = Orbit([R, 0.4, Lz / R, 0.0, 0.1, 0.0]) ts = numpy.linspace(0.0, 10.0, 101) o.integrate(ts, np) assert ( numpy.amax( numpy.fabs( o.rguiding(ts, pot=npaxi) - numpy.array([rl(npaxi, o.Lz(t)) for t in ts]) ) ) < 1e-10 ), ( "Guiding center radius returned by Orbit interface rguiding is different from that returned by potential interface rl for integrated orbit" ) return None def test_orbit_rguiding_planar(): from galpy.orbit import Orbit from galpy.potential import ( LogarithmicHaloPotential, MWPotential2014, TriaxialNFWPotential, rl, ) # For a single potential lp = LogarithmicHaloPotential(normalize=1.0) R, Lz = 1.0, 1.4 o = Orbit([R, 0.4, Lz / R, 0.0]) assert numpy.fabs(o.rguiding(pot=lp) - rl(lp, Lz)) < 1e-10, ( "Guiding center radius returned by Orbit interface rguiding is different from that returned by potential interface rl" ) # For a list of potentials R, Lz = 1.4, 0.9 o = Orbit([R, 0.4, Lz / R, 0.0]) assert ( numpy.fabs(o.rguiding(pot=MWPotential2014) - rl(MWPotential2014, Lz)) < 1e-10 ), ( "Guiding center radius returned by Orbit interface rguiding is different from that returned by potential interface rl" ) # For an orbit integrated in a non-axisymmetric potential, such that Lz varies np = TriaxialNFWPotential(amp=20.0, c=0.8, b=0.7) npaxi = TriaxialNFWPotential(amp=20.0, c=0.8) R, Lz = 1.2, 2.4 o = Orbit([R, 0.4, Lz / R, 0.0]) ts = numpy.linspace(0.0, 10.0, 101) o.integrate(ts, np) assert ( numpy.amax( numpy.fabs( o.rguiding(ts, pot=npaxi) - numpy.array([rl(npaxi, o.Lz(t)) for t in ts]) ) ) < 1e-10 ), ( "Guiding center radius returned by Orbit interface rguiding is different from that returned by potential interface rl for integrated orbit" ) return None def test_orbit_rE(): from galpy.orbit import Orbit from galpy.potential import ( DehnenBarPotential, LogarithmicHaloPotential, MWPotential2014, rE, ) # For a single potential lp = LogarithmicHaloPotential(normalize=1.0) R, Lz = 1.0, 1.4 o = Orbit([R, 0.4, Lz / R, 0.0, 0.1, 0.0]) E = o.E(pot=lp) assert numpy.fabs(o.rE(pot=lp) - rE(lp, E)) < 1e-10, ( "rE returned by Orbit interface rE is different from that returned by potential interface rE" ) # For a list of potentials R, Lz = 1.4, 0.9 o = Orbit([R, 0.4, Lz / R, 0.0, 0.1, 0.0]) E = o.E(pot=MWPotential2014) assert numpy.fabs(o.rE(pot=MWPotential2014) - rE(MWPotential2014, E)) < 1e-10, ( "rE returned by Orbit interface rE is different from that returned by potential interface rE" ) # For an orbit integrated in a time-dependent potential, such that E varies dp = DehnenBarPotential() R, Lz = 0.9, 1 o = Orbit([R, 0.0, Lz / R, 0.0, 0.1, 0.0]) E = o.E(pot=lp) ts = numpy.linspace(0.0, 10.0, 101) o.integrate(ts, lp + dp) assert ( numpy.amax( numpy.fabs( o.rE(ts, pot=lp) - numpy.array([rE(lp, o.E(t, pot=lp)) for t in ts]) ) ) < 1e-10 ), ( "rE returned by Orbit interface rE is different from that returned by potential interface rE for integrated orbit" ) return None def test_orbit_rE_planar(): from galpy.orbit import Orbit from galpy.potential import ( DehnenBarPotential, LogarithmicHaloPotential, MWPotential2014, rE, ) # For a single potential lp = LogarithmicHaloPotential(normalize=1.0) R, Lz = 1.0, 1.4 o = Orbit([R, 0.4, Lz / R, 0.0]) E = o.E(pot=lp) assert numpy.fabs(o.rE(pot=lp) - rE(lp, E)) < 1e-10, ( "rE returned by Orbit interface rE is different from that returned by potential interface rE" ) # For a list of potentials R, Lz = 1.4, 0.9 o = Orbit([R, 0.4, Lz / R, 0.0]) E = o.E(pot=MWPotential2014) assert numpy.fabs(o.rE(pot=MWPotential2014) - rE(MWPotential2014, E)) < 1e-10, ( "rE returned by Orbit interface rE is different from that returned by potential interface rE" ) # For an orbit integrated in a time-dependent potential, such that E varies dp = DehnenBarPotential() R, Lz = 0.9, 1 o = Orbit( [ R, 0.0, Lz / R, 0.0, ] ) ts = numpy.linspace(0.0, 10.0, 101) o.integrate(ts, lp + dp) assert ( numpy.amax( numpy.fabs( o.rE(ts, pot=lp) - numpy.array([rE(lp, o.E(t, pot=lp)) for t in ts]) ) ) < 1e-10 ), ( "rE returned by Orbit interface rE is different from that returned by potential interface rE for integrated orbit" ) return None def test_orbit_LcE(): from galpy.orbit import Orbit from galpy.potential import ( DehnenBarPotential, LcE, LogarithmicHaloPotential, MWPotential2014, ) # For a single potential lp = LogarithmicHaloPotential(normalize=1.0) R, Lz = 1.0, 1.4 o = Orbit([R, 0.4, Lz / R, 0.0, 0.1, 0.0]) E = o.E(pot=lp) assert numpy.fabs(o.LcE(pot=lp) - LcE(lp, E)) < 1e-10, ( "LcE returned by Orbit interface LcE is different from that returned by potential interface LcE" ) # For a list of potentials R, Lz = 1.4, 0.9 o = Orbit([R, 0.4, Lz / R, 0.0, 0.1, 0.0]) E = o.E(pot=MWPotential2014) assert numpy.fabs(o.LcE(pot=MWPotential2014) - LcE(MWPotential2014, E)) < 1e-10, ( "LcE returned by Orbit interface LcE is different from that returned by potential interface LcE" ) # For an orbit integrated in a time-dependent potential, such that E varies dp = DehnenBarPotential() R, Lz = 0.9, 1 o = Orbit([R, 0.0, Lz / R, 0.0, 0.1, 0.0]) E = o.E(pot=lp) ts = numpy.linspace(0.0, 10.0, 101) o.integrate(ts, lp + dp) assert ( numpy.amax( numpy.fabs( o.LcE(ts, pot=lp) - numpy.array([LcE(lp, o.E(t, pot=lp)) for t in ts]) ) ) < 1e-10 ), ( "LcE returned by Orbit interface LcE is different from that returned by potential interface LcE for integrated orbit" ) return None def test_orbit_LcE_planar(): from galpy.orbit import Orbit from galpy.potential import ( DehnenBarPotential, LcE, LogarithmicHaloPotential, MWPotential2014, ) # For a single potential lp = LogarithmicHaloPotential(normalize=1.0) R, Lz = 1.0, 1.4 o = Orbit([R, 0.4, Lz / R, 0.0]) E = o.E(pot=lp) assert numpy.fabs(o.LcE(pot=lp) - LcE(lp, E)) < 1e-10, ( "LcE returned by Orbit interface LcE is different from that returned by potential interface LcE" ) # For a list of potentials R, Lz = 1.4, 0.9 o = Orbit([R, 0.4, Lz / R, 0.0]) E = o.E(pot=MWPotential2014) assert numpy.fabs(o.LcE(pot=MWPotential2014) - LcE(MWPotential2014, E)) < 1e-10, ( "LcE returned by Orbit interface LcE is different from that returned by potential interface LcE" ) # For an orbit integrated in a time-dependent potential, such that E varies dp = DehnenBarPotential() R, Lz = 0.9, 1 o = Orbit( [ R, 0.0, Lz / R, 0.0, ] ) ts = numpy.linspace(0.0, 10.0, 101) o.integrate(ts, lp + dp) assert ( numpy.amax( numpy.fabs( o.LcE(ts, pot=lp) - numpy.array([LcE(lp, o.E(t, pot=lp)) for t in ts]) ) ) < 1e-10 ), ( "LcE returned by Orbit interface LcE is different from that returned by potential interface LcE for integrated orbit" ) return None # Check that zmax calculated analytically agrees with numerical calculation def test_analytic_zmax(): # Basic parameters for the test times = numpy.linspace(0.0, 20.0, 251) # ~10 Gyr at the Solar circle integrators = [ "dopr54_c", # first, because we do it for all potentials "odeint", # direct python solver "dop853", "dop853_c", "leapfrog", "leapfrog_c", "rk4_c", "rk6_c", "symplec4_c", "symplec6_c", "ias15_c", ] # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("testMWPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] rmpots.append("SphericalShellPotential") rmpots.append("RingPotential") rmpots.append( "HomogeneousSpherePotential" ) # fails currently, because delta estimation gives a NaN due to a 0/0; delta should just be zero, but don't want to special-case # No C and therefore annoying rmpots.append("AnyAxisymmetricRazorThinDiskPotential") if False: # _GHACTIONS: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) # tolerances in log10 tol = {} tol["default"] = -9.0 tol["IsochronePotential"] = -4.0 # these are more difficult tol["DoubleExponentialDiskPotential"] = -6.0 # these are more difficult tol["RazorThinExponentialDiskPotential"] = -4.0 # these are more difficult tol["KuzminKutuzovStaeckelPotential"] = -4.0 # these are more difficult tol["PlummerPotential"] = -4.0 # these are more difficult tol["PseudoIsothermalPotential"] = -4.0 # these are more difficult tol["DehnenSphericalPotential"] = -8.0 # these are more difficult tol["DehnenCoreSphericalPotential"] = -8.0 # these are more difficult tol["HernquistPotential"] = -8.0 # these are more difficult tol["TriaxialHernquistPotential"] = -8.0 # these are more difficult tol["JaffePotential"] = -8.0 # these are more difficult tol["TriaxialJaffePotential"] = -8.0 # these are more difficult tol["TriaxialNFWPotential"] = -8.0 # these are more difficult tol["MiyamotoNagaiPotential"] = -7.0 # these are more difficult tol["MN3ExponentialDiskPotential"] = -6.0 # these are more difficult tol["LogarithmicHaloPotential"] = -7.0 # these are more difficult tol["KeplerPotential"] = -7.0 # these are more difficult tol["PowerSphericalPotentialwCutoff"] = -8.0 # these are more difficult tol["FlattenedPowerPotential"] = -8.0 # these are more difficult tol["testMWPotential"] = -6.0 # these are more difficult tol["KuzminDiskPotential"] = -4 # these are more difficult tol["SCFPotential"] = -8.0 # these are more difficult tol["DiskSCFPotential"] = -6.0 # these are more difficult for p in pots: # Setup instance of potential if p in list(tol.keys()): ttol = tol[p] else: ttol = tol["default"] if p == "MWPotential": tp = potential.MWPotential else: try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "isNonAxi") and tp.isNonAxi: continue # skip, bc eccentricity of circ. =/= 0 if not hasattr(tp, "normalize"): continue # skip these tp.normalize(1.0) for integrator in integrators: for ii in range(2): if ii == 0: # axi, full # First do axi o = setup_orbit_analytic_zmax(tp, axi=True) elif ii == 1: # track azimuth, full # First do axi o = setup_orbit_analytic_zmax(tp, axi=False) if isinstance(tp, testMWPotential): o.integrate(times, tp._potlist, method=integrator) else: o.integrate(times, tp, method=integrator) tzmax = o.zmax() if ii < 2 and ( p == "BurkertPotential" or "SCFPotential" in p or "FlattenedPower" in p or "RazorThinExponential" in p or "TwoPowerSpherical" in p ): # no Rzderiv currently tzmax_analytic = o.zmax(analytic=True, type="adiabatic") else: tzmax_analytic = o.zmax(analytic=True) # print(p, integrator, tzmax, tzmax_analytic, (tzmax-tzmax_analytic)**2.) assert (tzmax - tzmax_analytic) ** 2.0 < 10.0**ttol, ( "Analytically computed zmax does not agree by %g with numerical estimate for potential %s and integrator %s" % (numpy.fabs(tzmax - tzmax_analytic), p, integrator) ) assert (o.zmax(ro=8.0) / 8.0 - tzmax_analytic) ** 2.0 < 10.0**ttol, ( "Zmax in physical coordinates does not agree with physical-scale times zmax in normalized coordinates for potential %s and integrator %s" % (p, integrator) ) if _QUICKTEST and (not "NFW" in p or tp.isNonAxi): break # raise AssertionError return None # Test the error for when explicit stepsize does not divide the output stepsize def test_check_integrate_dt(): from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) o = Orbit([1.0, 0.1, 1.2, 0.3, 0.2, 2.0]) times = numpy.linspace(0.0, 7.0, 251) # This shouldn't work try: o.integrate(times, lp, dt=(times[1] - times[0]) / 4.0 * 1.1) except ValueError: pass else: raise AssertionError( "dt that is not an integer divisor of the output step size does not raise a ValueError" ) # This should try: o.integrate(times, lp, dt=(times[1] - times[0]) / 4.0) except ValueError: raise AssertionError( "dt that is an integer divisor of the output step size raises a ValueError" ) return None # Test that fixing the stepsize works, issue #207 def test_fixedstepsize(): if WIN32: return None # skip on windows, because fails for reason that I can't figure out (runtimes[0] == 0.) and not that important import time from galpy.potential import LogarithmicHaloPotential # Integrators for which it should work integrators = ["leapfrog_c", "rk4_c", "rk6_c", "symplec4_c", "symplec6_c"] # Somewhat long time times = numpy.linspace(0.0, 100.0, 30001) # Test the following multiples mults = [1.0, 10.0] # Just do this for LogarithmicHaloPotential pot = LogarithmicHaloPotential(normalize=1.0) planarpot = pot.toPlanar() types = ["full", "rz", "planar", "r"] # Loop through integrators and different types of orbits for integrator in integrators: for type in types: if type == "full": o = setup_orbit_energy(pot, axi=False) elif type == "rz": o = setup_orbit_energy(pot, axi=True) elif type == "planar": o = setup_orbit_energy(planarpot, axi=False) elif type == "r": o = setup_orbit_energy(planarpot, axi=True) runtimes = numpy.empty(len(mults)) for ii, mult in enumerate(mults): start = time.time() o.integrate( times, pot, dt=(times[1] - times[0]) / mult, method=integrator ) runtimes[ii] = time.time() - start for ii, mult in enumerate(mults): if ii == 0: continue # Pretty loose test, because hard to get exactly right with overhead assert ( numpy.fabs(runtimes[ii] / runtimes[0] / mults[ii] * mults[0] - 1.0) < 0.85 ), ( "Runtime of integration with fixed stepsize for integrator %s, type or orbit %s, stepsize reduction %i is not %i times less (residual is %g, times %g and %g)" % ( integrator, type, mults[ii], mults[ii], numpy.fabs( runtimes[ii] / runtimes[0] / mults[ii] * mults[0] - 1.0 ), mults[ii] / mults[0], runtimes[ii] / runtimes[0], ) ) return None # Test that fixing the stepsize works for integrate_dxdv def test_fixedstepsize_dxdv(): if WIN32: return None # skip on windows, because test_fixedstepsize fails for reason that I can't figure out (runtimes[0] == 0.) and not that important import time from galpy.potential import LogarithmicHaloPotential # Integrators for which it should work integrators = ["rk4_c", "rk6_c"] # Somewhat long time from astropy import units times = numpy.linspace(0.0, 100.0, 90001) / 280.0 * units.Gyr # Test the following multiples mults = [1.0, 10.0] # Just do this for LogarithmicHaloPotential pot = LogarithmicHaloPotential(normalize=1.0) planarpot = pot.toPlanar() # Loop through integrators and different types of orbits for integrator in integrators: o = setup_orbit_energy(planarpot, axi=False) runtimes = numpy.empty(len(mults)) for ii, mult in enumerate(mults): start = time.time() o.integrate_dxdv( 1e-2 * numpy.ones(4), times, planarpot, dt=(times[1] - times[0]) / mult, method=integrator, ) runtimes[ii] = time.time() - start for ii, mult in enumerate(mults): if ii == 0: continue # Pretty loose test, because hard to get exactly right with overhead assert ( numpy.fabs(runtimes[ii] / runtimes[0] / mults[ii] * mults[0] - 1.0) < 0.85 ), ( "Runtime of integration with fixed stepsize for integrator %s, type or orbit %s, stepsize reduction %i is not %i times less (residual is %g, times %g and %g)" % ( integrator, type, mults[ii], mults[ii], numpy.fabs(runtimes[ii] / runtimes[0] / mults[ii] * mults[0] - 1.0), mults[ii] / mults[0], runtimes[ii] / runtimes[0], ) ) return None # Check that adding a linear orbit to a planar orbit gives a FullOrbit @pytest.mark.skip(reason="Not implemented for Orbits currently") def test_add_linear_planar_orbit(): from galpy.orbit import FullOrbit, RZOrbit kg = potential.KGPotential() ol = setup_orbit_energy(kg) # w/ azimuth plp = potential.NFWPotential().toPlanar() op = setup_orbit_energy(plp) of = ol + op assert isinstance(of._orb, FullOrbit.FullOrbit), ( "Sum of linearOrbit and planarOrbit does not give a FullOrbit" ) of = op + ol assert isinstance(of._orb, FullOrbit.FullOrbit), ( "Sum of linearOrbit and planarOrbit does not give a FullOrbit" ) # w/o azimuth op = setup_orbit_energy(plp, axi=True) of = ol + op assert isinstance(of._orb, RZOrbit.RZOrbit), ( "Sum of linearOrbit and planarROrbit does not give a FullOrbit" ) of = op + ol assert isinstance(of._orb, RZOrbit.RZOrbit), ( "Sum of linearOrbit and planarROrbit does not give a FullOrbit" ) # op + op shouldn't work try: of = op + op except AttributeError: pass else: raise AssertionError( "Adding a planarOrbit to a planarOrbit did not raise AttributeError" ) # w/ physical scale and coordinate-transformation parameters ro, vo, zo, solarmotion = 10.0, 300.0, 0.01, "dehnen" op = setup_orbit_flip(plp, ro, vo, zo, solarmotion, axi=True) of = op + ol assert isinstance(of._orb, RZOrbit.RZOrbit), ( "Sum of linearOrbit and planarROrbit does not give a FullOrbit" ) assert numpy.fabs(op._orb._ro - of._orb._ro) < 10.0**-15.0, ( "Sum of orbits does not properly propagate physical scales and coordinate-transformation parameters" ) assert numpy.fabs(op._orb._vo - of._orb._vo) < 10.0**-15.0, ( "Sum of orbits does not properly propagate physical scales and coordinate-transformation parameters" ) assert numpy.fabs(op._orb._zo - of._orb._zo) < 10.0**-15.0, ( "Sum of orbits does not properly propagate physical scales and coordinate-transformation parameters" ) assert numpy.all( numpy.fabs(op._orb._solarmotion - of._orb._solarmotion) < 10.0**-15.0 ), ( "Sum of orbits does not properly propagate physical scales and coordinate-transformation parameters" ) assert op._orb._roSet == of._orb._roSet, ( "Sum of orbits does not properly propagate physical scales and coordinate-transformation parameters" ) assert op._orb._voSet == of._orb._voSet, ( "Sum of orbits does not properly propagate physical scales and coordinate-transformation parameters" ) return None # Check that pickling orbits works def test_pickle(): import pickle from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.1, 0.2, 2.0]) po = pickle.dumps(o) upo = pickle.loads(po) assert o.R() == upo.R(), ( "Pickled/unpickled orbit does not agree with original orbut for R" ) assert o.vR() == upo.vR(), ( "Pickled/unpickled orbit does not agree with original orbut for vR" ) assert o.vT() == upo.vT(), ( "Pickled/unpickled orbit does not agree with original orbut for vT" ) assert o.z() == upo.z(), ( "Pickled/unpickled orbit does not agree with original orbut for z" ) assert o.vz() == upo.vz(), ( "Pickled/unpickled orbit does not agree with original orbut for vz" ) assert o.phi() == upo.phi(), ( "Pickled/unpickled orbit does not agree with original orbut for phi" ) assert (True ^ o._roSet) * (True ^ upo._roSet), ( "Pickled/unpickled orbit does not agree with original orbut for roSet" ) assert (True ^ o._voSet) * (True ^ upo._voSet), ( "Pickled/unpickled orbit does not agree with original orbut for voSet" ) # w/ physical scales etc. o = Orbit([1.0, 0.1, 1.1, 0.1, 0.2, 2.0], ro=10.0, vo=300.0) po = pickle.dumps(o) upo = pickle.loads(po) assert o.R() == upo.R(), ( "Pickled/unpickled orbit does not agree with original orbut for R" ) assert o.vR() == upo.vR(), ( "Pickled/unpickled orbit does not agree with original orbut for vR" ) assert o.vT() == upo.vT(), ( "Pickled/unpickled orbit does not agree with original orbut for vT" ) assert o.z() == upo.z(), ( "Pickled/unpickled orbit does not agree with original orbut for z" ) assert o.vz() == upo.vz(), ( "Pickled/unpickled orbit does not agree with original orbut for vz" ) assert o.phi() == upo.phi(), ( "Pickled/unpickled orbit does not agree with original orbut for phi" ) assert o._ro == upo._ro, ( "Pickled/unpickled orbit does not agree with original orbut for ro" ) assert o._vo == upo._vo, ( "Pickled/unpickled orbit does not agree with original orbut for vo" ) assert o._zo == upo._zo, ( "Pickled/unpickled orbit does not agree with original orbut for zo" ) assert numpy.all(o._solarmotion == upo._solarmotion), ( "Pickled/unpickled orbit does not agree with original orbut for solarmotion" ) assert (o._roSet) * (upo._roSet), ( "Pickled/unpickled orbit does not agree with original orbut for roSet" ) assert (o._voSet) * (upo._voSet), ( "Pickled/unpickled orbit does not agree with original orbut for voSet" ) return None # Basic checks of the angular momentum function def test_angularmomentum(): from galpy.orbit import Orbit # Shouldn't work for a 1D orbit o = Orbit([1.0, 0.1]) try: o.L() except AttributeError: pass else: raise AssertionError("Orbit.L() for linearOrbit did not raise AttributeError") # Also shouldn't work for an RZOrbit o = Orbit([1.0, 0.1, 1.1, 0.1, 0.2]) try: o.L() except AttributeError: pass else: raise AssertionError("Orbit.L() for RZOrbit did not raise AttributeError") # For a planarROrbit, should return Lz o = Orbit([1.0, 0.1, 1.1]) assert numpy.ndim(o.L()) == 0, "planarOrbit's angular momentum isn't 1D" assert o.L() == 1.1, "planarOrbit's angular momentum isn't correct" if False: # JB 5/23/2019 isn'tn sure why he ever implemented the Omega # keyword for L, so decided not to support this in new Orbits # If Omega is given, then it should be subtracted times = numpy.linspace(0.0, 2.0, 51) from galpy.potential import MWPotential o.integrate(times, MWPotential) assert numpy.fabs(o.L(t=1.0, Omega=1.0) - 0.1) < 10.0**-16.0, ( "o.L() w/ Omega does not work" ) # For a FullOrbit, angular momentum should be 3D o = Orbit([1.0, 0.1, 1.1, 0.1, 0.0, numpy.pi / 2.0]) assert o.L().shape[0] == 3, "FullOrbit's angular momentum is not 3D" assert numpy.fabs(o.L()[2] - 1.1) < 10.0**-16.0, "FullOrbit's Lz is not correct" assert numpy.fabs(o.L()[0] + 0.01) < 10.0**-16.0, "FullOrbit's Lx is not correct" assert numpy.fabs(o.L()[1] + 0.11) < 10.0**-16.0, "FullOrbit's Ly is not correct" return None # Check that ER + Ez = E and that ER and EZ are separately conserved for orbits that stay close to the plane for the MWPotential def test_ER_EZ(): from galpy.potential import MWPotential ona = setup_orbit_analytic_EREz(MWPotential, axi=False) oa = setup_orbit_analytic_EREz(MWPotential, axi=True) os = [ona, oa] for o in os: times = numpy.linspace(0.0, 7.0, 251) # ~10 Gyr at the Solar circle o.integrate(times, MWPotential) ERs = o.ER(times) Ezs = o.Ez(times) ERdiff = numpy.fabs(numpy.std(ERs - numpy.mean(ERs)) / numpy.mean(ERs)) assert ERdiff < 10.0**-4.0, ( "ER conservation for orbits close to the plane in MWPotential fails at %g%%" % (100.0 * ERdiff) ) Ezdiff = numpy.fabs(numpy.std(Ezs - numpy.mean(Ezs)) / numpy.mean(Ezs)) assert Ezdiff < 10.0**-1.7, ( "Ez conservation for orbits close to the plane in MWPotential fails at %g%%" % (100.0 * Ezdiff) ) # Some basic checking assert numpy.fabs(o.ER() - o.ER(pot=MWPotential)) < 10.0**-16.0, ( "o.ER() not equal to o.ER(pot=)" ) assert numpy.fabs(o.Ez() - o.Ez(pot=MWPotential)) < 10.0**-16.0, ( "o.ER() not equal to o.Ez(pot=)" ) assert numpy.fabs(o.ER(pot=None) - o.ER(pot=MWPotential)) < 10.0**-16.0, ( "o.ER() not equal to o.ER(pot=)" ) assert numpy.fabs(o.Ez(pot=None) - o.Ez(pot=MWPotential)) < 10.0**-16.0, ( "o.ER() not equal to o.Ez(pot=)" ) o = setup_orbit_analytic_EREz(MWPotential, axi=False) try: o.Ez() except AttributeError: pass else: raise AssertionError( "o.Ez() w/o potential before the orbit was integrated did not raise AttributeError" ) try: o.ER() except AttributeError: pass else: raise AssertionError( "o.ER() w/o potential before the orbit was integrated did not raise AttributeError" ) o = setup_orbit_analytic_EREz(MWPotential, axi=True) try: o.Ez() except AttributeError: pass else: raise AssertionError( "o.Ez() w/o potential before the orbit was integrated did not raise AttributeError" ) try: o.ER() except AttributeError: pass else: raise AssertionError( "o.ER() w/o potential before the orbit was integrated did not raise AttributeError" ) return None # Check that the different setups work def test_orbit_setup_linear(): from galpy.orbit import Orbit # linearOrbit o = Orbit([1.0, 0.1]) assert o.dim() == 1, "linearOrbit does not have dim == 1" assert numpy.fabs(o.x() - 1.0) < 10.0**-16.0, ( "linearOrbit x setup does not agree with o.x()" ) assert numpy.fabs(o.vx() - 0.1) < 10.0**-16.0, ( "linearOrbit vx setup does not agree with o.vx()" ) assert numpy.fabs(o.vr() - 0.1) < 10.0**-16.0, ( "linearOrbit vx setup does not agree with o.vr()" ) if False: # setphi was deprecated when moving to Orbits try: o.setphi(3.0) except AttributeError: pass else: raise AssertionError( "setphi applied to linearOrbit did not raise AttributeError" ) return None def test_orbit_setup_planar(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1]) assert o.dim() == 2, "planarROrbit does not have dim == 2" assert numpy.fabs(o.R() - 1.0) < 10.0**-16.0, ( "planarOrbit R setup does not agree with o.R()" ) assert numpy.fabs(o.vR() - 0.1) < 10.0**-16.0, ( "planarOrbit vR setup does not agree with o.vR()" ) assert numpy.fabs(o.vT() - 1.1) < 10.0**-16.0, ( "planarOrbit vT setup does not agree with o.vT()" ) if False: # setphi was deprecated when moving to Orbits o.setphi(3.0) assert numpy.fabs(o.phi() - 3.0) < 10.0**-16.0, ( "Orbit setphi does not agree with o.phi()" ) # planarROrbit no longer exists after moving to Orbits # assert not isinstance(o._orb,planarROrbit), 'After applying setphi, planarROrbit did not become planarOrbit' o = Orbit([1.0, 0.1, 1.1, 2.0]) assert o.dim() == 2, "planarOrbit does not have dim == 2" assert numpy.fabs(o.R() - 1.0) < 10.0**-16.0, ( "planarOrbit R setup does not agree with o.R()" ) assert numpy.fabs(o.vR() - 0.1) < 10.0**-16.0, ( "planarOrbit vR setup does not agree with o.vR()" ) assert numpy.fabs(o.vT() - 1.1) < 10.0**-16.0, ( "planarOrbit vT setup does not agree with o.vT()" ) assert numpy.fabs(o.phi() - 2.0) < 10.0**-16.0, ( "planarOrbit phi setup does not agree with o.phi()" ) if False: # setphi was deprecated when moving to Orbits o.setphi(3.0) assert numpy.fabs(o.phi() - 3.0) < 10.0**-16.0, ( "Orbit setphi does not agree with o.phi()" ) # lb, plane w/ default o = Orbit([120.0, 2.0, 0.5, 30.0], lb=True, zo=0.0, solarmotion=[-10.0, 10.0, 0.0]) obs = [8.0, 0.0] assert numpy.fabs(o.ll(obs=obs) - 120.0) < 10.0**-10.0, ( "Orbit ll setup does not agree with o.ll()" ) assert numpy.fabs(o.bb(obs=obs) - 0.0) < 10.0**-10.0, ( "Orbit bb setup does not agree with o.bb()" ) assert numpy.fabs(o.dist(obs=obs) - 2.0) < 10.0**-10.0, ( "Orbit dist setup does not agree with o.dist()" ) obs = [8.0, 0.0, -10.0, 230.0] assert numpy.fabs(o.pmll(obs=obs) - 0.5) < 10.0**-10.0, ( "Orbit pmll setup does not agree with o.pmbb()" ) assert numpy.fabs(o.pmbb(obs=obs) - 0.0) < 10.0**-5.5, ( "Orbit pmbb setup does not agree with o.pmbb()" ) assert numpy.fabs(o.vlos(obs=obs) - 30.0) < 10.0**-10.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # also check that the ro,vo,solarmotion values are stored and used properly (issue #158 solution) o = Orbit( [120.0, 2.0, 0.5, 30.0], lb=True, zo=0.0, solarmotion=[-10.0, 10.0, 0.0], ro=7.5 ) assert numpy.fabs(o.ll() - 120.0) < 10.0**-10.0, ( "Orbit ll setup does not agree with o.ll()" ) assert numpy.fabs(o.bb() - 0.0) < 10.0**-10.0, ( "Orbit bb setup does not agree with o.bb()" ) assert numpy.fabs(o.dist() - 2.0) < 10.0**-10.0, ( "Orbit dist setup does not agree with o.dist()" ) obs = [8.5, 0.0, -10.0, 245.0] assert numpy.fabs(o.pmll() - 0.5) < 10.0**-10.0, ( "Orbit pmll setup does not agree with o.pmbb()" ) assert numpy.fabs(o.pmbb() - 0.0) < 10.0**-5.5, ( "Orbit pmbb setup does not agree with o.pmbb()" ) assert numpy.fabs(o.vlos() - 30.0) < 10.0**-10.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # lb in plane and obs=Orbit o = Orbit([120.0, 2.0, 0.5, 30.0], lb=True, zo=0.0, solarmotion=[-10.1, 4.0, 0.0]) obs = Orbit([1.0, -10.1 / 220.0, 224.0 / 220, 0.0], solarmotion="hogg") assert numpy.fabs(o.ll(obs=obs) - 120.0) < 10.0**-10.0, ( "Orbit ll setup does not agree with o.ll()" ) assert numpy.fabs(o.bb(obs=obs) - 0.0) < 10.0**-10.0, ( "Orbit bb setup does not agree with o.bb()" ) assert numpy.fabs(o.dist(obs=obs) - 2.0) < 10.0**-10.0, ( "Orbit dist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmll(obs=obs) - 0.5) < 10.0**-10.0, ( "Orbit pmll setup does not agree with o.pmll()" ) assert numpy.fabs(o.pmbb(obs=obs) - 0.0) < 10.0**-5.5, ( "Orbit pmbb setup does not agree with o.pmbb()" ) assert numpy.fabs(o.vlos(obs=obs) - 30.0) < 10.0**-10.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # lb in plane and obs=Orbit in the plane o = Orbit([120.0, 2.0, 0.5, 30.0], lb=True, zo=0.0, solarmotion=[-10.1, 4.0, 0.0]) obs = Orbit([1.0, -10.1 / 220.0, 224.0 / 220, 0.0, 0.0, 0.0], solarmotion="hogg") assert numpy.fabs(o.ll(obs=obs) - 120.0) < 10.0**-10.0, ( "Orbit ll setup does not agree with o.ll()" ) assert numpy.fabs(o.bb(obs=obs) - 0.0) < 10.0**-10.0, ( "Orbit bb setup does not agree with o.bb()" ) assert numpy.fabs(o.dist(obs=obs) - 2.0) < 10.0**-10.0, ( "Orbit dist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmll(obs=obs) - 0.5) < 10.0**-10.0, ( "Orbit pmll setup does not agree with o.pmll()" ) assert numpy.fabs(o.pmbb(obs=obs) - 0.0) < 10.0**-5.5, ( "Orbit pmbb setup does not agree with o.pmbb()" ) assert numpy.fabs(o.vlos(obs=obs) - 30.0) < 10.0**-10.0, ( "Orbit vlos setup does not agree with o.vlos()" ) return None def test_orbit_setup(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.2, 0.3]) assert o.dim() == 3, "RZOrbitOrbit does not have dim == 3" assert numpy.fabs(o.R() - 1.0) < 10.0**-16.0, ( "Orbit R setup does not agree with o.R()" ) assert numpy.fabs(o.vR() - 0.1) < 10.0**-16.0, ( "Orbit vR setup does not agree with o.vR()" ) assert numpy.fabs(o.vT() - 1.1) < 10.0**-16.0, ( "Orbit vT setup does not agree with o.vT()" ) assert numpy.fabs(o.vphi() - 1.1) < 10.0**-16.0, ( "Orbit vT setup does not agree with o.vphi()" ) assert numpy.fabs(o.z() - 0.2) < 10.0**-16.0, ( "Orbit z setup does not agree with o.z()" ) assert numpy.fabs(o.vz() - 0.3) < 10.0**-16.0, ( "Orbit vz setup does not agree with o.vz()" ) if False: # setphi was deprecated when moving to Orbits o.setphi(3.0) assert numpy.fabs(o.phi() - 3.0) < 10.0**-16.0, ( "Orbit setphi does not agree with o.phi()" ) # FullOrbit no longer exists after switch to Orbits # assert isinstance(o._orb,FullOrbit), 'After applying setphi, RZOrbit did not become FullOrbit' o = Orbit((1.0, 0.1, 1.1, 0.2, 0.3, 2.0)) # also testing tuple input assert o.dim() == 3, "FullOrbit does not have dim == 3" assert numpy.fabs(o.R() - 1.0) < 10.0**-16.0, ( "Orbit R setup does not agree with o.R()" ) assert numpy.fabs(o.vR() - 0.1) < 10.0**-16.0, ( "Orbit vR setup does not agree with o.vR()" ) assert numpy.fabs(o.vT() - 1.1) < 10.0**-16.0, ( "Orbit vT setup does not agree with o.vT()" ) assert numpy.fabs(o.z() - 0.2) < 10.0**-16.0, ( "Orbit z setup does not agree with o.z()" ) assert numpy.fabs(o.vz() - 0.3) < 10.0**-16.0, ( "Orbit vz setup does not agree with o.vz()" ) assert numpy.fabs(o.phi() - 2.0) < 10.0**-16.0, ( "Orbit phi setup does not agree with o.phi()" ) if False: # setphi was deprecated when moving to Orbits o.setphi(3.0) assert numpy.fabs(o.phi() - 3.0) < 10.0**-16.0, ( "Orbit setphi does not agree with o.phi()" ) # Radec w/ default o = Orbit([120.0, 60.0, 2.0, 0.5, 0.4, 30.0], radec=True) assert numpy.fabs(o.ra() - 120.0) < 10.0**-12.0, ( "Orbit ra setup does not agree with o.ra()" ) assert numpy.fabs(o.dec() - 60.0) < 10.0**-13.0, ( "Orbit dec setup does not agree with o.dec()" ) assert numpy.fabs(o.dist() - 2.0) < 10.0**-13.0, ( "Orbit dist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmra() - 0.5) < 10.0**-13.0, ( "Orbit pmra setup does not agree with o.pmra()" ) assert numpy.fabs(o.pmdec() - 0.4) < 10.0**-13.0, ( "Orbit pmdec setup does not agree with o.pmdec()" ) assert numpy.fabs(o.vlos() - 30.0) < 10.0**-13.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # Radec w/ hogg o = Orbit([120.0, 60.0, 2.0, 0.5, 0.4, 30.0], radec=True, solarmotion="hogg") assert numpy.fabs(o.ra() - 120.0) < 10.0**-12.0, ( "Orbit ra setup does not agree with o.ra()" ) assert numpy.fabs(o.dec() - 60.0) < 10.0**-13.0, ( "Orbit dec setup does not agree with o.dec()" ) assert numpy.fabs(o.dist() - 2.0) < 10.0**-13.0, ( "Orbit dist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmra() - 0.5) < 10.0**-13.0, ( "Orbit pmra setup does not agree with o.pmra()" ) assert numpy.fabs(o.pmdec() - 0.4) < 10.0**-13.0, ( "Orbit pmdec setup does not agree with o.pmdec()" ) assert numpy.fabs(o.vlos() - 30.0) < 10.0**-13.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # Radec w/ dehnen and diff ro,vo o = Orbit( [120.0, 60.0, 2.0, 0.5, 0.4, 30.0], radec=True, solarmotion="dehnen", vo=240.0, ro=7.5, zo=0.01, ) obs = [7.5, 0.0, 0.01, -10.0, 245.25, 7.17] assert numpy.fabs(o.ra(obs=obs, ro=7.5) - 120.0) < 10.0**-13.0, ( "Orbit ra setup does not agree with o.ra()" ) assert numpy.fabs(o.dec(obs=obs, ro=7.5) - 60.0) < 10.0**-13.0, ( "Orbit dec setup does not agree with o.dec()" ) assert numpy.fabs(o.dist(obs=obs, ro=7.5) - 2.0) < 10.0**-13.0, ( "Orbit dist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmra(obs=obs, ro=7.5, vo=240.0) - 0.5) < 10.0**-13.0, ( "Orbit pmra setup does not agree with o.pmra()" ) assert numpy.fabs(o.pmdec(obs=obs, ro=7.5, vo=240.0) - 0.4) < 10.0**-13.0, ( "Orbit pmdec setup does not agree with o.pmdec()" ) assert numpy.fabs(o.vlos(obs=obs, ro=7.5, vo=240.0) - 30.0) < 10.0**-13.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # also check that the ro,vo,solarmotion values are stored and used properly (issue #158 solution) assert numpy.fabs(o.ra() - 120.0) < 10.0**-13.0, ( "Orbit ra setup does not agree with o.ra()" ) assert numpy.fabs(o.dec() - 60.0) < 10.0**-13.0, ( "Orbit dec setup does not agree with o.dec()" ) assert numpy.fabs(o.dist() - 2.0) < 10.0**-13.0, ( "Orbit dist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmra() - 0.5) < 10.0**-13.0, ( "Orbit pmra setup does not agree with o.pmra()" ) assert numpy.fabs(o.pmdec() - 0.4) < 10.0**-13.0, ( "Orbit pmdec setup does not agree with o.pmdec()" ) assert numpy.fabs(o.vlos() - 30.0) < 10.0**-13.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # Radec w/ schoenrich and diff ro,vo o = Orbit( [120.0, 60.0, 2.0, 0.5, 0.4, 30.0], radec=True, solarmotion="schoenrich", vo=240.0, ro=7.5, zo=0.035, ) obs = [7.5, 0.0, 0.035, -11.1, 252.24, 7.25] assert numpy.fabs(o.ra(obs=obs, ro=7.5) - 120.0) < 10.0**-13.0, ( "Orbit ra setup does not agree with o.ra()" ) assert numpy.fabs(o.dec(obs=obs, ro=7.5) - 60.0) < 10.0**-13.0, ( "Orbit dec setup does not agree with o.dec()" ) assert numpy.fabs(o.dist(obs=obs, ro=7.5) - 2.0) < 10.0**-13.0, ( "Orbit dist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmra(obs=obs, ro=7.5, vo=240.0) - 0.5) < 10.0**-13.0, ( "Orbit pmra setup does not agree with o.pmra()" ) assert numpy.fabs(o.pmdec(obs=obs, ro=7.5, vo=240.0) - 0.4) < 10.0**-13.0, ( "Orbit pmdec setup does not agree with o.pmdec()" ) assert numpy.fabs(o.vlos(obs=obs, ro=7.5, vo=240.0) - 30.0) < 10.0**-13.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # Radec w/ custom solarmotion and diff ro,vo o = Orbit( [120.0, 60.0, 2.0, 0.5, 0.4, 30.0], radec=True, solarmotion=[10.0, 20.0, 15.0], vo=240.0, ro=7.5, zo=0.035, ) obs = [7.5, 0.0, 0.035, 10.0, 260.0, 15.0] assert numpy.fabs(o.ra(obs=obs, ro=7.5) - 120.0) < 10.0**-13.0, ( "Orbit ra setup does not agree with o.ra()" ) assert numpy.fabs(o.dec(obs=obs, ro=7.5) - 60.0) < 10.0**-13.0, ( "Orbit dec setup does not agree with o.dec()" ) assert numpy.fabs(o.dist(obs=obs, ro=7.5) - 2.0) < 10.0**-13.0, ( "Orbit dist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmra(obs=obs, ro=7.5, vo=240.0) - 0.5) < 10.0**-13.0, ( "Orbit pmra setup does not agree with o.pmra()" ) assert numpy.fabs(o.pmdec(obs=obs, ro=7.5, vo=240.0) - 0.4) < 10.0**-13.0, ( "Orbit pmdec setup does not agree with o.pmdec()" ) assert numpy.fabs(o.vlos(obs=obs, ro=7.5, vo=240.0) - 30.0) < 10.0**-13.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # lb w/ default o = Orbit([120.0, 60.0, 2.0, 0.5, 0.4, 30.0], lb=True) assert numpy.fabs(o.ll() - 120.0) < 10.0**-10.0, ( "Orbit ll setup does not agree with o.ll()" ) assert numpy.fabs(o.bb() - 60.0) < 10.0**-10.0, ( "Orbit bb setup does not agree with o.bb()" ) assert numpy.fabs(o.dist() - 2.0) < 10.0**-10.0, ( "Orbit dist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmll() - 0.5) < 10.0**-10.0, ( "Orbit pmll setup does not agree with o.pmbb()" ) assert numpy.fabs(o.vll() - _K) < 10.0**-10.0, ( "Orbit pmll setup does not agree with o.vll()" ) assert numpy.fabs(o.pmbb() - 0.4) < 10.0**-10.0, ( "Orbit pmbb setup does not agree with o.pmbb()" ) assert numpy.fabs(o.vbb() - 0.8 * _K) < 10.0**-10.0, ( "Orbit pmbb setup does not agree with o.vbb()" ) assert numpy.fabs(o.vlos() - 30.0) < 10.0**-10.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # lb w/ default at the Sun o = Orbit([120.0, 60.0, 0.0, 10.0, 20.0, 30.0], uvw=True, lb=True, zo=0.0) assert numpy.fabs(o.dist() - 0.0) < 10.0**-2.0, ( "Orbit dist setup does not agree with o.dist()" ) # because of tweak in the code to deal with at the Sun assert ( o.U() ** 2.0 + o.V() ** 2.0 + o.W() ** 2.0 - 10.0**2.0 - 20.0**2.0 - 30.0**2.0 ) < 10.0**-10.0, ( "Velocity wrt the Sun when looking at Orbit at the Sun does not agree" ) assert (o.vlos() ** 2.0 - 10.0**2.0 - 20.0**2.0 - 30.0**2.0) < 10.0**-10.0, ( "Velocity wrt the Sun when looking at Orbit at the Sun does not agree" ) # lb w/ default and UVW o = Orbit([120.0, 60.0, 2.0, -10.0, 20.0, -25.0], lb=True, uvw=True) assert numpy.fabs(o.ll() - 120.0) < 10.0**-10.0, ( "Orbit ll setup does not agree with o.ll()" ) assert numpy.fabs(o.bb() - 60.0) < 10.0**-10.0, ( "Orbit bb setup does not agree with o.bb()" ) assert numpy.fabs(o.dist() - 2.0) < 10.0**-10.0, ( "Orbit dist setup does not agree with o.dist()" ) assert numpy.fabs(o.U() + 10.0) < 10.0**-10.0, ( "Orbit U setup does not agree with o.U()" ) assert numpy.fabs(o.V() - 20.0) < 10.0**-10.0, ( "Orbit V setup does not agree with o.V()" ) assert numpy.fabs(o.W() + 25.0) < 10.0**-10.0, ( "Orbit W setup does not agree with o.W()" ) # lb w/ default and UVW, test wrt helioXYZ o = Orbit([180.0, 0.0, 2.0, -10.0, 20.0, -25.0], lb=True, uvw=True) assert numpy.fabs(o.helioX() + 2.0) < 10.0**-10.0, ( "Orbit helioX setup does not agree with o.helioX()" ) assert numpy.fabs(o.helioY() - 0.0) < 10.0**-10.0, ( "Orbit helioY setup does not agree with o.helioY()" ) assert numpy.fabs(o.helioZ() - 0.0) < 10.0**-10.0, ( "Orbit helioZ setup does not agree with o.helioZ()" ) assert numpy.fabs(o.U() + 10.0) < 10.0**-10.0, ( "Orbit U setup does not agree with o.U()" ) assert numpy.fabs(o.V() - 20.0) < 10.0**-10.0, ( "Orbit V setup does not agree with o.V()" ) assert numpy.fabs(o.W() + 25.0) < 10.0**-10.0, ( "Orbit W setup does not agree with o.W()" ) # Radec w/ hogg and obs=Orbit o = Orbit([120.0, 60.0, 2.0, 0.5, 0.4, 30.0], radec=True, solarmotion="hogg") obs = Orbit( [1.0, -10.1 / 220.0, 224.0 / 220, 0.0208 / 8.0, 6.7 / 220.0, 0.0], solarmotion="hogg", ) assert numpy.fabs(o.ra(obs=obs) - 120.0) < 10.0**-10.0, ( "Orbit ra setup does not agree with o.ra()" ) assert numpy.fabs(o.dec(obs=obs) - 60.0) < 10.0**-10.0, ( "Orbit dec setup does not agree with o.dec()" ) assert numpy.fabs(o.dist(obs=obs) - 2.0) < 10.0**-10.0, ( "Orbit dist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmra(obs=obs) - 0.5) < 10.0**-10.0, ( "Orbit pmra setup does not agree with o.pmra()" ) assert numpy.fabs(o.vra(obs=obs) - _K) < 10.0**-10.0, ( "Orbit pmra setup does not agree with o.vra()" ) assert numpy.fabs(o.pmdec(obs=obs) - 0.4) < 10.0**-10.0, ( "Orbit pmdec setup does not agree with o.pmdec()" ) assert numpy.fabs(o.vdec(obs=obs) - 0.8 * _K) < 10.0**-10.0, ( "Orbit pmdec setup does not agree with o.vdec()" ) assert numpy.fabs(o.vlos(obs=obs) - 30.0) < 10.0**-10.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # lb, plane w/ default o = Orbit( [120.0, 0.0, 2.0, 0.5, 0.0, 30.0], lb=True, zo=0.0, solarmotion=[-10.0, 10.0, 0.0], ) obs = [8.0, 0.0] assert numpy.fabs(o.ll(obs=obs) - 120.0) < 10.0**-10.0, ( "Orbit ll setup does not agree with o.ll()" ) assert numpy.fabs(o.bb(obs=obs) - 0.0) < 10.0**-10.0, ( "Orbit bb setup does not agree with o.bb()" ) assert numpy.fabs(o.dist(obs=obs) - 2.0) < 10.0**-10.0, ( "Orbit dist setup does not agree with o.dist()" ) obs = [8.0, 0.0, -10.0, 230.0] assert numpy.fabs(o.pmll(obs=obs) - 0.5) < 10.0**-10.0, ( "Orbit pmll setup does not agree with o.pmll()" ) assert numpy.fabs(o.pmbb(obs=obs) - 0.0) < 10.0**-10.0, ( "Orbit pmbb setup does not agree with o.pmbb()" ) assert numpy.fabs(o.vlos(obs=obs) - 30.0) < 10.0**-10.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # lb in plane and obs=Orbit o = Orbit( [120.0, 0.0, 2.0, 0.5, 0.0, 30.0], lb=True, zo=0.0, solarmotion=[-10.1, 4.0, 0.0], ) obs = Orbit([1.0, -10.1 / 220.0, 224.0 / 220, 0.0], solarmotion="hogg") assert numpy.fabs(o.ll(obs=obs) - 120.0) < 10.0**-10.0, ( "Orbit ll setup does not agree with o.ll()" ) assert numpy.fabs(o.bb(obs=obs) - 0.0) < 10.0**-10.0, ( "Orbit bb setup does not agree with o.bb()" ) assert numpy.fabs(o.dist(obs=obs) - 2.0) < 10.0**-10.0, ( "Orbit dist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmll(obs=obs) - 0.5) < 10.0**-10.0, ( "Orbit pmll setup does not agree with o.pmll()" ) assert numpy.fabs(o.pmbb(obs=obs) - 0.0) < 10.0**-10.0, ( "Orbit pmbb setup does not agree with o.pmbb()" ) assert numpy.fabs(o.vlos(obs=obs) - 30.0) < 10.0**-10.0, ( "Orbit vlos setup does not agree with o.vlos()" ) # test galactocentric spherical coordinates o = Orbit([2.0, 2.0**0.5, 1.0, 2.0, 2.0**0.5, 0.5]) assert numpy.fabs(o.vr() - 2.0) < 10.0**-10.0, ( "Orbit galactocentric spherical coordinates are not correct" ) assert numpy.fabs(o.vtheta() - 0.0) < 10.0**-10.0, ( "Orbit galactocentric spherical coordinates are not correct" ) assert numpy.fabs(o.theta() - numpy.pi / 4) < 10.0**-10.0, ( "Orbit galactocentric spherical coordinates are not correct" ) return None def test_orbit_setup_SkyCoord(): # Only run this for astropy>3 if not _APY3: return None import astropy.coordinates as apycoords import astropy.units as u from galpy.orbit import Orbit ra = 120.0 * u.deg dec = 60.0 * u.deg distance = 2.0 * u.kpc pmra = 0.5 * u.mas / u.yr pmdec = 0.4 * u.mas / u.yr vlos = 30.0 * u.km / u.s c = apycoords.SkyCoord( ra=ra, dec=dec, distance=distance, pm_ra_cosdec=pmra, pm_dec=pmdec, radial_velocity=vlos, frame="icrs", ) # w/ default o = Orbit(c) # galpy's sky is not exactly aligned with astropy's sky, so celestials are slightly off assert numpy.fabs(o.ra() - 120.0) < 10.0**-8.0, ( "Orbit SkyCoord ra setup does not agree with o.ra()" ) assert numpy.fabs(o.dec() - 60.0) < 10.0**-8.0, ( "Orbit SkyCoord dec setup does not agree with o.dec()" ) assert numpy.fabs(o.dist() - 2.0) < 10.0**-14.0, ( "Orbit SkyCoorddist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmra() - 0.5) < 10.0**-8.0, ( "Orbit SkyCoordpmra setup does not agree with o.pmra()" ) assert numpy.fabs(o.pmdec() - 0.4) < 10.0**-8.0, ( "Orbit SkyCoordpmdec setup does not agree with o.pmdec()" ) assert numpy.fabs(o.vlos() - 30.0) < 10.0**-13.0, ( "Orbit SkyCoordvlos setup does not agree with o.vlos()" ) # Radec w/ hogg o = Orbit(c, solarmotion="hogg") assert numpy.fabs(o.ra() - 120.0) < 10.0**-8.0, ( "Orbit SkyCoordra setup does not agree with o.ra()" ) assert numpy.fabs(o.dec() - 60.0) < 10.0**-8.0, ( "Orbit SkyCoorddec setup does not agree with o.dec()" ) assert numpy.fabs(o.dist() - 2.0) < 10.0**-13.0, ( "Orbit SkyCoorddist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmra() - 0.5) < 10.0**-8.0, ( "Orbit SkyCoordpmra setup does not agree with o.pmra()" ) assert numpy.fabs(o.pmdec() - 0.4) < 10.0**-8.0, ( "Orbit SkyCoordpmdec setup does not agree with o.pmdec()" ) assert numpy.fabs(o.vlos() - 30.0) < 10.0**-13.0, ( "Orbit SkyCoordvlos setup does not agree with o.vlos()" ) # Radec w/ dehnen and diff ro,vo o = Orbit(c, solarmotion="dehnen", vo=240.0, ro=7.5, zo=0.01) obs = [7.5, 0.0, 0.01, -10.0, 245.25, 7.17] assert numpy.fabs(o.ra(obs=obs, ro=7.5) - 120.0) < 10.0**-8.0, ( "Orbit SkyCoordra setup does not agree with o.ra()" ) assert numpy.fabs(o.dec(obs=obs, ro=7.5) - 60.0) < 10.0**-8.0, ( "Orbit SkyCoorddec setup does not agree with o.dec()" ) assert numpy.fabs(o.dist(obs=obs, ro=7.5) - 2.0) < 10.0**-13.0, ( "Orbit SkyCoorddist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmra(obs=obs, ro=7.5, vo=240.0) - 0.5) < 10.0**-8.0, ( "Orbit SkyCoordpmra setup does not agree with o.pmra()" ) assert numpy.fabs(o.pmdec(obs=obs, ro=7.5, vo=240.0) - 0.4) < 10.0**-8.0, ( "Orbit SkyCoordpmdec setup does not agree with o.pmdec()" ) assert numpy.fabs(o.vlos(obs=obs, ro=7.5, vo=240.0) - 30.0) < 10.0**-13.0, ( "Orbit SkyCoordvlos setup does not agree with o.vlos()" ) # Now specifying the coordinate conversion parameters in the SkyCoord v_sun = apycoords.CartesianDifferential([-11.1, 215.0, 3.25] * u.km / u.s) c = apycoords.SkyCoord( ra=ra, dec=dec, distance=distance, pm_ra_cosdec=pmra, pm_dec=pmdec, radial_velocity=vlos, frame="icrs", galcen_distance=10.0 * u.kpc, z_sun=1.0 * u.kpc, galcen_v_sun=v_sun, ) o = Orbit(c) assert numpy.fabs(o.ra() - 120.0) < 10.0**-8.0, ( "Orbit SkyCoord ra setup does not agree with o.ra()" ) assert numpy.fabs(o.dec() - 60.0) < 10.0**-8.0, ( "Orbit SkyCoord dec setup does not agree with o.dec()" ) assert numpy.fabs(o.dist() - 2.0) < 10.0**-14.0, ( "Orbit SkyCoorddist setup does not agree with o.dist()" ) assert numpy.fabs(o.pmra() - 0.5) < 10.0**-8.0, ( "Orbit SkyCoordpmra setup does not agree with o.pmra()" ) assert numpy.fabs(o.pmdec() - 0.4) < 10.0**-8.0, ( "Orbit SkyCoordpmdec setup does not agree with o.pmdec()" ) assert numpy.fabs(o.vlos() - 30.0) < 10.0**-13.0, ( "Orbit SkyCoordvlos setup does not agree with o.vlos()" ) # Also test that the coordinate-transformation parameters are properly read assert numpy.fabs(o._ro - numpy.sqrt(10.0**2.0 - 1.0**2.0)) < 1e-12, ( "Orbit SkyCoord setup does not properly store ro" ) assert numpy.fabs(o._ro - numpy.sqrt(10.0**2.0 - 1.0**2.0)) < 1e-12, ( "Orbit SkyCoord setup does not properly store ro" ) assert numpy.fabs(o._zo - 1.0) < 1e-12, ( "Orbit SkyCoord setup does not properly store zo" ) assert numpy.all( numpy.fabs(o._solarmotion - numpy.array([[11.1, -5.0, 3.25]])) < 1e-12 ), "Orbit SkyCoord setup does not properly store solarmotion" # If we only specify galcen_distance, but not z_sun, zo --> 0 # Now specifying the coordinate conversion parameters in the SkyCoord v_sun = apycoords.CartesianDifferential([-11.1, 215.0, 3.25] * u.km / u.s) c = apycoords.SkyCoord( ra=ra, dec=dec, distance=distance, pm_ra_cosdec=pmra, pm_dec=pmdec, radial_velocity=vlos, frame="icrs", galcen_distance=10.0 * u.kpc, galcen_v_sun=v_sun, ) o = Orbit(c) assert numpy.fabs(o._zo - 0.0) < 1e-12, ( "Orbit SkyCoord setup does not properly store zo" ) # If we specify both z_sun and zo, they need to be consistent c = apycoords.SkyCoord( ra=ra, dec=dec, distance=distance, pm_ra_cosdec=pmra, pm_dec=pmdec, radial_velocity=vlos, frame="icrs", galcen_distance=10.0 * u.kpc, z_sun=1.0 * u.kpc, galcen_v_sun=v_sun, ) with pytest.raises(ValueError) as excinfo: o = Orbit(c, zo=0.025) # If ro and galcen_distance are both specified, warn if they are not consistent c = apycoords.SkyCoord( ra=ra, dec=dec, distance=distance, pm_ra_cosdec=pmra, pm_dec=pmdec, radial_velocity=vlos, frame="icrs", galcen_distance=10.0 * u.kpc, z_sun=1.0 * u.kpc, galcen_v_sun=v_sun, ) with pytest.warns(galpyWarning) as record: o = Orbit(c, ro=10.0) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "Orbit's initialization normalization ro and zo are incompatible with SkyCoord's galcen_distance (should have galcen_distance^2 = ro^2 + zo^2)" ) assert raisedWarning, ( "Orbit initialization with SkyCoord with galcen_distance incompatible with ro should have raised a warning, but didn't" ) # If ro and galcen_distance are both specified, don't warn if they *are* consistent (issue #370) c = apycoords.SkyCoord( ra=ra, dec=dec, distance=distance, pm_ra_cosdec=pmra, pm_dec=pmdec, radial_velocity=vlos, frame="icrs", galcen_distance=10.0 * u.kpc, z_sun=1.0 * u.kpc, galcen_v_sun=v_sun, ) with warnings.catch_warnings(record=True) as w: o = Orbit(c, ro=numpy.sqrt(10.0**2.0 - 1.0**2.0)) for wi in w: assert not issubclass(wi.category, galpyWarning), ( "Orbit initialization with SkyCoord with galcen_distance compatible with ro shouldn't have raised a warning, but did" ) # If we specify both v_sun and solarmotion, they need to be consistent v_sun = apycoords.CartesianDifferential([-11.1, 215.0, 3.25] * u.km / u.s) c = apycoords.SkyCoord( ra=ra, dec=dec, distance=distance, pm_ra_cosdec=pmra, pm_dec=pmdec, radial_velocity=vlos, frame="icrs", galcen_distance=10.0 * u.kpc, galcen_v_sun=v_sun, ) # This should be fine o = Orbit(c, solarmotion=[11.1, -5.0, 3.25]) # This shouldn't be with pytest.raises(ValueError) as excinfo: o = Orbit(c, solarmotion=[11.0, -4.0, 2.25]) # Should get error if we give a SkyCoord without velocities c = apycoords.SkyCoord( ra=ra, dec=dec, distance=distance, frame="icrs", galcen_distance=10.0 * u.kpc, galcen_v_sun=v_sun, ) with pytest.raises(TypeError) as excinfo: o = Orbit(c) return None # Check that toPlanar works def test_toPlanar(): from galpy.orbit import Orbit obs = Orbit([1.0, 0.1, 1.1, 0.3, 0.0, 2.0]) obsp = obs.toPlanar() assert obsp.dim() == 2, "toPlanar does not generate an Orbit w/ dim=2 for FullOrbit" assert obsp.R() == obs.R(), ( "Planar orbit generated w/ toPlanar does not have the correct R" ) assert obsp.vR() == obs.vR(), ( "Planar orbit generated w/ toPlanar does not have the correct vR" ) assert obsp.vT() == obs.vT(), ( "Planar orbit generated w/ toPlanar does not have the correct vT" ) assert obsp.phi() == obs.phi(), ( "Planar orbit generated w/ toPlanar does not have the correct phi" ) obs = Orbit([1.0, 0.1, 1.1, 0.3, 0.0]) obsp = obs.toPlanar() assert obsp.dim() == 2, "toPlanar does not generate an Orbit w/ dim=2 for RZOrbit" assert obsp.R() == obs.R(), ( "Planar orbit generated w/ toPlanar does not have the correct R" ) assert obsp.vR() == obs.vR(), ( "Planar orbit generated w/ toPlanar does not have the correct vR" ) assert obsp.vT() == obs.vT(), ( "Planar orbit generated w/ toPlanar does not have the correct vT" ) ro, vo, zo, solarmotion = 10.0, 300.0, 0.01, "schoenrich" obs = Orbit([1.0, 0.1, 1.1, 0.3, 0.0], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion) obsp = obs.toPlanar() assert obsp.dim() == 2, "toPlanar does not generate an Orbit w/ dim=2 for RZOrbit" assert obsp.R() == obs.R(), ( "Planar orbit generated w/ toPlanar does not have the correct R" ) assert obsp.vR() == obs.vR(), ( "Planar orbit generated w/ toPlanar does not have the correct vR" ) assert obsp.vT() == obs.vT(), ( "Planar orbit generated w/ toPlanar does not have the correct vT" ) assert numpy.fabs(obs._ro - obsp._ro) < 10.0**-15.0, ( "Planar orbit generated w/ toPlanar does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert numpy.fabs(obs._vo - obsp._vo) < 10.0**-15.0, ( "Planar orbit generated w/ toPlanar does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert numpy.fabs(obs._zo - obsp._zo) < 10.0**-15.0, ( "Planar orbit generated w/ toPlanar does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert numpy.all(numpy.fabs(obs._solarmotion - obsp._solarmotion) < 10.0**-15.0), ( "Planar orbit generated w/ toPlanar does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert obs._roSet == obsp._roSet, ( "Planar orbit generated w/ toPlanar does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert obs._voSet == obsp._voSet, ( "Planar orbit generated w/ toPlanar does not have the proper physical scale and coordinate-transformation parameters associated with it" ) obs = Orbit([1.0, 0.1, 1.1, 2.0]) try: obs.toPlanar() except AttributeError: pass else: raise AttributeError( "toPlanar() applied to a planar Orbit did not raise an AttributeError" ) return None # Check that toLinear works def test_toLinear(): from galpy.orbit import Orbit obs = Orbit([1.0, 0.1, 1.1, 0.3, 0.0, 2.0]) obsl = obs.toLinear() assert obsl.dim() == 1, "toLinear does not generate an Orbit w/ dim=1 for FullOrbit" assert obsl.x() == obs.z(), ( "Linear orbit generated w/ toLinear does not have the correct z" ) assert obsl.vx() == obs.vz(), ( "Linear orbit generated w/ toLinear does not have the correct vx" ) obs = Orbit([1.0, 0.1, 1.1, 0.3, 0.0]) obsl = obs.toLinear() assert obsl.dim() == 1, "toLinear does not generate an Orbit w/ dim=1 for FullOrbit" assert obsl.x() == obs.z(), ( "Linear orbit generated w/ toLinear does not have the correct z" ) assert obsl.vx() == obs.vz(), ( "Linear orbit generated w/ toLinear does not have the correct vx" ) obs = Orbit([1.0, 0.1, 1.1, 2.0]) try: obs.toLinear() except AttributeError: pass else: raise AttributeError( "toLinear() applied to a planar Orbit did not raise an AttributeError" ) # w/ scales ro, vo = 10.0, 300.0 obs = Orbit([1.0, 0.1, 1.1, 0.3, 0.0, 2.0], ro=ro, vo=vo) obsl = obs.toLinear() assert obsl.dim() == 1, "toLinear does not generate an Orbit w/ dim=1 for FullOrbit" assert obsl.x() == obs.z(), ( "Linear orbit generated w/ toLinear does not have the correct z" ) assert obsl.vx() == obs.vz(), ( "Linear orbit generated w/ toLinear does not have the correct vx" ) assert numpy.fabs(obs._ro - obsl._ro) < 10.0**-15.0, ( "Linear orbit generated w/ toLinear does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert numpy.fabs(obs._vo - obsl._vo) < 10.0**-15.0, ( "Linear orbit generated w/ toLinear does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert obsl._zo is None, ( "Linear orbit generated w/ toLinear does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert obsl._solarmotion is None, ( "Linear orbit generated w/ toLinear does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert obs._roSet == obsl._roSet, ( "Linear orbit generated w/ toLinear does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert obs._voSet == obsl._voSet, ( "Linear orbit generated w/ toLinear does not have the proper physical scale and coordinate-transformation parameters associated with it" ) return None # Check that some relevant errors are being raised def test_attributeerrors(): from galpy.orbit import Orbit # Vertical functions for planarOrbits o = Orbit([1.0, 0.1, 1.0, 0.1]) try: o.z() except AttributeError: pass else: raise AssertionError( "o.z() for planarOrbit should have raised AttributeError, but did not" ) try: o.vz() except AttributeError: pass else: raise AssertionError( "o.vz() for planarOrbit should have raised AttributeError, but did not" ) try: o.theta() except AttributeError: pass else: raise AssertionError( "o.theta() for planarROrbit should have raise AttributeError, but did not" ) try: o.vtheta() except AttributeError: pass else: raise AssertionError( "o.vtheta() for planarROrbit should have raise AttributeError, but did not" ) # phi, x, y, vx, vy for Orbits that don't track phi o = Orbit([1.0, 0.1, 1.1, 0.1, 0.2]) try: o.phi() except AttributeError: pass else: raise AssertionError( "o.phi() for RZOrbit should have raised AttributeError, but did not" ) try: o.x() except AttributeError: pass else: raise AssertionError( "o.x() for RZOrbit should have raised AttributeError, but did not" ) try: o.y() except AttributeError: pass else: raise AssertionError( "o.y() for RZOrbit should have raised AttributeError, but did not" ) try: o.vx() except AttributeError: pass else: raise AssertionError( "o.vx() for RZOrbit should have raised AttributeError, but did not" ) try: o.vy() except AttributeError: pass else: raise AssertionError( "o.vy() for RZOrbit should have raised AttributeError, but did not" ) o = Orbit([1.0, 0.1, 1.1]) try: o.phi() except AttributeError: pass else: raise AssertionError( "o.phi() for planarROrbit should have raised AttributeError, but did not" ) try: o.x() except AttributeError: pass else: raise AssertionError( "o.x() for planarROrbit should have raised AttributeError, but did not" ) try: o.y() except AttributeError: pass else: raise AssertionError( "o.y() for planarROrbit should have raised AttributeError, but did not" ) try: o.vx() except AttributeError: pass else: raise AssertionError( "o.vx() for planarROrbit should have raised AttributeError, but did not" ) try: o.vy() except AttributeError: pass else: raise AssertionError( "o.vy() for planarROrbit should have raised AttributeError, but did not" ) try: o.theta() except AttributeError: pass else: raise AssertionError( "o.theta() for planarROrbit should have raise AttributeError, but did not" ) try: o.vtheta() except AttributeError: pass else: raise AssertionError( "o.vtheta() for planarROrbit should have raise AttributeError, but did not" ) return None # Test reversing an orbit def test_flip(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) plp = lp.toPlanar() llp = lp.toVertical(1.0) for ii in range(5): # Scales to test that these are properly propagated to the new Orbit ro, vo, zo, solarmotion = 10.0, 300.0, 0.01, "schoenrich" if ii == 0: # axi, full o = setup_orbit_flip(lp, ro, vo, zo, solarmotion, axi=True) elif ii == 1: # track azimuth, full o = setup_orbit_flip(lp, ro, vo, zo, solarmotion, axi=False) elif ii == 2: # axi, planar o = setup_orbit_flip(plp, ro, vo, zo, solarmotion, axi=True) elif ii == 3: # track azimuth, full o = setup_orbit_flip(plp, ro, vo, zo, solarmotion, axi=False) elif ii == 4: # linear orbit o = setup_orbit_flip(llp, ro, vo, zo, solarmotion, axi=False) of = o.flip() # First check that the scales have been propagated properly assert numpy.fabs(o._ro - of._ro) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.fabs(o._vo - of._vo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert (o._zo is None) * (of._zo is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert (o._solarmotion is None) * (of._solarmotion is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) else: assert numpy.fabs(o._zo - of._zo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.all( numpy.fabs(o._solarmotion - of._solarmotion) < 10.0**-15.0 ), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._roSet == of._roSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._voSet == of._voSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert numpy.abs(o.x() - of.x()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vx() + of.vx()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) else: assert numpy.abs(o.R() - of.R()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vR() + of.vR()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vT() + of.vT()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) if ii % 2 == 1: assert numpy.abs(o.phi() - of.phi()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) if ii < 2: assert numpy.abs(o.z() - of.z()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vz() + of.vz()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) return None # Test reversing an orbit inplace def test_flip_inplace(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) plp = lp.toPlanar() llp = lp.toVertical(1.0) for ii in range(5): # Scales (not really necessary for this test) ro, vo, zo, solarmotion = 10.0, 300.0, 0.01, "schoenrich" if ii == 0: # axi, full o = setup_orbit_flip(lp, ro, vo, zo, solarmotion, axi=True) elif ii == 1: # track azimuth, full o = setup_orbit_flip(lp, ro, vo, zo, solarmotion, axi=False) elif ii == 2: # axi, planar o = setup_orbit_flip(plp, ro, vo, zo, solarmotion, axi=True) elif ii == 3: # track azimuth, full o = setup_orbit_flip(plp, ro, vo, zo, solarmotion, axi=False) elif ii == 4: # linear orbit o = setup_orbit_flip(llp, ro, vo, zo, solarmotion, axi=False) of = o() of.flip(inplace=True) # First check that the scales have been propagated properly assert numpy.fabs(o._ro - of._ro) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.fabs(o._vo - of._vo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert (o._zo is None) * (of._zo is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert (o._solarmotion is None) * (of._solarmotion is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) else: assert numpy.fabs(o._zo - of._zo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.all( numpy.fabs(o._solarmotion - of._solarmotion) < 10.0**-15.0 ), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._roSet == of._roSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._voSet == of._voSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert numpy.abs(o.x() - of.x()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vx() + of.vx()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) else: assert numpy.abs(o.R() - of.R()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vR() + of.vR()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vT() + of.vT()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) if ii % 2 == 1: assert numpy.abs(o.phi() - of.phi()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) if ii < 2: assert numpy.abs(o.z() - of.z()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vz() + of.vz()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) return None # Test reversing an orbit inplace after orbit integration def test_flip_inplace_integrated(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) plp = lp.toPlanar() llp = lp.toVertical(1.0) ts = numpy.linspace(0.0, 1.0, 11) for ii in range(5): # Scales (not really necessary for this test) ro, vo, zo, solarmotion = 10.0, 300.0, 0.01, "schoenrich" if ii == 0: # axi, full o = setup_orbit_flip(lp, ro, vo, zo, solarmotion, axi=True) elif ii == 1: # track azimuth, full o = setup_orbit_flip(lp, ro, vo, zo, solarmotion, axi=False) elif ii == 2: # axi, planar o = setup_orbit_flip(plp, ro, vo, zo, solarmotion, axi=True) elif ii == 3: # track azimuth, full o = setup_orbit_flip(plp, ro, vo, zo, solarmotion, axi=False) elif ii == 4: # linear orbit o = setup_orbit_flip(llp, ro, vo, zo, solarmotion, axi=False) of = o() if ii < 2 or ii == 3: o.integrate(ts, lp) of.integrate(ts, lp) elif ii == 2: o.integrate(ts, plp) of.integrate(ts, plp) else: o.integrate(ts, llp) of.integrate(ts, llp) of.flip(inplace=True) # Just check one time, allows code duplication! o = o(0.5) of = of(0.5) # First check that the scales have been propagated properly assert numpy.fabs(o._ro - of._ro) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.fabs(o._vo - of._vo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert (o._zo is None) * (of._zo is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert (o._solarmotion is None) * (of._solarmotion is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) else: assert numpy.fabs(o._zo - of._zo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.all( numpy.fabs(o._solarmotion - of._solarmotion) < 10.0**-15.0 ), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._roSet == of._roSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._voSet == of._voSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert numpy.abs(o.x() - of.x()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vx() + of.vx()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) else: assert numpy.abs(o.R() - of.R()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vR() + of.vR()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vT() + of.vT()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) if ii % 2 == 1: assert numpy.abs(o.phi() - of.phi()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) if ii < 2: assert numpy.abs(o.z() - of.z()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vz() + of.vz()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) return None # Test reversing an orbit inplace after orbit integration, and after having # once evaluated the orbit before flipping inplace (#345) # only difference wrt previous test is a line that evaluates of before # flipping def test_flip_inplace_integrated_evaluated(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) plp = lp.toPlanar() llp = lp.toVertical(1.0) ts = numpy.linspace(0.0, 1.0, 11) for ii in range(5): # Scales (not really necessary for this test) ro, vo, zo, solarmotion = 10.0, 300.0, 0.01, "schoenrich" if ii == 0: # axi, full o = setup_orbit_flip(lp, ro, vo, zo, solarmotion, axi=True) elif ii == 1: # track azimuth, full o = setup_orbit_flip(lp, ro, vo, zo, solarmotion, axi=False) elif ii == 2: # axi, planar o = setup_orbit_flip(plp, ro, vo, zo, solarmotion, axi=True) elif ii == 3: # track azimuth, full o = setup_orbit_flip(plp, ro, vo, zo, solarmotion, axi=False) elif ii == 4: # linear orbit o = setup_orbit_flip(llp, ro, vo, zo, solarmotion, axi=False) of = o() if ii < 2 or ii == 3: o.integrate(ts, lp) of.integrate(ts, lp) elif ii == 2: o.integrate(ts, plp) of.integrate(ts, plp) else: o.integrate(ts, llp) of.integrate(ts, llp) # Evaluate, make sure it is at an interpolated time! dumb = of.R(0.52) # Now flip of.flip(inplace=True) # Just check one time, allows code duplication! o = o(0.52) of = of(0.52) # First check that the scales have been propagated properly assert numpy.fabs(o._ro - of._ro) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.fabs(o._vo - of._vo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert (o._zo is None) * (of._zo is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert (o._solarmotion is None) * (of._solarmotion is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) else: assert numpy.fabs(o._zo - of._zo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.all( numpy.fabs(o._solarmotion - of._solarmotion) < 10.0**-15.0 ), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._roSet == of._roSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._voSet == of._voSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert numpy.abs(o.x() - of.x()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vx() + of.vx()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) else: assert numpy.abs(o.R() - of.R()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vR() + of.vR()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vT() + of.vT()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) if ii % 2 == 1: assert numpy.abs(o.phi() - of.phi()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) if ii < 2: assert numpy.abs(o.z() - of.z()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) assert numpy.abs(o.vz() + of.vz()) < 10.0**-10.0, ( "o.flip() did not work as expected" ) return None # test getOrbit def test_getOrbit(): from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) o = Orbit([1.0, 0.1, 1.2, 0.3, 0.2, 2.0]) times = numpy.linspace(0.0, 7.0, 251) o.integrate(times, lp) Rs = o.R(times) vRs = o.vR(times) vTs = o.vT(times) zs = o.z(times) vzs = o.vz(times) phis = o.phi(times) orbarray = o.getOrbit() assert numpy.all(numpy.fabs(Rs - orbarray[:, 0])) < 10.0**-16.0, ( "getOrbit does not work as expected for R" ) assert numpy.all(numpy.fabs(vRs - orbarray[:, 1])) < 10.0**-16.0, ( "getOrbit does not work as expected for vR" ) assert numpy.all(numpy.fabs(vTs - orbarray[:, 2])) < 10.0**-16.0, ( "getOrbit does not work as expected for vT" ) assert numpy.all(numpy.fabs(zs - orbarray[:, 3])) < 10.0**-16.0, ( "getOrbit does not work as expected for z" ) assert numpy.all(numpy.fabs(vzs - orbarray[:, 4])) < 10.0**-16.0, ( "getOrbit does not work as expected for vz" ) assert numpy.all(numpy.fabs(phis - orbarray[:, 5])) < 10.0**-16.0, ( "getOrbit does not work as expected for phi" ) return None # Test new orbits formed from __call__ def test_newOrbit(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.1, 0.0, 0.0]) ts = numpy.linspace(0.0, 1.0, 21) # v. quick orbit integration lp = potential.LogarithmicHaloPotential(normalize=1.0) o.integrate(ts, lp) no = o(ts[-1]) # new orbit assert no.R() == o.R(ts[-1]), ( "New orbit formed from calling an old orbit does not have the correct R" ) assert no.vR() == o.vR(ts[-1]), ( "New orbit formed from calling an old orbit does not have the correct vR" ) assert no.vT() == o.vT(ts[-1]), ( "New orbit formed from calling an old orbit does not have the correct vT" ) assert no.z() == o.z(ts[-1]), ( "New orbit formed from calling an old orbit does not have the correct z" ) assert no.vz() == o.vz(ts[-1]), ( "New orbit formed from calling an old orbit does not have the correct vz" ) assert no.phi() == o.phi(ts[-1]), ( "New orbit formed from calling an old orbit does not have the correct phi" ) assert not no._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not no._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not no._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not no._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) # Also test this for multiple time outputs nos = o(ts[-2:]) # new orbits # First t assert numpy.fabs(nos[0].R() - o.R(ts[-2])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct R" ) assert numpy.fabs(nos[0].vR() - o.vR(ts[-2])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vR" ) assert numpy.fabs(nos[0].vT() - o.vT(ts[-2])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vT" ) assert numpy.fabs(nos[0].z() - o.z(ts[-2])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct z" ) assert numpy.fabs(nos[0].vz() - o.vz(ts[-2])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vz" ) assert numpy.fabs(nos[0].phi() - o.phi(ts[-2])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct phi" ) assert not nos[0]._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos[0]._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos[0]._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos[0]._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) # Second t assert numpy.fabs(nos[1].R() - o.R(ts[-1])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct R" ) assert numpy.fabs(nos[1].vR() - o.vR(ts[-1])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vR" ) assert numpy.fabs(nos[1].vT() - o.vT(ts[-1])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vT" ) assert numpy.fabs(nos[1].z() - o.z(ts[-1])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct z" ) assert numpy.fabs(nos[1].vz() - o.vz(ts[-1])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vz" ) assert numpy.fabs(nos[1].phi() - o.phi(ts[-1])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct phi" ) assert not nos[1]._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos[1]._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos[1]._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos[1]._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) return None # Test new orbits formed from __call__, before integration def test_newOrbit_b4integration(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.1, 0.0, 0.0]) no = o(0.0) # New orbit formed before integration assert numpy.fabs(no.R() - o.R()) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct R" ) assert numpy.fabs(no.vR() - o.vR()) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vR" ) assert numpy.fabs(no.vT() - o.vT()) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vT" ) assert numpy.fabs(no.z() - o.z()) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct z" ) assert numpy.fabs(no.vz() - o.vz()) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vz" ) assert numpy.fabs(no.phi() - o.phi()) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct phi" ) assert not no._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not no._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not no._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not no._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) return None # Test that we can still get outputs when there aren't enough points for an actual interpolation def test_newOrbit_badinterpolation(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.1, 0.0, 0.0]) ts = numpy.linspace( 0.0, 1.0, 3 ) # v. quick orbit integration, w/ not enough points for interpolation lp = potential.LogarithmicHaloPotential(normalize=1.0) o.integrate(ts, lp) no = o(ts[-1]) # new orbit print("Done") assert no.R() == o.R(ts[-1]), ( "New orbit formed from calling an old orbit does not have the correct R" ) assert no.vR() == o.vR(ts[-1]), ( "New orbit formed from calling an old orbit does not have the correct vR" ) assert no.vT() == o.vT(ts[-1]), ( "New orbit formed from calling an old orbit does not have the correct vT" ) assert no.z() == o.z(ts[-1]), ( "New orbit formed from calling an old orbit does not have the correct z" ) assert no.vz() == o.vz(ts[-1]), ( "New orbit formed from calling an old orbit does not have the correct vz" ) assert no.phi() == o.phi(ts[-1]), ( "New orbit formed from calling an old orbit does not have the correct phi" ) assert not no._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not no._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not no._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not no._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) # Also test this for multiple time outputs nos = o(ts[-2:]) # new orbits # First t assert numpy.fabs(nos[0].R() - o.R(ts[-2])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct R" ) assert numpy.fabs(nos[0].vR() - o.vR(ts[-2])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vR" ) assert numpy.fabs(nos[0].vT() - o.vT(ts[-2])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vT" ) assert numpy.fabs(nos[0].z() - o.z(ts[-2])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct z" ) assert numpy.fabs(nos[0].vz() - o.vz(ts[-2])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vz" ) assert numpy.fabs(nos[0].phi() - o.phi(ts[-2])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct phi" ) assert not nos[0]._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos[0]._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos[0]._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos[0]._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) # Second t assert numpy.fabs(nos[1].R() - o.R(ts[-1])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct R" ) assert numpy.fabs(nos[1].vR() - o.vR(ts[-1])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vR" ) assert numpy.fabs(nos[1].vT() - o.vT(ts[-1])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vT" ) assert numpy.fabs(nos[1].z() - o.z(ts[-1])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct z" ) assert numpy.fabs(nos[1].vz() - o.vz(ts[-1])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct vz" ) assert numpy.fabs(nos[1].phi() - o.phi(ts[-1])) < 10.0**-10.0, ( "New orbit formed from calling an old orbit does not have the correct phi" ) assert not nos[1]._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos[1]._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos[1]._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos[1]._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) # Try point in between, shouldn't work try: no = o(0.6) except LookupError: pass else: raise AssertionError( "Orbit interpolation with not enough points to interpolate should raise LookUpError, but did not" ) return None # Check the routines that should return physical coordinates def test_physical_output(): from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion lp = LogarithmicHaloPotential(normalize=1.0) plp = lp.toPlanar() for ii in range(4): ro, vo = 7.0, 200.0 if ii == 0: # axi, full o = setup_orbit_physical(lp, axi=True, ro=ro, vo=vo) elif ii == 1: # track azimuth, full o = setup_orbit_physical(lp, axi=False, ro=ro, vo=vo) elif ii == 2: # axi, planar o = setup_orbit_physical(plp, axi=True, ro=ro, vo=vo) elif ii == 3: # track azimuth, full o = setup_orbit_physical(plp, axi=False, ro=ro, vo=vo) # Test positions assert numpy.fabs(o.R() / ro - o.R(use_physical=False)) < 10.0**-10.0, ( "o.R() output for Orbit setup with ro= does not work as expected" ) if ii % 2 == 1: assert numpy.fabs(o.x() / ro - o.x(use_physical=False)) < 10.0**-10.0, ( "o.x() output for Orbit setup with ro= does not work as expected" ) assert numpy.fabs(o.y() / ro - o.y(use_physical=False)) < 10.0**-10.0, ( "o.y() output for Orbit setup with ro= does not work as expected" ) if ii < 2: assert numpy.fabs(o.r() / ro - o.r(use_physical=False)) < 10.0**-10.0, ( "o.r() output for Orbit setup with ro= does not work as expected" ) assert numpy.fabs(o.z() / ro - o.z(use_physical=False)) < 10.0**-10.0, ( "o.z() output for Orbit setup with ro= does not work as expected" ) # Test velocities assert numpy.fabs(o.vR() / vo - o.vR(use_physical=False)) < 10.0**-10.0, ( "o.vR() output for Orbit setup with vo= does not work as expected" ) assert numpy.fabs(o.vT() / vo - o.vT(use_physical=False)) < 10.0**-10.0, ( "o.vT() output for Orbit setup with vo= does not work as expected" ) assert ( numpy.fabs(o.vphi() * ro / vo - o.vphi(use_physical=False)) < 10.0**-10.0 ), "o.vphi() output for Orbit setup with vo= does not work as expected" if ii % 2 == 1: assert numpy.fabs(o.vx() / vo - o.vx(use_physical=False)) < 10.0**-10.0, ( "o.vx() output for Orbit setup with vo= does not work as expected" ) assert numpy.fabs(o.vy() / vo - o.vy(use_physical=False)) < 10.0**-10.0, ( "o.vy() output for Orbit setup with vo= does not work as expected" ) if ii < 2: assert numpy.fabs(o.vz() / vo - o.vz(use_physical=False)) < 10.0**-10.0, ( "o.vz() output for Orbit setup with vo= does not work as expected" ) # Test energies assert ( numpy.fabs(o.E(pot=lp) / vo**2.0 - o.E(pot=lp, use_physical=False)) < 10.0**-10.0 ), "o.E() output for Orbit setup with vo= does not work as expected" assert ( numpy.fabs( o.Jacobi(pot=lp) / vo**2.0 - o.Jacobi(pot=lp, use_physical=False) ) < 10.0**-10.0 ), "o.E() output for Orbit setup with vo= does not work as expected" if ii < 2: assert ( numpy.fabs(o.ER(pot=lp) / vo**2.0 - o.ER(pot=lp, use_physical=False)) < 10.0**-10.0 ), "o.ER() output for Orbit setup with vo= does not work as expected" assert ( numpy.fabs(o.Ez(pot=lp) / vo**2.0 - o.Ez(pot=lp, use_physical=False)) < 10.0**-10.0 ), "o.Ez() output for Orbit setup with vo= does not work as expected" # Test angular momentun if ii > 0: assert numpy.all( numpy.fabs(o.L() / vo / ro - o.L(use_physical=False)) < 10.0**-10.0 ), "o.L() output for Orbit setup with ro=,vo= does not work as expected" # Test action-angle functions if ii == 1: assert ( numpy.fabs( o.jr(pot=lp, type="staeckel", delta=0.5) / vo / ro - o.jr(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.jr() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.jp(pot=lp, type="staeckel", delta=0.5) / vo / ro - o.jp(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.jp() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.jz(pot=lp, type="staeckel", delta=0.5) / vo / ro - o.jz(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.jz() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tr(pot=lp, type="staeckel", delta=0.5) / conversion.time_in_Gyr(vo, ro) - o.Tr(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Tr() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tp(pot=lp, type="staeckel", delta=0.5) / conversion.time_in_Gyr(vo, ro) - o.Tp(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Tp() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tz(pot=lp, type="staeckel", delta=0.5) / conversion.time_in_Gyr(vo, ro) - o.Tz(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Tz() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Or(pot=lp, type="staeckel", delta=0.5) / conversion.freq_in_Gyr(vo, ro) - o.Or(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Or() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Op(pot=lp, type="staeckel", delta=0.5) / conversion.freq_in_Gyr(vo, ro) - o.Op(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Op() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Oz(pot=lp, type="staeckel", delta=0.5) / conversion.freq_in_Gyr(vo, ro) - o.Oz(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Oz() output for Orbit setup with ro=,vo= does not work as expected" # Also test the times assert numpy.fabs(o.time(1.0) - ro / vo / 1.0227121655399913) < 10.0**-10.0, ( "o.time() in physical coordinates does not work as expected" ) assert ( numpy.fabs(o.time(1.0, ro=ro, vo=vo) - ro / vo / 1.0227121655399913) < 10.0**-10.0 ), "o.time() in physical coordinates does not work as expected" assert numpy.fabs(o.time(1.0, use_physical=False) - 1.0) < 10.0**-10.0, ( "o.time() in physical coordinates does not work as expected" ) return None # Check that the routines that should return physical coordinates are turned off by turn_physical_off def test_physical_output_off(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0) plp = lp.toPlanar() for ii in range(4): ro, vo = 7.0, 200.0 if ii == 0: # axi, full o = setup_orbit_physical(lp, axi=True, ro=ro, vo=vo) elif ii == 1: # track azimuth, full o = setup_orbit_physical(lp, axi=False, ro=ro, vo=vo) elif ii == 2: # axi, planar o = setup_orbit_physical(plp, axi=True, ro=ro, vo=vo) elif ii == 3: # track azimuth, full o = setup_orbit_physical(plp, axi=False, ro=ro, vo=vo) # turn off o.turn_physical_off() # Test positions assert numpy.fabs(o.R() - o.R(use_physical=False)) < 10.0**-10.0, ( "o.R() output for Orbit setup with ro= does not work as expected when turned off" ) if ii % 2 == 1: assert numpy.fabs(o.x() - o.x(use_physical=False)) < 10.0**-10.0, ( "o.x() output for Orbit setup with ro= does not work as expected when turned off" ) assert numpy.fabs(o.y() - o.y(use_physical=False)) < 10.0**-10.0, ( "o.y() output for Orbit setup with ro= does not work as expected when turned off" ) if ii < 2: assert numpy.fabs(o.z() - o.z(use_physical=False)) < 10.0**-10.0, ( "o.z() output for Orbit setup with ro= does not work as expected when turned off" ) assert numpy.fabs(o.r() - o.r(use_physical=False)) < 10.0**-10.0, ( "o.r() output for Orbit setup with ro= does not work as expected when turned off" ) # Test velocities assert numpy.fabs(o.vR() - o.vR(use_physical=False)) < 10.0**-10.0, ( "o.vR() output for Orbit setup with vo= does not work as expected when turned off" ) assert numpy.fabs(o.vT() - o.vT(use_physical=False)) < 10.0**-10.0, ( "o.vT() output for Orbit setup with vo= does not work as expected" ) assert numpy.fabs(o.vphi() - o.vphi(use_physical=False)) < 10.0**-10.0, ( "o.vphi() output for Orbit setup with vo= does not work as expected when turned off" ) if ii % 2 == 1: assert numpy.fabs(o.vx() - o.vx(use_physical=False)) < 10.0**-10.0, ( "o.vx() output for Orbit setup with vo= does not work as expected when turned off" ) assert numpy.fabs(o.vy() - o.vy(use_physical=False)) < 10.0**-10.0, ( "o.vy() output for Orbit setup with vo= does not work as expected when turned off" ) if ii < 2: assert numpy.fabs(o.vz() - o.vz(use_physical=False)) < 10.0**-10.0, ( "o.vz() output for Orbit setup with vo= does not work as expected when turned off" ) # Test energies assert ( numpy.fabs(o.E(pot=lp) - o.E(pot=lp, use_physical=False)) < 10.0**-10.0 ), ( "o.E() output for Orbit setup with vo= does not work as expected when turned off" ) assert ( numpy.fabs(o.Jacobi(pot=lp) - o.Jacobi(pot=lp, use_physical=False)) < 10.0**-10.0 ), ( "o.E() output for Orbit setup with vo= does not work as expected when turned off" ) if ii < 2: assert ( numpy.fabs(o.ER(pot=lp) - o.ER(pot=lp, use_physical=False)) < 10.0**-10.0 ), ( "o.ER() output for Orbit setup with vo= does not work as expected when turned off" ) assert ( numpy.fabs(o.Ez(pot=lp) - o.Ez(pot=lp, use_physical=False)) < 10.0**-10.0 ), ( "o.Ez() output for Orbit setup with vo= does not work as expected when turned off" ) # Test angular momentun if ii > 0: assert numpy.all( numpy.fabs(o.L() - o.L(use_physical=False)) < 10.0**-10.0 ), ( "o.L() output for Orbit setup with ro=,vo= does not work as expected when turned off" ) # Test action-angle functions if ii == 1: assert ( numpy.fabs( o.jr(pot=lp, type="staeckel", delta=0.5) - o.jr(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.jr() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.jp(pot=lp, type="staeckel", delta=0.5) - o.jp(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.jp() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.jz(pot=lp, type="staeckel", delta=0.5) - o.jz(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.jz() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tr(pot=lp, type="staeckel", delta=0.5) - o.Tr(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Tr() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tp(pot=lp, type="staeckel", delta=0.5) - o.Tp(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Tp() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tz(pot=lp, type="staeckel", delta=0.5) - o.Tz(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Tz() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Or(pot=lp, type="staeckel", delta=0.5) - o.Or(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Or() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Op(pot=lp, type="staeckel", delta=0.5) - o.Op(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Op() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Oz(pot=lp, type="staeckel", delta=0.5) - o.Oz(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Oz() output for Orbit setup with ro=,vo= does not work as expected" # Also test the times assert numpy.fabs(o.time(1.0) - 1.0) < 10.0**-10.0, ( "o.time() in physical coordinates does not work as expected when turned off" ) assert ( numpy.fabs(o.time(1.0, ro=ro, vo=vo) - ro / vo / 1.0227121655399913) < 10.0**-10.0 ), "o.time() in physical coordinates does not work as expected when turned off" return None # Check that the routines that should return physical coordinates are turned # back on by turn_physical_on def test_physical_output_on(): from astropy import units from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0) plp = lp.toPlanar() for ii in range(4): ro, vo = 7.0, 200.0 if ii == 0: # axi, full o = setup_orbit_physical(lp, axi=True, ro=ro, vo=vo) elif ii == 1: # track azimuth, full o = setup_orbit_physical(lp, axi=False, ro=ro, vo=vo) elif ii == 2: # axi, planar o = setup_orbit_physical(plp, axi=True, ro=ro, vo=vo) elif ii == 3: # track azimuth, full o = setup_orbit_physical(plp, axi=False, ro=ro, vo=vo) o_orig = o() # turn off and on o.turn_physical_off() if ii == 0: o.turn_physical_on(ro=ro, vo=vo) elif ii == 1: o.turn_physical_on(ro=ro * units.kpc, vo=vo * units.km / units.s) else: o.turn_physical_on() # Test positions assert numpy.fabs(o.R() - o_orig.R(use_physical=True)) < 10.0**-10.0, ( "o.R() output for Orbit setup with ro= does not work as expected when turned back on" ) if ii % 2 == 1: assert numpy.fabs(o.x() - o_orig.x(use_physical=True)) < 10.0**-10.0, ( "o.x() output for Orbit setup with ro= does not work as expected when turned back on" ) assert numpy.fabs(o.y() - o_orig.y(use_physical=True)) < 10.0**-10.0, ( "o.y() output for Orbit setup with ro= does not work as expected when turned back on" ) if ii < 2: assert numpy.fabs(o.z() - o_orig.z(use_physical=True)) < 10.0**-10.0, ( "o.z() output for Orbit setup with ro= does not work as expected when turned back on" ) # Test velocities assert numpy.fabs(o.vR() - o_orig.vR(use_physical=True)) < 10.0**-10.0, ( "o.vR() output for Orbit setup with vo= does not work as expected when turned back on" ) assert numpy.fabs(o.vT() - o_orig.vT(use_physical=True)) < 10.0**-10.0, ( "o.vT() output for Orbit setup with vo= does not work as expected" ) assert numpy.fabs(o.vphi() - o_orig.vphi(use_physical=True)) < 10.0**-10.0, ( "o.vphi() output for Orbit setup with vo= does not work as expected when turned back on" ) if ii % 2 == 1: assert numpy.fabs(o.vx() - o_orig.vx(use_physical=True)) < 10.0**-10.0, ( "o.vx() output for Orbit setup with vo= does not work as expected when turned back on" ) assert numpy.fabs(o.vy() - o_orig.vy(use_physical=True)) < 10.0**-10.0, ( "o.vy() output for Orbit setup with vo= does not work as expected when turned back on" ) if ii < 2: assert numpy.fabs(o.vz() - o_orig.vz(use_physical=True)) < 10.0**-10.0, ( "o.vz() output for Orbit setup with vo= does not work as expected when turned back on" ) # Test energies assert ( numpy.fabs(o.E(pot=lp) - o_orig.E(pot=lp, use_physical=True)) < 10.0**-10.0 ), ( "o.E() output for Orbit setup with vo= does not work as expected when turned back on" ) assert ( numpy.fabs(o.Jacobi(pot=lp) - o_orig.Jacobi(pot=lp, use_physical=True)) < 10.0**-10.0 ), ( "o.E() output for Orbit setup with vo= does not work as expected when turned back on" ) if ii < 2: assert ( numpy.fabs(o.ER(pot=lp) - o_orig.ER(pot=lp, use_physical=True)) < 10.0**-10.0 ), ( "o.ER() output for Orbit setup with vo= does not work as expected when turned back on" ) assert ( numpy.fabs(o.Ez(pot=lp) - o_orig.Ez(pot=lp, use_physical=True)) < 10.0**-10.0 ), ( "o.Ez() output for Orbit setup with vo= does not work as expected when turned back on" ) # Test angular momentun if ii > 0: assert numpy.all( numpy.fabs(o.L() - o_orig.L(use_physical=True)) < 10.0**-10.0 ), ( "o.L() output for Orbit setup with ro=,vo= does not work as expected when turned back on" ) # Test action-angle functions if ii == 1: assert ( numpy.fabs( o.jr(pot=lp, type="staeckel", delta=0.5) - o_orig.jr(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.jr() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.jp(pot=lp, type="staeckel", delta=0.5) - o_orig.jp(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.jp() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.jz(pot=lp, type="staeckel", delta=0.5) - o_orig.jz(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.jz() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tr(pot=lp, type="staeckel", delta=0.5) - o_orig.Tr(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.Tr() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tp(pot=lp, type="staeckel", delta=0.5) - o_orig.Tp(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.Tp() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tz(pot=lp, type="staeckel", delta=0.5) - o_orig.Tz(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.Tz() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Or(pot=lp, type="staeckel", delta=0.5) - o_orig.Or(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.Or() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Op(pot=lp, type="staeckel", delta=0.5) - o_orig.Op(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.Op() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Oz(pot=lp, type="staeckel", delta=0.5) - o_orig.Oz(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.Oz() output for Orbit setup with ro=,vo= does not work as expected" # Also test the times assert ( numpy.fabs(o.time(1.0) - o_orig.time(1.0, use_physical=True)) < 10.0**-10.0 ), ( "o_orig.time() in physical coordinates does not work as expected when turned back on" ) return None # Test that physical scales are propagated correctly when a new orbit is formed by calling an old orbit def test_physical_newOrbit(): from galpy.orbit import Orbit o = Orbit( [1.0, 0.1, 1.1, 0.1, 0.0, 0.0], ro=9.0, vo=230.0, zo=0.02, solarmotion=[-5.0, 15.0, 25.0], ) ts = numpy.linspace(0.0, 1.0, 21) # v. quick orbit integration lp = potential.LogarithmicHaloPotential(normalize=1.0) o.integrate(ts, lp) no = o(ts[-1]) # new orbit assert no._ro == 9.0, ( "New orbit formed from calling old orbit's ro is not that of the old orbit" ) assert no._vo == 230.0, ( "New orbit formed from calling old orbit's vo is not that of the old orbit" ) assert no._ro == 9.0, ( "New orbit formed from calling old orbit's ro is not that of the old orbit" ) assert no._vo == 230.0, ( "New orbit formed from calling old orbit's vo is not that of the old orbit" ) assert no._roSet, ( "New orbit formed from calling old orbit's roSet is not that of the old orbit" ) assert no._voSet, ( "New orbit formed from calling old orbit's roSet is not that of the old orbit" ) assert no._roSet, ( "New orbit formed from calling old orbit's roSet is not that of the old orbit" ) assert no._voSet, ( "New orbit formed from calling old orbit's roSet is not that of the old orbit" ) assert no._zo == 0.02, ( "New orbit formed from calling old orbit's zo is not that of the old orbit" ) assert no._solarmotion[0] == -5.0, ( "New orbit formed from calling old orbit's solarmotion is not that of the old orbit" ) assert no._solarmotion[1] == 15.0, ( "New orbit formed from calling old orbit's solarmotion is not that of the old orbit" ) assert no._solarmotion[2] == 25.0, ( "New orbit formed from calling old orbit's solarmotion is not that of the old orbit" ) return None # Test the orbit interpolation def test_interpolation_issue187(): # Test that fails because of issue 187 reported by Mark Fardal from scipy import interpolate from galpy.orbit import Orbit pot = potential.IsochronePotential(b=1.0 / 7.0, normalize=True) R, vR, vT, z, vz, phi = 1.0, 0.0, 0.8, 0.0, 0.0, 0.0 orb = Orbit(vxvv=[R, vR, vT, z, vz, phi]) ts = numpy.linspace(0.0, 10.0, 1000) orb.integrate(ts, pot) orbpts = orb.getOrbit() # detect phase wrap pdiff = orbpts[:, 5] - numpy.roll(orbpts[:, 5], 1) phaseWrapIndx = numpy.where(pdiff < -6.0)[0][0] tsPreWrap = numpy.linspace( ts[phaseWrapIndx] - 5.0e-2, ts[phaseWrapIndx] - 0.002, 100 ) tsPostWrap = numpy.linspace( ts[phaseWrapIndx] + 0.002, ts[phaseWrapIndx] + 5.0e-2, 100 ) # Interpolate just before and after the phase-wrap preWrapInterpolate = interpolate.InterpolatedUnivariateSpline( ts[phaseWrapIndx - 11 : phaseWrapIndx - 1], orbpts[phaseWrapIndx - 11 : phaseWrapIndx - 1, 5], ) postWrapInterpolate = interpolate.InterpolatedUnivariateSpline( ts[phaseWrapIndx : phaseWrapIndx + 10], orbpts[phaseWrapIndx : phaseWrapIndx + 10, 5], ) assert numpy.all( numpy.fabs( ((preWrapInterpolate(tsPreWrap) + numpy.pi) % (2.0 * numpy.pi) - numpy.pi) - orb.phi(tsPreWrap) ) < 10.0**-5.0 ), "phase interpolation near a phase-wrap does not work" assert numpy.all( numpy.fabs( ((postWrapInterpolate(tsPostWrap) + numpy.pi) % (2.0 * numpy.pi) - numpy.pi) - orb.phi(tsPostWrap) ) < 10.0**-5.0 ), "phase interpolation near a phase-wrap does not work" return None # Test that fitting an orbit works def test_orbitfit(): from galpy.orbit import Orbit lp = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9) o = Orbit([0.8, 0.3, 1.3, 0.4, 0.2, 2.0]) ts = numpy.linspace(0.0, 1.0, 1001) o.integrate(ts, lp) # Create orbit points from this integrated orbit, each 100th point vxvv = o.getOrbit()[::100, :] # now fit of = Orbit.from_fit(o.vxvv[0], vxvv, pot=lp, tintJ=1.5) assert numpy.all( comp_orbfit(of, vxvv, numpy.linspace(0.0, 2.0, 1001), lp) < 10.0**-7.0 ), "Orbit fit in configuration space does not work" return None def test_orbitfit_potinput(): from galpy.orbit import Orbit from galpy.potential import PotentialError lp = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9) o = Orbit([0.8, 0.3, 1.3, 0.4, 0.2, 2.0]) ts = numpy.linspace(0.0, 1.0, 1001) o.integrate(ts, lp) # Create orbit points from this integrated orbit, each 100th point vxvv = o.getOrbit()[::100, :] # now fit, using another orbit instance, without potential, should error of = o() try: Orbit.from_fit(o.vxvv[0], vxvv, pot=None, tintJ=1.5) except PotentialError: pass else: raise AssertionError("Orbit fit w/o potential does not raise PotentialError") # Now give a potential to of of = Orbit.from_fit(o.vxvv[0], vxvv, pot=lp, tintJ=1.5) assert numpy.all( comp_orbfit(of, vxvv, numpy.linspace(0.0, 2.0, 1001), lp) < 10.0**-7.0 ), "Orbit fit in configuration space does not work" return None # Test orbit fit in observed Galactic coordinates def test_orbitfit_lb(): from galpy.orbit import Orbit lp = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9) o = Orbit([0.8, 0.3, 1.3, 0.4, 0.2, 2.0]) ts = numpy.linspace(0.0, 1.0, 1001) o.integrate(ts, lp) # Create orbit points from this integrated orbit, each 100th point vxvv = [] for ii in range(10): vxvv.append( [ o.ll(ii / 10.0), o.bb(ii / 10.0), o.dist(ii / 10.0), o.pmll(ii / 10.0), o.pmbb(ii / 10.0), o.vlos(ii / 10.0), ] ) vxvv = numpy.array(vxvv) # now fit of = Orbit.from_fit( [o.ll(), o.bb(), o.dist(), o.pmll(), o.pmbb(), o.vlos()], vxvv, pot=lp, tintJ=1.5, lb=True, vxvv_err=0.01 * numpy.ones_like(vxvv), ) compf = comp_orbfit(of, vxvv, numpy.linspace(0.0, 2.0, 1001), lp, lb=True) assert numpy.all(compf < 10.0**-4.0), "Orbit fit in lb space does not work" return None # Test orbit fit in observed equatorial coordinates def test_orbitfit_radec(): from galpy.orbit import Orbit lp = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9) ro, vo = 9.0, 230.0 o = Orbit([0.8, 0.3, 1.3, 0.4, 0.2, 2.0], ro=ro, vo=vo) ts = numpy.linspace(0.0, 1.0, 1001) o.integrate(ts, lp) # Create orbit points from this integrated orbit, each 100th point vxvv = [] for ii in range(10): vxvv.append( [ o.ra(ii / 10.0), o.dec(ii / 10.0), o.dist(ii / 10.0), o.pmra(ii / 10.0), o.pmdec(ii / 10.0), o.vlos(ii / 10.0), ] ) vxvv = numpy.array(vxvv) # now fit of = Orbit.from_fit( [o.ra(), o.dec(), o.dist(), o.pmra(), o.pmdec(), o.vlos()], vxvv, pot=lp, tintJ=1.5, radec=True, ro=ro, vo=vo, ) compf = comp_orbfit( of, vxvv, numpy.linspace(0.0, 2.0, 1001), lp, lb=False, radec=True, ro=ro, vo=vo ) assert numpy.all(compf < 10.0**-4.0), "Orbit fit in radec space does not work" return None # Test orbit fit in custom coordinates (using Equatorial for testing) def test_orbitfit_custom(): from galpy.orbit import Orbit from galpy.util import coords lp = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9) ro, vo = 9.0, 230.0 o = Orbit([0.8, 0.3, 1.3, 0.4, 0.2, 2.0], ro=ro, vo=vo) ts = numpy.linspace(0.0, 1.0, 1001) o.integrate(ts, lp) # Create orbit points from this integrated orbit, each 100th point vxvv = [] for ii in range(10): vxvv.append( [ o.ra(ii / 10.0), o.dec(ii / 10.0), o.dist(ii / 10.0), o.pmra(ii / 10.0), o.pmdec(ii / 10.0), o.vlos(ii / 10.0), ] ) vxvv = numpy.array(vxvv) # now fit # First test the exception try: Orbit.from_fit( [o.ra(), o.dec(), o.dist(), o.pmra(), o.pmdec(), o.vlos()], vxvv, pot=lp, tintJ=1.5, customsky=True, ro=ro, vo=vo, ) except OSError: pass else: raise AssertionError( "Orbit fit with custom sky coordinates but without the necessary coordinate-transformation functions did not raise an exception" ) of = Orbit.from_fit( [o.ra(), o.dec(), o.dist(), o.pmra(), o.pmdec(), o.vlos()], vxvv, pot=lp, tintJ=1.5, customsky=True, lb_to_customsky=coords.lb_to_radec, pmllpmbb_to_customsky=coords.pmllpmbb_to_pmrapmdec, ro=ro, vo=vo, ) compf = comp_orbfit( of, vxvv, numpy.linspace(0.0, 2.0, 1001), lp, lb=False, radec=True, ro=ro, vo=vo ) assert numpy.all(compf < 10.0**-4.0), "Orbit fit in radec space does not work" return None def comp_orbfit(of, vxvv, ts, pot, lb=False, radec=False, ro=None, vo=None): """Compare the output of the orbit fit properly, ro and vo only implemented for radec""" from galpy.util import coords coords._APY_COORDS_ORIG = coords._APY_COORDS coords._APY_COORDS = False # too slow otherwise of.integrate(ts, pot) off = of.flip() off.integrate(ts, pot) # Flip velocities again off.vxvv[..., 1] *= -1.0 off.vxvv[..., 2] *= -1.0 off.vxvv[..., 4] *= -1.0 if lb: allvxvv = [] for ii in range(len(ts)): allvxvv.append( [ of.ll(ts[ii]), of.bb(ts[ii]), of.dist(ts[ii]), of.pmll(ts[ii]), of.pmbb(ts[ii]), of.vlos(ts[ii]), ] ) allvxvv.append( [ off.ll(ts[ii]), off.bb(ts[ii]), off.dist(ts[ii]), off.pmll(ts[ii]), off.pmbb(ts[ii]), off.vlos(ts[ii]), ] ) allvxvv = numpy.array(allvxvv) elif radec: allvxvv = [] for ii in range(len(ts)): allvxvv.append( [ of.ra(ts[ii], ro=ro, vo=vo), of.dec(ts[ii], ro=ro, vo=vo), of.dist(ts[ii], ro=ro, vo=vo), of.pmra(ts[ii], ro=ro, vo=vo), of.pmdec(ts[ii], ro=ro, vo=vo), of.vlos(ts[ii], ro=ro, vo=vo), ] ) allvxvv.append( [ off.ra(ts[ii]), off.dec(ts[ii], ro=ro, vo=vo), off.dist(ts[ii], ro=ro, vo=vo), off.pmra(ts[ii], ro=ro, vo=vo), off.pmdec(ts[ii], ro=ro, vo=vo), off.vlos(ts[ii], ro=ro, vo=vo), ] ) allvxvv = numpy.array(allvxvv) else: allvxvv = numpy.concatenate((of.getOrbit(), off.getOrbit()), axis=0) out = [] for ii in range(vxvv.shape[0]): out.append(numpy.amin(numpy.sum((allvxvv - vxvv[ii]) ** 2.0, axis=1))) coords._APY_COORDS = coords._APY_COORDS_ORIG return numpy.array(out) def test_MWPotential_warning(): # Test that using MWPotential throws a warning, see #229 ts = numpy.linspace(0.0, 100.0, 1001) psis = numpy.linspace(0.0, 20.0 * numpy.pi, 1001) o = setup_orbit_energy(potential.MWPotential, axi=False) with pytest.warns(galpyWarning) as record: if PY2: reset_warning_registry("galpy") warnings.simplefilter("always", galpyWarning) o.integrate(ts, potential.MWPotential) # Should raise warning bc of MWPotential, might raise others raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "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" ) assert raisedWarning, ( "Orbit integration with MWPotential should have thrown a warning, but didn't" ) # Also test for SOS integration with pytest.warns(galpyWarning) as record: if PY2: reset_warning_registry("galpy") warnings.simplefilter("always", galpyWarning) o.integrate_SOS(psis, potential.MWPotential) # Should raise warning bc of MWPotential, might raise others raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "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" ) assert raisedWarning, ( "Orbit integration with MWPotential should have thrown a warning, but didn't" ) return None # Test the new Orbit.time function def test_time(): # Setup orbit o = setup_orbit_energy(potential.MWPotential, axi=False) # Prior to integration, should return zero assert numpy.fabs(o.time() - 0.0) < 10.0**-10.0, ( "Orbit.time before integration does not return zero" ) # Then integrate times = numpy.linspace(0.0, 10.0, 1001) o.integrate(times, potential.MWPotential) assert numpy.all((o.time() - times) < 10.0**-8.0), ( "Orbit.time after integration does not return the integration times" ) return None # Test interpolation with backwards orbit integration def test_backinterpolation_issue204(): # Setup orbit and its flipped version o = setup_orbit_energy(potential.MWPotential, axi=False) of = o.flip() # Times to integrate backward and forward of flipped (should agree) ntimes = numpy.linspace(0.0, -10.0, 1001) ptimes = -ntimes # Integrate the orbits o.integrate(ntimes, potential.MWPotential) of.integrate(ptimes, potential.MWPotential) # Test that interpolation works and gives the same result nitimes = numpy.linspace(0.0, -10.0, 2501) pitimes = -nitimes assert numpy.all((o.R(nitimes) - of.R(pitimes)) < 10.0**-8.0), ( "Forward and backward integration with interpolation do not agree" ) assert numpy.all((o.z(nitimes) - of.z(pitimes)) < 10.0**-8.0), ( "Forward and backward integration with interpolation do not agree" ) # Velocities should be flipped assert numpy.all((o.vR(nitimes) + of.vR(pitimes)) < 10.0**-8.0), ( "Forward and backward integration with interpolation do not agree" ) assert numpy.all((o.vT(nitimes) + of.vT(pitimes)) < 10.0**-8.0), ( "Forward and backward integration with interpolation do not agree" ) assert numpy.all((o.vT(nitimes) + of.vT(pitimes)) < 10.0**-8.0), ( "Forward and backward integration with interpolation do not agree" ) return None # Test that Orbit.x .y .vx and .vy return a scalar for scalar time input def test_scalarxyvzvz_issue247(): # Setup an orbit lp = potential.LogarithmicHaloPotential(normalize=1.0) o = setup_orbit_energy(lp, axi=False) assert isinstance(o.x(), float), "Orbit.x() does not return a scalar" assert isinstance(o.y(), float), "Orbit.y() does not return a scalar" assert isinstance(o.vx(), float), "Orbit.vx() does not return a scalar" assert isinstance(o.vy(), float), "Orbit.vy() does not return a scalar" # Also integrate and then test times = numpy.linspace(0.0, 10.0, 1001) o.integrate(times, lp) assert isinstance(o.x(5.0), float), "Orbit.x() does not return a scalar" assert isinstance(o.y(5.0), float), "Orbit.y() does not return a scalar" assert isinstance(o.vx(5.0), float), "Orbit.vx() does not return a scalar" assert isinstance(o.vy(5.0), float), "Orbit.vy() does not return a scalar" return None # Test that all Orbit methods return a scalar for scalar time input (mentioned # in #294) def test_scalar_all(): # Setup an orbit lp = potential.LogarithmicHaloPotential(normalize=1.0) o = setup_orbit_energy(lp, axi=False) assert isinstance(o.R(), float), "Orbit.R() does not return a scalar" assert isinstance(o.vR(), float), "Orbit.vR() does not return a scalar" assert isinstance(o.vT(), float), "Orbit.vT() does not return a scalar" assert isinstance(o.z(), float), "Orbit.z() does not return a scalar" assert isinstance(o.vz(), float), "Orbit.vz() does not return a scalar" assert isinstance(o.phi(), float), "Orbit.phi() does not return a scalar" assert isinstance(o.r(), float), "Orbit.r() does not return a scalar" assert isinstance(o.x(), float), "Orbit.x() does not return a scalar" assert isinstance(o.y(), float), "Orbit.y() does not return a scalar" assert isinstance(o.vx(), float), "Orbit.vx() does not return a scalar" assert isinstance(o.vy(), float), "Orbit.vy() does not return a scalar" assert isinstance(o.theta(), float), "Orbit.theta() does not return a scalar" assert isinstance(o.vtheta(), float), "Orbit.vtheta() does not return a scalar" assert isinstance(o.vr(), float), "Orbit.vr() does not return a scalar" assert isinstance(o.ra(), float), "Orbit.ra() does not return a scalar" assert isinstance(o.dec(), float), "Orbit.dec() does not return a scalar" assert isinstance(o.ll(), float), "Orbit.ll() does not return a scalar" assert isinstance(o.bb(), float), "Orbit.bb() does not return a scalar" assert isinstance(o.dist(), float), "Orbit.dist() does not return a scalar" assert isinstance(o.pmra(), float), "Orbit.pmra() does not return a scalar" assert isinstance(o.pmdec(), float), "Orbit.pmdec() does not return a scalar" assert isinstance(o.pmll(), float), "Orbit.pmll() does not return a scalar" assert isinstance(o.pmbb(), float), "Orbit.pmbb() does not return a scalar" assert isinstance(o.vra(), float), "Orbit.vra() does not return a scalar" assert isinstance(o.vdec(), float), "Orbit.vdec() does not return a scalar" assert isinstance(o.vll(), float), "Orbit.vll() does not return a scalar" assert isinstance(o.vbb(), float), "Orbit.vbb() does not return a scalar" assert isinstance(o.vlos(), float), "Orbit.vlos() does not return a scalar" assert isinstance(o.helioX(), float), "Orbit.helioX() does not return a scalar" assert isinstance(o.helioY(), float), "Orbit.helioY() does not return a scalar" assert isinstance(o.helioZ(), float), "Orbit.helioZ() does not return a scalar" assert isinstance(o.U(), float), "Orbit.U() does not return a scalar" assert isinstance(o.V(), float), "Orbit.V() does not return a scalar" assert isinstance(o.W(), float), "Orbit.W() does not return a scalar" assert isinstance(o.E(pot=lp), float), "Orbit.E() does not return a scalar" assert isinstance(o.Jacobi(pot=lp), float), ( "Orbit.Jacobi() does not return a scalar" ) assert isinstance(o.ER(pot=lp), float), "Orbit.ER() does not return a scalar" assert isinstance(o.Ez(pot=lp), float), "Orbit.Ez() does not return a scalar" # Also integrate and then test times = numpy.linspace(0.0, 10.0, 1001) o.integrate(times, lp) assert isinstance(o.R(5.0), float), "Orbit.R() does not return a scalar" assert isinstance(o.vR(5.0), float), "Orbit.vR() does not return a scalar" assert isinstance(o.vT(5.0), float), "Orbit.vT() does not return a scalar" assert isinstance(o.z(5.0), float), "Orbit.z() does not return a scalar" assert isinstance(o.vz(5.0), float), "Orbit.vz() does not return a scalar" assert isinstance(o.phi(5.0), float), "Orbit.phi() does not return a scalar" assert isinstance(o.r(5.0), float), "Orbit.r() does not return a scalar" assert isinstance(o.x(5.0), float), "Orbit.x() does not return a scalar" assert isinstance(o.y(5.0), float), "Orbit.y() does not return a scalar" assert isinstance(o.vx(5.0), float), "Orbit.vx() does not return a scalar" assert isinstance(o.vy(5.0), float), "Orbit.vy() does not return a scalar" assert isinstance(o.theta(5.0), float), "Orbit.theta() does not return a scalar" assert isinstance(o.vtheta(5.0), float), "Orbit.vtheta() does not return a scalar" assert isinstance(o.vr(5.0), float), "Orbit.vr() does not return a scalar" assert isinstance(o.ra(5.0), float), "Orbit.ra() does not return a scalar" assert isinstance(o.dec(5.0), float), "Orbit.dec() does not return a scalar" assert isinstance(o.ll(5.0), float), "Orbit.ll() does not return a scalar" assert isinstance(o.bb(5.0), float), "Orbit.bb() does not return a scalar" assert isinstance(o.dist(5.0), float), "Orbit.dist() does not return a scalar" assert isinstance(o.pmra(5.0), float), "Orbit.pmra() does not return a scalar" assert isinstance(o.pmdec(5.0), float), "Orbit.pmdec() does not return a scalar" assert isinstance(o.pmll(5.0), float), "Orbit.pmll() does not return a scalar" assert isinstance(o.pmbb(5.0), float), "Orbit.pmbb() does not return a scalar" assert isinstance(o.vra(5.0), float), "Orbit.vra() does not return a scalar" assert isinstance(o.vdec(5.0), float), "Orbit.vdec() does not return a scalar" assert isinstance(o.vll(5.0), float), "Orbit.vll() does not return a scalar" assert isinstance(o.vbb(5.0), float), "Orbit.vbb() does not return a scalar" assert isinstance(o.vlos(5.0), float), "Orbit.vlos() does not return a scalar" assert isinstance(o.helioX(5.0), float), "Orbit.helioX() does not return a scalar" assert isinstance(o.helioY(5.0), float), "Orbit.helioY() does not return a scalar" assert isinstance(o.helioZ(5.0), float), "Orbit.helioZ() does not return a scalar" assert isinstance(o.U(5.0), float), "Orbit.U() does not return a scalar" assert isinstance(o.V(5.0), float), "Orbit.V() does not return a scalar" assert isinstance(o.W(5.0), float), "Orbit.W() does not return a scalar" assert isinstance(o.E(5.0), float), "Orbit.E() does not return a scalar" assert isinstance(o.Jacobi(5.0), float), "Orbit.Jacobi() does not return a scalar" assert isinstance(o.ER(5.0), float), "Orbit.ER() does not return a scalar" assert isinstance(o.Ez(5.0), float), "Orbit.Ez() does not return a scalar" return None def test_call_issue256(): # Reported by Semyeong Oh: non-integrated orbit with t=/=0 should return error from galpy.orbit import Orbit o = Orbit(vxvv=[5.0, -1.0, 0.8, 3, -0.1, 0]) # no integration of the orbit with pytest.raises(ValueError) as excinfo: o.R(30) return None # Test whether the output from the SkyCoord function is correct def test_SkyCoord(): from astropy import units from galpy.orbit import Orbit # In ra, dec o = Orbit([120.0, 60.0, 2.0, 0.5, 0.4, 30.0], radec=True) assert numpy.fabs(o.SkyCoord().ra.degree - o.ra()) < 10.0**-13.0, ( "Orbit SkyCoord ra and direct ra do not agree" ) assert numpy.fabs(o.SkyCoord().dec.degree - o.dec()) < 10.0**-13.0, ( "Orbit SkyCoord dec and direct dec do not agree" ) assert numpy.fabs(o.SkyCoord().distance.kpc - o.dist()) < 10.0**-13.0, ( "Orbit SkyCoord distance and direct distance do not agree" ) # For a list o = Orbit([120.0, 60.0, 2.0, 0.5, 0.4, 30.0], radec=True) times = numpy.linspace(0.0, 2.0, 51) from galpy.potential import MWPotential o.integrate(times, MWPotential) ras = numpy.array([s.ra.degree for s in o.SkyCoord(times)]) decs = numpy.array([s.dec.degree for s in o.SkyCoord(times)]) dists = numpy.array([s.distance.kpc for s in o.SkyCoord(times)]) assert numpy.all(numpy.fabs(ras - o.ra(times)) < 10.0**-13.0), ( "Orbit SkyCoord ra and direct ra do not agree" ) assert numpy.all(numpy.fabs(decs - o.dec(times)) < 10.0**-13.0), ( "Orbit SkyCoord dec and direct dec do not agree" ) assert numpy.all(numpy.fabs(dists - o.dist(times)) < 10.0**-13.0), ( "Orbit SkyCoord distance and direct distance do not agree" ) # Check that the GC frame parameters are correctly propagated if not _APY3: return None # not done in python 2 o = Orbit( [120.0, 60.0, 2.0, 0.5, 0.4, 30.0], radec=True, ro=10.0, zo=1.0, solarmotion=[-10.0, 34.0, 12.0], ) assert ( numpy.fabs( o.SkyCoord().galcen_distance.to(units.kpc).value - numpy.sqrt(10.0**2.0 + 1.0**2.0) ) < 10.0**-13.0 ), "Orbit SkyCoord GC frame attributes are incorrect" assert numpy.fabs(o.SkyCoord().z_sun.to(units.kpc).value - 1.0) < 10.0**-13.0, ( "Orbit SkyCoord GC frame attributes are incorrect" ) assert numpy.all( numpy.fabs( o.SkyCoord().galcen_v_sun.d_xyz.to(units.km / units.s).value - numpy.array([10.0, 220.0 + 34.0, 12.0]) ) < 10.0**-13.0 ), "Orbit SkyCoord GC frame attributes are incorrect" return None def test_orbit_obs_list_issue322(): # Further tests of obs= list parameter for orbit output, mainly in relation # to issue #322 from galpy.orbit import Orbit # The basic case, for a planar orbit o = Orbit([0.9, 0.1, 1.2, 0.0]) assert numpy.fabs(o.helioX(obs=[1.0, 0.0, 0.0], ro=1.0) - 0.1) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert numpy.fabs(o.helioY(obs=[1.0, 0.0, 0.0], ro=1.0)) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, numpy.pi / 2.0]) assert numpy.fabs(o.helioX(obs=[0.0, 1.0, 0.0], ro=1.0) - 0.1) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert numpy.fabs(o.helioY(obs=[0.0, 1.0, 0.0], ro=1.0)) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, 3.0 * numpy.pi / 2.0]) assert numpy.fabs(o.helioX(obs=[0.0, -1.0, 0.0], ro=1.0) - 0.1) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert numpy.fabs(o.helioY(obs=[0.0, -1.0, 0.0], ro=1.0)) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Full orbit # The basic case, for a full orbit o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, 0.0]) assert numpy.fabs(o.helioX(obs=[1.0, 0.0, 0.0], ro=1.0) - 0.1) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert numpy.fabs(o.helioY(obs=[1.0, 0.0, 0.0], ro=1.0)) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, numpy.pi / 2.0]) assert numpy.fabs(o.helioX(obs=[0.0, 1.0, 0.0], ro=1.0) - 0.1) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert numpy.fabs(o.helioY(obs=[0.0, 1.0, 0.0], ro=1.0)) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, 3.0 * numpy.pi / 2.0]) assert numpy.fabs(o.helioX(obs=[0.0, -1.0, 0.0], ro=1.0) - 0.1) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert numpy.fabs(o.helioY(obs=[0.0, -1.0, 0.0], ro=1.0)) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) return None def test_orbit_obs_Orbit_issue322(): # Further tests of obs= Orbit parameter for orbit output, mainly in relation # to issue #322 from galpy.orbit import Orbit # The basic case, for a planar orbit o = Orbit([0.9, 0.1, 1.2, 0.0]) assert ( numpy.fabs(o.helioX(obs=Orbit([1.0, 0.0, 0.0, 0.0]), ro=1.0) - 0.1) < 10.0**-7.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" assert numpy.fabs(o.helioY(obs=Orbit([1.0, 0.0, 0.0, 0.0]), ro=1.0)) < 10.0**-7.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, numpy.pi / 2.0]) assert ( numpy.fabs(o.helioX(obs=Orbit([1.0, 0.0, 0.0, numpy.pi / 2.0]), ro=1.0) - 0.1) < 10.0**-7.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" assert ( numpy.fabs(o.helioY(obs=Orbit([1.0, 0.0, 0.0, numpy.pi / 2.0]), ro=1.0)) < 10.0**-7.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, 3.0 * numpy.pi / 2.0]) assert ( numpy.fabs( o.helioX(obs=Orbit([1.0, 0.0, 0.0, 3.0 * numpy.pi / 2.0]), ro=1.0) - 0.1 ) < 10.0**-7.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" assert ( numpy.fabs(o.helioY(obs=Orbit([1.0, 0.0, 0.0, 3.0 * numpy.pi / 2.0]), ro=1.0)) < 10.0**-7.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" # Full orbit # The basic case, for a full orbit o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, 0.0]) assert ( numpy.fabs(o.helioX(obs=Orbit([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]), ro=1.0) - 0.1) < 10.0**-7.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" assert ( numpy.fabs(o.helioY(obs=Orbit([1.0, 0.0, 0.0, 0.0, 0.0, 0.0]), ro=1.0)) < 10.0**-7.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, numpy.pi / 2.0]) assert ( numpy.fabs( o.helioX(obs=Orbit([1.0, 0.0, 0.0, 0.0, 0.0, numpy.pi / 2.0]), ro=1.0) - 0.1 ) < 10.0**-7.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" assert ( numpy.fabs( o.helioY(obs=Orbit([1.0, 0.0, 0.0, 0.0, 0.0, numpy.pi / 2.0]), ro=1.0) ) < 10.0**-7.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, 3.0 * numpy.pi / 2.0]) assert ( numpy.fabs( o.helioX(obs=Orbit([1.0, 0.0, 0.0, 0.0, 0.0, 3.0 * numpy.pi / 2.0]), ro=1.0) - 0.1 ) < 10.0**-7.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" assert ( numpy.fabs( o.helioY(obs=Orbit([1.0, 0.0, 0.0, 0.0, 0.0, 3.0 * numpy.pi / 2.0]), ro=1.0) ) < 10.0**-7.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" return None def test_orbit_obs_Orbits_issue322(): # Further tests of obs= Orbit parameter for orbit output, mainly in relation # to issue #322; specific case where the orbit gets evaluated at multiple # times from galpy.orbit import Orbit # Do non-zero Ysun case for planarOrbit o = Orbit([0.9, 0.1, 1.2, numpy.pi / 2.0], ro=1.0) obs = Orbit([1.0, 0.0, 0.0, numpy.pi / 2.0], ro=1.0) times = numpy.linspace(0.0, 2.0, 2) from galpy.potential import MWPotential2014 o.integrate(times, MWPotential2014) obs.integrate(times, MWPotential2014) for ii in range(len(times)): # Test against individual assert ( numpy.fabs( o.helioX(times, obs=obs, ro=1.0)[ii] - o.helioX( times[ii], obs=[obs.x(times[ii]), obs.y(times[ii]), 0.0], ro=1.0 ) ) < 10.0**-10.0 ), ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert ( numpy.fabs( o.helioY(times, obs=obs, ro=1.0)[ii] - o.helioY( times[ii], obs=[obs.x(times[ii]), obs.y(times[ii]), 0.0], ro=1.0 ) ) < 10.0**-10.0 ), ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Do non-zero Ysun case for planarOrbit, but giving FullOrbit for obs o = Orbit([0.9, 0.1, 1.2, numpy.pi / 2.0], ro=1.0) obs = Orbit([1.0, 0.0, 0.0, 0.0, 0.0, numpy.pi / 2.0], ro=1.0) times = numpy.linspace(0.0, 2.0, 2) from galpy.potential import MWPotential2014 o.integrate(times, MWPotential2014) obs.integrate(times, MWPotential2014) for ii in range(len(times)): # Test against individual assert ( numpy.fabs( o.helioX(times, obs=obs, ro=1.0)[ii] - o.helioX( times[ii], obs=[obs.x(times[ii]), obs.y(times[ii]), obs.z(times[ii])], ro=1.0, ) ) < 10.0**-10.0 ), ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert ( numpy.fabs( o.helioY(times, obs=obs, ro=1.0)[ii] - o.helioY( times[ii], obs=[obs.x(times[ii]), obs.y(times[ii]), obs.z(times[ii])], ro=1.0, ) ) < 10.0**-10.0 ), ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Do non-zero Ysun case for FullOrbit o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, numpy.pi / 2.0], ro=1.0) obs = Orbit([1.0, 0.0, 0.0, 0.0, 0.0, numpy.pi / 2.0], ro=1.0) times = numpy.linspace(0.0, 2.0, 2) from galpy.potential import MWPotential2014 o.integrate(times, MWPotential2014) obs.integrate(times, MWPotential2014) for ii in range(len(times)): # Test against individual assert ( numpy.fabs( o.helioX(times, obs=obs, ro=1.0)[ii] - o.helioX( times[ii], obs=[obs.x(times[ii]), obs.y(times[ii]), obs.z(times[ii])], ro=1.0, ) ) < 10.0**-10.0 ), ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert ( numpy.fabs( o.helioY(times, obs=obs, ro=1.0)[ii] - o.helioY( times[ii], obs=[obs.x(times[ii]), obs.y(times[ii]), obs.z(times[ii])], ro=1.0, ) ) < 10.0**-10.0 ), ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) return None def test_orbit_obsvel_list_issue322(): # Further tests of obs= list parameter for orbit output, incl. velocity # mainly in relation to issue #322 from galpy.orbit import Orbit # The basic case, for a planar orbit o = Orbit([0.9, 0.1, 1.2, 0.0]) assert ( numpy.fabs(o.U(obs=[1.0, 0.0, 0.0, 0.0, 0.7, 0.0], ro=1.0, vo=1.0) + 0.1) < 10.0**-6.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" assert ( numpy.fabs(o.V(obs=[1.0, 0.0, 0.0, 0.0, 0.7, 0.0], ro=1.0, vo=1.0) - 0.5) < 10.0**-6.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, numpy.pi / 2.0]) assert ( numpy.fabs(o.U(obs=[0.0, 1.0, 0.0, 0.6, 0.8, 0.0], ro=1.0, vo=1.0) - 0.7) < 10.0**-5.7 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" assert ( numpy.fabs(o.V(obs=[0.0, 1.0, 0.0, 0.6, 0.8, 0.0], ro=1.0, vo=1.0) - 1.8) < 10.0**-6.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, 3.0 * numpy.pi / 2.0]) assert ( numpy.fabs(o.U(obs=[0.0, 1.0, 0.0, 0.6, 0.8, 0.0], ro=1.0, vo=1.0) - 0.9) < 10.0**-6.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" assert ( numpy.fabs(o.V(obs=[0.0, 1.0, 0.0, 0.6, 0.8, 0.0], ro=1.0, vo=1.0) + 0.6) < 10.0**-6.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" # Full orbit # The basic case, for a full orbit o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, 0.0]) assert ( numpy.fabs(o.U(obs=[1.0, 0.0, 0.0, 0.0, 0.7, 0.0], ro=1.0, vo=1.0) + 0.1) < 10.0**-6.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" assert ( numpy.fabs(o.V(obs=[1.0, 0.0, 0.0, 0.0, 0.7, 0.0], ro=1.0, vo=1.0) - 0.5) < 10.0**-6.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, numpy.pi / 2.0]) assert ( numpy.fabs(o.U(obs=[0.0, 1.0, 0.0, 0.6, 0.8, 0.0], ro=1.0, vo=1.0) - 0.7) < 10.0**-5.5 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" assert ( numpy.fabs(o.V(obs=[0.0, 1.0, 0.0, 0.6, 0.8, 0.0], ro=1.0, vo=1.0) - 1.8) < 10.0**-6.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, 3.0 * numpy.pi / 2.0]) assert ( numpy.fabs(o.U(obs=[0.0, 1.0, 0.0, 0.6, 0.8, 0.0], ro=1.0, vo=1.0) - 0.9) < 10.0**-6.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" assert ( numpy.fabs(o.V(obs=[0.0, 1.0, 0.0, 0.6, 0.8, 0.0], ro=1.0, vo=1.0) + 0.6) < 10.0**-6.0 ), "Relative position wrt the Sun from using obs= keyword does not work as expected" return None def test_orbit_obsvel_Orbit_issue322(): # Further tests of obs= Orbit parameter for orbit output, incl. velocity # mainly in relation to issue #322 from galpy.orbit import Orbit # The basic case, for a planar orbit o = Orbit([0.9, 0.1, 1.2, 0.0]) obs = Orbit([1.0, 0.0, 0.7, 0.0, 0.0, 0.0], ro=1.0, vo=1.0) assert numpy.fabs(o.U(obs=obs, ro=1.0, vo=1.0) + 0.1) < 10.0**-6.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert numpy.fabs(o.V(obs=obs, ro=1.0, vo=1.0) - 0.5) < 10.0**-6.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, numpy.pi / 2.0]) obs = Orbit([1.0, 0.0, 0.7, 0.0, 0.0, numpy.pi / 2.0], ro=1.0, vo=1.0) assert numpy.fabs(o.helioX(obs=obs, ro=1.0) - 0.1) < 10.0**-6.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert numpy.fabs(o.helioY(obs=obs, ro=1.0)) < 10.0**-6.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, 3.0 * numpy.pi / 2.0]) obs = Orbit([1.0, 0.0, 0.7, 0.0, 0.0, 3.0 * numpy.pi / 2.0], ro=1.0, vo=1.0) assert numpy.fabs(o.helioX(obs=obs, ro=1.0) - 0.1) < 10.0**-6.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert numpy.fabs(o.helioY(obs=obs, ro=1.0)) < 10.0**-6.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Full orbit # The basic case, for a full orbit o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, 0.0]) obs = Orbit([1.0, 0.0, 0.7, 0.0, 0.0, 0.0], ro=1.0, vo=1.0) assert numpy.fabs(o.U(obs=obs, ro=1.0, vo=1.0) + 0.1) < 10.0**-6.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert numpy.fabs(o.V(obs=obs, ro=1.0, vo=1.0) - 0.5) < 10.0**-6.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, numpy.pi / 2.0]) obs = Orbit([1.0, 0.0, 0.7, 0.0, 0.0, numpy.pi / 2.0], ro=1.0, vo=1.0) assert numpy.fabs(o.helioX(obs=obs, ro=1.0) - 0.1) < 10.0**-6.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert numpy.fabs(o.helioY(obs=obs, ro=1.0)) < 10.0**-6.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Now use non-zero Ysun o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, 3.0 * numpy.pi / 2.0]) obs = Orbit([1.0, 0.0, 0.7, 0.0, 0.0, 3.0 * numpy.pi / 2.0], ro=1.0, vo=1.0) assert numpy.fabs(o.helioX(obs=obs, ro=1.0) - 0.1) < 10.0**-6.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert numpy.fabs(o.helioY(obs=obs, ro=1.0)) < 10.0**-6.0, ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) return None def test_orbit_obsvel_Orbits_issue322(): # Further tests of obs= Orbit parameter for orbit output, mainly in relation # to issue #322; specific case where the orbit gets evaluated at multiple # times; for velocity from galpy.orbit import Orbit # Do non-zero Ysun case for planarOrbit o = Orbit([0.9, 0.1, 1.2, numpy.pi / 2.0], ro=1.0) obs = Orbit([1.0, 0.5, 1.3, numpy.pi / 2.0], ro=1.0) times = numpy.linspace(0.0, 2.0, 2) from galpy.potential import MWPotential2014 o.integrate(times, MWPotential2014) obs.integrate(times, MWPotential2014) for ii in range(len(times)): # Test against individual assert ( numpy.fabs( o.U(times, obs=obs, ro=1.0)[ii] - o.U( times[ii], obs=[ obs.x(times[ii]), obs.y(times[ii]), 0.0, obs.vx(times[ii]), obs.vy(times[ii]), 0.0, ], ro=1.0, ) ) < 10.0**-10.0 ), ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert ( numpy.fabs( o.V(times, obs=obs, ro=1.0)[ii] - o.V( times[ii], obs=[ obs.x(times[ii]), obs.y(times[ii]), 0.0, obs.vx(times[ii]), obs.vy(times[ii]), 0.0, ], ro=1.0, ) ) < 10.0**-10.0 ), ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Do non-zero Ysun case for planarOrbit, but giving FullOrbit for obs o = Orbit([0.9, 0.1, 1.2, numpy.pi / 2.0], ro=1.0) obs = Orbit([1.0, 0.5, 1.3, 0.0, 0.0, numpy.pi / 2.0], ro=1.0) times = numpy.linspace(0.0, 2.0, 2) from galpy.potential import MWPotential2014 o.integrate(times, MWPotential2014) obs.integrate(times, MWPotential2014) for ii in range(len(times)): # Test against individual assert ( numpy.fabs( o.U(times, obs=obs, ro=1.0)[ii] - o.U( times[ii], obs=[ obs.x(times[ii]), obs.y(times[ii]), obs.z(times[ii]), obs.vx(times[ii]), obs.vy(times[ii]), obs.vz(times[ii]), ], ro=1.0, ) ) < 10.0**-10.0 ), ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert ( numpy.fabs( o.V(times, obs=obs, ro=1.0)[ii] - o.V( times[ii], obs=[ obs.x(times[ii]), obs.y(times[ii]), obs.z(times[ii]), obs.vx(times[ii]), obs.vy(times[ii]), obs.vz(times[ii]), ], ro=1.0, ) ) < 10.0**-10.0 ), ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) # Do non-zero Ysun case for FullOrbit o = Orbit([0.9, 0.1, 1.2, 0.0, 0.0, numpy.pi / 2.0], ro=1.0) obs = Orbit([1.0, 0.5, 1.3, 0.0, 0.0, numpy.pi / 2.0], ro=1.0) times = numpy.linspace(0.0, 2.0, 2) from galpy.potential import MWPotential2014 o.integrate(times, MWPotential2014) obs.integrate(times, MWPotential2014) for ii in range(len(times)): # Test against individual assert ( numpy.fabs( o.U(times, obs=obs, ro=1.0)[ii] - o.U( times[ii], obs=[ obs.x(times[ii]), obs.y(times[ii]), obs.z(times[ii]), obs.vx(times[ii]), obs.vy(times[ii]), obs.vz(times[ii]), ], ro=1.0, ) ) < 10.0**-10.0 ), ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) assert ( numpy.fabs( o.V(times, obs=obs, ro=1.0)[ii] - o.V( times[ii], obs=[ obs.x(times[ii]), obs.y(times[ii]), obs.z(times[ii]), obs.vx(times[ii]), obs.vy(times[ii]), obs.vz(times[ii]), ], ro=1.0, ) ) < 10.0**-10.0 ), ( "Relative position wrt the Sun from using obs= keyword does not work as expected" ) return None def test_orbit_dim_2dPot_3dOrb(): # Test that orbit integration throws an error when using a potential that # is lower dimensional than the orbit (using ~Plevne's example) from galpy.orbit import Orbit from galpy.util import conversion b_p = potential.PowerSphericalPotentialwCutoff( alpha=1.8, rc=1.9 / 8.0, normalize=0.05 ) ell_p = potential.EllipticalDiskPotential() pota = [b_p, ell_p] o = Orbit(vxvv=[20.0, 10.0, 2.0, 3.2, 3.4, -100.0], radec=True, ro=8.0, vo=220.0) ts = numpy.linspace( 0.0, 3.5 / conversion.time_in_Gyr(vo=220.0, ro=8.0), 1000, endpoint=True ) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pota, method="odeint") return None def test_orbit_dim_1dPot_3dOrb(): # Test that orbit integration throws an error when using a potential that # is lower dimensional than the orbit, for a 1D potential from galpy.orbit import Orbit from galpy.util import conversion b_p = potential.PowerSphericalPotentialwCutoff( alpha=1.8, rc=1.9 / 8.0, normalize=0.05 ) pota = potential.RZToverticalPotential(b_p, 1.1) o = Orbit(vxvv=[20.0, 10.0, 2.0, 3.2, 3.4, -100.0], radec=True, ro=8.0, vo=220.0) ts = numpy.linspace( 0.0, 3.5 / conversion.time_in_Gyr(vo=220.0, ro=8.0), 1000, endpoint=True ) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pota, method="odeint") return None def test_orbit_dim_1dPot_2dOrb(): # Test that orbit integration throws an error when using a potential that # is lower dimensional than the orbit, for a 1D potential from galpy.orbit import Orbit b_p = potential.PowerSphericalPotentialwCutoff( alpha=1.8, rc=1.9 / 8.0, normalize=0.05 ) pota = [b_p.toVertical(1.1)] o = Orbit(vxvv=[1.1, 0.1, 1.1, 0.1]) ts = numpy.linspace(0.0, 10.0, 1001) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pota, method="leapfrog") with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pota, method="dop853") return None def test_orbit_dim_3dPot_1dOrb(): # Test that orbit integration throws an error when using a 3D potential # for a 1D orbit from galpy.orbit import Orbit pota = potential.PowerSphericalPotentialwCutoff( alpha=1.8, rc=1.9 / 8.0, normalize=0.05 ) o = Orbit(vxvv=[1.1, 0.1]) ts = numpy.linspace(0.0, 10.0, 1001) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pota, method="leapfrog") with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pota, method="dop853") return None def test_orbit_dim_2dPot_1dOrb(): # Test that orbit integration throws an error when using a 2D potential # for a 1D orbit from galpy.orbit import Orbit pota = potential.PowerSphericalPotentialwCutoff( alpha=1.8, rc=1.9 / 8.0, normalize=0.05 ).toPlanar() o = Orbit(vxvv=[1.1, 0.1]) ts = numpy.linspace(0.0, 10.0, 1001) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pota, method="leapfrog") with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pota, method="dop853") return None # Test whether ro warning is sounded when calling ra etc. def test_orbit_radecetc_roWarning(): from galpy.orbit import Orbit o = Orbit([1.1, 0.1, 1.1, 0.1, 0.1, 0.2]) check_radecetc_roWarning(o, "ra") check_radecetc_roWarning(o, "dec") check_radecetc_roWarning(o, "ll") check_radecetc_roWarning(o, "bb") check_radecetc_roWarning(o, "dist") check_radecetc_roWarning(o, "pmra") check_radecetc_roWarning(o, "pmdec") check_radecetc_roWarning(o, "pmll") check_radecetc_roWarning(o, "pmbb") check_radecetc_roWarning(o, "vra") check_radecetc_roWarning(o, "vdec") check_radecetc_roWarning(o, "vll") check_radecetc_roWarning(o, "vbb") check_radecetc_roWarning(o, "helioX") check_radecetc_roWarning(o, "helioY") check_radecetc_roWarning(o, "helioZ") check_radecetc_roWarning(o, "U") check_radecetc_roWarning(o, "V") check_radecetc_roWarning(o, "W") return None # Test whether vo warning is sounded when calling pmra etc. def test_orbit_radecetc_voWarning(): from galpy.orbit import Orbit o = Orbit([1.1, 0.1, 1.1, 0.1, 0.1, 0.2]) check_radecetc_voWarning(o, "pmra") check_radecetc_voWarning(o, "pmdec") check_radecetc_voWarning(o, "pmll") check_radecetc_voWarning(o, "pmbb") check_radecetc_voWarning(o, "vra") check_radecetc_voWarning(o, "vdec") check_radecetc_voWarning(o, "vll") check_radecetc_voWarning(o, "vbb") check_radecetc_voWarning(o, "U") check_radecetc_voWarning(o, "V") check_radecetc_voWarning(o, "W") return None # Test whether orbit evaluation methods sound warning when called with # unitless time when orbit is integrated with unitfull times def test_orbit_method_integrate_t_asQuantity_warning(): from astropy import units from galpy.orbit import Orbit from galpy.potential import MWPotential2014 # Setup and integrate orbit ts = numpy.linspace(0.0, 10.0, 1001) * units.Gyr o = Orbit([1.1, 0.1, 1.1, 0.1, 0.1, 0.2]) o.integrate(ts, MWPotential2014) # Now check check_integrate_t_asQuantity_warning(o, "R") check_integrate_t_asQuantity_warning(o, "vR") check_integrate_t_asQuantity_warning(o, "vT") check_integrate_t_asQuantity_warning(o, "z") check_integrate_t_asQuantity_warning(o, "vz") check_integrate_t_asQuantity_warning(o, "phi") check_integrate_t_asQuantity_warning(o, "r") check_integrate_t_asQuantity_warning(o, "x") check_integrate_t_asQuantity_warning(o, "y") check_integrate_t_asQuantity_warning(o, "vx") check_integrate_t_asQuantity_warning(o, "vy") check_integrate_t_asQuantity_warning(o, "theta") check_integrate_t_asQuantity_warning(o, "vtheta") check_integrate_t_asQuantity_warning(o, "vr") check_integrate_t_asQuantity_warning(o, "ra") check_integrate_t_asQuantity_warning(o, "dec") check_integrate_t_asQuantity_warning(o, "ll") check_integrate_t_asQuantity_warning(o, "bb") check_integrate_t_asQuantity_warning(o, "dist") check_integrate_t_asQuantity_warning(o, "pmra") check_integrate_t_asQuantity_warning(o, "pmdec") check_integrate_t_asQuantity_warning(o, "pmll") check_integrate_t_asQuantity_warning(o, "pmbb") check_integrate_t_asQuantity_warning(o, "vra") check_integrate_t_asQuantity_warning(o, "vdec") check_integrate_t_asQuantity_warning(o, "vll") check_integrate_t_asQuantity_warning(o, "vbb") check_integrate_t_asQuantity_warning(o, "vlos") check_integrate_t_asQuantity_warning(o, "helioX") check_integrate_t_asQuantity_warning(o, "helioY") check_integrate_t_asQuantity_warning(o, "helioZ") check_integrate_t_asQuantity_warning(o, "U") check_integrate_t_asQuantity_warning(o, "V") check_integrate_t_asQuantity_warning(o, "W") check_integrate_t_asQuantity_warning(o, "E") check_integrate_t_asQuantity_warning(o, "L") check_integrate_t_asQuantity_warning(o, "Jacobi") check_integrate_t_asQuantity_warning(o, "ER") check_integrate_t_asQuantity_warning(o, "Ez") return None # Test whether ro in methods using physical_conversion can be specified # as a Quantity def test_orbit_method_inputro_quantity(): from astropy import units from galpy.orbit import Orbit from galpy.potential import MWPotential2014 o = Orbit([1.1, 0.1, 1.1, 0.2, 0.3, 0.3]) ro = 11.0 assert ( numpy.fabs( o.E(pot=MWPotential2014, ro=ro * units.kpc) - o.E(pot=MWPotential2014, ro=ro) ) < 10.0**-8.0 ), "Orbit method E does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.ER(pot=MWPotential2014, ro=ro * units.kpc) - o.ER(pot=MWPotential2014, ro=ro) ) < 10.0**-8.0 ), "Orbit method ER does not return the correct value as Quantity" assert ( numpy.fabs( o.Ez(pot=MWPotential2014, ro=ro * units.kpc) - o.Ez(pot=MWPotential2014, ro=ro) ) < 10.0**-8.0 ), "Orbit method Ez does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.Jacobi(pot=MWPotential2014, ro=ro * units.kpc) - o.Jacobi(pot=MWPotential2014, ro=ro) ) < 10.0**-8.0 ), "Orbit method Jacobi does not return the correct value when input ro is Quantity" assert numpy.all( numpy.fabs( o.L(pot=MWPotential2014, ro=ro * units.kpc) - o.L(pot=MWPotential2014, ro=ro) ) < 10.0**-8.0 ), "Orbit method L does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.rap(pot=MWPotential2014, analytic=True, ro=ro * units.kpc) - o.rap(pot=MWPotential2014, analytic=True, ro=ro) ) < 10.0**-8.0 ), "Orbit method rap does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.rperi(pot=MWPotential2014, analytic=True, ro=ro * units.kpc) - o.rperi(pot=MWPotential2014, analytic=True, ro=ro) ) < 10.0**-8.0 ), "Orbit method rperi does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.zmax(pot=MWPotential2014, analytic=True, ro=ro * units.kpc) - o.zmax(pot=MWPotential2014, analytic=True, ro=ro) ) < 10.0**-8.0 ), "Orbit method zmax does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.jr(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro * units.kpc) - o.jr(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro) ) < 10.0**-8.0 ), "Orbit method jr does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.jp(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro * units.kpc) - o.jp(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro) ) < 10.0**-8.0 ), "Orbit method jp does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.jz(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro * units.kpc) - o.jz(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro) ) < 10.0**-8.0 ), "Orbit method jz does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.wr(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro * units.kpc) - o.wr(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro) ) < 10.0**-8.0 ), "Orbit method wr does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.wp(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro * units.kpc) - o.wp(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro) ) < 10.0**-8.0 ), "Orbit method wp does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.wz(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro * units.kpc) - o.wz(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro) ) < 10.0**-8.0 ), "Orbit method wz does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.Tr(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro * units.kpc) - o.Tr(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro) ) < 10.0**-8.0 ), "Orbit method Tr does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.Tp(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro * units.kpc) - o.Tp(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro) ) < 10.0**-8.0 ), "Orbit method Tp does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.Tz(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro * units.kpc) - o.Tz(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro) ) < 10.0**-8.0 ), "Orbit method Tz does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.Or(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro * units.kpc) - o.Or(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro) ) < 10.0**-8.0 ), "Orbit method Or does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.Op(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro * units.kpc) - o.Op(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro) ) < 10.0**-8.0 ), "Opbit method Or does not return the correct value when input ro is Quantity" assert ( numpy.fabs( o.Oz(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro * units.kpc) - o.Oz(pot=MWPotential2014, type="staeckel", delta=0.5, ro=ro) ) < 10.0**-8.0 ), "Ozbit method Or does not return the correct value when input ro is Quantity" assert numpy.fabs(o.time(ro=ro * units.kpc) - o.time(ro=ro)) < 10.0**-8.0, ( "Orbit method time does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.R(ro=ro * units.kpc) - o.R(ro=ro)) < 10.0**-8.0, ( "Orbit method R does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vR(ro=ro * units.kpc) - o.vR(ro=ro)) < 10.0**-8.0, ( "Orbit method vR does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vT(ro=ro * units.kpc) - o.vT(ro=ro)) < 10.0**-8.0, ( "Orbit method vT does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.z(ro=ro * units.kpc) - o.z(ro=ro)) < 10.0**-8.0, ( "Orbit method z does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vz(ro=ro * units.kpc) - o.vz(ro=ro)) < 10.0**-8.0, ( "Orbit method vz does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.phi(ro=ro * units.kpc) - o.phi(ro=ro)) < 10.0**-8.0, ( "Orbit method phi does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vphi(ro=ro * units.kpc) - o.vphi(ro=ro)) < 10.0**-8.0, ( "Orbit method vphi does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.x(ro=ro * units.kpc) - o.x(ro=ro)) < 10.0**-8.0, ( "Orbit method x does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.y(ro=ro * units.kpc) - o.y(ro=ro)) < 10.0**-8.0, ( "Orbit method y does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vx(ro=ro * units.kpc) - o.vx(ro=ro)) < 10.0**-8.0, ( "Orbit method vx does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vy(ro=ro * units.kpc) - o.vy(ro=ro)) < 10.0**-8.0, ( "Orbit method vy does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.theta(ro=ro * units.kpc) - o.theta(ro=ro)) < 10.0**-8.0, ( "Orbit method theta does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vtheta(ro=ro * units.kpc) - o.vtheta(ro=ro)) < 10.0**-8.0, ( "Orbit method vtheta does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vr(ro=ro * units.kpc) - o.vr(ro=ro)) < 10.0**-8.0, ( "Orbit method vr does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.ra(ro=ro * units.kpc) - o.ra(ro=ro)) < 10.0**-8.0, ( "Orbit method ra does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.dec(ro=ro * units.kpc) - o.dec(ro=ro)) < 10.0**-8.0, ( "Orbit method dec does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.ll(ro=ro * units.kpc) - o.ll(ro=ro)) < 10.0**-8.0, ( "Orbit method ll does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.bb(ro=ro * units.kpc) - o.bb(ro=ro)) < 10.0**-8.0, ( "Orbit method bb does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.dist(ro=ro * units.kpc) - o.dist(ro=ro)) < 10.0**-8.0, ( "Orbit method dist does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.pmra(ro=ro * units.kpc) - o.pmra(ro=ro)) < 10.0**-8.0, ( "Orbit method pmra does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.pmdec(ro=ro * units.kpc) - o.pmdec(ro=ro)) < 10.0**-8.0, ( "Orbit method pmdec does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.pmll(ro=ro * units.kpc) - o.pmll(ro=ro)) < 10.0**-8.0, ( "Orbit method pmll does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.pmbb(ro=ro * units.kpc) - o.pmbb(ro=ro)) < 10.0**-8.0, ( "Orbit method pmbb does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vlos(ro=ro * units.kpc) - o.vlos(ro=ro)) < 10.0**-8.0, ( "Orbit method vlos does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vra(ro=ro * units.kpc) - o.vra(ro=ro)) < 10.0**-8.0, ( "Orbit method vra does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vdec(ro=ro * units.kpc) - o.vdec(ro=ro)) < 10.0**-8.0, ( "Orbit method vdec does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vll(ro=ro * units.kpc) - o.vll(ro=ro)) < 10.0**-8.0, ( "Orbit method vll does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vbb(ro=ro * units.kpc) - o.vbb(ro=ro)) < 10.0**-8.0, ( "Orbit method vbb does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.helioX(ro=ro * units.kpc) - o.helioX(ro=ro)) < 10.0**-8.0, ( "Orbit method helioX does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.helioY(ro=ro * units.kpc) - o.helioY(ro=ro)) < 10.0**-8.0, ( "Orbit method helioY does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.helioZ(ro=ro * units.kpc) - o.helioZ(ro=ro)) < 10.0**-8.0, ( "Orbit method helioZ does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.U(ro=ro * units.kpc) - o.U(ro=ro)) < 10.0**-8.0, ( "Orbit method U does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.V(ro=ro * units.kpc) - o.V(ro=ro)) < 10.0**-8.0, ( "Orbit method V does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.W(ro=ro * units.kpc) - o.W(ro=ro)) < 10.0**-8.0, ( "Orbit method W does not return the correct value when input ro is Quantity" ) return None # Test whether vo in methods using physical_conversion can be specified # as a Quantity def test_orbit_method_inputvo_quantity(): from astropy import units from galpy.orbit import Orbit from galpy.potential import MWPotential2014 o = Orbit([1.1, 0.1, 1.1, 0.2, 0.3, 0.3]) vo = 222.0 assert ( numpy.fabs( o.E(pot=MWPotential2014, vo=vo * units.km / units.s) - o.E(pot=MWPotential2014, vo=vo) ) < 10.0**-8.0 ), "Orbit method E does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.ER(pot=MWPotential2014, vo=vo * units.km / units.s) - o.ER(pot=MWPotential2014, vo=vo) ) < 10.0**-8.0 ), "Orbit method ER does not return the correct value as Quantity" assert ( numpy.fabs( o.Ez(pot=MWPotential2014, vo=vo * units.km / units.s) - o.Ez(pot=MWPotential2014, vo=vo) ) < 10.0**-8.0 ), "Orbit method Ez does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.Jacobi(pot=MWPotential2014, vo=vo * units.km / units.s) - o.Jacobi(pot=MWPotential2014, vo=vo) ) < 10.0**-8.0 ), "Orbit method Jacobi does not return the correct value when input vo is Quantity" assert numpy.all( numpy.fabs( o.L(pot=MWPotential2014, vo=vo * units.km / units.s) - o.L(pot=MWPotential2014, vo=vo) ) < 10.0**-8.0 ), "Orbit method L does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.rap(pot=MWPotential2014, analytic=True, vo=vo * units.km / units.s) - o.rap(pot=MWPotential2014, analytic=True, vo=vo) ) < 10.0**-8.0 ), "Orbit method rap does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.rperi(pot=MWPotential2014, analytic=True, vo=vo * units.km / units.s) - o.rperi(pot=MWPotential2014, analytic=True, vo=vo) ) < 10.0**-8.0 ), "Orbit method rperi does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.zmax(pot=MWPotential2014, analytic=True, vo=vo * units.km / units.s) - o.zmax(pot=MWPotential2014, analytic=True, vo=vo) ) < 10.0**-8.0 ), "Orbit method zmax does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.jr( pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo * units.km / units.s, ) - o.jr(pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo) ) < 10.0**-8.0 ), "Orbit method jr does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.jp( pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo * units.km / units.s, ) - o.jp(pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo) ) < 10.0**-8.0 ), "Orbit method jp does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.jz( pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo * units.km / units.s, ) - o.jz(pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo) ) < 10.0**-8.0 ), "Orbit method jz does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.wr( pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo * units.km / units.s, ) - o.wr(pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo) ) < 10.0**-8.0 ), "Orbit method wr does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.wp( pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo * units.km / units.s, ) - o.wp(pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo) ) < 10.0**-8.0 ), "Orbit method wp does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.wz( pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo * units.km / units.s, ) - o.wz(pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo) ) < 10.0**-8.0 ), "Orbit method wz does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.Tr( pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo * units.km / units.s, ) - o.Tr(pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo) ) < 10.0**-8.0 ), "Orbit method Tr does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.Tp( pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo * units.km / units.s, ) - o.Tp(pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo) ) < 10.0**-8.0 ), "Orbit method Tp does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.Tz( pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo * units.km / units.s, ) - o.Tz(pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo) ) < 10.0**-8.0 ), "Orbit method Tz does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.Or( pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo * units.km / units.s, ) - o.Or(pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo) ) < 10.0**-8.0 ), "Orbit method Or does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.Op( pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo * units.km / units.s, ) - o.Op(pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo) ) < 10.0**-8.0 ), "Opbit method Or does not return the correct value when input vo is Quantity" assert ( numpy.fabs( o.Oz( pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo * units.km / units.s, ) - o.Oz(pot=MWPotential2014, type="staeckel", delta=0.5, vo=vo) ) < 10.0**-8.0 ), "Ozbit method Or does not return the correct value when input vo is Quantity" assert ( numpy.fabs(o.time(vo=vo * units.km / units.s) - o.time(vo=vo)) < 10.0**-8.0 ), "Orbit method time does not return the correct value when input vo is Quantity" assert numpy.fabs(o.R(vo=vo * units.km / units.s) - o.R(vo=vo)) < 10.0**-8.0, ( "Orbit method R does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.vR(vo=vo * units.km / units.s) - o.vR(vo=vo)) < 10.0**-8.0, ( "Orbit method vR does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.vT(vo=vo * units.km / units.s) - o.vT(vo=vo)) < 10.0**-8.0, ( "Orbit method vT does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.z(vo=vo * units.km / units.s) - o.z(vo=vo)) < 10.0**-8.0, ( "Orbit method z does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.vz(vo=vo * units.km / units.s) - o.vz(vo=vo)) < 10.0**-8.0, ( "Orbit method vz does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.phi(vo=vo * units.km / units.s) - o.phi(vo=vo)) < 10.0**-8.0, ( "Orbit method phi does not return the correct value when input vo is Quantity" ) assert ( numpy.fabs(o.vphi(vo=vo * units.km / units.s) - o.vphi(vo=vo)) < 10.0**-8.0 ), "Orbit method vphi does not return the correct value when input vo is Quantity" assert numpy.fabs(o.x(vo=vo * units.km / units.s) - o.x(vo=vo)) < 10.0**-8.0, ( "Orbit method x does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.y(vo=vo * units.km / units.s) - o.y(vo=vo)) < 10.0**-8.0, ( "Orbit method y does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.vx(vo=vo * units.km / units.s) - o.vx(vo=vo)) < 10.0**-8.0, ( "Orbit method vx does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.vy(vo=vo * units.km / units.s) - o.vy(vo=vo)) < 10.0**-8.0, ( "Orbit method vy does not return the correct value when input vo is Quantity" ) assert ( numpy.fabs(o.theta(vo=vo * units.km / units.s) - o.theta(vo=vo)) < 10.0**-8.0 ), "Orbit method theta does not return the correct value when input vo is Quantity" assert ( numpy.fabs(o.vtheta(vo=vo * units.km / units.s) - o.vtheta(vo=vo)) < 10.0**-8.0 ), "Orbit method vtheta does not return the correct value when input vo is Quantity" assert numpy.fabs(o.vr(vo=vo * units.km / units.s) - o.vr(vo=vo)) < 10.0**-8.0, ( "Orbit method vr does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.ra(vo=vo * units.km / units.s) - o.ra(vo=vo)) < 10.0**-8.0, ( "Orbit method ra does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.dec(vo=vo * units.km / units.s) - o.dec(vo=vo)) < 10.0**-8.0, ( "Orbit method dec does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.ll(vo=vo * units.km / units.s) - o.ll(vo=vo)) < 10.0**-8.0, ( "Orbit method ll does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.bb(vo=vo * units.km / units.s) - o.bb(vo=vo)) < 10.0**-8.0, ( "Orbit method bb does not return the correct value when input vo is Quantity" ) assert ( numpy.fabs(o.dist(vo=vo * units.km / units.s) - o.dist(vo=vo)) < 10.0**-8.0 ), "Orbit method dist does not return the correct value when input vo is Quantity" assert ( numpy.fabs(o.pmra(vo=vo * units.km / units.s) - o.pmra(vo=vo)) < 10.0**-8.0 ), "Orbit method pmra does not return the correct value when input vo is Quantity" assert ( numpy.fabs(o.pmdec(vo=vo * units.km / units.s) - o.pmdec(vo=vo)) < 10.0**-8.0 ), "Orbit method pmdec does not return the correct value when input vo is Quantity" assert ( numpy.fabs(o.pmll(vo=vo * units.km / units.s) - o.pmll(vo=vo)) < 10.0**-8.0 ), "Orbit method pmll does not return the correct value when input vo is Quantity" assert ( numpy.fabs(o.pmbb(vo=vo * units.km / units.s) - o.pmbb(vo=vo)) < 10.0**-8.0 ), "Orbit method pmbb does not return the correct value when input vo is Quantity" assert ( numpy.fabs(o.vlos(vo=vo * units.km / units.s) - o.vlos(vo=vo)) < 10.0**-8.0 ), "Orbit method vlos does not return the correct value when input vo is Quantity" assert numpy.fabs(o.vra(vo=vo * units.km / units.s) - o.vra(vo=vo)) < 10.0**-8.0, ( "Orbit method vra does not return the correct value when input vo is Quantity" ) assert ( numpy.fabs(o.vdec(vo=vo * units.km / units.s) - o.vdec(vo=vo)) < 10.0**-8.0 ), "Orbit method vdec does not return the correct value when input vo is Quantity" assert numpy.fabs(o.vll(vo=vo * units.km / units.s) - o.vll(vo=vo)) < 10.0**-8.0, ( "Orbit method vll does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.vbb(vo=vo * units.km / units.s) - o.vbb(vo=vo)) < 10.0**-8.0, ( "Orbit method vbb does not return the correct value when input vo is Quantity" ) assert ( numpy.fabs(o.helioX(vo=vo * units.km / units.s) - o.helioX(vo=vo)) < 10.0**-8.0 ), "Orbit method helioX does not return the correct value when input vo is Quantity" assert ( numpy.fabs(o.helioY(vo=vo * units.km / units.s) - o.helioY(vo=vo)) < 10.0**-8.0 ), "Orbit method helioY does not return the correct value when input vo is Quantity" assert ( numpy.fabs(o.helioZ(vo=vo * units.km / units.s) - o.helioZ(vo=vo)) < 10.0**-8.0 ), "Orbit method helioZ does not return the correct value when input vo is Quantity" assert numpy.fabs(o.U(vo=vo * units.km / units.s) - o.U(vo=vo)) < 10.0**-8.0, ( "Orbit method U does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.V(vo=vo * units.km / units.s) - o.V(vo=vo)) < 10.0**-8.0, ( "Orbit method V does not return the correct value when input vo is Quantity" ) assert numpy.fabs(o.W(vo=vo * units.km / units.s) - o.W(vo=vo)) < 10.0**-8.0, ( "Orbit method W does not return the correct value when input vo is Quantity" ) return None # Test whether obs in methods using physical_conversion can be specified # as a Quantity def test_orbit_method_inputobs_quantity(): from astropy import units from galpy.orbit import Orbit o = Orbit([1.1, 0.1, 1.1, 0.2, 0.3, 0.3]) obs = [11.0, 0.1, 0.2, -10.0, 245.0, 7.0] obs_units = [ 11.0 * units.kpc, 0.1 * units.kpc, 0.2 * units.kpc, -10.0 * units.km / units.s, 245.0 * units.km / units.s, 7.0 * units.km / units.s, ] assert numpy.fabs(o.ra(obs=obs_units) - o.ra(obs=obs)) < 10.0**-8.0, ( "Orbit method ra does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.dec(obs=obs_units) - o.dec(obs=obs)) < 10.0**-8.0, ( "Orbit method dec does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.ll(obs=obs_units) - o.ll(obs=obs)) < 10.0**-8.0, ( "Orbit method ll does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.bb(obs=obs_units) - o.bb(obs=obs)) < 10.0**-8.0, ( "Orbit method bb does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.dist(obs=obs_units) - o.dist(obs=obs)) < 10.0**-8.0, ( "Orbit method dist does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.pmra(obs=obs_units) - o.pmra(obs=obs)) < 10.0**-8.0, ( "Orbit method pmra does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.pmdec(obs=obs_units) - o.pmdec(obs=obs)) < 10.0**-8.0, ( "Orbit method pmdec does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.pmll(obs=obs_units) - o.pmll(obs=obs)) < 10.0**-8.0, ( "Orbit method pmll does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.pmbb(obs=obs_units) - o.pmbb(obs=obs)) < 10.0**-8.0, ( "Orbit method pmbb does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vlos(obs=obs_units) - o.vlos(obs=obs)) < 10.0**-8.0, ( "Orbit method vlos does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vra(obs=obs_units) - o.vra(obs=obs)) < 10.0**-8.0, ( "Orbit method vra does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vdec(obs=obs_units) - o.vdec(obs=obs)) < 10.0**-8.0, ( "Orbit method vdec does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vll(obs=obs_units) - o.vll(obs=obs)) < 10.0**-8.0, ( "Orbit method vll does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.vbb(obs=obs_units) - o.vbb(obs=obs)) < 10.0**-8.0, ( "Orbit method vbb does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.helioX(obs=obs_units) - o.helioX(obs=obs)) < 10.0**-8.0, ( "Orbit method helioX does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.helioY(obs=obs_units) - o.helioY(obs=obs)) < 10.0**-8.0, ( "Orbit method helioY does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.helioZ(obs=obs_units) - o.helioZ(obs=obs)) < 10.0**-8.0, ( "Orbit method helioZ does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.U(obs=obs_units) - o.U(obs=obs)) < 10.0**-8.0, ( "Orbit method U does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.V(obs=obs_units) - o.V(obs=obs)) < 10.0**-8.0, ( "Orbit method V does not return the correct value when input ro is Quantity" ) assert numpy.fabs(o.W(obs=obs_units) - o.W(obs=obs)) < 10.0**-8.0, ( "Orbit method W does not return the correct value when input ro is Quantity" ) return None # Test that orbit integration in C gets interrupted by SIGINT (CTRL-C) def test_orbit_c_sigint_full(): integrators = [ "dopr54_c", "leapfrog_c", "dop853_c", "rk4_c", "rk6_c", "symplec4_c", "symplec6_c", "ias15_c", ] scriptpath = "orbitint4sigint.py" if not "tests" in os.getcwd(): scriptpath = os.path.join("tests", scriptpath) ntries = 10 for integrator in integrators: p = subprocess.Popen( ["python", scriptpath, integrator, "full"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) for line in iter(p.stdout.readline, b""): if line.startswith(b"Starting long C integration ..."): break time.sleep(2) os.kill(p.pid, signal.SIGINT) time.sleep(1) cnt = 0 while p.poll() is None and cnt < ntries: # wait a little longer time.sleep(4) cnt += 1 if p.poll() == 2 and WIN32: break if p.poll() is None or (p.poll() != 1 and p.poll() != -2): if p.poll() is None: msg = -100 else: msg = p.poll() raise AssertionError( "Full orbit integration using %s should have been interrupted by SIGINT (CTRL-C), but was not because p.poll() == %i" % (integrator, msg) ) p.stdin.close() p.stdout.close() p.stderr.close() return None # Test that orbit integration in C gets interrupted by SIGINT (CTRL-C) def test_orbit_c_sigint_planar(): integrators = [ "dopr54_c", "leapfrog_c", "dop853_c", "rk4_c", "rk6_c", "symplec4_c", "symplec6_c", "ias15_c", ] scriptpath = "orbitint4sigint.py" if not "tests" in os.getcwd(): scriptpath = os.path.join("tests", scriptpath) ntries = 10 for integrator in integrators: p = subprocess.Popen( ["python", scriptpath, integrator, "planar"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) for line in iter(p.stdout.readline, b""): if line.startswith(b"Starting long C integration ..."): break time.sleep(2) os.kill(p.pid, signal.SIGINT) time.sleep(1) cnt = 0 while p.poll() is None and cnt < ntries: # wait a little longer time.sleep(4) cnt += 1 if p.poll() == 2 and WIN32: break if p.poll() is None or (p.poll() != 1 and p.poll() != -2): if p.poll() is None: msg = -100 else: msg = p.poll() raise AssertionError( "Full orbit integration using %s should have been interrupted by SIGINT (CTRL-C), but was not because p.poll() == %i" % (integrator, msg) ) p.stdin.close() p.stdout.close() p.stderr.close() return None # Test that orbit integration in C gets interrupted by SIGINT (CTRL-C) def test_orbit_c_sigint_planardxdv(): integrators = ["dopr54_c", "rk4_c", "rk6_c", "dop853_c"] scriptpath = "orbitint4sigint.py" if not "tests" in os.getcwd(): scriptpath = os.path.join("tests", scriptpath) ntries = 10 for integrator in integrators: p = subprocess.Popen( ["python", scriptpath, integrator, "planardxdv"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) for line in iter(p.stdout.readline, b""): if line.startswith(b"Starting long C integration ..."): break time.sleep(2) os.kill(p.pid, signal.SIGINT) time.sleep(1) cnt = 0 while p.poll() is None and cnt < ntries: # wait a little longer time.sleep(4) cnt += 1 if p.poll() == 2 and WIN32: break if p.poll() is None or (p.poll() != 1 and p.poll() != -2): if p.poll() is None: msg = -100 else: msg = p.poll() raise AssertionError( "Full orbit integration using %s should have been interrupted by SIGINT (CTRL-C), but was not because p.poll() == %i" % (integrator, msg) ) p.stdin.close() p.stdout.close() p.stderr.close() return None def test_orbitint_pythonfallback(): # Check if a warning is raised when the potential has no C integrator from galpy.orbit import Orbit bp = ( BurkertPotentialNoC() ) # BurkertPotentialNoC is already imported at the top of test_orbit.py bp.normalize(1.0) ts = numpy.linspace(0.0, 1.0, 101) for orb in [ Orbit([1.0, 0.1, 1.1, 0.1, 0.0, 1.0]), Orbit([1.0, 0.1, 1.1, 0.1, 0.0]), Orbit([1.0, 0.1, 1.1, 1.0]), Orbit([1.0, 0.1, 1.1]), ]: with pytest.warns(galpyWarning) as record: if PY2: reset_warning_registry("galpy") warnings.simplefilter("always", galpyWarning) # Test w/ dopr54_c orb.integrate(ts, bp, method="dopr54_c") raisedWarning = False for rec in record: # check that the message matches print(rec.message.args[0]) raisedWarning += ( str(rec.message.args[0]) == "Cannot use C integration because some of the potentials are not implemented in C (using odeint instead)" ) assert raisedWarning, "Orbit integration did not raise fallback warning" return None def test_orbitint_dissipativefallback(): # Check if a warning is raised when one tries to integrate an orbit # in a dissipative force law with a symplectic integrator from galpy.orbit import Orbit lp = potential.LogarithmicHaloPotential(normalize=1.0, q=1.0) cdf = potential.ChandrasekharDynamicalFrictionForce( GMs=0.01, dens=lp, sigmar=lambda r: 1.0 / numpy.sqrt(2.0) ) ts = numpy.linspace(0.0, 1.0, 101) for orb in [Orbit([1.0, 0.1, 1.1, 0.1, 0.0, 1.0])]: with pytest.warns(galpyWarning) as record: orb.integrate(ts, [lp, cdf], method="leapfrog") raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "Cannot use symplectic integration because some of the included forces are dissipative (using non-symplectic integrator odeint instead)" ) assert raisedWarning, ( "Orbit integration with symplectic integrator for dissipative force did not raise fallback warning" ) return None # Test that the functions that supposedly *always* return output in physical # units actually do so; see issue #294 def test_intrinsic_physical_output(): from galpy.orbit import Orbit from galpy.util import coords o = Orbit( [0.9, 0.0, 1.0, 0.0, 0.2, 0.0], ro=8.0, vo=220.0, zo=0.0, solarmotion=[-20.0, 30.0, 40.0], ) # 04/2018: not quite anylonger w/ astropy def. of plane, but close l_true = 0.0 b_true = 0.0 ra_true, dec_true = coords.lb_to_radec(l_true, b_true, degree=True, epoch=None) assert numpy.fabs(o.ra() - ra_true) < 10.0**-3.8, ( "Orbit.ra does not return correct ra in degree" ) assert numpy.fabs(o.dec() - dec_true) < 10.0**-3.8, ( "Orbit.dec does not return correct dec in degree" ) assert numpy.fabs(o.ll() - l_true) < 10.0**-4.0, ( "Orbit.ll does not return correct l in degree" ) assert numpy.fabs(o.bb() - b_true) < 10.0**-4.0, ( "Orbit.bb does not return correct b in degree" ) assert numpy.fabs(o.dist() - 0.8) < 10.0**-8.0, ( "Orbit.dist does not return correct dist in kpc" ) pmll_true = -30.0 / 0.8 / _K pmbb_true = 4.0 / 0.8 / _K pmra_true, pmdec_true = coords.pmllpmbb_to_pmrapmdec( pmll_true, pmbb_true, l_true, b_true, degree=True, epoch=None ) assert numpy.fabs(o.pmra() - pmra_true) < 10.0**-5.0, ( "Orbit.pmra does not return correct pmra in mas/yr" ) assert numpy.fabs(o.pmdec() - pmdec_true) < 10.0**-5.0, ( "Orbit.pmdec does not return correct pmdec in mas/yr" ) assert numpy.fabs(o.pmll() - pmll_true) < 10.0**-5.0, ( "Orbit.pmll does not return correct pmll in mas/yr" ) assert numpy.fabs(o.pmbb() - pmbb_true) < 10.0**-4.7, ( "Orbit.pmbb does not return correct pmbb in mas/yr" ) assert numpy.fabs(o.vra() - pmra_true * 0.8 * _K) < 10.0**-4.8, ( "Orbit.vra does not return correct vra in km/s" ) assert numpy.fabs(o.vdec() - pmdec_true * 0.8 * _K) < 10.0**-4.6, ( "Orbit.vdec does not return correct vdec in km/s" ) assert numpy.fabs(o.vll() - pmll_true * 0.8 * _K) < 10.0**-5.0, ( "Orbit.vll does not return correct vll in km/s" ) assert numpy.fabs(o.vbb() - pmbb_true * 0.8 * _K) < 10.0**-4.0, ( "Orbit.vbb does not return correct vbb in km/s" ) assert numpy.fabs(o.vlos() + 20.0) < 10.0**-8.0, ( "Orbit.vlos does not return correct vlos in km/s" ) assert numpy.fabs(o.U() + 20.0) < 10.0**-4.0, ( "Orbit.U does not return correct U in km/s" ) assert numpy.fabs(o.V() - pmll_true * 0.8 * _K) < 10.0**-4.8, ( "Orbit.V does not return correct V in km/s" ) assert numpy.fabs(o.W() - pmbb_true * 0.8 * _K) < 10.0**-4.0, ( "Orbit.W does not return correct W in km/s" ) assert numpy.fabs(o.helioX() - 0.8) < 10.0**-8.0, ( "Orbit.helioX does not return correct helioX in kpc" ) # For non-trivial helioY and helioZ tests o = Orbit( [1.0 / numpy.sqrt(2.0), 0.0, 1.0, 0.0, 0.2, numpy.pi / 4.0], ro=8.0, vo=220.0, zo=0.0, solarmotion=[-20.0, 30.0, 40.0], ) assert numpy.fabs(o.helioY() - 4.0) < 10.0**-5.0, ( "Orbit.helioY does not return correct helioY in kpc" ) o = Orbit( [0.9, 0.0, 1.0, 0.3, 0.2, numpy.pi / 4.0], ro=8.0, vo=220.0, zo=0.0, solarmotion=[-20.0, 30.0, 40.0], ) assert numpy.fabs(o.helioZ() - 0.3 * 8.0) < 10.0**-4.8, ( "Orbit.helioZ does not return correct helioZ in kpc" ) return None def test_doublewrapper_2d(): # Test that a doubly-wrapped potential gets passed to C correctly, # by comparing orbit integrated in C to that in python from galpy.orbit import Orbit from galpy.potential import ( DehnenBarPotential, DehnenSmoothWrapperPotential, LogarithmicHaloPotential, SolidBodyRotationWrapperPotential, ) # potential= flat vc + doubly-wrapped bar pot = [ LogarithmicHaloPotential(normalize=1.0), SolidBodyRotationWrapperPotential( pot=DehnenSmoothWrapperPotential( pot=DehnenBarPotential(omegab=1.0, rb=5.0 / 8.0, Af=1.0 / 100.0), tform=5.0, tsteady=15.0, ), omega=1.3, ), ] # Integrate orbit in C and python o = Orbit([1.0, 0.1, 1.1, 0.1]) oc = o() ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, pot, method="leapfrog") oc.integrate(ts, pot, method="leapfrog_c") # Check that they end up in the same point o = o(ts[-1]) oc = oc(ts[-1]) assert numpy.fabs(o.x() - oc.x()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.y() - oc.y()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vx() - oc.vx()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vy() - oc.vy()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) return None def test_doublewrapper_3d(): # Test that a doubly-wrapped potential gets passed to C correctly, # by comparing orbit integrated in C to that in python from galpy.orbit import Orbit from galpy.potential import ( DehnenBarPotential, DehnenSmoothWrapperPotential, LogarithmicHaloPotential, SolidBodyRotationWrapperPotential, ) # potential= flat vc + doubly-wrapped bar pot = [ LogarithmicHaloPotential(normalize=1.0), SolidBodyRotationWrapperPotential( pot=DehnenSmoothWrapperPotential( pot=DehnenBarPotential(omegab=1.0, rb=5.0 / 8.0, Af=1.0 / 100.0), tform=5.0, tsteady=15.0, ), omega=1.3, ), ] # Integrate orbit in C and python o = Orbit([1.0, 0.1, 1.1, 0.1, -0.03, numpy.pi]) oc = o() ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, pot, method="leapfrog") oc.integrate(ts, pot, method="leapfrog_c") # Check that they end up in the same point o = o(ts[-1]) oc = oc(ts[-1]) assert numpy.fabs(o.x() - oc.x()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.y() - oc.y()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.z() - oc.z()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vx() - oc.vx()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vy() - oc.vy()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vz() - oc.vz()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) return None def test_wrapper_followedbyanotherpotential_2d(): # Test that a wrapped potential that gets followed by another potential # gets passed to C correctly, # by comparing orbit integrated in C to that in python from galpy.orbit import Orbit from galpy.potential import ( DehnenBarPotential, DehnenSmoothWrapperPotential, LogarithmicHaloPotential, SolidBodyRotationWrapperPotential, SpiralArmsPotential, ) # potential= flat vc + doubly-wrapped bar pot = [ LogarithmicHaloPotential(normalize=1.0), SolidBodyRotationWrapperPotential( pot=DehnenSmoothWrapperPotential( pot=DehnenBarPotential(omegab=1.0, rb=5.0 / 8.0, Af=1.0 / 100.0), tform=5.0, tsteady=15.0, ), omega=1.3, ), SpiralArmsPotential(N=4, omega=0.79, amp=0.9), ] # Integrate orbit in C and python o = Orbit([1.0, 0.1, 1.1, 0.1]) oc = o() ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, pot, method="leapfrog") oc.integrate(ts, pot, method="leapfrog_c") # Check that they end up in the same point o = o(ts[-1]) oc = oc(ts[-1]) assert numpy.fabs(o.x() - oc.x()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.y() - oc.y()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vx() - oc.vx()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vy() - oc.vy()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) return None def test_wrapper_followedbyanotherpotential_3d(): # Test that a wrapped potential that gets followed by another potential # gets passed to C correctly, # by comparing orbit integrated in C to that in python from galpy.orbit import Orbit from galpy.potential import ( DehnenBarPotential, DehnenSmoothWrapperPotential, LogarithmicHaloPotential, SolidBodyRotationWrapperPotential, SpiralArmsPotential, ) # potential= flat vc + doubly-wrapped bar pot = [ LogarithmicHaloPotential(normalize=1.0), SolidBodyRotationWrapperPotential( pot=DehnenSmoothWrapperPotential( pot=DehnenBarPotential(omegab=1.0, rb=5.0 / 8.0, Af=1.0 / 100.0), tform=5.0, tsteady=15.0, ), omega=1.3, ), SpiralArmsPotential(N=4, omega=0.79, amp=0.9), ] # Integrate orbit in C and python o = Orbit([1.0, 0.1, 1.1, 0.1, -0.03, numpy.pi]) oc = o() ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, pot, method="leapfrog") oc.integrate(ts, pot, method="leapfrog_c") # Check that they end up in the same point o = o(ts[-1]) oc = oc(ts[-1]) assert numpy.fabs(o.x() - oc.x()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.y() - oc.y()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.z() - oc.z()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vx() - oc.vx()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vy() - oc.vy()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vz() - oc.vz()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) return None def test_wrapper_complicatedsequence_2d(): # Test that a complicated combination of potentials and wrapped potentials # gets passed to C correctly, by comparing orbit integrated in C to that # in python from galpy.orbit import Orbit from galpy.potential import ( DehnenBarPotential, DehnenSmoothWrapperPotential, LogarithmicHaloPotential, SolidBodyRotationWrapperPotential, SpiralArmsPotential, ) # potential= flat vc + doubly-wrapped bar + spiral-arms pot = [ LogarithmicHaloPotential(normalize=0.2), SolidBodyRotationWrapperPotential( pot=DehnenSmoothWrapperPotential( pot=DehnenBarPotential(omegab=1.0, rb=5.0 / 8.0, Af=1.0 / 100.0), tform=5.0, tsteady=15.0, ), omega=1.3, ), DehnenSmoothWrapperPotential( pot=SpiralArmsPotential(N=4, omega=0.79, amp=0.9), tform=5.0, tsteady=15.0 ), LogarithmicHaloPotential(normalize=0.8), ] # Integrate orbit in C and python o = Orbit([1.0, 0.1, 1.1, 0.1]) oc = o() ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, pot, method="leapfrog") oc.integrate(ts, pot, method="leapfrog_c") # Check that they end up in the same point o = o(ts[-1]) oc = oc(ts[-1]) assert numpy.fabs(o.x() - oc.x()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.y() - oc.y()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vx() - oc.vx()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vy() - oc.vy()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) return None def test_wrapper_complicatedsequence_3d(): # Test that a complicated combination of potentials and wrapped potentials # gets passed to C correctly, by comparing orbit integrated in C to that # in python from galpy.orbit import Orbit from galpy.potential import ( DehnenBarPotential, DehnenSmoothWrapperPotential, LogarithmicHaloPotential, SolidBodyRotationWrapperPotential, SpiralArmsPotential, ) # potential= flat vc + doubly-wrapped bar + spiral-arms pot = [ LogarithmicHaloPotential(normalize=0.2), SolidBodyRotationWrapperPotential( pot=DehnenSmoothWrapperPotential( pot=DehnenBarPotential(omegab=1.0, rb=5.0 / 8.0, Af=1.0 / 100.0), tform=5.0, tsteady=15.0, ), omega=1.3, ), DehnenSmoothWrapperPotential( pot=SpiralArmsPotential(N=4, omega=0.79, amp=0.9), tform=5.0, tsteady=15.0 ), LogarithmicHaloPotential(normalize=0.8), ] # Integrate orbit in C and python o = Orbit([1.0, 0.1, 1.1, 0.1, -0.03, numpy.pi]) oc = o() ts = numpy.linspace(0.0, 20.0, 1001) o.integrate(ts, pot, method="leapfrog") oc.integrate(ts, pot, method="leapfrog_c") # Check that they end up in the same point o = o(ts[-1]) oc = oc(ts[-1]) assert numpy.fabs(o.x() - oc.x()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.y() - oc.y()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.z() - oc.z()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vx() - oc.vx()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vy() - oc.vy()) < 10.0**-4.0, ( "Final orbit velocity between C and Python integration of a doubly-wrapped orbit is too large" ) assert numpy.fabs(o.vz() - oc.vz()) < 10.0**-4.0, ( "Final orbit position between C and Python integration of a doubly-wrapped orbit is too large" ) return None def test_orbit_sun_setup(): # Test that setting up an Orbit with no vxvv returns the Orbit of the Sun from galpy.orbit import Orbit o = Orbit() assert numpy.fabs(o.dist()) < 1e-10, ( "Orbit with no vxvv does not produce an orbit with zero distance" ) assert numpy.fabs(o.vll()) < 1e-10, ( "Orbit with no vxvv does not produce an orbit with zero velocity in the Galactic longitude direction" ) assert numpy.fabs(o.vbb()) < 1e-10, ( "Orbit with no vxvv does not produce an orbit with zero velocity in the Galactic latitude direction" ) assert numpy.fabs(o.vlos()) < 1e-10, ( "Orbit with no vxvv does not produce an orbit with zero line-of-sight velocity" ) def test_integrate_dxdv_errors(): from galpy.orbit import Orbit ts = numpy.linspace(0.0, 10.0, 1001) # Test that attempting to use integrate_dxdv with a non-phasedim==4 orbit # raises error o = Orbit([1.0, 0.1]) with pytest.raises(AttributeError) as excinfo: o.integrate_dxdv(None, ts, potential.toVertical(potential.MWPotential, 1.0)) o = Orbit([1.0, 0.1, 1.0]) with pytest.raises(AttributeError) as excinfo: o.integrate_dxdv(None, ts, potential.MWPotential) o = Orbit([1.0, 0.1, 1.0, 0.1, 0.1]) with pytest.raises(AttributeError) as excinfo: o.integrate_dxdv(None, ts, potential.MWPotential) o = Orbit([1.0, 0.1, 1.0, 0.1, 0.1, 3.0]) with pytest.raises(AttributeError) as excinfo: o.integrate_dxdv(None, ts, potential.MWPotential) # Test that a random string as the integrator doesn't work o = Orbit([1.0, 0.1, 1.0, 3.0]) with pytest.raises(ValueError) as excinfo: o.integrate_dxdv( None, ts, potential.MWPotential, method="some non-existent integrator" ) return None # Test that the internal interpolator is reset when the orbit is re-integrated def test_orbinterp_reset_integrate(): from galpy.orbit import Orbit from galpy.potential import MWPotential, MWPotential2014 o = Orbit([1.0, 0.1, 1.1, 0.1, -0.03, numpy.pi]) op = o() ts = numpy.linspace(0.0, 100.0, 10001) o.integrate(ts, MWPotential) o.R(numpy.linspace(0.0, o.t[-1], 1001)) o.integrate(ts, MWPotential2014) op.integrate(ts, MWPotential2014) # If things are reset correctly, o and op should now agree on everything assert numpy.all(numpy.fabs(o.R(ts) - op.R(ts)) < 10.0**-10.0), ( "Orbit R not reset correctly" ) assert numpy.all(numpy.fabs(o.vR(ts) - op.vR(ts)) < 10.0**-10.0), ( "Orbit vR not reset correctly" ) assert numpy.all(numpy.fabs(o.vT(ts) - op.vT(ts)) < 10.0**-10.0), ( "Orbit vT not reset correctly" ) assert numpy.all(numpy.fabs(o.z(ts) - op.z(ts)) < 10.0**-10.0), ( "Orbit z not reset correctly" ) assert numpy.all(numpy.fabs(o.vz(ts) - op.vz(ts)) < 10.0**-10.0), ( "Orbit vz not reset correctly" ) assert numpy.all(numpy.fabs(o.phi(ts) - op.phi(ts)) < 10.0**-10.0), ( "Orbit phi not reset correctly" ) assert numpy.fabs(o.rperi() - op.rperi()) < 10.0**-10.0, ( "Orbit rperi not reset correctly" ) assert numpy.fabs(o.rap() - op.rap()) < 10.0**-10.0, "Orbit rap not reset correctly" assert numpy.fabs(o.e() - op.e()) < 10.0**-10.0, "Orbit e not reset correctly" return None # Test that the internal interpolator is reset when the orbit is re-integrated # with integrate_SOS def test_orbinterp_reset_integrateSOS(): from galpy.orbit import Orbit from galpy.potential import MWPotential, MWPotential2014 o = Orbit([1.0, 0.1, 1.1, 0.1, -0.03, numpy.pi]) op = o() ts = numpy.linspace(0.0, 100.0, 10001) psis = numpy.linspace(0.0, 100.0, 10001) o.integrate(ts, MWPotential) o.R(numpy.linspace(0.0, o.t[-1], 1001)) o.integrate_SOS(psis, MWPotential2014) op.integrate_SOS(psis, MWPotential2014) ts = o.t # If things are reset correctly, o and op should now agree on everything assert numpy.all(numpy.fabs(o.R(ts) - op.R(ts)) < 10.0**-10.0), ( "Orbit R not reset correctly" ) assert numpy.all(numpy.fabs(o.vR(ts) - op.vR(ts)) < 10.0**-10.0), ( "Orbit vR not reset correctly" ) assert numpy.all(numpy.fabs(o.vT(ts) - op.vT(ts)) < 10.0**-10.0), ( "Orbit vT not reset correctly" ) assert numpy.all(numpy.fabs(o.z(ts) - op.z(ts)) < 10.0**-10.0), ( "Orbit z not reset correctly" ) assert numpy.all(numpy.fabs(o.vz(ts) - op.vz(ts)) < 10.0**-10.0), ( "Orbit vz not reset correctly" ) assert numpy.all(numpy.fabs(o.phi(ts) - op.phi(ts)) < 10.0**-10.0), ( "Orbit phi not reset correctly" ) assert numpy.fabs(o.rperi() - op.rperi()) < 10.0**-10.0, ( "Orbit rperi not reset correctly" ) assert numpy.fabs(o.rap() - op.rap()) < 10.0**-10.0, "Orbit rap not reset correctly" assert numpy.fabs(o.e() - op.e()) < 10.0**-10.0, "Orbit e not reset correctly" return None # Test that the internal interpolator is reset when the orbit is re-integrated # with bruteSOS def test_orbinterp_reset_bruteSOS(): from galpy.orbit import Orbit from galpy.potential import MWPotential, MWPotential2014 o = Orbit([1.0, 0.1, 1.1, 0.1, -0.03, numpy.pi]) op = o() ts = numpy.linspace(0.0, 100.0, 10001) ts2 = numpy.linspace(0.0, 99.0, 10001) o.integrate(ts, MWPotential) o.R(numpy.linspace(0.0, o.t[-1], 1001)) o.bruteSOS(ts2, MWPotential2014) op.bruteSOS(ts2, MWPotential2014) ts = o.t # If things are reset correctly, o and op should now agree on everything assert numpy.all(numpy.fabs(o.R(ts) - op.R(ts)) < 10.0**-10.0), ( "Orbit R not reset correctly" ) assert numpy.all(numpy.fabs(o.vR(ts) - op.vR(ts)) < 10.0**-10.0), ( "Orbit vR not reset correctly" ) assert numpy.all(numpy.fabs(o.vT(ts) - op.vT(ts)) < 10.0**-10.0), ( "Orbit vT not reset correctly" ) assert numpy.all(numpy.fabs(o.z(ts) - op.z(ts)) < 10.0**-10.0), ( "Orbit z not reset correctly" ) assert numpy.all(numpy.fabs(o.vz(ts) - op.vz(ts)) < 10.0**-10.0), ( "Orbit vz not reset correctly" ) assert numpy.all(numpy.fabs(o.phi(ts) - op.phi(ts)) < 10.0**-10.0), ( "Orbit phi not reset correctly" ) assert numpy.fabs(o.rperi() - op.rperi()) < 10.0**-10.0, ( "Orbit rperi not reset correctly" ) assert numpy.fabs(o.rap() - op.rap()) < 10.0**-10.0, "Orbit rap not reset correctly" assert numpy.fabs(o.e() - op.e()) < 10.0**-10.0, "Orbit e not reset correctly" return None # Test that the internal interpolator is reset when the orbit is re-integrated # with integratedxdv def test_orbinterp_reset_integratedxdv(): from galpy.orbit import Orbit from galpy.potential import MWPotential, MWPotential2014 o = Orbit([1.0, 0.1, 1.1, numpy.pi]) op = o() ts = numpy.linspace(0.0, 100.0, 10001) o.integrate_dxdv([1.0, 0.0, 0.0, 0.0], ts, MWPotential) o.R(numpy.linspace(0.0, o.t[-1], 1001)) o.integrate_dxdv([1.0, 0.0, 0.0, 0.0], ts, MWPotential2014) op.integrate_dxdv([1.0, 0.0, 0.0, 0.0], ts, MWPotential2014) # If things are reset correctly, o and op should now agree on everything assert numpy.all(numpy.fabs(o.R(ts) - op.R(ts)) < 10.0**-10.0), ( "Orbit R not reset correctly" ) assert numpy.all(numpy.fabs(o.vR(ts) - op.vR(ts)) < 10.0**-10.0), ( "Orbit vR not reset correctly" ) assert numpy.all(numpy.fabs(o.vT(ts) - op.vT(ts)) < 10.0**-10.0), ( "Orbit vT not reset correctly" ) assert numpy.all(numpy.fabs(o.phi(ts) - op.phi(ts)) < 10.0**-10.0), ( "Orbit phi not reset correctly" ) assert numpy.fabs(o.rperi() - op.rperi()) < 10.0**-10.0, ( "Orbit rperi not reset correctly" ) assert numpy.fabs(o.rap() - op.rap()) < 10.0**-10.0, "Orbit rap not reset correctly" assert numpy.fabs(o.e() - op.e()) < 10.0**-10.0, "Orbit e not reset correctly" return None # Test that an error is raised when integration time array is not equally spaced (see #700) def test_integrate_notevenlyspaced_issue700(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 times = numpy.concatenate( [numpy.linspace(0, 10, 21), numpy.linspace(12.0, 50.0, 20)] ) orb = Orbit() # Test that the correct error is raised when the time array is not equally spaced with pytest.raises(ValueError) as excinfo: orb.integrate(times, MWPotential2014, method="symplec6_c") assert ( str(excinfo.value) == "Input time array must be equally spaced for method symplec6_c, use method='dop853_c', method='dop853', or method='odeint' instead for non-equispaced time arrays" ), "Input time array must be equally spaced error not raised" # Also test backwards integration with pytest.raises(ValueError) as excinfo: orb.integrate(-times, MWPotential2014, method="symplec6_c") assert ( str(excinfo.value) == "Input time array must be equally spaced for method symplec6_c, use method='dop853_c', method='dop853', or method='odeint' instead for non-equispaced time arrays" ), "Input time array must be equally spaced error not raised" # Also test integrate_dxdv with pytest.raises(ValueError) as excinfo: orb.toPlanar().integrate_dxdv(None, times, MWPotential2014, method="dopr54_c") assert ( str(excinfo.value) == "Input time array must be equally spaced for method dopr54_c, use method='dop853_c', method='dop853', or method='odeint' instead for non-equispaced time arrays" ), "Input time array must be equally spaced error not raised" # Also test integrateSOS, just use times for psi... with pytest.raises(ValueError) as excinfo: orb.integrate_SOS(times, MWPotential2014, method="dopr54_c") assert ( str(excinfo.value) == "Input psi array must be equally spaced for method dopr54_c, use method='dop853_c', method='dop853', or method='odeint' instead for non-equispaced psi arrays" ), "Input time array must be equally spaced error not raised" return None # Test that integrators that should be fine with unevenly-spaced times are fine with it def test_integrate_notevenlyspaced_ok(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 time_1 = numpy.linspace(0, 50.0, 1001) time_2 = numpy.concatenate( [numpy.linspace(0, 20.0, 1001), numpy.linspace(20.02, 50.0, 1001)] ) for integrator in ["odeint", "dop853", "dop853_c"]: o_1 = Orbit() o_1.integrate(time_1, MWPotential2014, method=integrator) o_2 = Orbit() o_2.integrate(time_2, MWPotential2014, method=integrator) assert numpy.all(numpy.fabs(o_1.R(time_1) - o_2.R(time_1)) < 10.0**-5.0), ( "Integration with unevenly-spaced times does not work" ) assert numpy.all(numpy.fabs(o_1.vR(time_1) - o_2.vR(time_1)) < 10.0**-4.0), ( "Integration with unevenly-spaced times does not work" ) assert numpy.all(numpy.fabs(o_1.vT(time_1) - o_2.vT(time_1)) < 10.0**-4.0), ( "Integration with unevenly-spaced times does not work" ) for integrator in [ "leapfrog", "leapfrog_c", "symplec4_c", "symplec6_c", "rk4_c", "rk6_c", "dopr54_c", "ias15_c", ]: o_1 = Orbit() o_1.integrate(time_1, MWPotential2014, method=integrator) o_2 = Orbit() with pytest.raises(ValueError) as excinfo: o_2.integrate(time_2, MWPotential2014, method=integrator) assert ( str(excinfo.value) == f"Input time array must be equally spaced for method {integrator}, use method='dop853_c', method='dop853', or method='odeint' instead for non-equispaced time arrays" ), f"Input time array must be equally spaced for method{integrator}" return None def test_linear_plotting(): from galpy.orbit import Orbit from galpy.potential.verticalPotential import RZToverticalPotential o = Orbit([1.0, 1.0]) times = numpy.linspace(0.0, 7.0, 251) from galpy.potential import LogarithmicHaloPotential lp = RZToverticalPotential(LogarithmicHaloPotential(normalize=1.0, q=0.8), 1.0) try: o.plotE() except AttributeError: pass else: raise AssertionError( "o.plotE() before the orbit was integrated did not raise AttributeError for planarOrbit" ) # Integrate o.integrate(times, lp) # Energy o.plotE() o.plotE(pot=lp, d1="x", xlabel=r"$xlabel$") o.plotE(pot=lp, d1="vx", ylabel=r"$ylabel$") # Plot the orbit itself, defaults o.plot() o.plot(ro=8.0) # Plot the orbit itself in 3D try: o.plot3d() except AttributeError: pass else: raise AssertionError("o.plot3d for linearOrbit did not raise Exception") return None # Check plotting routines def test_planar_plotting(): from galpy.orbit import Orbit from galpy.potential.planarPotential import RZToplanarPotential o = Orbit([1.0, 0.1, 1.1, 2.0]) oa = Orbit([1.0, 0.1, 1.1]) times = numpy.linspace(0.0, 7.0, 251) from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.8) try: o.plotE() except AttributeError: pass else: raise AssertionError( "o.plotE() before the orbit was integrated did not raise AttributeError for planarOrbit" ) try: o.plotJacobi() except AttributeError: pass else: raise AssertionError( "o.plotJacobi() before the orbit was integrated did not raise AttributeError for planarOrbit" ) try: oa.plotE() except AttributeError: pass else: raise AssertionError( "o.plotE() before the orbit was integrated did not raise AttributeError for planarROrbit" ) try: oa.plotJacobi() except AttributeError: pass else: raise AssertionError( "o.plotJacobi() before the orbit was integrated did not raise AttributeError for planarROrbit" ) # Integrate o.integrate(times, lp) oa.integrate(times, lp) # Energy o.plotE() o.plotE(pot=lp, d1="R") o.plotE(pot=lp, d1="vR") o.plotE(pot=lp, d1="phi") o.plotE(pot=[lp, RZToplanarPotential(lp)], d1="vT") oa.plotE() oa.plotE(pot=lp, d1="R") oa.plotE(pot=lp, d1="vR") oa.plotE(pot=[lp, RZToplanarPotential(lp)], d1="vT") # Jacobi o.plotJacobi() o.plotJacobi(pot=lp, d1="R", OmegaP=1.0) o.plotJacobi(pot=lp, d1="vR") o.plotJacobi(pot=lp, d1="phi") o.plotJacobi(pot=[lp, RZToplanarPotential(lp)], d1="vT") oa.plotJacobi() oa.plotJacobi(pot=lp, d1="R", OmegaP=1.0) oa.plotJacobi(pot=lp, d1="vR") oa.plotJacobi(pot=[lp, RZToplanarPotential(lp)], d1="vT") # Plot the orbit itself, defaults o.plot() o.plot(ro=8.0) oa.plot() o.plotx(d1="vx") o.plotvx(d1="y") o.ploty(d1="vy") o.plotvy(d1="x") # Plot the orbit itself in 3D, defaults o.plot3d() o.plot3d(ro=8.0) oa.plot3d() o.plot3d(d1="x", d2="vx", d3="y") o.plot3d(d1="vx", d2="y", d3="vy") o.plot3d(d1="y", d2="vy", d3="x") o.plot3d(d1="vy", d2="x", d3="vx") return None # Check plotting routines def test_full_plotting(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.1, 0.2, 2.0]) oa = Orbit([1.0, 0.1, 1.1, 0.1, 0.2]) times = numpy.linspace(0.0, 7.0, 251) from galpy.potential import LogarithmicHaloPotential if True: # not _GHACTIONS: from galpy.potential import DoubleExponentialDiskPotential dp = DoubleExponentialDiskPotential(normalize=1.0) lp = LogarithmicHaloPotential(normalize=1.0, q=0.8) try: o.plotE() except AttributeError: pass else: raise AssertionError( "o.plotE() before the orbit was integrated did not raise AttributeError for planarOrbit" ) try: o.plotEz() except AttributeError: pass else: raise AssertionError( "o.plotEz() before the orbit was integrated did not raise AttributeError for planarOrbit" ) try: o.plotJacobi() except AttributeError: pass else: raise AssertionError( "o.plotJacobi() before the orbit was integrated did not raise AttributeError for planarOrbit" ) try: oa.plotE() except AttributeError: pass else: raise AssertionError( "o.plotE() before the orbit was integrated did not raise AttributeError for planarROrbit" ) try: oa.plotEz() except AttributeError: pass else: raise AssertionError( "o.plotEz() before the orbit was integrated did not raise AttributeError for planarROrbit" ) try: oa.plotJacobi() except AttributeError: pass else: raise AssertionError( "o.plotJacobi() before the orbit was integrated did not raise AttributeError for planarROrbit" ) # Integrate o.integrate(times, lp) oa.integrate(times, lp) # Energy o.plotE() o.plotE(normed=True) o.plotE(pot=lp, d1="R") o.plotE(pot=lp, d1="vR") o.plotE(pot=lp, d1="vT") o.plotE(pot=lp, d1="z") o.plotE(pot=lp, d1="vz") o.plotE(pot=lp, d1="phi") if True: # not _GHACTIONS: o.plotE(pot=dp, d1="phi") oa.plotE() oa.plotE(pot=lp, d1="R") oa.plotE(pot=lp, d1="vR") oa.plotE(pot=lp, d1="vT") oa.plotE(pot=lp, d1="z") oa.plotE(pot=lp, d1="vz") # Vertical energy o.plotEz() o.plotEz(normed=True) o.plotEz(pot=lp, d1="R") o.plotEz(pot=lp, d1="vR") o.plotEz(pot=lp, d1="vT") o.plotEz(pot=lp, d1="z") o.plotEz(pot=lp, d1="vz") o.plotEz(pot=lp, d1="phi") if True: # not _GHACTIONS: o.plotEz(pot=dp, d1="phi") oa.plotEz() oa.plotEz(normed=True) oa.plotEz(pot=lp, d1="R") oa.plotEz(pot=lp, d1="vR") oa.plotEz(pot=lp, d1="vT") oa.plotEz(pot=lp, d1="z") oa.plotEz(pot=lp, d1="vz") # Radial energy o.plotER() o.plotER(normed=True) # Radial energy oa.plotER() oa.plotER(normed=True) # Jacobi o.plotJacobi() o.plotJacobi(normed=True) o.plotJacobi(pot=lp, d1="R", OmegaP=1.0) o.plotJacobi(pot=lp, d1="vR") o.plotJacobi(pot=lp, d1="vT") o.plotJacobi(pot=lp, d1="z") o.plotJacobi(pot=lp, d1="vz") o.plotJacobi(pot=lp, d1="phi") oa.plotJacobi() oa.plotJacobi(pot=lp, d1="R", OmegaP=1.0) oa.plotJacobi(pot=lp, d1="vR") oa.plotJacobi(pot=lp, d1="vT") oa.plotJacobi(pot=lp, d1="z") oa.plotJacobi(pot=lp, d1="vz") # Plot the orbit itself o.plot() # defaults oa.plot() o.plot(d1="vR", label="vR") o.plot(d2="vR", label=["vR"]) o.plotR() o.plotvR(d1="vT") o.plotvT(d1="z") o.plotz(d1="vz") o.plotvz(d1="phi") o.plotphi(d1="vR") o.plotx(d1="vx") o.plotvx(d1="y") o.ploty(d1="vy") o.plotvy(d1="x") # Remaining attributes o.plot(d1="ra", d2="dec") o.plot(d2="ra", d1="dec") o.plot(d1="pmra", d2="pmdec") o.plot(d2="pmra", d1="pmdec") o.plot(d1="ll", d2="bb") o.plot(d2="ll", d1="bb") o.plot(d1="pmll", d2="pmbb") o.plot(d2="pmll", d1="pmbb") o.plot(d1="vlos", d2="dist") o.plot(d2="vlos", d1="dist") o.plot(d1="helioX", d2="U") o.plot(d2="helioX", d1="U") o.plot(d1="helioY", d2="V") o.plot(d2="helioY", d1="V") o.plot(d1="helioZ", d2="W") o.plot(d2="helioZ", d1="W") o.plot(d2="r", d1="R") o.plot(d2="R", d1="r") # Some more energies etc. o.plot(d1="E", d2="R") o.plot(d1="Enorm", d2="R") o.plot(d1="Ez", d2="R") o.plot(d1="Eznorm", d2="R") o.plot(d1="ER", d2="R") o.plot(d1="ERnorm", d2="R") o.plot(d1="Jacobi", d2="R") o.plot(d1="Jacobinorm", d2="R") # callables o.plot(d1=lambda t: t, d2=lambda t: o.R(t)) # Expressions o.plot(d1="t", d2="r*R/vR") o.plot( d1=f"R*cos(phi-{o.Op(quantity=False) - o.Or(quantity=False) / 2:f}*t)", d2=f"R*sin(phi-{o.Op(quantity=False) - o.Or(quantity=False) / 2:f}*t)", ) with pytest.raises(TypeError) as excinfo: # Unparsable expression gives TypeError o.plot(d1="t", d2="r^2") # Test AttributeErrors try: oa.plotx() except AttributeError: pass else: raise AssertionError("plotx() applied to RZOrbit did not raise AttributeError") try: oa.plotvx() except AttributeError: pass else: raise AssertionError("plotvx() applied to RZOrbit did not raise AttributeError") try: oa.ploty() except AttributeError: pass else: raise AssertionError("ploty() applied to RZOrbit did not raise AttributeError") try: oa.plotvy() except AttributeError: pass else: raise AssertionError("plotvy() applied to RZOrbit did not raise AttributeError") try: oa.plot(d1="x") except AttributeError: pass else: raise AssertionError( "plot(d1='x') applied to RZOrbit did not raise AttributeError" ) try: oa.plot(d1="vx") except AttributeError: pass else: raise AssertionError( "plot(d1='vx') applied to RZOrbit did not raise AttributeError" ) try: oa.plot(d1="y") except AttributeError: pass else: raise AssertionError( "plot(d1='y') applied to RZOrbit did not raise AttributeError" ) try: oa.plot(d1="vy") except AttributeError: pass else: raise AssertionError( "plot(d1='vy') applied to RZOrbit did not raise AttributeError" ) # Plot the orbit itself in 3D o.plot3d() # defaults oa.plot3d() o.plot3d(d1="t", d2="z", d3="R") o.plot3d(d1="r", d2="t", d3="phi") o.plot3d(d1="vT", d2="vR", d3="t") o.plot3d(d1="z", d2="vT", d3="vz") o.plot3d(d1="vz", d2="z", d3="phi") o.plot3d(d1="phi", d2="vz", d3="R") o.plot3d(d1="vR", d2="phi", d3="vR") o.plot3d(d1="vx", d2="x", d3="y") o.plot3d(d1="y", d2="vx", d3="vy") o.plot3d(d1="vy", d2="y", d3="x") o.plot3d(d1="x", d2="vy", d3="vx") o.plot3d(d1="x", d2="r", d3="vx") o.plot3d(d1="x", d2="vy", d3="r") # Remaining attributes o.plot3d(d1="ra", d2="dec", d3="pmra") o.plot3d(d2="ra", d1="dec", d3="pmdec") o.plot3d(d1="pmra", d2="pmdec", d3="ra") o.plot3d(d2="pmra", d1="pmdec", d3="dec") o.plot3d(d1="ll", d2="bb", d3="pmll") o.plot3d(d2="ll", d1="bb", d3="pmbb") o.plot3d(d1="pmll", d2="pmbb", d3="ll") o.plot3d(d2="pmll", d1="pmbb", d3="bb") o.plot3d(d1="vlos", d2="dist", d3="vlos") o.plot3d(d2="vlos", d1="dist", d3="dist") o.plot3d(d1="helioX", d2="U", d3="V") o.plot3d(d2="helioX", d1="U", d3="helioY") o.plot3d(d1="helioY", d2="V", d3="W") o.plot3d(d2="helioY", d1="V", d3="helioZ") o.plot3d(d1="helioZ", d2="W", d3="U") o.plot3d(d2="helioZ", d1="W", d3="helioX") # callables don't work o.plot3d(d1=lambda t: t, d2=lambda t: o.R(t), d3=lambda t: o.z(t)) # Test AttributeErrors try: o.plot3d(d1="R") # shouldn't work, bc there is no default except AttributeError: pass else: raise AssertionError( "plot3d with just d1= set should have raised AttributeError, but did not" ) try: oa.plot3d(d2="x", d1="R", d3="t") except AttributeError: pass else: raise AssertionError( "plot3d(d2='x') applied to RZOrbit did not raise AttributeError" ) try: oa.plot3d(d2="vx", d1="R", d3="t") except AttributeError: pass else: raise AssertionError( "plot3d(d2='vx') applied to RZOrbit did not raise AttributeError" ) try: oa.plot3d(d2="y", d1="R", d3="t") except AttributeError: pass else: raise AssertionError( "plot3d(d2='y') applied to RZOrbit did not raise AttributeError" ) try: oa.plot(d2="vy", d1="R", d3="t") except AttributeError: pass else: raise AssertionError( "plot3d(d2='vy') applied to RZOrbit did not raise AttributeError" ) try: oa.plot3d(d1="x", d2="R", d3="t") except AttributeError: pass else: raise AssertionError( "plot3d(d1='x') applied to RZOrbit did not raise AttributeError" ) try: oa.plot3d(d1="vx", d2="R", d3="t") except AttributeError: pass else: raise AssertionError( "plot3d(d1='vx') applied to RZOrbit did not raise AttributeError" ) try: oa.plot3d(d1="y", d2="R", d3="t") except AttributeError: pass else: raise AssertionError( "plot3d(d1='y') applied to RZOrbit did not raise AttributeError" ) try: oa.plot3d(d1="vy", d2="R", d3="t") except AttributeError: pass else: raise AssertionError( "plot3d(d1='vy') applied to RZOrbit did not raise AttributeError" ) try: oa.plot3d(d3="x", d2="R", d1="t") except AttributeError: pass else: raise AssertionError( "plot3d(d3='x') applied to RZOrbit did not raise AttributeError" ) try: oa.plot3d(d3="vx", d2="R", d1="t") except AttributeError: pass else: raise AssertionError( "plot3d(d3='vx') applied to RZOrbit did not raise AttributeError" ) try: oa.plot3d(d3="y", d2="R", d1="t") except AttributeError: pass else: raise AssertionError( "plot3d(d3='y') applied to RZOrbit did not raise AttributeError" ) try: oa.plot3d(d3="vy", d2="R", d1="t") except AttributeError: pass else: raise AssertionError( "plot3d(d3='vy') applied to RZOrbit did not raise AttributeError" ) return None def test_plotSOS(): # 3D pot = potential.MWPotential2014 o = setup_orbit_energy(pot) o.plotSOS(pot) o.plotSOS(pot, use_physical=True) # 2D pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() o = setup_orbit_energy(pot) o.plotSOS(pot, label="test") o.plotSOS(pot, use_physical=True, label=["test"]) o.plotSOS(pot, surface="y") o.plotSOS(pot, surface="y", use_physical=True) return None def test_plotBruteSOS(): # 3D pot = potential.MWPotential2014 o = setup_orbit_energy(pot) o.plotBruteSOS(numpy.linspace(0.0, 100.0, 100001), pot) o.plotBruteSOS(numpy.linspace(0.0, 100.0, 100001), pot, use_physical=True) # 2D pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() o = setup_orbit_energy(pot) o.plotBruteSOS(numpy.linspace(0.0, 100.0, 100001), pot, label="test") o.plotBruteSOS( numpy.linspace(0.0, 100.0, 100001), pot, use_physical=True, label=["test"] ) o.plotBruteSOS(numpy.linspace(0.0, 100.0, 100001), pot, surface="y") o.plotBruteSOS( numpy.linspace(0.0, 100.0, 100001), pot, surface="y", use_physical=True ) return None def test_from_name_values(): from galpy.orbit import Orbit # test Vega o = Orbit.from_name("Vega") assert numpy.isclose(o.ra(), 279.23473479), "RA of Vega does not match SIMBAD value" assert numpy.isclose(o.dec(), 38.78368896), ( "DEC of Vega does not match SIMBAD value" ) assert numpy.isclose(o.dist(), 1 / 130.23), ( "Parallax of Vega does not match SIMBAD value" ) assert numpy.isclose(o.pmra(), 200.94), "PMRA of Vega does not match SIMBAD value" assert numpy.isclose(o.pmdec(), 286.23), "PMDec of Vega does not match SIMBAD value" assert numpy.isclose(o.vlos(), -13.50), ( "radial velocity of Vega does not match SIMBAD value" ) # test Lacaille 8760 o = Orbit.from_name("Lacaille 8760") assert numpy.isclose(o.ra(), 319.31362024), ( "RA of Lacaille 8760 does not match SIMBAD value" ) assert numpy.isclose(o.dec(), -38.86736390), ( "DEC of Lacaille 8760 does not match SIMBAD value" ) assert numpy.isclose(o.dist(), 1 / 251.9124), ( "Parallax of Lacaille 8760 does not match SIMBAD value" ) assert numpy.isclose(o.pmra(), -3258.996), ( "PMRA of Lacaille 8760 does not match SIMBAD value" ) assert numpy.isclose(o.pmdec(), -1145.862), ( "PMDec of Lacaille 8760 does not match SIMBAD value" ) assert numpy.isclose(o.vlos(), 20.56), ( "radial velocity of Lacaille 8760 does not match SIMBAD value" ) # test LMC o = Orbit.from_name("LMC") assert numpy.isclose(o.ra(), 78.77), "RA of LMC does not match value on file" assert numpy.isclose(o.dec(), -69.01), "DEC of LMC does not match value on file" # Remove distance for now, because SIMBAD has the wrong distance (100 Mpc) assert numpy.isclose(o.dist(), 50.1), "Parallax of LMC does not match value on file" assert numpy.isclose(o.pmra(), 1.850), "PMRA of LMC does not match value on file" assert numpy.isclose(o.pmdec(), 0.234), "PMDec of LMC does not match value on file" assert numpy.isclose(o.vlos(), 262.2), ( "radial velocity of LMC does not match value on file" ) # test a distant hypervelocity star o = Orbit.from_name("[BGK2006] HV 5") assert numpy.isclose(o.ra(), 139.498), ( "RA of [BGK2006] HV 5 does not match value on file" ) assert numpy.isclose(o.dec(), 67.377), ( "DEC of [BGK2006] HV 5 does not match SIMBAD value" ) assert numpy.isclose(o.dist(), 55.0), ( "Parallax of [BGK2006] HV 5 does not match SIMBAD value" ) assert numpy.isclose(o.pmra(), 0.001), ( "PMRA of [BGK2006] HV 5 does not match SIMBAD value" ) assert numpy.isclose(o.pmdec(), -0.989), ( "PMDec of [BGK2006] HV 5 does not match SIMBAD value" ) assert numpy.isclose(o.vlos(), 553.0), ( "radial velocity of [BGK2006] HV 5 does not match SIMBAD value" ) def test_from_name_errors(): from galpy.orbit import Orbit # test GJ 440 with pytest.raises(ValueError) as excinfo: Orbit.from_name("GJ 440") msg = "failed to find some coordinates for GJ 440 in SIMBAD" assert str(excinfo.value) == msg, ( f"expected message '{msg}' but got '{str(excinfo.value)}' instead" ) # test with a fake object with pytest.raises(ValueError) as excinfo: Orbit.from_name("abc123") msg = "failed to find abc123 in SIMBAD" assert str(excinfo.value) == msg, ( f"expected message '{msg}' but got '{str(excinfo.value)}' instead" ) # test GRB 090423 with pytest.raises(ValueError) as excinfo: Orbit.from_name("GRB 090423") msg = "failed to find some coordinates for GRB 090423 in SIMBAD" assert str(excinfo.value) == msg, ( f"expected message '{msg}' but got '{str(excinfo.value)}' instead" ) def test_from_name_named(): # Test that the values from the JSON file are correctly transferred import json # Read the JSON file import os import galpy.orbit from galpy.orbit import Orbit named_objects_file = os.path.join( os.path.dirname(os.path.realpath(galpy.orbit.__file__)), "named_objects.json" ) with open(named_objects_file) as json_file: named_data = json.load(json_file) del named_data["_collections"] del named_data["_synonyms"] for obj in named_data: o = Orbit.from_name(obj) for attr in named_data[obj]: if "source" in attr or "dr2" in attr: continue # Skip entries with missing vlos for now if numpy.isnan(named_data[obj]["vlos"]): continue # Skip errors until we use them if attr == "pmcorr" or "_e" in attr: continue if attr == "ro" or attr == "vo" or attr == "zo" or attr == "solarmotion": assert numpy.all( numpy.isclose(getattr(o, f"_{attr:s}"), named_data[obj][attr]) ) elif attr == "distance": assert numpy.isclose(o.dist(), named_data[obj][attr]) else: assert numpy.isclose(getattr(o, f"{attr:s}")(), named_data[obj][attr]) return None def test_from_name_collections(): # Test that the values from the JSON file are correctly transferred, # for collections of objects import json # Read the JSON file import os import galpy.orbit from galpy.orbit import Orbit from galpy.orbit.Orbits import _known_objects_collections_original_keys named_objects_file = os.path.join( os.path.dirname(os.path.realpath(galpy.orbit.__file__)), "named_objects.json" ) with open(named_objects_file) as json_file: named_data = json.load(json_file) for obj in _known_objects_collections_original_keys: o = Orbit.from_name(obj) for ii, individual_obj in enumerate(named_data["_collections"][obj]): for attr in named_data[individual_obj]: if "source" in attr or "dr2 in attr": continue if ( attr == "ro" or attr == "vo" or attr == "zo" or attr == "solarmotion" ): continue # don't test these here elif attr == "distance": assert numpy.isclose(o.dist()[ii], named_data[individual_obj][attr]) else: assert numpy.isclose( getattr(o, f"{attr:s}")()[ii], named_data[individual_obj][attr] ) return None def test_from_name_solarsystem(): # Test that the solar system matches Bovy et al. (2010)'s input data from astropy import units from galpy.orbit import Orbit correct_xyz = numpy.array( [ [ 0.324190175, 0.090955208, -0.022920510, -4.627851589, 10.390063716, 1.273504997, ], [ -0.701534590, -0.168809218, 0.037947785, 1.725066954, -7.205747212, -0.198268558, ], [ -0.982564148, -0.191145980, -0.000014724, 1.126784520, -6.187988860, 0.000330572, ], [ 1.104185888, -0.826097003, -0.044595990, 3.260215854, 4.524583075, 0.014760239, ], [ 3.266443877, -3.888055863, -0.057015321, 2.076140727, 1.904040630, -0.054374153, ], [ -9.218802228, 1.788299816, 0.335737817, -0.496457364, -2.005021061, 0.054667082, ], [ 19.930781147, -2.555241579, -0.267710968, 0.172224285, 1.357933443, 0.002836325, ], [ 24.323085642, -17.606227355, -0.197974999, 0.664855006, 0.935497207, -0.034716967, ], ] ) os = Orbit.from_name("solar system") for ii, o in enumerate(os): assert ( numpy.fabs((o.x() * units.kpc).to(units.AU).value - correct_xyz[ii, 0]) < 1e-8 ), "Orbit.from_name('solar system') does not agree with Bovy et al. (2010) data" assert ( numpy.fabs((o.y() * units.kpc).to(units.AU).value - correct_xyz[ii, 1]) < 1e-8 ), "Orbit.from_name('solar system') does not agree with Bovy et al. (2010) data" assert ( numpy.fabs((o.z() * units.kpc).to(units.AU).value - correct_xyz[ii, 2]) < 1e-8 ), "Orbit.from_name('solar system') does not agree with Bovy et al. (2010) data" assert ( numpy.fabs( (o.vx() * units.km / units.s).to(units.AU / units.yr).value - correct_xyz[ii, 3] ) < 1e-8 ), "Orbit.from_name('solar system') does not agree with Bovy et al. (2010) data" assert ( numpy.fabs( (o.vy() * units.km / units.s).to(units.AU / units.yr).value - correct_xyz[ii, 4] ) < 1e-8 ), "Orbit.from_name('solar system') does not agree with Bovy et al. (2010) data" assert ( numpy.fabs( (o.vz() * units.km / units.s).to(units.AU / units.yr).value - correct_xyz[ii, 5] ) < 1e-8 ), "Orbit.from_name('solar system') does not agree with Bovy et al. (2010) data" return None def test_rguiding_errors(): from galpy.orbit import Orbit from galpy.potential import TriaxialNFWPotential R, Lz = 1.0, 1.4 o = Orbit([R, 0.4, Lz / R, 0.0]) # No potential raises error with pytest.raises(RuntimeError) as excinfo: o.rguiding() # non-axi potential raises error np = TriaxialNFWPotential(amp=20.0, c=0.8, b=0.7) with pytest.raises(RuntimeError) as excinfo: o.rguiding(pot=np) return None def test_rE_errors(): from galpy.orbit import Orbit from galpy.potential import TriaxialNFWPotential R, Lz = 1.0, 1.4 o = Orbit([R, 0.4, Lz / R, 0.0]) # No potential raises error with pytest.raises(RuntimeError) as excinfo: o.rE() # non-axi potential raises error np = TriaxialNFWPotential(amp=20.0, c=0.8, b=0.7) with pytest.raises(RuntimeError) as excinfo: o.rE(pot=np) return None def test_LcE_errors(): from galpy.orbit import Orbit from galpy.potential import TriaxialNFWPotential R, Lz = 1.0, 1.4 o = Orbit([R, 0.4, Lz / R, 0.0]) # No potential raises error with pytest.raises(RuntimeError) as excinfo: o.LcE() # non-axi potential raises error np = TriaxialNFWPotential(amp=20.0, c=0.8, b=0.7) with pytest.raises(RuntimeError) as excinfo: o.LcE(pot=np) return None def test_phi_range(): # Test that the range returned by Orbit.phi is [-pi,pi], # example from Jeremy Webb from galpy.orbit import Orbit from galpy.potential import MWPotential2014 o = Orbit() ts = numpy.linspace(0.0, -30, 5000) o.integrate(ts, MWPotential2014) assert numpy.all(o.phi(ts) <= numpy.pi), "o.phi does not return values <= pi" assert numpy.all(o.phi(ts) >= -numpy.pi), "o.phi does not return values >= pi" assert numpy.all(o.phi(ts[::-1]) <= numpy.pi), "o.phi does not return values <= pi" assert numpy.all(o.phi(ts[::-1]) >= -numpy.pi), "o.phi does not return values >= pi" # Also really interpolated its = numpy.linspace(0.0, -30, 5001) assert numpy.all(o.phi(its) <= numpy.pi), "o.phi does not return values <= pi" assert numpy.all(o.phi(its) >= -numpy.pi), "o.phi does not return values >= pi" return None def test_orbit_time(): # Test that Orbit.time returns the time correctly, with units when that's # required from astropy import units as u from galpy.orbit import Orbit from galpy.potential import MWPotential2014 from galpy.util import conversion ts = numpy.linspace(0.0, 1.0, 1001) * u.Gyr o = Orbit() o.integrate(ts, MWPotential2014) # No argument, in this case should be times in Gyr assert numpy.all(numpy.fabs((ts - o.time(quantity=True)) / ts[-1]).value < 1e-10), ( "Orbit.time does not return the correct times" ) # with argument, should be time in Gyr assert numpy.all( numpy.fabs((ts - o.time(ts, quantity=True)) / ts[-1]).value < 1e-10 ), "Orbit.time does not return the correct times" assert ( numpy.fabs((ts[-1] - o.time(ts[-1], quantity=True)) / ts[-1]).value < 1e-10 ), "Orbit.time does not return the correct times" # with argument without units --> units assert numpy.all( numpy.fabs( ts - o.time( ts.to(u.Gyr).value / conversion.time_in_Gyr( MWPotential2014[0]._vo, MWPotential2014[0]._ro ), quantity=True, ) ).value < 1e-10 ), "Orbit.time does not return the correct times" assert ( numpy.fabs( ts[-1] - o.time( ts[-1].to(u.Gyr).value / conversion.time_in_Gyr( MWPotential2014[0]._vo, MWPotential2014[0]._ro ), quantity=True, ) ).value < 1e-10 ), "Orbit.time does not return the correct times" # Now should get without units o.turn_physical_off() assert numpy.all( numpy.fabs( ts.to(u.Gyr).value / conversion.time_in_Gyr(MWPotential2014[0]._vo, MWPotential2014[0]._ro) - o.time( ts.to(u.Gyr).value / conversion.time_in_Gyr(MWPotential2014[0]._vo, MWPotential2014[0]._ro) ) ) < 1e-10 ), "Orbit.time does not return the correct times" assert ( numpy.fabs( ts[-1].to(u.Gyr).value / conversion.time_in_Gyr(MWPotential2014[0]._vo, MWPotential2014[0]._ro) - o.time( ts[-1].to(u.Gyr).value / conversion.time_in_Gyr(MWPotential2014[0]._vo, MWPotential2014[0]._ro) ) ) < 1e-10 ), "Orbit.time does not return the correct times" return None # Test that issue 402 is resolved: initialization with a SkyCoord when radec=True should work fine def test_SkyCoord_init_with_radecisTrue(): if not _APY3: return None # not done in python 2 from astropy import units as u from astropy.coordinates import SkyCoord from galpy.orbit import Orbit # Example is for NGC5466 from @jjensen4571 rv_ngc5466 = 106.93 pmra_ngc5466 = -5.41 pmdec_ngc5466 = -0.79 ra_ngc5466 = 211.363708 dec_ngc5466 = 28.534445 rhel_ngc5466 = 16.0 mean_ngc5466 = SkyCoord( ra=ra_ngc5466 * u.deg, dec=dec_ngc5466 * u.deg, distance=rhel_ngc5466 * u.kpc, pm_ra_cosdec=pmra_ngc5466 * u.mas / u.yr, pm_dec=pmdec_ngc5466 * u.mas / u.yr, radial_velocity=rv_ngc5466 * u.km / u.s, ) o_sky = Orbit(mean_ngc5466, radec=True, ro=8.1, vo=229.0, solarmotion="schoenrich") o_radec = Orbit( [ ra_ngc5466, dec_ngc5466, rhel_ngc5466, pmra_ngc5466, pmdec_ngc5466, rv_ngc5466, ], radec=True, ro=8.1, vo=229.0, solarmotion="schoenrich", ) assert numpy.fabs(o_sky.ra() - o_radec.ra()) < 1e-8, ( "Orbit setup with SkyCoord and radec=True does not agree with Orbit setup directly with radec" ) assert numpy.fabs(o_sky.dec() - o_radec.dec()) < 1e-8, ( "Orbit setup with SkyCoord and radec=True does not agree with Orbit setup directly with radec" ) assert numpy.fabs(o_sky.dist() - o_radec.dist()) < 1e-8, ( "Orbit setup with SkyCoord and radec=True does not agree with Orbit setup directly with radec" ) assert numpy.fabs(o_sky.pmra() - o_radec.pmra()) < 1e-8, ( "Orbit setup with SkyCoord and radec=True does not agree with Orbit setup directly with radec" ) assert numpy.fabs(o_sky.pmdec() - o_radec.pmdec()) < 1e-8, ( "Orbit setup with SkyCoord and radec=True does not agree with Orbit setup directly with radec" ) assert numpy.fabs(o_sky.vlos() - o_radec.vlos()) < 1e-8, ( "Orbit setup with SkyCoord and radec=True does not agree with Orbit setup directly with radec" ) # Let's also test lb=True for good measure o_sky = Orbit(mean_ngc5466, lb=True, ro=8.1, vo=229.0, solarmotion="schoenrich") assert numpy.fabs(o_sky.ra() - o_radec.ra()) < 1e-8, ( "Orbit setup with SkyCoord and lb=True does not agree with Orbit setup directly with lb" ) assert numpy.fabs(o_sky.dec() - o_radec.dec()) < 1e-8, ( "Orbit setup with SkyCoord and lb=True does not agree with Orbit setup directly with lb" ) assert numpy.fabs(o_sky.dist() - o_radec.dist()) < 1e-8, ( "Orbit setup with SkyCoord and lb=True does not agree with Orbit setup directly with lb" ) assert numpy.fabs(o_sky.pmra() - o_radec.pmra()) < 1e-8, ( "Orbit setup with SkyCoord and lb=True does not agree with Orbit setup directly with lb" ) assert numpy.fabs(o_sky.pmdec() - o_radec.pmdec()) < 1e-8, ( "Orbit setup with SkyCoord and lb=True does not agree with Orbit setup directly with lb" ) assert numpy.fabs(o_sky.vlos() - o_radec.vlos()) < 1e-8, ( "Orbit setup with SkyCoord and lb=True does not agree with Orbit setup directly with lb" ) return None # Test related to issue #415: calling an Orbit with a single int time does not # work properly # Test from @jamesmlane def test_orbit_call_single_time_as_int(): from galpy import orbit, potential pot = potential.MWPotential2014 o = orbit.Orbit() times = numpy.array([0, 1, 2]) o.integrate(times, pot) # Make sure this does not raise TypeErrpr try: o.x(times[0]) except TypeError: raise # Test that the value makes sense assert numpy.fabs(o.x(times[0]) - o.x()) < 1e-10 return None # Test related to issue #415: calling an Orbit with a single Quantity time # does not work properly # Test from @jamesmlane def test_orbit_call_single_time_as_Quantity(): from astropy import units as u from galpy import orbit, potential pot = potential.MWPotential2014 o = orbit.Orbit() times = numpy.array([0, 1, 2]) * u.Gyr o.integrate(times, pot) # Make sure this does not raise TypeErrpr try: o.x(times[0]) except TypeError: raise # Test that the value makes sense assert numpy.fabs(o.x(times[0]) - o.x()) < 1e-10 return None # Setup the orbit for the energy test def setup_orbit_energy(tp, axi=False, henon=False): # Need to treat Henon sep. here, bc cannot be scaled to be reasonable from galpy.orbit import Orbit if isinstance(tp, potential.linearPotential): o = Orbit([1.0, 1.0]) elif isinstance(tp, potential.planarPotential): if henon: if axi: o = Orbit( [ 0.1, 0.3, 0.0, ] ) else: o = Orbit([0.1, 0.3, 0.0, numpy.pi]) else: if axi: o = Orbit([1.0, 1.1, 1.1]) else: o = Orbit([1.0, 1.1, 1.1, numpy.pi / 2.0]) else: if axi: o = Orbit([1.0, 1.1, 1.1, 0.1, 0.1]) else: o = Orbit([1.0, 1.1, 1.1, 0.1, 0.1, 0.0]) return o # Setup the orbit for the Liouville test def setup_orbit_liouville(tp, axi=False, henon=False): from galpy.orbit import Orbit if isinstance(tp, potential.linearPotential): o = Orbit([1.0, 1.0]) elif isinstance(tp, potential.planarPotential): if henon: if axi: o = Orbit( [ 0.1, 0.3, 0.0, ] ) else: o = Orbit([0.1, 0.3, 0.0, numpy.pi]) else: if axi: o = Orbit([1.0, 0.1, 1.1]) else: o = Orbit([1.0, 0.1, 1.1, 0.0]) else: if axi: o = Orbit([1.0, 0.1, 1.1, 0.1, 0.1]) else: o = Orbit([1.0, 0.1, 1.1, 0.1, 0.1, 0.0]) return o # Setup the orbit for the eccentricity test def setup_orbit_eccentricity(tp, axi=False): from galpy.orbit import Orbit if isinstance(tp, potential.planarPotential): if axi: o = Orbit([1.0, 0.0, 1.0]) else: o = Orbit([1.0, 0.0, 1.0, 0.0]) else: if axi: o = Orbit([1.0, 0.0, 1.0, 0.0, 0.0]) else: o = Orbit([1.0, 0.0, 1.0, 0.0, 0.0, 0.0]) return o # Setup the orbit for the pericenter test def setup_orbit_pericenter(tp, axi=False): from galpy.orbit import Orbit if isinstance(tp, potential.planarPotential): if axi: o = Orbit([1.0, 0.0, 1.1]) else: o = Orbit([1.0, 0.0, 1.1, 0.0]) else: if axi: o = Orbit([1.0, 0.0, 1.1, 0.0, 0.0]) else: o = Orbit([1.0, 0.0, 1.1, 0.0, 0.0, 0.0]) return o # Setup the orbit for the apocenter test def setup_orbit_apocenter(tp, axi=False): from galpy.orbit import Orbit if isinstance(tp, potential.planarPotential): if axi: o = Orbit([1.0, 0.0, 0.9]) else: o = Orbit([1.0, 0.0, 0.9, 0.0]) else: if axi: o = Orbit([1.0, 0.0, 0.9, 0.0, 0.0]) else: o = Orbit([1.0, 0.0, 0.9, 0.0, 0.0, 0.0]) return o # Setup the orbit for the zmax test def setup_orbit_zmax(tp, axi=False): from galpy.orbit import Orbit if axi: o = Orbit([1.0, 0.0, 0.98, 0.05, 0.0]) else: o = Orbit([1.0, 0.0, 0.98, 0.05, 0.0, 0.0]) return o # Setup the orbit for the apocenter test def setup_orbit_analytic(tp, axi=False): from galpy.orbit import Orbit if isinstance(tp, potential.planarPotential): if axi: o = Orbit([1.0, 0.1, 0.9]) else: o = Orbit([1.0, 0.1, 0.9, 0.0]) else: if axi: o = Orbit([1.0, 0.1, 0.9, 0.0, 0.0]) else: o = Orbit([1.0, 0.1, 0.9, 0.0, 0.0, 0.0]) return o # Setup the orbit for the zmax test def setup_orbit_analytic_zmax(tp, axi=False): from galpy.orbit import Orbit if axi: o = Orbit([1.0, 0.0, 1.0, 0.05, 0.03]) else: o = Orbit([1.0, 0.0, 1.0, 0.05, 0.03, 0.0]) return o # Setup the orbit for the ER, EZ test def setup_orbit_analytic_EREz(tp, axi=False): from galpy.orbit import Orbit if axi: o = Orbit([1.0, 0.03, 1.0, 0.05, 0.03]) else: o = Orbit([1.0, 0.03, 1.0, 0.05, 0.03, 0.0]) return o # Setup the orbit for the physical-coordinates test def setup_orbit_physical(tp, axi=False, ro=None, vo=None): from galpy.orbit import Orbit if isinstance(tp, potential.planarPotential): if axi: o = Orbit([1.0, 1.1, 1.1], ro=ro, vo=vo) else: o = Orbit([1.0, 1.1, 1.1, 0.0], ro=ro, vo=vo) else: if axi: o = Orbit([1.0, 1.1, 1.1, 0.1, 0.1], ro=ro, vo=vo) else: o = Orbit([1.0, 1.1, 1.1, 0.1, 0.1, 0.0], ro=ro, vo=vo) return o # Setup the orbit for the energy test def setup_orbit_flip(tp, ro, vo, zo, solarmotion, axi=False): from galpy.orbit import Orbit if isinstance(tp, potential.linearPotential): o = Orbit([1.0, 1.0], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion) elif isinstance(tp, potential.planarPotential): if axi: o = Orbit([1.0, 1.1, 1.1], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion) else: o = Orbit( [1.0, 1.1, 1.1, 0.0], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion ) else: if axi: o = Orbit( [1.0, 1.1, 1.1, 0.1, 0.1], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion ) else: o = Orbit( [1.0, 1.1, 1.1, 0.1, 0.1, 0.0], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion, ) return o def check_radecetc_roWarning(o, funcName): # Convenience function to check whether the ro-needs-to-be-specified # warning is sounded with pytest.warns(galpyWarning) as record: if PY2: reset_warning_registry("galpy") warnings.simplefilter("always", galpyWarning) getattr(o, funcName)() raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == f"Method {funcName}(.) requires ro to be given at Orbit initialization or at method evaluation; using default ro which is {8.0:f} kpc" ) assert raisedWarning, ( "Orbit method %s without ro specified should have thrown a warning, but didn't" % funcName ) return None def check_radecetc_voWarning(o, funcName): # Convenience function to check whether the vo-needs-to-be-specified # warning is sounded with pytest.warns(galpyWarning) as record: if PY2: reset_warning_registry("galpy") warnings.simplefilter("always", galpyWarning) getattr(o, funcName)() raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == f"Method {funcName}(.) requires vo to be given at Orbit initialization or at method evaluation; using default vo which is {220.0:f} km/s" ) assert raisedWarning, ( "Orbit method %s without vo specified should have thrown a warning, but didn't" % funcName ) return None def check_integrate_t_asQuantity_warning(o, funcName): with pytest.warns(galpyWarning) as record: getattr(o, funcName)(1.0) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "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)" ) assert raisedWarning, ( "Orbit method %s with unitless time after integrating with unitful time should have thrown a warning, but didn't" % funcName ) return None def test_integrate_method_warning(): """Test Orbit.integrate raises an error if method is invalid""" from galpy.orbit import Orbit from galpy.potential import MWPotential2014 o = Orbit(vxvv=[1.0, 0.1, 0.1, 0.5, 0.1, 0.0]) t = numpy.arange(0.0, 10.0, 0.001) with pytest.raises(ValueError): o.integrate(t, MWPotential2014, method="rk4") def test_MovingObjectPotential_orbit(): # Test integration of an object with a MovingObjectPotential # Test that orbits integrated by C and Python are the same from galpy.orbit import Orbit from galpy.potential import ( HernquistPotential, MovingObjectPotential, MWPotential2014, ) tmax = 5.0 times = numpy.linspace(0, tmax, 101) orbit_pot = HernquistPotential(amp=1e-1, a=1e-1, ro=8, vo=220) o = Orbit([1.0, 0.03, 1.0, 0.05, 0.03, 0.0]) o.integrate(times, MWPotential2014) orbit_potential = MovingObjectPotential(o, pot=orbit_pot) total_potential = [MWPotential2014, orbit_potential] oc = Orbit([0.5, 0.5, 0.5, 0.05, 0.03, 0.0]) op = Orbit([0.5, 0.5, 0.5, 0.05, 0.03, 0.0]) oc.integrate(times, total_potential, method="leapfrog_c") op.integrate(times, total_potential, method="leapfrog") assert numpy.fabs(oc.x(tmax) - op.x(tmax)) < 10.0**-3.0, ( "Final orbit position between C and Python integration in a MovingObjectPotential is too large" ) assert numpy.fabs(oc.y(tmax) - op.y(tmax)) < 10.0**-3.0, ( "Final orbit position between C and Python integration in a MovingObjectPotential is too large" ) assert numpy.fabs(oc.z(tmax) - op.z(tmax)) < 10.0**-3.0, ( "Final orbit position between C and Python integration in a MovingObjectPotential is too large" ) assert numpy.fabs(oc.vx(tmax) - op.vx(tmax)) < 10.0**-3.0, ( "Final orbit velocity between C and Python integration in a MovingObjectPotential is too large" ) assert numpy.fabs(oc.vy(tmax) - op.vy(tmax)) < 10.0**-3.0, ( "Final orbit velocity between C and Python integration in a MovingObjectPotential is too large" ) assert numpy.fabs(oc.vz(tmax) - op.vz(tmax)) < 10.0**-3.0, ( "Final orbit velocity between C and Python integration in a MovingObjectPotential is too large" ) return None def test_MovingObjectPotential_planar_orbit(): # Test integration of an object with a MovingObjectPotential # Test that orbits integrated by C and Python are the same from galpy.orbit import Orbit from galpy.potential import ( HernquistPotential, MovingObjectPotential, MWPotential2014, ) tmax = 5.0 times = numpy.linspace(0, tmax, 101) orbit_pot = HernquistPotential(amp=0.1, a=0.1, ro=8.0, vo=220.0) o = Orbit([0.4, 0.1, 0.6, 0.0]) o.integrate(times, MWPotential2014) orbit_potential = MovingObjectPotential(o, pot=orbit_pot) total_potential = [MWPotential2014, orbit_potential] oc = Orbit([0.5, -0.1, 0.5, 1.0]) op = Orbit([0.5, -0.1, 0.5, 1.0]) oc.integrate(times, total_potential, method="leapfrog_c") op.integrate(times, total_potential, method="leapfrog") assert numpy.fabs(oc.x(tmax) - op.x(tmax)) < 10.0**-3.0, ( "Final orbit position between C and Python integration in a planar MovingObjectPotential is too large" ) assert numpy.fabs(oc.y(tmax) - op.y(tmax)) < 10.0**-3.0, ( "Final orbit position between C and Python integration in a planar MovingObjectPotential is too large" ) assert numpy.fabs(oc.vx(tmax) - op.vx(tmax)) < 10.0**-3.0, ( "Final orbit velocity between C and Python integration in a planar MovingObjectPotential is too large" ) assert numpy.fabs(oc.vy(tmax) - op.vy(tmax)) < 10.0**-3.0, ( "Final orbit velocity between C and Python integration in a planar MovingObjectPotential is too large" ) return None # Test that all integrators can start from a negative time def test_integrate_negative_time(): from galpy.orbit import Orbit from galpy.potential import DehnenBarPotential, MWPotential2014 dp = DehnenBarPotential() methods = [ "odeint", "leapfrog", "leapfrog_c", "rk4_c", "rk6_c", "symplec4_c", "symplec6_c", "dopr54_c", "dop853_c", "ias15_c", ] # negative time to negative time times = numpy.linspace(-70.0, -30.0, 1001) for method in methods: o = Orbit([1.0, 0.1, 1.1, 0.1, 0.1, 0.1]) o.integrate(times, MWPotential2014 + dp, method=method) assert ( numpy.std(o.Jacobi(times)) / numpy.fabs(numpy.mean(o.Jacobi(times))) < 1e-7 ), ( f"Orbit integration with method {method} does not conserve energy when integrating from a negative time to a negative time" ) # negative time to positive time times = numpy.linspace(-30.0, 10.0, 1001) for method in methods: o = Orbit([1.0, 0.1, 1.1, 0.1, 0.1, 0.1]) o.integrate(times, MWPotential2014 + dp, method=method) assert ( numpy.std(o.Jacobi(times)) / numpy.fabs(numpy.mean(o.Jacobi(times))) < 1e-4 ), ( f"Orbit integration with method {method} does not conserve energy when integrating from a negative time to a positive time" ) return None # Test that all integrators can integrate backwards in time def test_integrate_backwards(): from galpy.orbit import Orbit from galpy.potential import DehnenBarPotential, MWPotential2014 dp = DehnenBarPotential() methods = [ "odeint", "leapfrog", "leapfrog_c", "rk4_c", "rk6_c", "symplec4_c", "symplec6_c", "dopr54_c", "dop853_c", "ias15_c", ] # negative time to negative time times = numpy.linspace(-30.0, -70.0, 1001) for method in methods: o = Orbit([1.0, 0.1, 1.1, 0.1, 0.1, 0.1]) o.integrate(times, MWPotential2014 + dp, method=method) assert ( numpy.std(o.Jacobi(times)) / numpy.fabs(numpy.mean(o.Jacobi(times))) < 1e-7 ), ( f"Orbit integration with method {method} does not conserve energy when integrating from a negative time to a negative time" ) # positive time to negative time times = numpy.linspace(30.0, -10.0, 1001) for method in methods: o = Orbit([1.0, 0.1, 1.1, 0.1, 0.1, 0.1]) o.integrate(times, MWPotential2014 + dp, method=method) assert ( numpy.std(o.Jacobi(times)) / numpy.fabs(numpy.mean(o.Jacobi(times))) < 1e-4 ), ( f"Orbit integration with method {method} does not conserve energy when integrating from a negative time to a positive time" ) # positive time to positive time times = numpy.linspace(70.0, 30.0, 1001) for method in methods: o = Orbit([1.0, 0.1, 1.1, 0.1, 0.1, 0.1]) o.integrate(times, MWPotential2014 + dp, method=method) assert ( numpy.std(o.Jacobi(times)) / numpy.fabs(numpy.mean(o.Jacobi(times))) < 1e-4 ), ( f"Orbit integration with method {method} does not conserve energy when integrating from a negative time to a positive time" ) return None # Test that Orbit._call_internal(t0) and Orbit._call_internal(t=t0) return the same results def test_call_internal_kwargs(): from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) o = Orbit([1.0, 0.1, 1.2, 0.3, 0.2, 2.0]) times = numpy.array([0.0, 10.0]) o.integrate(times, lp) assert numpy.array_equal(o._call_internal(10.0), o._call_internal(t=10.0)), ( "Orbit._call_internal(t0) and Orbit._call_internal(t=t0) return different results" ) return None def test_apy_sunkeywords_not_supplied(): # Test for issues #709: print warning when a SkyCoord is used to initialize an # Orbit object, but the Sun's position and velocity are not specified through # galcen_distance, galcen_v_sun, and z_sun from astropy import units as u from astropy.coordinates import SkyCoord from galpy.orbit import Orbit # Just missing galcen_distance vxvv = SkyCoord( ra=1 * u.deg, dec=1 * u.deg, distance=20.8 * u.pc, pm_ra_cosdec=0.0 * u.mas / u.yr, pm_dec=0.0 * u.mas / u.yr, radial_velocity=0.0 * u.km / u.s, # galcen_distance=8.3 * u.kpc, z_sun=0.025 * u.kpc, galcen_v_sun=[-11.1, 220.0, 7.25] * u.km / u.s, ) with pytest.warns(galpyWarning) as record: o = Orbit(vxvv) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "Supplied SkyCoord does not contain (galcen_distance) and this was not explicitly set in the Orbit initialization using the keywords (ro); this is required for Orbit initialization; proceeding with default value" ) assert raisedWarning, ( "Orbit initialization without galcen_distance should have thrown a warning, but didn't" ) # Just missing z_sun vxvv = SkyCoord( ra=1 * u.deg, dec=1 * u.deg, distance=20.8 * u.pc, pm_ra_cosdec=0.0 * u.mas / u.yr, pm_dec=0.0 * u.mas / u.yr, radial_velocity=0.0 * u.km / u.s, galcen_distance=8.3 * u.kpc, # z_sun=0.025 * u.kpc, galcen_v_sun=[-11.1, 220.0, 7.25] * u.km / u.s, ) with pytest.warns(galpyWarning) as record: o = Orbit(vxvv) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "Supplied SkyCoord does not contain (z_sun) and this was not explicitly set in the Orbit initialization using the keywords (zo); this is required for Orbit initialization; proceeding with default value" ) assert raisedWarning, ( "Orbit initialization without z_sun should have thrown a warning, but didn't" ) # Just missing galcen_v_sun vxvv = SkyCoord( ra=1 * u.deg, dec=1 * u.deg, distance=20.8 * u.pc, pm_ra_cosdec=0.0 * u.mas / u.yr, pm_dec=0.0 * u.mas / u.yr, radial_velocity=0.0 * u.km / u.s, galcen_distance=8.3 * u.kpc, z_sun=0.025 * u.kpc, # galcen_v_sun=[-11.1, 220.0, 7.25] * u.km / u.s, ) with pytest.warns(galpyWarning) as record: o = Orbit(vxvv) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "Supplied SkyCoord does not contain (galcen_v_sun) and this was not explicitly set in the Orbit initialization using the keywords (vo, solarmotion); this is required for Orbit initialization; proceeding with default value" ) assert raisedWarning, ( "Orbit initialization without galcen_v_sun should have thrown a warning, but didn't" ) # Missing galcen_distance and z_sun vxvv = SkyCoord( ra=1 * u.deg, dec=1 * u.deg, distance=20.8 * u.pc, pm_ra_cosdec=0.0 * u.mas / u.yr, pm_dec=0.0 * u.mas / u.yr, radial_velocity=0.0 * u.km / u.s, # galcen_distance=8.3 * u.kpc, # z_sun=0.025 * u.kpc, galcen_v_sun=[-11.1, 220.0, 7.25] * u.km / u.s, ) with pytest.warns(galpyWarning) as record: o = Orbit(vxvv) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "Supplied SkyCoord does not contain (galcen_distance, z_sun) and these were not explicitly set in the Orbit initialization using the keywords (ro, zo); these are required for Orbit initialization; proceeding with default values" ) assert raisedWarning, ( "Orbit initialization without galcen_distance and z_sun should have thrown a warning, but didn't" ) # Missing galcen_distance and galcen_v_sun vxvv = SkyCoord( ra=1 * u.deg, dec=1 * u.deg, distance=20.8 * u.pc, pm_ra_cosdec=0.0 * u.mas / u.yr, pm_dec=0.0 * u.mas / u.yr, radial_velocity=0.0 * u.km / u.s, # galcen_distance=8.3 * u.kpc, z_sun=0.025 * u.kpc, # galcen_v_sun=[-11.1, 220.0, 7.25] * u.km / u.s, ) with pytest.warns(galpyWarning) as record: o = Orbit(vxvv) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "Supplied SkyCoord does not contain (galcen_distance, galcen_v_sun) and these were not explicitly set in the Orbit initialization using the keywords (ro, vo, solarmotion); these are required for Orbit initialization; proceeding with default values" ) assert raisedWarning, ( "Orbit initialization without galcen_distance and galcen_v_sun should have thrown a warning, but didn't" ) # Missing z_sun and galcen_v_sun vxvv = SkyCoord( ra=1 * u.deg, dec=1 * u.deg, distance=20.8 * u.pc, pm_ra_cosdec=0.0 * u.mas / u.yr, pm_dec=0.0 * u.mas / u.yr, radial_velocity=0.0 * u.km / u.s, galcen_distance=8.3 * u.kpc, # z_sun=0.025 * u.kpc, # galcen_v_sun=[-11.1, 220.0, 7.25] * u.km / u.s, ) with pytest.warns(galpyWarning) as record: o = Orbit(vxvv) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "Supplied SkyCoord does not contain (z_sun, galcen_v_sun) and these were not explicitly set in the Orbit initialization using the keywords (zo, vo, solarmotion); these are required for Orbit initialization; proceeding with default values" ) assert raisedWarning, ( "Orbit initialization without z_sun and galcen_v_sun should have thrown a warning, but didn't" ) # Missing all: galcen_distance, z_sun, galcen_v_sun vxvv = SkyCoord( ra=1 * u.deg, dec=1 * u.deg, distance=20.8 * u.pc, pm_ra_cosdec=0.0 * u.mas / u.yr, pm_dec=0.0 * u.mas / u.yr, radial_velocity=0.0 * u.km / u.s, # galcen_distance=8.3 * u.kpc, # z_sun=0.025 * u.kpc, # galcen_v_sun=[-11.1, 220.0, 7.25] * u.km / u.s, ) with pytest.warns(galpyWarning) as record: o = Orbit(vxvv) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "Supplied SkyCoord does not contain (galcen_distance, z_sun, galcen_v_sun) and these were not explicitly set in the Orbit initialization using the keywords (ro, zo, vo, solarmotion); these are required for Orbit initialization; proceeding with default values" ) assert raisedWarning, ( "Orbit initialization without galcen_distance, z_sun, and galcen_v_sun should have thrown a warning, but didn't" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_orbits.py0000644000175100001660000135266414761352023016413 0ustar00runnerdocker##########################TESTS ON MULTIPLE ORBITS############################# import astropy import astropy.coordinates as apycoords import astropy.units as u import numpy import pytest from galpy import potential _APY3 = astropy.__version__ > "3" # Test Orbits initialization def test_initialization_vxvv(): from galpy.orbit import Orbit # 1D vxvvs = [[1.0, 0.1], [0.1, 3.0]] orbits = Orbit(vxvvs) assert orbits.dim() == 1, ( "Orbits initialization with vxvv in 1D does not work as expected" ) assert orbits.phasedim() == 2, ( "Orbits initialization with vxvv in 1D does not work as expected" ) assert numpy.fabs(orbits.x()[0] - 1.0) < 1e-10, ( "Orbits initialization with vxvv in 1D does not work as expected" ) assert numpy.fabs(orbits.x()[1] - 0.1) < 1e-10, ( "Orbits initialization with vxvv in 1D does not work as expected" ) assert numpy.fabs(orbits.vx()[0] - 0.1) < 1e-10, ( "Orbits initialization with vxvv in 1D does not work as expected" ) assert numpy.fabs(orbits.vx()[1] - 3.0) < 1e-10, ( "Orbits initialization with vxvv in 1D does not work as expected" ) # 2D, 3 phase-D vxvvs = [[1.0, 0.1, 1.0], [0.1, 3.0, 1.1]] orbits = Orbit(vxvvs) assert orbits.dim() == 2, ( "Orbits initialization with vxvv in 2D, 3 phase-D does not work as expected" ) assert orbits.phasedim() == 3, ( "Orbits initialization with vxvv in 2D, 3 phase-D does not work as expected" ) assert numpy.fabs(orbits.R()[0] - 1.0) < 1e-10, ( "Orbits initialization with vxvv in 2D, 3 phase-D does not work as expected" ) assert numpy.fabs(orbits.R()[1] - 0.1) < 1e-10, ( "Orbits initialization with vxvv in 2D, 3 phase-D does not work as expected" ) assert numpy.fabs(orbits.vR()[0] - 0.1) < 1e-10, ( "Orbits initialization with vxvv in 2D, 3 phase-D does not work as expected" ) assert numpy.fabs(orbits.vR()[1] - 3.0) < 1e-10, ( "Orbits initialization with vxvv in 2D, 3 phase-D does not work as expected" ) assert numpy.fabs(orbits.vT()[0] - 1.0) < 1e-10, ( "Orbits initialization with vxvv in 2D, 3 phase-D does not work as expected" ) assert numpy.fabs(orbits.vT()[1] - 1.1) < 1e-10, ( "Orbits initialization with vxvv in 2D, 3 phase-D does not work as expected" ) # 2D, 4 phase-D vxvvs = [[1.0, 0.1, 1.0, 1.5], [0.1, 3.0, 1.1, 2.0]] orbits = Orbit(vxvvs) assert orbits.dim() == 2, ( "Orbits initialization with vxvv 2D, 4 phase-D does not work as expected" ) assert orbits.phasedim() == 4, ( "Orbits initialization with vxvv 2D, 4 phase-D does not work as expected" ) assert numpy.fabs(orbits.R()[0] - 1.0) < 1e-10, ( "Orbits initialization with vxvv 2D, 4 phase-D does not work as expected" ) assert numpy.fabs(orbits.R()[1] - 0.1) < 1e-10, ( "Orbits initialization with vxvv 2D, 4 phase-D does not work as expected" ) assert numpy.fabs(orbits.vR()[0] - 0.1) < 1e-10, ( "Orbits initialization with vxvv 2D, 4 phase-D does not work as expected" ) assert numpy.fabs(orbits.vR()[1] - 3.0) < 1e-10, ( "Orbits initialization with vxvv 2D, 4 phase-D does not work as expected" ) assert numpy.fabs(orbits.vT()[0] - 1.0) < 1e-10, ( "Orbits initialization with vxvv 2D, 4 phase-D does not work as expected" ) assert numpy.fabs(orbits.vT()[1] - 1.1) < 1e-10, ( "Orbits initialization with vxvv 2D, 4 phase-D does not work as expected" ) assert numpy.fabs(orbits.phi()[0] - 1.5) < 1e-10, ( "Orbits initialization with vxvv 2D, 4 phase-D does not work as expected" ) assert numpy.fabs(orbits.phi()[1] - 2.0) < 1e-10, ( "Orbits initialization with vxvv 2D, 4 phase-D does not work as expected" ) # 3D, 5 phase-D vxvvs = [[1.0, 0.1, 1.0, 0.1, -0.2], [0.1, 3.0, 1.1, -0.3, 0.4]] orbits = Orbit(vxvvs) assert orbits.dim() == 3, ( "Orbits initialization with vxvv 3D, 5 phase-D does not work as expected" ) assert orbits.phasedim() == 5, ( "Orbits initialization with vxvv 3D, 5 phase-D does not work as expected" ) assert numpy.fabs(orbits.R()[0] - 1.0) < 1e-10, ( "Orbits initialization with vxvv 3D, 5 phase-D does not work as expected" ) assert numpy.fabs(orbits.R()[1] - 0.1) < 1e-10, ( "Orbits initialization with vxvv 3D, 5 phase-D does not work as expected" ) assert numpy.fabs(orbits.vR()[0] - 0.1) < 1e-10, ( "Orbits initialization with vxvv 3D, 5 phase-D does not work as expected" ) assert numpy.fabs(orbits.vR()[1] - 3.0) < 1e-10, ( "Orbits initialization with vxvv 3D, 5 phase-D does not work as expected" ) assert numpy.fabs(orbits.vT()[0] - 1.0) < 1e-10, ( "Orbits initialization with vxvv 3D, 5 phase-D does not work as expected" ) assert numpy.fabs(orbits.vT()[1] - 1.1) < 1e-10, ( "Orbits initialization with vxvv 3D, 5 phase-D does not work as expected" ) assert numpy.fabs(orbits.z()[0] - 0.1) < 1e-10, ( "Orbits initialization with vxvv 3D, 5 phase-D does not work as expected" ) assert numpy.fabs(orbits.z()[1] + 0.3) < 1e-10, ( "Orbits initialization with vxvv 3D, 5 phase-D does not work as expected" ) assert numpy.fabs(orbits.vz()[0] + 0.2) < 1e-10, ( "Orbits initialization with vxvv 3D, 5 phase-D does not work as expected" ) assert numpy.fabs(orbits.vz()[1] - 0.4) < 1e-10, ( "Orbits initialization with vxvv 3D, 5 phase-D does not work as expected" ) # 3D, 6 phase-D vxvvs = [[1.0, 0.1, 1.0, 0.1, -0.2, 1.5], [0.1, 3.0, 1.1, -0.3, 0.4, 2.0]] orbits = Orbit(vxvvs) assert orbits.dim() == 3, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert orbits.phasedim() == 6, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.R()[0] - 1.0) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.R()[1] - 0.1) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.vR()[0] - 0.1) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.vR()[1] - 3.0) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.vT()[0] - 1.0) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.vT()[1] - 1.1) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.z()[0] - 0.1) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.z()[1] + 0.3) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.vz()[0] + 0.2) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.vz()[1] - 0.4) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.phi()[0] - 1.5) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.phi()[1] - 2.0) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) return None def test_initialization_SkyCoord(): # Only run this for astropy>3 if not _APY3: return None from galpy.orbit import Orbit numpy.random.seed(1) nrand = 30 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s # Without any custom coordinate-transformation parameters co = apycoords.SkyCoord( ra=ras, dec=decs, distance=dists, pm_ra_cosdec=pmras, pm_dec=pmdecs, radial_velocity=vloss, frame="icrs", ) orbits = Orbit(co) assert orbits.dim() == 3, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert orbits.phasedim() == 6, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) for ii in range(nrand): to = Orbit(co[ii]) assert numpy.fabs(orbits.R()[ii] - to.R()) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.vR()[ii] - to.vR()) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.vT()[ii] - to.vT()) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.z()[ii] - to.z()) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.vz()[ii] - to.vz()) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.phi()[ii] - to.phi()) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) # Also test list of Quantities orbits = Orbit([ras, decs, dists, pmras, pmdecs, vloss], radec=True) for ii in range(nrand): to = Orbit(co[ii]) assert numpy.fabs((orbits.R()[ii] - to.R()) / to.R()) < 1e-7, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs((orbits.vR()[ii] - to.vR()) / to.vR()) < 1e-7, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs((orbits.vT()[ii] - to.vT()) / to.vT()) < 1e-7, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs((orbits.z()[ii] - to.z()) / to.z()) < 1e-7, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs((orbits.vz()[ii] - to.vz()) / to.vz()) < 1e-7, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert ( numpy.fabs( ((orbits.phi()[ii] - to.phi() + numpy.pi) % (2.0 * numpy.pi)) - numpy.pi ) < 1e-7 ), "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" # With custom coordinate-transformation parameters v_sun = apycoords.CartesianDifferential([-11.1, 215.0, 3.25] * u.km / u.s) co = apycoords.SkyCoord( ra=ras, dec=decs, distance=dists, pm_ra_cosdec=pmras, pm_dec=pmdecs, radial_velocity=vloss, frame="icrs", galcen_distance=10.0 * u.kpc, z_sun=1.0 * u.kpc, galcen_v_sun=v_sun, ) orbits = Orbit(co) assert orbits.dim() == 3, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert orbits.phasedim() == 6, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) for ii in range(nrand): to = Orbit(co[ii]) assert numpy.fabs(orbits.R()[ii] - to.R()) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.vR()[ii] - to.vR()) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.vT()[ii] - to.vT()) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.z()[ii] - to.z()) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.vz()[ii] - to.vz()) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits.phi()[ii] - to.phi()) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) return None def test_initialization_list_of_arrays(): # Test that initialization using a list of arrays works (see #548) from galpy.orbit import Orbit numpy.random.seed(1) nrand = 30 ras = numpy.random.uniform(size=nrand) * 360.0 decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) dists = numpy.random.uniform(size=nrand) * 10.0 pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) orbits = Orbit([ras, decs, dists, pmras, pmdecs, vloss], radec=True) for ii in range(nrand): to = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], radec=True ) assert numpy.fabs((orbits.R()[ii] - to.R()) / to.R()) < 1e-9, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs((orbits.vR()[ii] - to.vR()) / to.vR()) < 1e-9, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs((orbits.vT()[ii] - to.vT()) / to.vT()) < 1e-7, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs((orbits.z()[ii] - to.z()) / to.z()) < 1e-9, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs((orbits.vz()[ii] - to.vz()) / to.vz()) < 1e-9, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert ( numpy.fabs( ((orbits.phi()[ii] - to.phi() + numpy.pi) % (2.0 * numpy.pi)) - numpy.pi ) < 1e-10 ), "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" # Also test with R,vR, etc. input, badly orbits = Orbit([ras, decs, dists, pmras, pmdecs, vloss]) for ii in range(nrand): to = Orbit([ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]]) assert numpy.fabs((orbits.R()[ii] - to.R()) / to.R()) < 1e-9, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs((orbits.vR()[ii] - to.vR()) / to.vR()) < 1e-9, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs((orbits.vT()[ii] - to.vT()) / to.vT()) < 1e-7, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs((orbits.z()[ii] - to.z()) / to.z()) < 1e-9, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs((orbits.vz()[ii] - to.vz()) / to.vz()) < 1e-9, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert ( numpy.fabs( ((orbits.phi()[ii] - to.phi() + numpy.pi) % (2.0 * numpy.pi)) - numpy.pi ) < 1e-10 ), "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" return None def test_initialization_diffro(): # Test that supplying an array of ro values works as expected from galpy.orbit import Orbit numpy.random.seed(1) nrand = 30 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s ros = (6.0 + numpy.random.uniform(size=nrand) * 2.0) * u.kpc all_orbs = Orbit([ras, decs, dists, pmras, pmdecs, vloss], ro=ros, radec=True) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], ro=ros[ii], radec=True, ) assert numpy.fabs((all_orbs.R()[ii] - orb.R()) / orb.R()) < 1e-7, ( "Orbits initialization with array of ro does not work as expected" ) assert numpy.fabs((all_orbs.vR()[ii] - orb.vR()) / orb.vR()) < 1e-7, ( "Orbits initialization with array of ro does not work as expected" ) assert numpy.fabs((all_orbs.vT()[ii] - orb.vT()) / orb.vT()) < 1e-7, ( "Orbits initialization with array of ro does not work as expected" ) assert numpy.fabs((all_orbs.z()[ii] - orb.z()) / orb.z()) < 1e-7, ( "Orbits initialization with array of ro does not work as expected" ) assert numpy.fabs((all_orbs.vz()[ii] - orb.vz()) / orb.vz()) < 1e-7, ( "Orbits initialization with array of ro does not work as expected" ) assert ( numpy.fabs( ((all_orbs.phi()[ii] - orb.phi() + numpy.pi) % (2.0 * numpy.pi)) - numpy.pi ) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" # Also some observed values like ra, dec, ... assert numpy.fabs((all_orbs.ra()[ii] - orb.ra()) / orb.ra()) < 1e-7, ( "Orbits initialization with array of ro does not work as expected" ) assert numpy.fabs((all_orbs.dec()[ii] - orb.dec()) / orb.dec()) < 1e-7, ( "Orbits initialization with array of ro does not work as expected" ) assert numpy.fabs((all_orbs.dist()[ii] - orb.dist()) / orb.dist()) < 1e-7, ( "Orbits initialization with array of ro does not work as expected" ) assert numpy.fabs((all_orbs.pmra()[ii] - orb.pmra()) / orb.pmra()) < 1e-7, ( "Orbits initialization with array of ro does not work as expected" ) assert numpy.fabs((all_orbs.pmdec()[ii] - orb.pmdec()) / orb.pmdec()) < 1e-7, ( "Orbits initialization with array of ro does not work as expected" ) assert numpy.fabs((all_orbs.vlos()[ii] - orb.vlos()) / orb.vlos()) < 1e-7, ( "Orbits initialization with array of ro does not work as expected" ) return None def test_initialization_diffzo(): # Test that supplying an array of zo values works as expected from galpy.orbit import Orbit numpy.random.seed(1) nrand = 30 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s zos = (-1.0 + numpy.random.uniform(size=nrand) * 2.0) * 50.0 * u.pc all_orbs = Orbit([ras, decs, dists, pmras, pmdecs, vloss], zo=zos, radec=True) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], zo=zos[ii], radec=True, ) assert numpy.fabs((all_orbs.R()[ii] - orb.R()) / orb.R()) < 1e-7, ( "Orbits initialization with array of zo does not work as expected" ) assert numpy.fabs((all_orbs.vR()[ii] - orb.vR()) / orb.vR()) < 1e-7, ( "Orbits initialization with array of zo does not work as expected" ) assert numpy.fabs((all_orbs.vT()[ii] - orb.vT()) / orb.vT()) < 1e-7, ( "Orbits initialization with array of zo does not work as expected" ) assert numpy.fabs((all_orbs.z()[ii] - orb.z()) / orb.z()) < 1e-7, ( "Orbits initialization with array of zo does not work as expected" ) assert numpy.fabs((all_orbs.vz()[ii] - orb.vz()) / orb.vz()) < 1e-7, ( "Orbits initialization with array of zo does not work as expected" ) assert ( numpy.fabs( ((all_orbs.phi()[ii] - orb.phi() + numpy.pi) % (2.0 * numpy.pi)) - numpy.pi ) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" # Also some observed values like ra, dec, ... assert numpy.fabs((all_orbs.ra()[ii] - orb.ra()) / orb.ra()) < 1e-7, ( "Orbits initialization with array of zo does not work as expected" ) assert numpy.fabs((all_orbs.dec()[ii] - orb.dec()) / orb.dec()) < 1e-7, ( "Orbits initialization with array of zo does not work as expected" ) assert numpy.fabs((all_orbs.dist()[ii] - orb.dist()) / orb.dist()) < 1e-7, ( "Orbits initialization with array of zo does not work as expected" ) assert numpy.fabs((all_orbs.pmra()[ii] - orb.pmra()) / orb.pmra()) < 1e-7, ( "Orbits initialization with array of zo does not work as expected" ) assert numpy.fabs((all_orbs.pmdec()[ii] - orb.pmdec()) / orb.pmdec()) < 1e-7, ( "Orbits initialization with array of zo does not work as expected" ) assert numpy.fabs((all_orbs.vlos()[ii] - orb.vlos()) / orb.vlos()) < 1e-7, ( "Orbits initialization with array of zo does not work as expected" ) return None def test_initialization_diffvo(): # Test that supplying a single vo value works as expected from galpy.orbit import Orbit numpy.random.seed(1) nrand = 30 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s vos = (200.0 + numpy.random.uniform(size=nrand) * 40.0) * u.km / u.s all_orbs = Orbit([ras, decs, dists, pmras, pmdecs, vloss], vo=vos, radec=True) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], vo=vos[ii], radec=True, ) assert numpy.fabs((all_orbs.R()[ii] - orb.R()) / orb.R()) < 1e-7, ( "Orbits initialization with single vo does not work as expected" ) assert numpy.fabs((all_orbs.vR()[ii] - orb.vR()) / orb.vR()) < 1e-7, ( "Orbits initialization with single vo does not work as expected" ) assert numpy.fabs((all_orbs.vT()[ii] - orb.vT()) / orb.vT()) < 1e-7, ( "Orbits initialization with single vo does not work as expected" ) assert numpy.fabs((all_orbs.z()[ii] - orb.z()) / orb.z()) < 1e-7, ( "Orbits initialization with single vo does not work as expected" ) assert numpy.fabs((all_orbs.vz()[ii] - orb.vz()) / orb.vz()) < 1e-7, ( "Orbits initialization with single vo does not work as expected" ) assert ( numpy.fabs( ((all_orbs.phi()[ii] - orb.phi() + numpy.pi) % (2.0 * numpy.pi)) - numpy.pi ) < 1e-7 ), "Orbits initialization with single vo does not work as expected" # Also some observed values like ra, dec, ... assert numpy.fabs((all_orbs.ra()[ii] - orb.ra()) / orb.ra()) < 1e-7, ( "Orbits initialization with single vo does not work as expected" ) assert numpy.fabs((all_orbs.dec()[ii] - orb.dec()) / orb.dec()) < 1e-7, ( "Orbits initialization with single vo does not work as expected" ) assert numpy.fabs((all_orbs.dist()[ii] - orb.dist()) / orb.dist()) < 1e-7, ( "Orbits initialization with single vo does not work as expected" ) assert numpy.fabs((all_orbs.pmra()[ii] - orb.pmra()) / orb.pmra()) < 1e-7, ( "Orbits initialization with single vo does not work as expected" ) assert numpy.fabs((all_orbs.pmdec()[ii] - orb.pmdec()) / orb.pmdec()) < 1e-7, ( "Orbits initialization with single vo does not work as expected" ) assert numpy.fabs((all_orbs.vlos()[ii] - orb.vlos()) / orb.vlos()) < 1e-7, ( "Orbits initialization with single vo does not work as expected" ) return None def test_initialization_diffsolarmotion(): # Test that supplying an array of solarmotion values works as expected from galpy.orbit import Orbit numpy.random.seed(1) nrand = 30 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s solarmotions = ( (2.0 * numpy.random.uniform(size=(3, nrand)) - 1.0) * 10.0 * u.km / u.s ) all_orbs = Orbit( [ras, decs, dists, pmras, pmdecs, vloss], solarmotion=solarmotions, radec=True ) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], solarmotion=solarmotions[:, ii], radec=True, ) assert numpy.fabs((all_orbs.R()[ii] - orb.R()) / orb.R()) < 1e-7, ( "Orbits initialization with array of solarmotion does not work as expected" ) assert numpy.fabs((all_orbs.vR()[ii] - orb.vR()) / orb.vR()) < 1e-7, ( "Orbits initialization with array of solarmotion does not work as expected" ) assert numpy.fabs((all_orbs.vT()[ii] - orb.vT()) / orb.vT()) < 1e-7, ( "Orbits initialization with array of solarmotion does not work as expected" ) assert numpy.fabs((all_orbs.z()[ii] - orb.z()) / orb.z()) < 1e-7, ( "Orbits initialization with array of solarmotion does not work as expected" ) assert numpy.fabs((all_orbs.vz()[ii] - orb.vz()) / orb.vz()) < 1e-7, ( "Orbits initialization with array of solarmotion does not work as expected" ) assert ( numpy.fabs( ((all_orbs.phi()[ii] - orb.phi() + numpy.pi) % (2.0 * numpy.pi)) - numpy.pi ) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" # Also some observed values like ra, dec, ... assert numpy.fabs((all_orbs.ra()[ii] - orb.ra()) / orb.ra()) < 1e-7, ( "Orbits initialization with array of solarmotion does not work as expected" ) assert numpy.fabs((all_orbs.dec()[ii] - orb.dec()) / orb.dec()) < 1e-7, ( "Orbits initialization with array of solarmotion does not work as expected" ) assert numpy.fabs((all_orbs.dist()[ii] - orb.dist()) / orb.dist()) < 1e-7, ( "Orbits initialization with array of solarmotion does not work as expected" ) assert numpy.fabs((all_orbs.pmra()[ii] - orb.pmra()) / orb.pmra()) < 1e-7, ( "Orbits initialization with array of solarmotion does not work as expected" ) assert numpy.fabs((all_orbs.pmdec()[ii] - orb.pmdec()) / orb.pmdec()) < 1e-7, ( "Orbits initialization with array of solarmotion does not work as expected" ) assert numpy.fabs((all_orbs.vlos()[ii] - orb.vlos()) / orb.vlos()) < 1e-7, ( "Orbits initialization with array of solarmotion does not work as expected" ) return None def test_initialization_allsolarparams(): # Test that supplying all parameters works as expected from galpy.orbit import Orbit numpy.random.seed(1) nrand = 30 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s ros = (6.0 + numpy.random.uniform(size=nrand) * 2.0) * u.kpc zos = (-1.0 + numpy.random.uniform(size=nrand) * 2.0) * 50.0 * u.pc vos = (200.0 + numpy.random.uniform(size=nrand) * 40.0) * u.km / u.s solarmotions = ( (2.0 * numpy.random.uniform(size=(3, nrand)) - 1.0) * 10.0 * u.km / u.s ) all_orbs = Orbit( [ras, decs, dists, pmras, pmdecs, vloss], ro=ros, zo=zos, vo=vos, solarmotion=solarmotions, radec=True, ) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], ro=ros[ii], zo=zos[ii], vo=vos[ii], solarmotion=solarmotions[:, ii], radec=True, ) assert numpy.fabs((all_orbs.R()[ii] - orb.R()) / orb.R()) < 1e-7, ( "Orbits initialization with all parameters does not work as expected" ) assert numpy.fabs((all_orbs.vR()[ii] - orb.vR()) / orb.vR()) < 1e-7, ( "Orbits initialization with all parameters does not work as expected" ) assert numpy.fabs((all_orbs.vT()[ii] - orb.vT()) / orb.vT()) < 1e-7, ( "Orbits initialization with all parameters does not work as expected" ) assert numpy.fabs((all_orbs.z()[ii] - orb.z()) / orb.z()) < 1e-7, ( "Orbits initialization with all parameters does not work as expected" ) assert numpy.fabs((all_orbs.vz()[ii] - orb.vz()) / orb.vz()) < 1e-7, ( "Orbits initialization with all parameters does not work as expected" ) assert ( numpy.fabs( ((all_orbs.phi()[ii] - orb.phi() + numpy.pi) % (2.0 * numpy.pi)) - numpy.pi ) < 1e-7 ), "Orbits initialization with all parameters does not work as expected" # Also some observed values like ra, dec, ... assert numpy.fabs((all_orbs.ra()[ii] - orb.ra()) / orb.ra()) < 1e-7, ( "Orbits initialization with all parameters does not work as expected" ) assert numpy.fabs((all_orbs.dec()[ii] - orb.dec()) / orb.dec()) < 1e-7, ( "Orbits initialization with all parameters does not work as expected" ) assert numpy.fabs((all_orbs.dist()[ii] - orb.dist()) / orb.dist()) < 1e-7, ( "Orbits initialization with all parameters does not work as expected" ) assert numpy.fabs((all_orbs.pmra()[ii] - orb.pmra()) / orb.pmra()) < 1e-7, ( "Orbits initialization with all parameters does not work as expected" ) assert numpy.fabs((all_orbs.pmdec()[ii] - orb.pmdec()) / orb.pmdec()) < 1e-7, ( "Orbits initialization with all parameters does not work as expected" ) assert numpy.fabs((all_orbs.vlos()[ii] - orb.vlos()) / orb.vlos()) < 1e-7, ( "Orbits initialization with all parameters does not work as expected" ) return None def test_initialization_diffsolarparams_shape_error(): # Test that we get the correct error when providing the wrong shape for array # ro/zo/vo/solarmotion inputs from galpy.orbit import Orbit numpy.random.seed(1) nrand = 30 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s ros = (6.0 + numpy.random.uniform(size=nrand * 2) * 2.0) * u.kpc with pytest.raises(RuntimeError) as excinfo: Orbit([ras, decs, dists, pmras, pmdecs, vloss], ro=ros, radec=True) assert ( excinfo.value.args[0] == "ro must have the same shape as the input orbits for an array of orbits" ), "Orbits initialization with wrong shape for ro does not raise correct error" zos = (-1.0 + numpy.random.uniform(size=2 * nrand) * 2.0) * 50.0 * u.pc with pytest.raises(RuntimeError) as excinfo: Orbit([ras, decs, dists, pmras, pmdecs, vloss], zo=zos, radec=True) assert ( excinfo.value.args[0] == "zo must have the same shape as the input orbits for an array of orbits" ), "Orbits initialization with wrong shape for zo does not raise correct error" vos = (200.0 + numpy.random.uniform(size=2 * nrand) * 40.0) * u.km / u.s with pytest.raises(RuntimeError) as excinfo: Orbit([ras, decs, dists, pmras, pmdecs, vloss], vo=vos, radec=True) assert ( excinfo.value.args[0] == "vo must have the same shape as the input orbits for an array of orbits" ), "Orbits initialization with wrong shape for vo does not raise correct error" solarmotions = ( (2.0 * numpy.random.uniform(size=(3, 2 * nrand)) - 1.0) * 10.0 * u.km / u.s ) with pytest.raises(RuntimeError) as excinfo: Orbit( [ras, decs, dists, pmras, pmdecs, vloss], solarmotion=solarmotions, radec=True, ) assert ( excinfo.value.args[0] == "solarmotion must have the shape [3,...] where the ... matches the shape of the input orbits for an array of orbits" ), ( "Orbits initialization with wrong shape for solarmotion does not raise correct error" ) return None # Tests that integrating Orbits agrees with integrating multiple Orbit # instances def test_integration_1d(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [Orbit([1.0, 0.1]), Orbit([0.1, 1.0]), Orbit([-0.2, 0.3])] orbits = Orbit(orbits_list) # Integrate as Orbits, twice to make sure initial cond. isn't changed orbits.integrate( times, potential.toVerticalPotential(potential.MWPotential2014, 1.0) ) orbits.integrate( times, potential.toVerticalPotential(potential.MWPotential2014, 1.0) ) # Integrate as multiple Orbits for o in orbits_list: o.integrate( times, potential.toVerticalPotential(potential.MWPotential2014, 1.0) ) # Compare for ii in range(len(orbits)): assert ( numpy.amax(numpy.fabs(orbits_list[ii].x(times) - orbits.x(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vx(times) - orbits.vx(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None def test_integration_2d(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0]), Orbit([0.9, 0.3, 1.0, -0.3]), Orbit([1.2, -0.3, 0.7, 5.0]), ] orbits = Orbit(orbits_list) # Integrate as Orbits, twice to make sure initial cond. isn't changed orbits.integrate(times, potential.MWPotential) orbits.integrate(times, potential.MWPotential) # Integrate as multiple Orbits for o in orbits_list: o.integrate(times, potential.MWPotential) # Compare for ii in range(len(orbits)): assert ( numpy.amax(numpy.fabs(orbits_list[ii].x(times) - orbits.x(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vx(times) - orbits.vx(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].y(times) - orbits.y(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vy(times) - orbits.vy(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].R(times) - orbits.R(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vR(times) - orbits.vR(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vT(times) - orbits.vT(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( ( (orbits_list[ii].phi(times) - orbits.phi(times)[ii] + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None def test_integration_p3d(): # 3D phase-space integration from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0]), Orbit([0.9, 0.3, 1.0]), Orbit([1.2, -0.3, 0.7]), ] orbits = Orbit(orbits_list) # Integrate as Orbits, twice to make sure initial cond. isn't changed orbits.integrate(times, potential.MWPotential2014) orbits.integrate(times, potential.MWPotential2014) # Integrate as multiple Orbits for o in orbits_list: o.integrate(times, potential.MWPotential2014) # Compare for ii in range(len(orbits)): assert ( numpy.amax(numpy.fabs(orbits_list[ii].R(times) - orbits.R(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vR(times) - orbits.vR(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vT(times) - orbits.vT(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None def test_integration_3d(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0, 0.1, 0.0]), Orbit([0.9, 0.3, 1.0, -0.3, 0.4, 3.0]), Orbit([1.2, -0.3, 0.7, 0.5, -0.5, 6.0]), ] orbits = Orbit(orbits_list) # Integrate as Orbits, twice to make sure initial cond. isn't changed orbits.integrate(times, potential.MWPotential2014) orbits.integrate(times, potential.MWPotential2014) # Integrate as multiple Orbits for o in orbits_list: o.integrate(times, potential.MWPotential2014) # Compare for ii in range(len(orbits)): assert ( numpy.amax(numpy.fabs(orbits_list[ii].x(times) - orbits.x(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vx(times) - orbits.vx(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].y(times) - orbits.y(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vy(times) - orbits.vy(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].z(times) - orbits.z(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vz(times) - orbits.vz(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].R(times) - orbits.R(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vR(times) - orbits.vR(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vT(times) - orbits.vT(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( ( ( (orbits_list[ii].phi(times) - orbits.phi(times)[ii]) + numpy.pi ) % (2.0 * numpy.pi) ) - numpy.pi ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None def test_integration_p5d(): # 5D phase-space integration from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0, 0.1]), Orbit([0.9, 0.3, 1.0, -0.3, 0.4]), Orbit([1.2, -0.3, 0.7, 0.5, -0.5]), ] orbits = Orbit(orbits_list) # Integrate as Orbits, twice to make sure initial cond. isn't changed orbits.integrate(times, potential.MWPotential2014) orbits.integrate(times, potential.MWPotential2014) # Integrate as multiple Orbits for o in orbits_list: o.integrate(times, potential.MWPotential2014) # Compare for ii in range(len(orbits)): assert ( numpy.amax(numpy.fabs(orbits_list[ii].z(times) - orbits.z(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vz(times) - orbits.vz(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].R(times) - orbits.R(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vR(times) - orbits.vR(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vT(times) - orbits.vT(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None def test_integrate_3d_diffro(): # Test that supplying an array of ro values works as expected when integrating an orbit from galpy.orbit import Orbit numpy.random.seed(1) nrand = 4 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s ros = (6.0 + numpy.random.uniform(size=nrand) * 2.0) * u.kpc all_orbs = Orbit([ras, decs, dists, pmras, pmdecs, vloss], ro=ros, radec=True) times = numpy.linspace(0.0, 10.0, 1001) all_orbs.integrate(times, potential.MWPotential2014) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], ro=ros[ii], radec=True, ) orb.integrate(times, potential.MWPotential2014) assert numpy.all( numpy.fabs((all_orbs.R(times)[ii] - orb.R(times)) / orb.R(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vR(times)[ii] - orb.vR(times)) / orb.vR(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vT(times)[ii] - orb.vT(times)) / orb.vT(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.z(times)[ii] - orb.z(times)) / orb.z(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vz(times)[ii] - orb.vz(times)) / orb.vz(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs( ( (all_orbs.phi(times)[ii] - orb.phi(times) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" # Also some observed values like ra, dec, ... assert numpy.all( numpy.fabs((all_orbs.ra(times)[ii] - orb.ra(times)) / orb.ra(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dec(times)[ii] - orb.dec(times)) / orb.dec(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dist(times)[ii] - orb.dist(times)) / orb.dist(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.pmra(times)[ii] - orb.pmra(times)) / orb.pmra(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs( (all_orbs.pmdec(times)[ii] - orb.pmdec(times)) / orb.pmdec(times) ) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vlos(times)[ii] - orb.vlos(times)) / orb.vlos(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" return None def test_integrate_3d_diffzo(): # Test that supplying an array of zo values works as expected when integrating an orbit from galpy.orbit import Orbit numpy.random.seed(1) nrand = 4 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s zos = (-1.0 + numpy.random.uniform(size=nrand) * 2.0) * 100.0 * u.pc all_orbs = Orbit([ras, decs, dists, pmras, pmdecs, vloss], zo=zos, radec=True) times = numpy.linspace(0.0, 10.0, 1001) all_orbs.integrate(times, potential.MWPotential2014) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], zo=zos[ii], radec=True, ) orb.integrate(times, potential.MWPotential2014) assert numpy.all( numpy.fabs((all_orbs.R(times)[ii] - orb.R(times)) / orb.R(times)) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vR(times)[ii] - orb.vR(times)) / orb.vR(times)) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vT(times)[ii] - orb.vT(times)) / orb.vT(times)) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.z(times)[ii] - orb.z(times)) / orb.z(times)) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vz(times)[ii] - orb.vz(times)) / orb.vz(times)) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" assert numpy.all( numpy.fabs( ( (all_orbs.phi(times)[ii] - orb.phi(times) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" # Also some observed values like ra, dec, ... assert numpy.all( numpy.fabs((all_orbs.ra(times)[ii] - orb.ra(times)) / orb.ra(times)) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dec(times)[ii] - orb.dec(times)) / orb.dec(times)) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dist(times)[ii] - orb.dist(times)) / orb.dist(times)) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.pmra(times)[ii] - orb.pmra(times)) / orb.pmra(times)) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" assert numpy.all( numpy.fabs( (all_orbs.pmdec(times)[ii] - orb.pmdec(times)) / orb.pmdec(times) ) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vlos(times)[ii] - orb.vlos(times)) / orb.vlos(times)) < 1e-7 ), "Orbits initialization with array of zo does not work as expected" return None def test_integrate_3d_diffvo(): # Test that supplying an array of zo values works as expected when integrating an orbit from galpy.orbit import Orbit numpy.random.seed(1) nrand = 4 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s vos = (200.0 + numpy.random.uniform(size=nrand) * 40.0) * u.km / u.s all_orbs = Orbit([ras, decs, dists, pmras, pmdecs, vloss], vo=vos, radec=True) times = numpy.linspace(0.0, 10.0, 1001) all_orbs.integrate(times, potential.MWPotential2014) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], vo=vos[ii], radec=True, ) orb.integrate(times, potential.MWPotential2014) assert numpy.all( numpy.fabs((all_orbs.R(times)[ii] - orb.R(times)) / orb.R(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vR(times)[ii] - orb.vR(times)) / orb.vR(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vT(times)[ii] - orb.vT(times)) / orb.vT(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.z(times)[ii] - orb.z(times)) / orb.z(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vz(times)[ii] - orb.vz(times)) / orb.vz(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs( ( (all_orbs.phi(times)[ii] - orb.phi(times) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" # Also some observed values like ra, dec, ... assert numpy.all( numpy.fabs((all_orbs.ra(times)[ii] - orb.ra(times)) / orb.ra(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dec(times)[ii] - orb.dec(times)) / orb.dec(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dist(times)[ii] - orb.dist(times)) / orb.dist(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.pmra(times)[ii] - orb.pmra(times)) / orb.pmra(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs( (all_orbs.pmdec(times)[ii] - orb.pmdec(times)) / orb.pmdec(times) ) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vlos(times)[ii] - orb.vlos(times)) / orb.vlos(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" return None def test_integrate_3d_diffsolarmotion(): # Test that supplying an array of zo values works as expected when integrating an orbit from galpy.orbit import Orbit numpy.random.seed(1) nrand = 4 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s solarmotions = ( (2.0 * numpy.random.uniform(size=(3, nrand)) - 1.0) * 10.0 * u.km / u.s ) all_orbs = Orbit( [ras, decs, dists, pmras, pmdecs, vloss], solarmotion=solarmotions, radec=True ) times = numpy.linspace(0.0, 10.0, 1001) all_orbs.integrate(times, potential.MWPotential2014) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], solarmotion=solarmotions[:, ii], radec=True, ) orb.integrate(times, potential.MWPotential2014) assert numpy.all( numpy.fabs((all_orbs.R(times)[ii] - orb.R(times)) / orb.R(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vR(times)[ii] - orb.vR(times)) / orb.vR(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vT(times)[ii] - orb.vT(times)) / orb.vT(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.z(times)[ii] - orb.z(times)) / orb.z(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vz(times)[ii] - orb.vz(times)) / orb.vz(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs( ( (all_orbs.phi(times)[ii] - orb.phi(times) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" # Also some observed values like ra, dec, ... assert numpy.all( numpy.fabs((all_orbs.ra(times)[ii] - orb.ra(times)) / orb.ra(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dec(times)[ii] - orb.dec(times)) / orb.dec(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dist(times)[ii] - orb.dist(times)) / orb.dist(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.pmra(times)[ii] - orb.pmra(times)) / orb.pmra(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs( (all_orbs.pmdec(times)[ii] - orb.pmdec(times)) / orb.pmdec(times) ) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vlos(times)[ii] - orb.vlos(times)) / orb.vlos(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" return None def test_integrate_3d_diffallsolarparams(): # Test that supplying an array of solar values works as expected when integrating an orbit from galpy.orbit import Orbit numpy.random.seed(1) nrand = 4 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s ros = (6.0 + numpy.random.uniform(size=nrand) * 2.0) * u.kpc zos = (-1.0 + numpy.random.uniform(size=nrand) * 2.0) * 100.0 * u.pc vos = (200.0 + numpy.random.uniform(size=nrand) * 40.0) * u.km / u.s solarmotions = ( (2.0 * numpy.random.uniform(size=(3, nrand)) - 1.0) * 10.0 * u.km / u.s ) all_orbs = Orbit( [ras, decs, dists, pmras, pmdecs, vloss], ro=ros, zo=zos, vo=vos, solarmotion=solarmotions, radec=True, ) times = numpy.linspace(0.0, 10.0, 1001) all_orbs.integrate(times, potential.MWPotential2014) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], ro=ros[ii], zo=zos[ii], vo=vos[ii], solarmotion=solarmotions[:, ii], radec=True, ) orb.integrate(times, potential.MWPotential2014) assert numpy.all( numpy.fabs((all_orbs.R(times)[ii] - orb.R(times)) / orb.R(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vR(times)[ii] - orb.vR(times)) / orb.vR(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vT(times)[ii] - orb.vT(times)) / orb.vT(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.z(times)[ii] - orb.z(times)) / orb.z(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vz(times)[ii] - orb.vz(times)) / orb.vz(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs( ( (all_orbs.phi(times)[ii] - orb.phi(times) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" # Also some observed values like ra, dec, ... assert numpy.all( numpy.fabs((all_orbs.ra(times)[ii] - orb.ra(times)) / orb.ra(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dec(times)[ii] - orb.dec(times)) / orb.dec(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dist(times)[ii] - orb.dist(times)) / orb.dist(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.pmra(times)[ii] - orb.pmra(times)) / orb.pmra(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs( (all_orbs.pmdec(times)[ii] - orb.pmdec(times)) / orb.pmdec(times) ) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vlos(times)[ii] - orb.vlos(times)) / orb.vlos(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" return None def test_integrate_2d_diffro(): # Test that supplying an array of ro values works as expected when integrating an orbit from galpy.orbit import Orbit numpy.random.seed(1) nrand = 4 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s ros = (6.0 + numpy.random.uniform(size=nrand) * 2.0) * u.kpc all_orbs = Orbit( [ras, decs, dists, pmras, pmdecs, vloss], ro=ros, radec=True ).toPlanar() times = numpy.linspace(0.0, 10.0, 1001) all_orbs.integrate(times, potential.MWPotential2014) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], ro=ros[ii], radec=True, ).toPlanar() orb.integrate(times, potential.MWPotential2014) assert numpy.all( numpy.fabs((all_orbs.R(times)[ii] - orb.R(times)) / orb.R(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vR(times)[ii] - orb.vR(times)) / orb.vR(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vT(times)[ii] - orb.vT(times)) / orb.vT(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs( ( (all_orbs.phi(times)[ii] - orb.phi(times) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" # Also some observed values like ra, dec, ... assert numpy.all( numpy.fabs((all_orbs.ra(times)[ii] - orb.ra(times)) / orb.ra(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dec(times)[ii] - orb.dec(times)) / orb.dec(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dist(times)[ii] - orb.dist(times)) / orb.dist(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.pmra(times)[ii] - orb.pmra(times)) / orb.pmra(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs( (all_orbs.pmdec(times)[ii] - orb.pmdec(times)) / orb.pmdec(times) ) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vlos(times)[ii] - orb.vlos(times)) / orb.vlos(times)) < 1e-7 ), "Orbits initialization with array of ro does not work as expected" return None def test_integrate_2d_diffvo(): # Test that supplying an array of zo values works as expected when integrating an orbit from galpy.orbit import Orbit numpy.random.seed(1) nrand = 4 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s vos = (200.0 + numpy.random.uniform(size=nrand) * 40.0) * u.km / u.s all_orbs = Orbit( [ras, decs, dists, pmras, pmdecs, vloss], vo=vos, radec=True ).toPlanar() times = numpy.linspace(0.0, 10.0, 1001) all_orbs.integrate(times, potential.MWPotential2014) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], vo=vos[ii], radec=True, ).toPlanar() orb.integrate(times, potential.MWPotential2014) assert numpy.all( numpy.fabs((all_orbs.R(times)[ii] - orb.R(times)) / orb.R(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vR(times)[ii] - orb.vR(times)) / orb.vR(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vT(times)[ii] - orb.vT(times)) / orb.vT(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs( ( (all_orbs.phi(times)[ii] - orb.phi(times) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" # Also some observed values like ra, dec, ... assert numpy.all( numpy.fabs((all_orbs.ra(times)[ii] - orb.ra(times)) / orb.ra(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dec(times)[ii] - orb.dec(times)) / orb.dec(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dist(times)[ii] - orb.dist(times)) / orb.dist(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.pmra(times)[ii] - orb.pmra(times)) / orb.pmra(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs( (all_orbs.pmdec(times)[ii] - orb.pmdec(times)) / orb.pmdec(times) ) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vlos(times)[ii] - orb.vlos(times)) / orb.vlos(times)) < 1e-7 ), "Orbits initialization with array of vo does not work as expected" return None def test_integrate_2d_diffsolarmotion(): # Test that supplying an array of zo values works as expected when integrating an orbit from galpy.orbit import Orbit numpy.random.seed(1) nrand = 4 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s solarmotions = ( (2.0 * numpy.random.uniform(size=(3, nrand)) - 1.0) * 10.0 * u.km / u.s ) all_orbs = Orbit( [ras, decs, dists, pmras, pmdecs, vloss], solarmotion=solarmotions, radec=True ).toPlanar() times = numpy.linspace(0.0, 10.0, 1001) all_orbs.integrate(times, potential.MWPotential2014) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], solarmotion=solarmotions[:, ii], radec=True, ).toPlanar() orb.integrate(times, potential.MWPotential2014) assert numpy.all( numpy.fabs((all_orbs.R(times)[ii] - orb.R(times)) / orb.R(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vR(times)[ii] - orb.vR(times)) / orb.vR(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vT(times)[ii] - orb.vT(times)) / orb.vT(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs( ( (all_orbs.phi(times)[ii] - orb.phi(times) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" # Also some observed values like ra, dec, ... assert numpy.all( numpy.fabs((all_orbs.ra(times)[ii] - orb.ra(times)) / orb.ra(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dec(times)[ii] - orb.dec(times)) / orb.dec(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dist(times)[ii] - orb.dist(times)) / orb.dist(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.pmra(times)[ii] - orb.pmra(times)) / orb.pmra(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs( (all_orbs.pmdec(times)[ii] - orb.pmdec(times)) / orb.pmdec(times) ) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vlos(times)[ii] - orb.vlos(times)) / orb.vlos(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" return None def test_integrate_2d_diffallsolarparams(): # Test that supplying an array of solar values works as expected when integrating an orbit from galpy.orbit import Orbit numpy.random.seed(1) nrand = 4 ras = numpy.random.uniform(size=nrand) * 360.0 * u.deg decs = 90.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.deg dists = numpy.random.uniform(size=nrand) * 10.0 * u.kpc pmras = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr pmdecs = 20.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * 20.0 * u.mas / u.yr vloss = 200.0 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) * u.km / u.s ros = (6.0 + numpy.random.uniform(size=nrand) * 2.0) * u.kpc zos = (-1.0 + numpy.random.uniform(size=nrand) * 2.0) * 100.0 * u.pc vos = (200.0 + numpy.random.uniform(size=nrand) * 40.0) * u.km / u.s solarmotions = ( (2.0 * numpy.random.uniform(size=(3, nrand)) - 1.0) * 10.0 * u.km / u.s ) all_orbs = Orbit( [ras, decs, dists, pmras, pmdecs, vloss], ro=ros, zo=zos, vo=vos, solarmotion=solarmotions, radec=True, ).toPlanar() times = numpy.linspace(0.0, 10.0, 1001) all_orbs.integrate(times, potential.MWPotential2014) for ii in range(nrand): orb = Orbit( [ras[ii], decs[ii], dists[ii], pmras[ii], pmdecs[ii], vloss[ii]], ro=ros[ii], zo=zos[ii], vo=vos[ii], solarmotion=solarmotions[:, ii], radec=True, ).toPlanar() orb.integrate(times, potential.MWPotential2014) assert numpy.all( numpy.fabs((all_orbs.R(times)[ii] - orb.R(times)) / orb.R(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vR(times)[ii] - orb.vR(times)) / orb.vR(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vT(times)[ii] - orb.vT(times)) / orb.vT(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs( ( (all_orbs.phi(times)[ii] - orb.phi(times) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" # Also some observed values like ra, dec, ... assert numpy.all( numpy.fabs((all_orbs.ra(times)[ii] - orb.ra(times)) / orb.ra(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dec(times)[ii] - orb.dec(times)) / orb.dec(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.dist(times)[ii] - orb.dist(times)) / orb.dist(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.pmra(times)[ii] - orb.pmra(times)) / orb.pmra(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs( (all_orbs.pmdec(times)[ii] - orb.pmdec(times)) / orb.pmdec(times) ) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" assert numpy.all( numpy.fabs((all_orbs.vlos(times)[ii] - orb.vlos(times)) / orb.vlos(times)) < 1e-7 ), "Orbits initialization with array of solarmotion does not work as expected" return None # Tests that integrating Orbits agrees with integrating multiple Orbit # instances when using parallel_map Python parallelization def test_integration_forcemap_1d(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [Orbit([1.0, 0.1]), Orbit([0.1, 1.0]), Orbit([-0.2, 0.3])] orbits = Orbit(orbits_list) # Integrate as Orbits orbits.integrate( times, potential.toVerticalPotential(potential.MWPotential2014, 1.0), force_map=True, ) # Integrate as multiple Orbits for o in orbits_list: o.integrate( times, potential.toVerticalPotential(potential.MWPotential2014, 1.0) ) # Compare for ii in range(len(orbits)): assert ( numpy.amax(numpy.fabs(orbits_list[ii].x(times) - orbits.x(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vx(times) - orbits.vx(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None def test_integration_forcemap_2d(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0]), Orbit([0.9, 0.3, 1.0, -0.3]), Orbit([1.2, -0.3, 0.7, 5.0]), ] orbits = Orbit(orbits_list) # Integrate as Orbits orbits.integrate(times, potential.MWPotential2014, force_map=True) # Integrate as multiple Orbits for o in orbits_list: o.integrate(times, potential.MWPotential2014) # Compare for ii in range(len(orbits)): assert ( numpy.amax(numpy.fabs(orbits_list[ii].x(times) - orbits.x(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vx(times) - orbits.vx(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].y(times) - orbits.y(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vy(times) - orbits.vy(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].R(times) - orbits.R(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vR(times) - orbits.vR(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vT(times) - orbits.vT(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( ( ( (orbits_list[ii].phi(times) - orbits.phi(times)[ii]) + numpy.pi ) % (2.0 * numpy.pi) ) - numpy.pi ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None def test_integration_forcemap_3d(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0, 0.1, 0.0]), Orbit([0.9, 0.3, 1.0, -0.3, 0.4, 3.0]), Orbit([1.2, -0.3, 0.7, 0.5, -0.5, 6.0]), ] orbits = Orbit(orbits_list) # Integrate as Orbits orbits.integrate(times, potential.MWPotential2014, force_map=True) # Integrate as multiple Orbits for o in orbits_list: o.integrate(times, potential.MWPotential2014) # Compare for ii in range(len(orbits)): assert ( numpy.amax(numpy.fabs(orbits_list[ii].x(times) - orbits.x(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vx(times) - orbits.vx(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].y(times) - orbits.y(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vy(times) - orbits.vy(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].z(times) - orbits.z(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vz(times) - orbits.vz(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].R(times) - orbits.R(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vR(times) - orbits.vR(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vT(times) - orbits.vT(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( ( ( (orbits_list[ii].phi(times) - orbits.phi(times)[ii]) + numpy.pi ) % (2.0 * numpy.pi) ) - numpy.pi ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None def test_integration_dxdv_2d(): from galpy.orbit import Orbit lp = potential.LogarithmicHaloPotential(normalize=1.0) times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0]), Orbit([0.9, 0.3, 1.0, -0.3]), Orbit([1.2, -0.3, 0.7, 5.0]), ] orbits = Orbit(orbits_list) numpy.random.seed(1) dxdv = (2.0 * numpy.random.uniform(size=orbits.shape + (4,)) - 1) / 10.0 # Default, C integration integrator = "dopr54_c" orbits.integrate_dxdv(dxdv, times, lp, method=integrator) # Integrate as multiple Orbits for o, tdxdv in zip(orbits_list, dxdv): o.integrate_dxdv(tdxdv, times, lp, method=integrator) assert ( numpy.amax( numpy.fabs( orbits.getOrbit_dxdv() - numpy.array([o.getOrbit_dxdv() for o in orbits_list]) ) ) < 1e-8 ), ( "Integration of the phase-space volume of multiple orbits as Orbits does not agree with integrating the phase-space volume of multiple orbits" ) # Python integration integrator = "odeint" orbits.integrate_dxdv(dxdv, times, lp, method=integrator) # Integrate as multiple Orbits for o, tdxdv in zip(orbits_list, dxdv): o.integrate_dxdv(tdxdv, times, lp, method=integrator) assert ( numpy.amax( numpy.fabs( orbits.getOrbit_dxdv() - numpy.array([o.getOrbit_dxdv() for o in orbits_list]) ) ) < 1e-8 ), ( "Integration of the phase-space volume of multiple orbits as Orbits does not agree with integrating the phase-space volume of multiple orbits" ) return None def test_integration_dxdv_2d_rectInOut(): from galpy.orbit import Orbit lp = potential.LogarithmicHaloPotential(normalize=1.0) times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0]), Orbit([0.9, 0.3, 1.0, -0.3]), Orbit([1.2, -0.3, 0.7, 5.0]), ] orbits = Orbit(orbits_list) numpy.random.seed(1) dxdv = (2.0 * numpy.random.uniform(size=orbits.shape + (4,)) - 1) / 10.0 # Default, C integration integrator = "dopr54_c" orbits.integrate_dxdv(dxdv, times, lp, method=integrator, rectIn=True, rectOut=True) # Integrate as multiple Orbits for o, tdxdv in zip(orbits_list, dxdv): o.integrate_dxdv(tdxdv, times, lp, method=integrator, rectIn=True, rectOut=True) assert ( numpy.amax( numpy.fabs( orbits.getOrbit_dxdv() - numpy.array([o.getOrbit_dxdv() for o in orbits_list]) ) ) < 1e-8 ), ( "Integration of the phase-space volume of multiple orbits as Orbits does not agree with integrating the phase-space volume of multiple orbits" ) # Python integration integrator = "odeint" orbits.integrate_dxdv(dxdv, times, lp, method=integrator, rectIn=True, rectOut=True) # Integrate as multiple Orbits for o, tdxdv in zip(orbits_list, dxdv): o.integrate_dxdv(tdxdv, times, lp, method=integrator, rectIn=True, rectOut=True) assert ( numpy.amax( numpy.fabs( orbits.getOrbit_dxdv() - numpy.array([o.getOrbit_dxdv() for o in orbits_list]) ) ) < 1e-8 ), ( "Integration of the phase-space volume of multiple orbits as Orbits does not agree with integrating the phase-space volume of multiple orbits" ) return None # Test that the 3D SOS function returns points with z=0, vz > 0 def test_SOS_3D(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0, 0.1, 0.0]), Orbit([0.9, 0.3, 1.0, -0.3, 0.4, 3.0]), Orbit([1.2, -0.3, 0.7, 0.5, -0.5, 6.0]), ] orbits = Orbit(orbits_list) pot = potential.MWPotential2014 for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: orbits.SOS( pot, method=method, ncross=500 if "_c" in method else 20, force_map="rk" in method, t0=numpy.arange(len(orbits)), ) zs = orbits.z(orbits.t) vzs = orbits.vz(orbits.t) assert (numpy.fabs(zs) < 10.0**-6.0).all(), ( f"z on SOS is not zero for integrate_sos for method={method}" ) assert (vzs > 0.0).all(), ( f"vz on SOS is not positive for integrate_sos for method={method}" ) return None # Test that the 2D SOS function returns points with x=0, vx > 0 def test_SOS_2Dx(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0]), Orbit([0.9, 0.3, 1.0, 3.0]), Orbit([1.2, -0.3, 0.7, 6.0]), ] orbits = Orbit(orbits_list) pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: orbits.SOS( pot, method=method, ncross=500 if "_c" in method else 20, force_map="rk" in method, t0=numpy.arange(len(orbits)), surface="x", ) xs = orbits.x(orbits.t) vxs = orbits.vx(orbits.t) assert (numpy.fabs(xs) < 10.0**-6.0).all(), ( f"x on SOS is not zero for integrate_sos for method={method}" ) assert (vxs > 0.0).all(), ( f"vx on SOS is not positive for integrate_sos for method={method}" ) return None # Test that the 2D SOS function returns points with y=0, vy > 0 def test_SOS_2Dy(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0]), Orbit([0.9, 0.3, 1.0, 3.0]), Orbit([1.2, -0.3, 0.7, 6.0]), ] orbits = Orbit(orbits_list) pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: orbits.SOS( pot, method=method, ncross=500 if "_c" in method else 20, force_map="rk" in method, t0=numpy.arange(len(orbits)), surface="y", ) ys = orbits.y(orbits.t) vys = orbits.vy(orbits.t) assert (numpy.fabs(ys) < 10.0**-6.0).all(), ( f"y on SOS is not zero for integrate_sos for method={method}" ) assert (vys > 0.0).all(), ( f"vy on SOS is not positive for integrate_sos for method={method}" ) return None # Test that the SOS integration returns an error # when one orbit does not leave the surface def test_SOS_onsurfaceerror_3D(): from galpy.orbit import Orbit o = Orbit([[1.0, 0.1, 1.1, 0.1, 0.0, 0.0], [1.0, 0.1, 1.1, 0.0, 0.0, 0.0]]) with pytest.raises( RuntimeError, match="An orbit appears to be within the SOS surface. Refusing to perform specialized SOS integration, please use normal integration instead", ): o.SOS(potential.MWPotential2014) return None # Test that the 3D bruteSOS function returns points with z=0, vz > 0 def test_bruteSOS_3D(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0, 0.1, 0.0]), Orbit([0.9, 0.3, 1.0, -0.3, 0.4, 3.0]), Orbit([1.2, -0.3, 0.7, 0.5, -0.5, 6.0]), ] orbits = Orbit(orbits_list) pot = potential.MWPotential2014 for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: orbits.bruteSOS( numpy.linspace(0.0, 20.0 * numpy.pi, 100001), pot, method=method, force_map="rk" in method, ) zs = orbits.z(orbits.t) vzs = orbits.vz(orbits.t) assert (numpy.fabs(zs[~numpy.isnan(zs)]) < 10.0**-3.0).all(), ( f"z on bruteSOS is not zero for bruteSOS for method={method}" ) assert (vzs[~numpy.isnan(zs)] > 0.0).all(), ( f"vz on bruteSOS is not positive for bruteSOS for method={method}" ) return None # Test that the 2D bruteSOS function returns points with x=0, vx > 0 def test_bruteSOS_2Dx(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0]), Orbit([0.9, 0.3, 1.0, 3.0]), Orbit([1.2, -0.3, 0.7, 6.0]), ] orbits = Orbit(orbits_list) pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: orbits.bruteSOS( numpy.linspace(0.0, 20.0 * numpy.pi, 100001), pot, method=method, force_map="rk" in method, surface="x", ) xs = orbits.x(orbits.t) vxs = orbits.vx(orbits.t) assert (numpy.fabs(xs[~numpy.isnan(xs)]) < 10.0**-3.0).all(), ( f"x on SOS is not zero for bruteSOS for method={method}" ) assert (vxs[~numpy.isnan(xs)] > 0.0).all(), ( f"vx on SOS is not zero for bruteSOS for method={method}" ) return None # Test that the 2D bruteSOS function returns points with y=0, vy > 0 def test_bruteSOS_2Dy(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0]), Orbit([0.9, 0.3, 1.0, 3.0]), Orbit([1.2, -0.3, 0.7, 6.0]), ] orbits = Orbit(orbits_list) pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() for method in ["dopr54_c", "dop853_c", "rk4_c", "rk6_c", "dop853", "odeint"]: orbits.bruteSOS( numpy.linspace(0.0, 20.0 * numpy.pi, 100001), pot, method=method, force_map="rk" in method, surface="y", ) ys = orbits.y(orbits.t) vys = orbits.vy(orbits.t) assert (numpy.fabs(ys[~numpy.isnan(ys)]) < 10.0**-3.0).all(), ( f"y on SOS is not zero for bruteSOS for method={method}" ) assert (vys[~numpy.isnan(ys)] > 0.0).all(), ( f"vy on SOS is not zero for bruteSOS for method={method}" ) return None # Test slicing of orbits def test_slice_singleobject(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0, 0.1, 0.0]), Orbit([0.9, 0.3, 1.0, -0.3, 0.4, 3.0]), Orbit([1.2, -0.3, 0.7, 0.5, -0.5, 6.0]), ] orbits = Orbit(orbits_list) orbits.integrate(times, potential.MWPotential2014) indices = [0, 1, -1] for ii in indices: assert ( numpy.amax(numpy.fabs(orbits[ii].x(times) - orbits.x(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].vx(times) - orbits.vx(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].y(times) - orbits.y(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].vy(times) - orbits.vy(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].z(times) - orbits.z(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].vz(times) - orbits.vz(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].R(times) - orbits.R(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].vR(times) - orbits.vR(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].vT(times) - orbits.vT(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( ( ((orbits[ii].phi(times) - orbits.phi(times)[ii]) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None # Test slicing of orbits def test_slice_multipleobjects(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0, 0.1, 0.0]), Orbit([0.9, 0.3, 1.0, -0.3, 0.4, 3.0]), Orbit([1.2, -0.3, 0.7, 0.5, -0.5, 6.0]), Orbit([0.6, -0.4, 0.4, 0.25, -0.5, 6.0]), Orbit([1.1, -0.13, 0.17, 0.35, -0.5, 2.0]), ] orbits = Orbit(orbits_list) # Pre-integration orbits_slice = orbits[1:4] for ii in range(3): assert ( numpy.amax(numpy.fabs(orbits_slice.x()[ii] - orbits.x()[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_slice.vx()[ii] - orbits.vx()[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_slice.y()[ii] - orbits.y()[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_slice.vy()[ii] - orbits.vy()[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_slice.z()[ii] - orbits.z()[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_slice.vz()[ii] - orbits.vz()[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_slice.R()[ii] - orbits.R()[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_slice.vR()[ii] - orbits.vR()[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_slice.vT()[ii] - orbits.vT()[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_slice.phi()[ii] - orbits.phi()[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) # After integration orbits.integrate(times, potential.MWPotential2014) orbits_slice = orbits[1:4] for ii in range(3): assert ( numpy.amax(numpy.fabs(orbits_slice.x(times)[ii] - orbits.x(times)[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs(orbits_slice.vx(times)[ii] - orbits.vx(times)[ii + 1]) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_slice.y(times)[ii] - orbits.y(times)[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs(orbits_slice.vy(times)[ii] - orbits.vy(times)[ii + 1]) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_slice.z(times)[ii] - orbits.z(times)[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs(orbits_slice.vz(times)[ii] - orbits.vz(times)[ii + 1]) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_slice.R(times)[ii] - orbits.R(times)[ii + 1])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs(orbits_slice.vR(times)[ii] - orbits.vR(times)[ii + 1]) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs(orbits_slice.vT(times)[ii] - orbits.vT(times)[ii + 1]) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs(orbits_slice.phi(times)[ii] - orbits.phi(times)[ii + 1]) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None # Test slicing of orbits with non-trivial shapes def test_slice_singleobject_multidim(): from galpy.orbit import Orbit numpy.random.seed(1) nrand = (5, 1, 3) Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vxvv = numpy.rollaxis(numpy.array([Rs, vRs, vTs, zs, vzs, phis]), 0, 4) orbits = Orbit(vxvv) times = numpy.linspace(0.0, 10.0, 1001) orbits.integrate(times, potential.MWPotential2014) indices = [(0, 0, 0), (1, 0, 2), (-1, 0, 1)] for ii in indices: assert ( numpy.amax(numpy.fabs(orbits[ii].x(times) - orbits.x(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].vx(times) - orbits.vx(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].y(times) - orbits.y(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].vy(times) - orbits.vy(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].z(times) - orbits.z(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].vz(times) - orbits.vz(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].R(times) - orbits.R(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].vR(times) - orbits.vR(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits[ii].vT(times) - orbits.vT(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( ( ((orbits[ii].phi(times) - orbits.phi(times)[ii]) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None # Test slicing of orbits with non-trivial shapes def test_slice_multipleobjects_multidim(): from galpy.orbit import Orbit numpy.random.seed(1) nrand = (5, 1, 3) Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vxvv = numpy.rollaxis(numpy.array([Rs, vRs, vTs, zs, vzs, phis]), 0, 4) orbits = Orbit(vxvv) times = numpy.linspace(0.0, 10.0, 1001) # Pre-integration orbits_slice = orbits[1:4, 0, :2] for ii in range(3): for jj in range(1): for kk in range(2): assert ( numpy.amax( numpy.fabs( orbits_slice.x()[ii, kk] - orbits.x()[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.vx()[ii, kk] - orbits.vx()[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.y()[ii, kk] - orbits.y()[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.vy()[ii, kk] - orbits.vy()[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.z()[ii, kk] - orbits.z()[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.vz()[ii, kk] - orbits.vz()[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.R()[ii, kk] - orbits.R()[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.vR()[ii, kk] - orbits.vR()[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.vT()[ii, kk] - orbits.vT()[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.phi()[ii, kk] - orbits.phi()[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) # After integration orbits.integrate(times, potential.MWPotential2014) orbits_slice = orbits[1:4, 0, :2] for ii in range(3): for jj in range(1): for kk in range(2): assert ( numpy.amax( numpy.fabs( orbits_slice.x(times)[ii, kk] - orbits.x(times)[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.vx(times)[ii, kk] - orbits.vx(times)[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.y(times)[ii, kk] - orbits.y(times)[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.vy(times)[ii, kk] - orbits.vy(times)[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.z(times)[ii, kk] - orbits.z(times)[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.vz(times)[ii, kk] - orbits.vz(times)[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.R(times)[ii, kk] - orbits.R(times)[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.vR(times)[ii, kk] - orbits.vR(times)[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.vT(times)[ii, kk] - orbits.vT(times)[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( orbits_slice.phi(times)[ii, kk] - orbits.phi(times)[ii + 1, jj, kk] ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None def test_slice_integratedorbit_wrapperpot_367(): # Test related to issue 367: slicing orbits with a potential that includes # a wrapper potential (from Ted Mackereth) from galpy.orbit import Orbit from galpy.potential import ( DehnenBarPotential, DehnenSmoothWrapperPotential, LogarithmicHaloPotential, ) # initialise a wrapper potential tform = -10.0 tsteady = 5.0 omega = 1.85 angle = 25.0 / 180.0 * numpy.pi dp = DehnenBarPotential( omegab=omega, rb=3.5 / 8.0, Af=(1.0 / 75.0), tform=tform, tsteady=tsteady, barphi=angle, ) lhp = LogarithmicHaloPotential(normalize=1.0) dswp = DehnenSmoothWrapperPotential( pot=dp, tform=-4.0 * 2.0 * numpy.pi / dp.OmegaP(), tsteady=2.0 * 2.0 * numpy.pi / dp.OmegaP(), ) pot = [lhp, dswp] # initialise 2 random orbits r = numpy.random.randn(2) * 0.01 + 1.0 z = numpy.random.randn(2) * 0.01 + 0.2 phi = numpy.random.randn(2) * 0.01 + 0.0 vR = numpy.random.randn(2) * 0.01 + 0.0 vT = numpy.random.randn(2) * 0.01 + 1.0 vz = numpy.random.randn(2) * 0.01 + 0.02 vxvv = numpy.dstack([r, vR, vT, z, vz, phi])[0] os = Orbit(vxvv) times = numpy.linspace(0.0, 100.0, 3000) os.integrate(times, pot) # This failed in #367 assert not os[0] is None, ( "Slicing an integrated Orbits instance with a WrapperPotential does not work" ) return None # Test that slicing of orbits propagates unit info def test_slice_physical_issue385(): from galpy.orbit import Orbit ra = [17.2875, 302.2875, 317.79583333, 306.60833333, 9.65833333, 147.2] dec = [61.54730278, 42.86525833, 17.72774722, 9.45011944, -7.69072222, 13.74425556] dist = [0.16753225, 0.08499065, 0.03357057, 0.05411548, 0.11946004, 0.0727802] pmra = [633.01, 119.536, -122.216, 116.508, 20.34, 373.05] pmdec = [65.303, 540.224, -899.263, -548.329, -546.373, -774.38] vlos = [-317.86, -195.44, -44.15, -246.76, -46.79, -15.17] orbits = Orbit( numpy.column_stack([ra, dec, dist, pmra, pmdec, vlos]), radec=True, ro=9.0, vo=230.0, solarmotion=[-11.1, 24.0, 7.25], ) assert orbits._roSet, ( "Test Orbit instance that was supposed to have physical output turned does not" ) assert orbits._voSet, ( "Test Orbit instance that was supposed to have physical output turned does not" ) assert numpy.fabs(orbits._ro - 9.0) < 1e-10, ( "Test Orbit instance that was supposed to have physical output turned does not have the right ro" ) assert numpy.fabs(orbits._vo - 230.0) < 1e-10, ( "Test Orbit instance that was supposed to have physical output turned does not have the right vo" ) for ii in range(orbits.size): assert orbits[ii]._roSet, ( "Sliced Orbit instance that was supposed to have physical output turned does not" ) assert orbits[ii]._voSet, ( "Sliced Orbit instance that was supposed to have physical output turned does not" ) assert numpy.fabs(orbits[ii]._ro - 9.0) < 1e-10, ( "Sliced Orbit instance that was supposed to have physical output turned does not have the right ro" ) assert numpy.fabs(orbits[ii]._vo - 230.0) < 1e-10, ( "Sliced Orbit instance that was supposed to have physical output turned does not have the right vo" ) assert numpy.fabs(orbits[ii]._zo - orbits._zo) < 1e-10, ( "Sliced Orbit instance that was supposed to have physical output turned does not have the right zo" ) assert numpy.all( numpy.fabs(orbits[ii]._solarmotion - orbits._solarmotion) < 1e-10 ), ( "Sliced Orbit instance that was supposed to have physical output turned does not have the right zo" ) assert numpy.amax(numpy.fabs(orbits[ii].x() - orbits.x()[ii])) < 1e-10, ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert numpy.amax(numpy.fabs(orbits[ii].vx() - orbits.vx()[ii])) < 1e-10, ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert numpy.amax(numpy.fabs(orbits[ii].y() - orbits.y()[ii])) < 1e-10, ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert numpy.amax(numpy.fabs(orbits[ii].vy() - orbits.vy()[ii])) < 1e-10, ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert numpy.amax(numpy.fabs(orbits[ii].z() - orbits.z()[ii])) < 1e-10, ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert numpy.amax(numpy.fabs(orbits[ii].vz() - orbits.vz()[ii])) < 1e-10, ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert numpy.amax(numpy.fabs(orbits[ii].R() - orbits.R()[ii])) < 1e-10, ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert numpy.amax(numpy.fabs(orbits[ii].vR() - orbits.vR()[ii])) < 1e-10, ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert numpy.amax(numpy.fabs(orbits[ii].vT() - orbits.vT()[ii])) < 1e-10, ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax( numpy.fabs( ( ((orbits[ii].phi() - orbits.phi()[ii]) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) ) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None # Test that slicing in the case of individual time arrays works as expected # Currently, the only way individual time arrays occur is through SOS integration # so we implementing this test using SOS integration def test_slice_indivtimes(): from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.0, 0.1, 0.0]), Orbit([0.9, 0.3, 1.0, -0.3, 0.4, 3.0]), Orbit([1.2, -0.3, 0.7, 0.5, -0.5, 6.0]), ] orbits = Orbit(orbits_list) pot = potential.MWPotential2014 orbits.SOS(pot, t0=numpy.arange(len(orbits))) # First check that we actually have individual times assert len(orbits.t.shape) >= len(orbits.orbit.shape) - 1, ( "Test should be using individual time arrays, but a single time array was found" ) # Now slice single and multiple assert numpy.all(orbits[0].t == orbits.t[0]), ( "Individually sliced orbit with individual time arrays does not produce the correct time array in the slice" ) assert numpy.all(orbits[1].t == orbits.t[1]), ( "Individually sliced orbit with individual time arrays does not produce the correct time array in the slice" ) assert numpy.all(orbits[:2].t == orbits.t[:2]), ( "Multiply-sliced orbit with individual time arrays does not produce the correct time array in the slice" ) assert numpy.all(orbits[1:4].t == orbits.t[1:4]), ( "Multiply-sliced orbit with individual time arrays does not produce the correct time array in the slice" ) return None # Test that initializing Orbits with orbits with different phase-space # dimensions raises an error def test_initialize_diffphasedim_error(): from galpy.orbit import Orbit # 2D with 3D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([[1.0, 0.1], [1.0, 0.1, 1.0]]) # 2D with 4D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([[1.0, 0.1], [1.0, 0.1, 1.0, 0.1]]) # 2D with 5D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([[1.0, 0.1], [1.0, 0.1, 1.0, 0.1, 0.2]]) # 2D with 6D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([[1.0, 0.1], [1.0, 0.1, 1.0, 0.1, 0.2, 3.0]]) # 3D with 4D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([[1.0, 0.1, 1.0], [1.0, 0.1, 1.0, 0.1]]) # 3D with 5D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([[1.0, 0.1, 1.0], [1.0, 0.1, 1.0, 0.1, 0.2]]) # 3D with 6D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([[1.0, 0.1, 1.0], [1.0, 0.1, 1.0, 0.1, 0.2, 6.0]]) # 4D with 5D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([[1.0, 0.1, 1.0, 2.0], [1.0, 0.1, 1.0, 0.1, 0.2]]) # 4D with 6D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([[1.0, 0.1, 1.0, 2.0], [1.0, 0.1, 1.0, 0.1, 0.2, 6.0]]) # 5D with 6D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([[1.0, 0.1, 1.0, 0.2, -0.2], [1.0, 0.1, 1.0, 0.1, 0.2, 6.0]]) # Also as Orbit inputs # 2D with 3D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([Orbit([1.0, 0.1]), Orbit([1.0, 0.1, 1.0])]) # 2D with 4D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([Orbit([1.0, 0.1]), Orbit([1.0, 0.1, 1.0, 0.1])]) # 2D with 5D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([Orbit([1.0, 0.1]), Orbit([1.0, 0.1, 1.0, 0.1, 0.2])]) # 2D with 6D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([Orbit([1.0, 0.1]), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, 3.0])]) # 3D with 4D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([Orbit([1.0, 0.1, 1.0]), Orbit([1.0, 0.1, 1.0, 0.1])]) # 3D with 5D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([Orbit([1.0, 0.1, 1.0]), Orbit([1.0, 0.1, 1.0, 0.1, 0.2])]) # 3D with 6D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([Orbit([1.0, 0.1, 1.0]), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, 6.0])]) # 4D with 5D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([Orbit([1.0, 0.1, 1.0, 2.0]), Orbit([1.0, 0.1, 1.0, 0.1, 0.2])]) # 4D with 6D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit([Orbit([1.0, 0.1, 1.0, 2.0]), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, 6.0])]) # 5D with 6D with pytest.raises( (RuntimeError, ValueError), match="All individual orbits in an Orbit class must have the same phase-space dimensionality", ) as excinfo: Orbit( [Orbit([1.0, 0.1, 1.0, 0.2, -0.2]), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, 6.0])] ) return None # Test that initializing Orbits with a list of non-scalar Orbits raises an error def test_initialize_listorbits_error(): from galpy.orbit import Orbit with pytest.raises(RuntimeError) as excinfo: Orbit([Orbit([[1.0, 0.1], [1.0, 0.1]]), Orbit([[1.0, 0.1], [1.0, 0.1]])]) return None # Test that initializing Orbits with an array of the wrong shape raises an error, that is, the phase-space dim part is > 6 or 1 def test_initialize_wrongshape(): from galpy.orbit import Orbit with pytest.raises(RuntimeError) as excinfo: Orbit(numpy.random.uniform(size=(2, 12))) with pytest.raises(RuntimeError) as excinfo: Orbit(numpy.random.uniform(size=(3, 12))) with pytest.raises(RuntimeError) as excinfo: Orbit(numpy.random.uniform(size=(4, 12))) with pytest.raises(RuntimeError) as excinfo: Orbit(numpy.random.uniform(size=(2, 1))) with pytest.raises(RuntimeError) as excinfo: Orbit(numpy.random.uniform(size=(5, 12))) with pytest.raises(RuntimeError) as excinfo: Orbit(numpy.random.uniform(size=(6, 12))) with pytest.raises(RuntimeError) as excinfo: Orbit(numpy.random.uniform(size=(7, 12))) return None def test_orbits_consistentro(): from galpy.orbit import Orbit ro = 7.0 # Initialize Orbits from list of Orbit instances orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], ro=ro), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], ro=ro), ] orbits = Orbit(orbits_list) # Check that ro is taken correctly assert numpy.fabs(orbits._ro - orbits_list[0]._ro) < 1e-10, ( "Orbits' ro not correctly taken from input list of Orbit instances" ) assert orbits._roSet, ( "Orbits' ro not correctly taken from input list of Orbit instances" ) # Check that consistency of ros is enforced with pytest.raises(RuntimeError) as excinfo: orbits = Orbit(orbits_list, ro=6.0) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], ro=ro), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], ro=ro * 1.2), ] with pytest.raises(RuntimeError) as excinfo: orbits = Orbit(orbits_list, ro=ro) return None def test_orbits_consistentvo(): from galpy.orbit import Orbit vo = 230.0 # Initialize Orbits from list of Orbit instances orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], vo=vo), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], vo=vo), ] orbits = Orbit(orbits_list) # Check that vo is taken correctly assert numpy.fabs(orbits._vo - orbits_list[0]._vo) < 1e-10, ( "Orbits' vo not correctly taken from input list of Orbit instances" ) assert orbits._voSet, ( "Orbits' vo not correctly taken from input list of Orbit instances" ) # Check that consistency of vos is enforced with pytest.raises(RuntimeError) as excinfo: orbits = Orbit(orbits_list, vo=210.0) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], vo=vo), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], vo=vo * 1.2), ] with pytest.raises(RuntimeError) as excinfo: orbits = Orbit(orbits_list, vo=vo) return None def test_orbits_consistentzo(): from galpy.orbit import Orbit zo = 0.015 # Initialize Orbits from list of Orbit instances orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], zo=zo), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], zo=zo), ] orbits = Orbit(orbits_list) # Check that zo is taken correctly assert numpy.fabs(orbits._zo - orbits_list[0]._zo) < 1e-10, ( "Orbits' zo not correctly taken from input list of Orbit instances" ) # Check that consistency of zos is enforced with pytest.raises(RuntimeError) as excinfo: orbits = Orbit(orbits_list, zo=0.045) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], zo=zo), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], zo=zo * 1.2), ] with pytest.raises(RuntimeError) as excinfo: orbits = Orbit(orbits_list, zo=zo) return None def test_orbits_consistentsolarmotion(): from galpy.orbit import Orbit solarmotion = numpy.array([-10.0, 20.0, 30.0]) # Initialize Orbits from list of Orbit instances orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], solarmotion=solarmotion), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], solarmotion=solarmotion), ] orbits = Orbit(orbits_list) # Check that solarmotion is taken correctly assert numpy.all( numpy.fabs(orbits._solarmotion - orbits_list[0]._solarmotion) < 1e-10 ), "Orbits' solarmotion not correctly taken from input list of Orbit instances" # Check that consistency of solarmotions is enforced with pytest.raises(RuntimeError) as excinfo: orbits = Orbit(orbits_list, solarmotion=numpy.array([15.0, 20.0, 30])) with pytest.raises(RuntimeError) as excinfo: orbits = Orbit(orbits_list, solarmotion=numpy.array([-10.0, 25.0, 30])) with pytest.raises(RuntimeError) as excinfo: orbits = Orbit(orbits_list, solarmotion=numpy.array([-10.0, 20.0, -30])) orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], solarmotion=solarmotion), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], solarmotion=solarmotion * 1.2), ] with pytest.raises(RuntimeError) as excinfo: orbits = Orbit(orbits_list, solarmotion=solarmotion) return None def test_orbits_stringsolarmotion(): from galpy.orbit import Orbit solarmotion = "hogg" orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], solarmotion=solarmotion), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], solarmotion=solarmotion), ] orbits = Orbit(orbits_list, solarmotion="hogg") assert numpy.all( numpy.fabs(orbits._solarmotion - numpy.array([-10.1, 4.0, 6.7])) < 1e-10 ), "String solarmotion not parsed correctly" return None def test_orbits_dim_2dPot_3dOrb(): # Test that orbit integration throws an error when using a potential that # is lower dimensional than the orbit (using ~Plevne's example) from galpy.orbit import Orbit from galpy.util import conversion b_p = potential.PowerSphericalPotentialwCutoff( alpha=1.8, rc=1.9 / 8.0, normalize=0.05 ) ell_p = potential.EllipticalDiskPotential() pota = [b_p, ell_p] o = Orbit( [ Orbit( vxvv=[20.0, 10.0, 2.0, 3.2, 3.4, -100.0], radec=True, ro=8.0, vo=220.0 ), Orbit( vxvv=[20.0, 10.0, 2.0, 3.2, 3.4, -100.0], radec=True, ro=8.0, vo=220.0 ), ] ) ts = numpy.linspace( 0.0, 3.5 / conversion.time_in_Gyr(vo=220.0, ro=8.0), 1000, endpoint=True ) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pota, method="odeint") return None def test_orbit_dim_1dPot_3dOrb(): # Test that orbit integration throws an error when using a potential that # is lower dimensional than the orbit, for a 1D potential from galpy.orbit import Orbit from galpy.util import conversion b_p = potential.PowerSphericalPotentialwCutoff( alpha=1.8, rc=1.9 / 8.0, normalize=0.05 ) pota = potential.RZToverticalPotential(b_p, 1.1) o = Orbit( [ Orbit( vxvv=[20.0, 10.0, 2.0, 3.2, 3.4, -100.0], radec=True, ro=8.0, vo=220.0 ), Orbit( vxvv=[20.0, 10.0, 2.0, 3.2, 3.4, -100.0], radec=True, ro=8.0, vo=220.0 ), ] ) ts = numpy.linspace( 0.0, 3.5 / conversion.time_in_Gyr(vo=220.0, ro=8.0), 1000, endpoint=True ) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pota, method="odeint") return None def test_orbit_dim_1dPot_2dOrb(): # Test that orbit integration throws an error when using a potential that # is lower dimensional than the orbit, for a 1D potential from galpy.orbit import Orbit b_p = potential.PowerSphericalPotentialwCutoff( alpha=1.8, rc=1.9 / 8.0, normalize=0.05 ) pota = [b_p.toVertical(1.1)] o = Orbit([Orbit(vxvv=[1.1, 0.1, 1.1, 0.1]), Orbit(vxvv=[1.1, 0.1, 1.1, 0.1])]) ts = numpy.linspace(0.0, 10.0, 1001) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pota, method="leapfrog") with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pota, method="dop853") return None # Test the error for when explicit stepsize does not divide the output stepsize def test_check_integrate_dt(): from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) o = Orbit( [Orbit([1.0, 0.1, 1.2, 0.3, 0.2, 2.0]), Orbit([1.0, 0.1, 1.2, 0.3, 0.2, 2.0])] ) times = numpy.linspace(0.0, 7.0, 251) # This shouldn't work try: o.integrate(times, lp, dt=(times[1] - times[0]) / 4.0 * 1.1) except ValueError: pass else: raise AssertionError( "dt that is not an integer divisor of the output step size does not raise a ValueError" ) # This should try: o.integrate(times, lp, dt=(times[1] - times[0]) / 4.0) except ValueError: raise AssertionError( "dt that is an integer divisor of the output step size raises a ValueError" ) return None # Test that evaluating coordinate functions for integrated orbits works def test_coordinate_interpolation(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 10 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] # Before integration for ii in range(nrand): # .time is special, just a single array assert numpy.all(numpy.fabs(os.time() - list_os[ii].time()) < 1e-10), ( "Evaluating Orbits time does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.R()[ii] - list_os[ii].R()) < 1e-10), ( "Evaluating Orbits R does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.r()[ii] - list_os[ii].r()) < 1e-10), ( "Evaluating Orbits r does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vR()[ii] - list_os[ii].vR()) < 1e-10), ( "Evaluating Orbits vR does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vT()[ii] - list_os[ii].vT()) < 1e-10), ( "Evaluating Orbits vT does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.z()[ii] - list_os[ii].z()) < 1e-10), ( "Evaluating Orbits z does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vz()[ii] - list_os[ii].vz()) < 1e-10), ( "Evaluating Orbits vz does not agree with Orbit" ) assert numpy.all( numpy.fabs( ((os.phi()[ii] - list_os[ii].phi() + numpy.pi) % (2.0 * numpy.pi)) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all(numpy.fabs(os.x()[ii] - list_os[ii].x()) < 1e-10), ( "Evaluating Orbits x does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.y()[ii] - list_os[ii].y()) < 1e-10), ( "Evaluating Orbits y does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vx()[ii] - list_os[ii].vx()) < 1e-10), ( "Evaluating Orbits vx does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vy()[ii] - list_os[ii].vy()) < 1e-10), ( "Evaluating Orbits vy does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vphi()[ii] - list_os[ii].vphi()) < 1e-10), ( "Evaluating Orbits vphi does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.ra()[ii] - list_os[ii].ra()) < 1e-10), ( "Evaluating Orbits ra does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.dec()[ii] - list_os[ii].dec()) < 1e-10), ( "Evaluating Orbits dec does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.dist()[ii] - list_os[ii].dist()) < 1e-10), ( "Evaluating Orbits dist does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.ll()[ii] - list_os[ii].ll()) < 1e-10), ( "Evaluating Orbits ll does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.bb()[ii] - list_os[ii].bb()) < 1e-10), ( "Evaluating Orbits bb does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.pmra()[ii] - list_os[ii].pmra()) < 1e-10), ( "Evaluating Orbits pmra does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.pmdec()[ii] - list_os[ii].pmdec()) < 1e-10), ( "Evaluating Orbits pmdec does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.pmll()[ii] - list_os[ii].pmll()) < 1e-10), ( "Evaluating Orbits pmll does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.pmbb()[ii] - list_os[ii].pmbb()) < 1e-10), ( "Evaluating Orbits pmbb does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vra()[ii] - list_os[ii].vra()) < 1e-10), ( "Evaluating Orbits vra does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vdec()[ii] - list_os[ii].vdec()) < 1e-10), ( "Evaluating Orbits vdec does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vll()[ii] - list_os[ii].vll()) < 1e-10), ( "Evaluating Orbits vll does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vbb()[ii] - list_os[ii].vbb()) < 1e-10), ( "Evaluating Orbits vbb does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vlos()[ii] - list_os[ii].vlos()) < 1e-10), ( "Evaluating Orbits vlos does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.helioX()[ii] - list_os[ii].helioX()) < 1e-10), ( "Evaluating Orbits helioX does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.helioY()[ii] - list_os[ii].helioY()) < 1e-10), ( "Evaluating Orbits helioY does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.helioZ()[ii] - list_os[ii].helioZ()) < 1e-10), ( "Evaluating Orbits helioZ does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.U()[ii] - list_os[ii].U()) < 1e-10), ( "Evaluating Orbits U does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.V()[ii] - list_os[ii].V()) < 1e-10), ( "Evaluating Orbits V does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.W()[ii] - list_os[ii].W()) < 1e-10), ( "Evaluating Orbits W does not agree with Orbit" ) assert numpy.all( numpy.fabs(os.SkyCoord().ra[ii] - list_os[ii].SkyCoord().ra).to(u.deg).value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs(os.SkyCoord().dec[ii] - list_os[ii].SkyCoord().dec) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs(os.SkyCoord().distance[ii] - list_os[ii].SkyCoord().distance) .to(u.kpc) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" if _APY3: assert numpy.all( numpy.fabs( os.SkyCoord().pm_ra_cosdec[ii] - list_os[ii].SkyCoord().pm_ra_cosdec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs(os.SkyCoord().pm_dec[ii] - list_os[ii].SkyCoord().pm_dec) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord().radial_velocity[ii] - list_os[ii].SkyCoord().radial_velocity ) .to(u.km / u.s) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" # Integrate all times = numpy.linspace(0.0, 10.0, 1001) os.integrate(times, MWPotential2014) [o.integrate(times, MWPotential2014) for o in list_os] # Test exact times of integration for ii in range(nrand): # .time is special, just a single array assert numpy.all( numpy.fabs(os.time(times) - list_os[ii].time(times)) < 1e-10 ), "Evaluating Orbits time does not agree with Orbit" assert numpy.all(numpy.fabs(os.R(times)[ii] - list_os[ii].R(times)) < 1e-10), ( "Evaluating Orbits R does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.r(times)[ii] - list_os[ii].r(times)) < 1e-10), ( "Evaluating Orbits r does not agree with Orbit" ) assert numpy.all( numpy.fabs(os.vR(times)[ii] - list_os[ii].vR(times)) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(times)[ii] - list_os[ii].vT(times)) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all(numpy.fabs(os.z(times)[ii] - list_os[ii].z(times)) < 1e-10), ( "Evaluating Orbits z does not agree with Orbit" ) assert numpy.all( numpy.fabs(os.vz(times)[ii] - list_os[ii].vz(times)) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs( ( (os.phi(times)[ii] - list_os[ii].phi(times) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all(numpy.fabs(os.x(times)[ii] - list_os[ii].x(times)) < 1e-10), ( "Evaluating Orbits x does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.y(times)[ii] - list_os[ii].y(times)) < 1e-10), ( "Evaluating Orbits y does not agree with Orbit" ) assert numpy.all( numpy.fabs(os.vx(times)[ii] - list_os[ii].vx(times)) < 1e-10 ), "Evaluating Orbits vx does not agree with Orbit" assert numpy.all( numpy.fabs(os.vy(times)[ii] - list_os[ii].vy(times)) < 1e-10 ), "Evaluating Orbits vy does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(times)[ii] - list_os[ii].vphi(times)) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" assert numpy.all( numpy.fabs(os.ra(times)[ii] - list_os[ii].ra(times)) < 1e-10 ), "Evaluating Orbits ra does not agree with Orbit" assert numpy.all( numpy.fabs(os.dec(times)[ii] - list_os[ii].dec(times)) < 1e-10 ), "Evaluating Orbits dec does not agree with Orbit" assert numpy.all( numpy.fabs(os.dist(times)[ii] - list_os[ii].dist(times)) < 1e-10 ), "Evaluating Orbits dist does not agree with Orbit" assert numpy.all( numpy.fabs(os.ll(times)[ii] - list_os[ii].ll(times)) < 1e-10 ), "Evaluating Orbits ll does not agree with Orbit" assert numpy.all( numpy.fabs(os.bb(times)[ii] - list_os[ii].bb(times)) < 1e-10 ), "Evaluating Orbits bb does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmra(times)[ii] - list_os[ii].pmra(times)) < 1e-10 ), "Evaluating Orbits pmra does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmdec(times)[ii] - list_os[ii].pmdec(times)) < 1e-10 ), "Evaluating Orbits pmdec does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmll(times)[ii] - list_os[ii].pmll(times)) < 1e-10 ), "Evaluating Orbits pmll does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmbb(times)[ii] - list_os[ii].pmbb(times)) < 1e-10 ), "Evaluating Orbits pmbb does not agree with Orbit" assert numpy.all( numpy.fabs(os.vra(times)[ii] - list_os[ii].vra(times)) < 1e-10 ), "Evaluating Orbits vra does not agree with Orbit" assert numpy.all( numpy.fabs(os.vdec(times)[ii] - list_os[ii].vdec(times)) < 1e-10 ), "Evaluating Orbits vdec does not agree with Orbit" assert numpy.all( numpy.fabs(os.vll(times)[ii] - list_os[ii].vll(times)) < 1e-10 ), "Evaluating Orbits vll does not agree with Orbit" assert numpy.all( numpy.fabs(os.vbb(times)[ii] - list_os[ii].vbb(times)) < 1e-10 ), "Evaluating Orbits vbb does not agree with Orbit" assert numpy.all( numpy.fabs(os.vlos(times)[ii] - list_os[ii].vlos(times)) < 1e-9 ), "Evaluating Orbits vlos does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioX(times)[ii] - list_os[ii].helioX(times)) < 1e-10 ), "Evaluating Orbits helioX does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioY(times)[ii] - list_os[ii].helioY(times)) < 1e-10 ), "Evaluating Orbits helioY does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioZ(times)[ii] - list_os[ii].helioZ(times)) < 1e-10 ), "Evaluating Orbits helioZ does not agree with Orbit" assert numpy.all(numpy.fabs(os.U(times)[ii] - list_os[ii].U(times)) < 1e-10), ( "Evaluating Orbits U does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.V(times)[ii] - list_os[ii].V(times)) < 1e-10), ( "Evaluating Orbits V does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.W(times)[ii] - list_os[ii].W(times)) < 1e-10), ( "Evaluating Orbits W does not agree with Orbit" ) assert numpy.all( numpy.fabs(os.SkyCoord(times).ra[ii] - list_os[ii].SkyCoord(times).ra) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs(os.SkyCoord(times).dec[ii] - list_os[ii].SkyCoord(times).dec) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).distance[ii] - list_os[ii].SkyCoord(times).distance ) .to(u.kpc) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" if _APY3: assert numpy.all( numpy.fabs( os.SkyCoord(times).pm_ra_cosdec[ii] - list_os[ii].SkyCoord(times).pm_ra_cosdec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).pm_dec[ii] - list_os[ii].SkyCoord(times).pm_dec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).radial_velocity[ii] - list_os[ii].SkyCoord(times).radial_velocity ) .to(u.km / u.s) .value < 1e-9 ), "Evaluating Orbits SkyCoord does not agree with Orbit" # Also a single time in the array ... # .time is special, just a single array assert numpy.all( numpy.fabs(os.time(times[1]) - list_os[ii].time(times[1])) < 1e-10 ), "Evaluating Orbits time does not agree with Orbit" assert numpy.all( numpy.fabs(os.R(times[1])[ii] - list_os[ii].R(times[1])) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(times[1])[ii] - list_os[ii].r(times[1])) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(times[1])[ii] - list_os[ii].vR(times[1])) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(times[1])[ii] - list_os[ii].vT(times[1])) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z(times[1])[ii] - list_os[ii].z(times[1])) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz(times[1])[ii] - list_os[ii].vz(times[1])) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs( ( (os.phi(times[1])[ii] - list_os[ii].phi(times[1]) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all( numpy.fabs(os.x(times[1])[ii] - list_os[ii].x(times[1])) < 1e-10 ), "Evaluating Orbits x does not agree with Orbit" assert numpy.all( numpy.fabs(os.y(times[1])[ii] - list_os[ii].y(times[1])) < 1e-10 ), "Evaluating Orbits y does not agree with Orbit" assert numpy.all( numpy.fabs(os.vx(times[1])[ii] - list_os[ii].vx(times[1])) < 1e-10 ), "Evaluating Orbits vx does not agree with Orbit" assert numpy.all( numpy.fabs(os.vy(times[1])[ii] - list_os[ii].vy(times[1])) < 1e-10 ), "Evaluating Orbits vy does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(times[1])[ii] - list_os[ii].vphi(times[1])) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" assert numpy.all( numpy.fabs(os.ra(times[1])[ii] - list_os[ii].ra(times[1])) < 1e-10 ), "Evaluating Orbits ra does not agree with Orbit" assert numpy.all( numpy.fabs(os.dec(times[1])[ii] - list_os[ii].dec(times[1])) < 1e-10 ), "Evaluating Orbits dec does not agree with Orbit" assert numpy.all( numpy.fabs(os.dist(times[1])[ii] - list_os[ii].dist(times[1])) < 1e-10 ), "Evaluating Orbits dist does not agree with Orbit" assert numpy.all( numpy.fabs(os.ll(times[1])[ii] - list_os[ii].ll(times[1])) < 1e-10 ), "Evaluating Orbits ll does not agree with Orbit" assert numpy.all( numpy.fabs(os.bb(times[1])[ii] - list_os[ii].bb(times[1])) < 1e-10 ), "Evaluating Orbits bb does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmra(times[1])[ii] - list_os[ii].pmra(times[1])) < 1e-10 ), "Evaluating Orbits pmra does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmdec(times[1])[ii] - list_os[ii].pmdec(times[1])) < 1e-10 ), "Evaluating Orbits pmdec does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmll(times[1])[ii] - list_os[ii].pmll(times[1])) < 1e-10 ), "Evaluating Orbits pmll does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmbb(times[1])[ii] - list_os[ii].pmbb(times[1])) < 1e-10 ), "Evaluating Orbits pmbb does not agree with Orbit" assert numpy.all( numpy.fabs(os.vra(times[1])[ii] - list_os[ii].vra(times[1])) < 1e-10 ), "Evaluating Orbits vra does not agree with Orbit" assert numpy.all( numpy.fabs(os.vdec(times[1])[ii] - list_os[ii].vdec(times[1])) < 1e-10 ), "Evaluating Orbits vdec does not agree with Orbit" assert numpy.all( numpy.fabs(os.vll(times[1])[ii] - list_os[ii].vll(times[1])) < 1e-10 ), "Evaluating Orbits vll does not agree with Orbit" assert numpy.all( numpy.fabs(os.vbb(times[1])[ii] - list_os[ii].vbb(times[1])) < 1e-10 ), "Evaluating Orbits vbb does not agree with Orbit" assert numpy.all( numpy.fabs(os.vlos(times[1])[ii] - list_os[ii].vlos(times[1])) < 1e-10 ), "Evaluating Orbits vlos does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioX(times[1])[ii] - list_os[ii].helioX(times[1])) < 1e-10 ), "Evaluating Orbits helioX does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioY(times[1])[ii] - list_os[ii].helioY(times[1])) < 1e-10 ), "Evaluating Orbits helioY does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioZ(times[1])[ii] - list_os[ii].helioZ(times[1])) < 1e-10 ), "Evaluating Orbits helioZ does not agree with Orbit" assert numpy.all( numpy.fabs(os.U(times[1])[ii] - list_os[ii].U(times[1])) < 1e-10 ), "Evaluating Orbits U does not agree with Orbit" assert numpy.all( numpy.fabs(os.V(times[1])[ii] - list_os[ii].V(times[1])) < 1e-10 ), "Evaluating Orbits V does not agree with Orbit" assert numpy.all( numpy.fabs(os.W(times[1])[ii] - list_os[ii].W(times[1])) < 1e-10 ), "Evaluating Orbits W does not agree with Orbit" assert numpy.all( numpy.fabs(os.SkyCoord(times[1]).ra[ii] - list_os[ii].SkyCoord(times[1]).ra) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times[1]).dec[ii] - list_os[ii].SkyCoord(times[1]).dec ) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times[1]).distance[ii] - list_os[ii].SkyCoord(times[1]).distance ) .to(u.kpc) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" if _APY3: assert numpy.all( numpy.fabs( os.SkyCoord(times[1]).pm_ra_cosdec[ii] - list_os[ii].SkyCoord(times[1]).pm_ra_cosdec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times[1]).pm_dec[ii] - list_os[ii].SkyCoord(times[1]).pm_dec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times[1]).radial_velocity[ii] - list_os[ii].SkyCoord(times[1]).radial_velocity ) .to(u.km / u.s) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" # Test actual interpolated itimes = times[:-2] + (times[1] - times[0]) / 2.0 for ii in range(nrand): assert numpy.all( numpy.fabs(os.R(itimes)[ii] - list_os[ii].R(itimes)) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(itimes)[ii] - list_os[ii].r(itimes)) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(itimes)[ii] - list_os[ii].vR(itimes)) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(itimes)[ii] - list_os[ii].vT(itimes)) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z(itimes)[ii] - list_os[ii].z(itimes)) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz(itimes)[ii] - list_os[ii].vz(itimes)) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs( ( (os.phi(itimes)[ii] - list_os[ii].phi(itimes) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all( numpy.fabs(os.x(itimes)[ii] - list_os[ii].x(itimes)) < 1e-10 ), "Evaluating Orbits x does not agree with Orbit" assert numpy.all( numpy.fabs(os.y(itimes)[ii] - list_os[ii].y(itimes)) < 1e-10 ), "Evaluating Orbits y does not agree with Orbit" assert numpy.all( numpy.fabs(os.vx(itimes)[ii] - list_os[ii].vx(itimes)) < 1e-10 ), "Evaluating Orbits vx does not agree with Orbit" assert numpy.all( numpy.fabs(os.vy(itimes)[ii] - list_os[ii].vy(itimes)) < 1e-10 ), "Evaluating Orbits vy does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(itimes)[ii] - list_os[ii].vphi(itimes)) < 1e-10 ), "Evaluating Orbits vphidoes not agree with Orbit" assert numpy.all( numpy.fabs(os.ra(itimes)[ii] - list_os[ii].ra(itimes)) < 1e-10 ), "Evaluating Orbits ra does not agree with Orbit" assert numpy.all( numpy.fabs(os.dec(itimes)[ii] - list_os[ii].dec(itimes)) < 1e-10 ), "Evaluating Orbits dec does not agree with Orbit" assert numpy.all( numpy.fabs(os.dist(itimes)[ii] - list_os[ii].dist(itimes)) < 1e-10 ), "Evaluating Orbits dist does not agree with Orbit" assert numpy.all( numpy.fabs(os.ll(itimes)[ii] - list_os[ii].ll(itimes)) < 1e-10 ), "Evaluating Orbits ll does not agree with Orbit" assert numpy.all( numpy.fabs(os.bb(itimes)[ii] - list_os[ii].bb(itimes)) < 1e-10 ), "Evaluating Orbits bb does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmra(itimes)[ii] - list_os[ii].pmra(itimes)) < 1e-10 ), "Evaluating Orbits pmra does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmdec(itimes)[ii] - list_os[ii].pmdec(itimes)) < 1e-10 ), "Evaluating Orbits pmdec does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmll(itimes)[ii] - list_os[ii].pmll(itimes)) < 1e-10 ), "Evaluating Orbits pmll does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmbb(itimes)[ii] - list_os[ii].pmbb(itimes)) < 1e-10 ), "Evaluating Orbits pmbb does not agree with Orbit" assert numpy.all( numpy.fabs(os.vra(itimes)[ii] - list_os[ii].vra(itimes)) < 1e-10 ), "Evaluating Orbits vra does not agree with Orbit" assert numpy.all( numpy.fabs(os.vdec(itimes)[ii] - list_os[ii].vdec(itimes)) < 1e-10 ), "Evaluating Orbits vdec does not agree with Orbit" assert numpy.all( numpy.fabs(os.vll(itimes)[ii] - list_os[ii].vll(itimes)) < 1e-10 ), "Evaluating Orbits ll does not agree with Orbit" assert numpy.all( numpy.fabs(os.vbb(itimes)[ii] - list_os[ii].vbb(itimes)) < 1e-10 ), "Evaluating Orbits vbb does not agree with Orbit" assert numpy.all( numpy.fabs(os.vlos(itimes)[ii] - list_os[ii].vlos(itimes)) < 1e-10 ), "Evaluating Orbits vlos does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioX(itimes)[ii] - list_os[ii].helioX(itimes)) < 1e-10 ), "Evaluating Orbits helioX does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioY(itimes)[ii] - list_os[ii].helioY(itimes)) < 1e-10 ), "Evaluating Orbits helioY does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioZ(itimes)[ii] - list_os[ii].helioZ(itimes)) < 1e-10 ), "Evaluating Orbits helioZ does not agree with Orbit" assert numpy.all( numpy.fabs(os.U(itimes)[ii] - list_os[ii].U(itimes)) < 1e-10 ), "Evaluating Orbits U does not agree with Orbit" assert numpy.all( numpy.fabs(os.V(itimes)[ii] - list_os[ii].V(itimes)) < 1e-10 ), "Evaluating Orbits V does not agree with Orbit" assert numpy.all( numpy.fabs(os.W(itimes)[ii] - list_os[ii].W(itimes)) < 1e-10 ), "Evaluating Orbits W does not agree with Orbit" assert numpy.all( numpy.fabs(os.SkyCoord(itimes).ra[ii] - list_os[ii].SkyCoord(itimes).ra) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs(os.SkyCoord(itimes).dec[ii] - list_os[ii].SkyCoord(itimes).dec) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(itimes).distance[ii] - list_os[ii].SkyCoord(itimes).distance ) .to(u.kpc) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" if _APY3: assert numpy.all( numpy.fabs( os.SkyCoord(itimes).pm_ra_cosdec[ii] - list_os[ii].SkyCoord(itimes).pm_ra_cosdec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(itimes).pm_dec[ii] - list_os[ii].SkyCoord(itimes).pm_dec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(itimes).radial_velocity[ii] - list_os[ii].SkyCoord(itimes).radial_velocity ) .to(u.km / u.s) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" # Also a single time in the array ... assert numpy.all( numpy.fabs(os.R(itimes[1])[ii] - list_os[ii].R(itimes[1])) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(itimes[1])[ii] - list_os[ii].r(itimes[1])) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(itimes[1])[ii] - list_os[ii].vR(itimes[1])) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(itimes[1])[ii] - list_os[ii].vT(itimes[1])) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z(itimes[1])[ii] - list_os[ii].z(itimes[1])) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz(itimes[1])[ii] - list_os[ii].vz(itimes[1])) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs( ( (os.phi(itimes[1])[ii] - list_os[ii].phi(itimes[1]) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all( numpy.fabs(os.ra(itimes[1])[ii] - list_os[ii].ra(itimes[1])) < 1e-10 ), "Evaluating Orbits ra does not agree with Orbit" assert numpy.all( numpy.fabs(os.dec(itimes[1])[ii] - list_os[ii].dec(itimes[1])) < 1e-10 ), "Evaluating Orbits dec does not agree with Orbit" assert numpy.all( numpy.fabs(os.dist(itimes[1])[ii] - list_os[ii].dist(itimes[1])) < 1e-10 ), "Evaluating Orbits dist does not agree with Orbit" assert numpy.all( numpy.fabs(os.ll(itimes[1])[ii] - list_os[ii].ll(itimes[1])) < 1e-10 ), "Evaluating Orbits ll does not agree with Orbit" assert numpy.all( numpy.fabs(os.bb(itimes[1])[ii] - list_os[ii].bb(itimes[1])) < 1e-10 ), "Evaluating Orbits bb does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmra(itimes[1])[ii] - list_os[ii].pmra(itimes[1])) < 1e-10 ), "Evaluating Orbits pmra does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmdec(itimes[1])[ii] - list_os[ii].pmdec(itimes[1])) < 1e-10 ), "Evaluating Orbits pmdec does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmll(itimes[1])[ii] - list_os[ii].pmll(itimes[1])) < 1e-10 ), "Evaluating Orbits pmll does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmbb(itimes[1])[ii] - list_os[ii].pmbb(itimes[1])) < 1e-10 ), "Evaluating Orbits pmbb does not agree with Orbit" assert numpy.all( numpy.fabs(os.vra(itimes[1])[ii] - list_os[ii].vra(itimes[1])) < 1e-10 ), "Evaluating Orbits vra does not agree with Orbit" assert numpy.all( numpy.fabs(os.vdec(itimes[1])[ii] - list_os[ii].vdec(itimes[1])) < 1e-10 ), "Evaluating Orbits vdec does not agree with Orbit" assert numpy.all( numpy.fabs(os.vll(itimes[1])[ii] - list_os[ii].vll(itimes[1])) < 1e-10 ), "Evaluating Orbits vll does not agree with Orbit" assert numpy.all( numpy.fabs(os.vbb(itimes[1])[ii] - list_os[ii].vbb(itimes[1])) < 1e-10 ), "Evaluating Orbits vbb does not agree with Orbit" assert numpy.all( numpy.fabs(os.vlos(itimes[1])[ii] - list_os[ii].vlos(itimes[1])) < 1e-10 ), "Evaluating Orbits vlos does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioX(itimes[1])[ii] - list_os[ii].helioX(itimes[1])) < 1e-10 ), "Evaluating Orbits helioX does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioY(itimes[1])[ii] - list_os[ii].helioY(itimes[1])) < 1e-10 ), "Evaluating Orbits helioY does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioZ(itimes[1])[ii] - list_os[ii].helioZ(itimes[1])) < 1e-10 ), "Evaluating Orbits helioZ does not agree with Orbit" assert numpy.all( numpy.fabs(os.U(itimes[1])[ii] - list_os[ii].U(itimes[1])) < 1e-10 ), "Evaluating Orbits U does not agree with Orbit" assert numpy.all( numpy.fabs(os.V(itimes[1])[ii] - list_os[ii].V(itimes[1])) < 1e-10 ), "Evaluating Orbits V does not agree with Orbit" assert numpy.all( numpy.fabs(os.W(itimes[1])[ii] - list_os[ii].W(itimes[1])) < 1e-10 ), "Evaluating Orbits W does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(itimes[1]).ra[ii] - list_os[ii].SkyCoord(itimes[1]).ra ) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(itimes[1]).dec[ii] - list_os[ii].SkyCoord(itimes[1]).dec ) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(itimes[1]).distance[ii] - list_os[ii].SkyCoord(itimes[1]).distance ) .to(u.kpc) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" if _APY3: assert numpy.all( numpy.fabs( os.SkyCoord(itimes[1]).pm_ra_cosdec[ii] - list_os[ii].SkyCoord(itimes[1]).pm_ra_cosdec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(itimes[1]).pm_dec[ii] - list_os[ii].SkyCoord(itimes[1]).pm_dec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(itimes[1]).radial_velocity[ii] - list_os[ii].SkyCoord(itimes[1]).radial_velocity ) .to(u.km / u.s) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" return None # Test that evaluating coordinate functions for integrated orbits works, # for 5D orbits def test_coordinate_interpolation_5d(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 20 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs))) list_os = [ Orbit([R, vR, vT, z, vz]) for R, vR, vT, z, vz in zip(Rs, vRs, vTs, zs, vzs) ] # Before integration for ii in range(nrand): assert numpy.all(numpy.fabs(os.R()[ii] - list_os[ii].R()) < 1e-10), ( "Evaluating Orbits R does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.r()[ii] - list_os[ii].r()) < 1e-10), ( "Evaluating Orbits r does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vR()[ii] - list_os[ii].vR()) < 1e-10), ( "Evaluating Orbits vR does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vT()[ii] - list_os[ii].vT()) < 1e-10), ( "Evaluating Orbits vT does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.z()[ii] - list_os[ii].z()) < 1e-10), ( "Evaluating Orbits z does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vz()[ii] - list_os[ii].vz()) < 1e-10), ( "Evaluating Orbits vz does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vphi()[ii] - list_os[ii].vphi()) < 1e-10), ( "Evaluating Orbits vphi does not agree with Orbit" ) # Integrate all times = numpy.linspace(0.0, 10.0, 1001) os.integrate(times, MWPotential2014) [o.integrate(times, MWPotential2014) for o in list_os] # Test exact times of integration for ii in range(nrand): assert numpy.all(numpy.fabs(os.R(times)[ii] - list_os[ii].R(times)) < 1e-10), ( "Evaluating Orbits R does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.r(times)[ii] - list_os[ii].r(times)) < 1e-10), ( "Evaluating Orbits r does not agree with Orbit" ) assert numpy.all( numpy.fabs(os.vR(times)[ii] - list_os[ii].vR(times)) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(times)[ii] - list_os[ii].vT(times)) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all(numpy.fabs(os.z(times)[ii] - list_os[ii].z(times)) < 1e-10), ( "Evaluating Orbits z does not agree with Orbit" ) assert numpy.all( numpy.fabs(os.vz(times)[ii] - list_os[ii].vz(times)) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(times)[ii] - list_os[ii].vphi(times)) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" # Also a single time in the array ... assert numpy.all( numpy.fabs(os.R(times[1])[ii] - list_os[ii].R(times[1])) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(times[1])[ii] - list_os[ii].r(times[1])) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(times[1])[ii] - list_os[ii].vR(times[1])) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(times[1])[ii] - list_os[ii].vT(times[1])) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z(times[1])[ii] - list_os[ii].z(times[1])) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz(times[1])[ii] - list_os[ii].vz(times[1])) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(times[1])[ii] - list_os[ii].vphi(times[1])) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" # Test actual interpolated itimes = times[:-2] + (times[1] - times[0]) / 2.0 for ii in range(nrand): assert numpy.all( numpy.fabs(os.R(itimes)[ii] - list_os[ii].R(itimes)) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(itimes)[ii] - list_os[ii].r(itimes)) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(itimes)[ii] - list_os[ii].vR(itimes)) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(itimes)[ii] - list_os[ii].vT(itimes)) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z(itimes)[ii] - list_os[ii].z(itimes)) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz(itimes)[ii] - list_os[ii].vz(itimes)) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(itimes)[ii] - list_os[ii].vphi(itimes)) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" # Also a single time in the array ... assert numpy.all( numpy.fabs(os.R(itimes[1])[ii] - list_os[ii].R(itimes[1])) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(itimes[1])[ii] - list_os[ii].r(itimes[1])) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(itimes[1])[ii] - list_os[ii].vR(itimes[1])) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(itimes[1])[ii] - list_os[ii].vT(itimes[1])) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z(itimes[1])[ii] - list_os[ii].z(itimes[1])) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz(itimes[1])[ii] - list_os[ii].vz(itimes[1])) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(itimes[1])[ii] - list_os[ii].vphi(itimes[1])) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" with pytest.raises(AttributeError): os.phi() return None # Test that evaluating coordinate functions for integrated orbits works, # for 4D orbits def test_coordinate_interpolation_4d(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 20 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, phis))) list_os = [Orbit([R, vR, vT, phi]) for R, vR, vT, phi in zip(Rs, vRs, vTs, phis)] # Before integration for ii in range(nrand): assert numpy.all(numpy.fabs(os.R()[ii] - list_os[ii].R()) < 1e-10), ( "Evaluating Orbits R does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.r()[ii] - list_os[ii].r()) < 1e-10), ( "Evaluating Orbits r does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vR()[ii] - list_os[ii].vR()) < 1e-10), ( "Evaluating Orbits vR does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vT()[ii] - list_os[ii].vT()) < 1e-10), ( "Evaluating Orbits vT does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.phi()[ii] - list_os[ii].phi()) < 1e-10), ( "Evaluating Orbits phi does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vphi()[ii] - list_os[ii].vphi()) < 1e-10), ( "Evaluating Orbits vphi does not agree with Orbit" ) # Integrate all times = numpy.linspace(0.0, 10.0, 1001) os.integrate(times, MWPotential2014) [o.integrate(times, MWPotential2014) for o in list_os] # Test exact times of integration for ii in range(nrand): assert numpy.all(numpy.fabs(os.R(times)[ii] - list_os[ii].R(times)) < 1e-10), ( "Evaluating Orbits R does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.r(times)[ii] - list_os[ii].r(times)) < 1e-10), ( "Evaluating Orbits r does not agree with Orbit" ) assert numpy.all( numpy.fabs(os.vR(times)[ii] - list_os[ii].vR(times)) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(times)[ii] - list_os[ii].vT(times)) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.phi(times)[ii] - list_os[ii].phi(times)) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(times)[ii] - list_os[ii].vphi(times)) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" # Also a single time in the array ... assert numpy.all( numpy.fabs(os.R(times[1])[ii] - list_os[ii].R(times[1])) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(times[1])[ii] - list_os[ii].r(times[1])) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(times[1])[ii] - list_os[ii].vR(times[1])) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(times[1])[ii] - list_os[ii].vT(times[1])) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.phi(times[1])[ii] - list_os[ii].phi(times[1])) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(times[1])[ii] - list_os[ii].vphi(times[1])) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" # Test actual interpolated itimes = times[:-2] + (times[1] - times[0]) / 2.0 for ii in range(nrand): assert numpy.all( numpy.fabs(os.R(itimes)[ii] - list_os[ii].R(itimes)) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(itimes)[ii] - list_os[ii].r(itimes)) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(itimes)[ii] - list_os[ii].vR(itimes)) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(itimes)[ii] - list_os[ii].vT(itimes)) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.phi(itimes)[ii] - list_os[ii].phi(itimes)) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(itimes)[ii] - list_os[ii].vphi(itimes)) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" # Also a single time in the array ... assert numpy.all( numpy.fabs(os.R(itimes[1])[ii] - list_os[ii].R(itimes[1])) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(itimes[1])[ii] - list_os[ii].r(itimes[1])) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(itimes[1])[ii] - list_os[ii].vR(itimes[1])) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(itimes[1])[ii] - list_os[ii].vT(itimes[1])) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.phi(itimes[1])[ii] - list_os[ii].phi(itimes[1])) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(itimes[1])[ii] - list_os[ii].vphi(itimes[1])) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" with pytest.raises(AttributeError): os.z() with pytest.raises(AttributeError): os.vz() return None # Test that evaluating coordinate functions for integrated orbits works, # for 3D orbits def test_coordinate_interpolation_3d(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 20 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 os = Orbit(list(zip(Rs, vRs, vTs))) list_os = [Orbit([R, vR, vT]) for R, vR, vT in zip(Rs, vRs, vTs)] # Before integration for ii in range(nrand): assert numpy.all(numpy.fabs(os.R()[ii] - list_os[ii].R()) < 1e-10), ( "Evaluating Orbits R does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.r()[ii] - list_os[ii].r()) < 1e-10), ( "Evaluating Orbits r does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vR()[ii] - list_os[ii].vR()) < 1e-10), ( "Evaluating Orbits vR does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vT()[ii] - list_os[ii].vT()) < 1e-10), ( "Evaluating Orbits vT does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vphi()[ii] - list_os[ii].vphi()) < 1e-10), ( "Evaluating Orbits vphi does not agree with Orbit" ) # Integrate all times = numpy.linspace(0.0, 10.0, 1001) os.integrate(times, MWPotential2014) [o.integrate(times, MWPotential2014) for o in list_os] # Test exact times of integration for ii in range(nrand): assert numpy.all(numpy.fabs(os.R(times)[ii] - list_os[ii].R(times)) < 1e-10), ( "Evaluating Orbits R does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.r(times)[ii] - list_os[ii].r(times)) < 1e-10), ( "Evaluating Orbits r does not agree with Orbit" ) assert numpy.all( numpy.fabs(os.vR(times)[ii] - list_os[ii].vR(times)) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(times)[ii] - list_os[ii].vT(times)) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(times)[ii] - list_os[ii].vphi(times)) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" # Also a single time in the array ... assert numpy.all( numpy.fabs(os.R(times[1])[ii] - list_os[ii].R(times[1])) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(times[1])[ii] - list_os[ii].r(times[1])) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(times[1])[ii] - list_os[ii].vR(times[1])) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(times[1])[ii] - list_os[ii].vT(times[1])) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(times[1])[ii] - list_os[ii].vphi(times[1])) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" # Test actual interpolated itimes = times[:-2] + (times[1] - times[0]) / 2.0 for ii in range(nrand): assert numpy.all( numpy.fabs(os.R(itimes)[ii] - list_os[ii].R(itimes)) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(itimes)[ii] - list_os[ii].r(itimes)) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(itimes)[ii] - list_os[ii].vR(itimes)) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(itimes)[ii] - list_os[ii].vT(itimes)) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(itimes)[ii] - list_os[ii].vphi(itimes)) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" # Also a single time in the array ... assert numpy.all( numpy.fabs(os.R(itimes[1])[ii] - list_os[ii].R(itimes[1])) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(itimes[1])[ii] - list_os[ii].r(itimes[1])) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(itimes[1])[ii] - list_os[ii].vR(itimes[1])) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(itimes[1])[ii] - list_os[ii].vT(itimes[1])) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi(itimes[1])[ii] - list_os[ii].vphi(itimes[1])) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" with pytest.raises(AttributeError): os.phi() with pytest.raises(AttributeError): os.x() with pytest.raises(AttributeError): os.vx() with pytest.raises(AttributeError): os.y() with pytest.raises(AttributeError): os.vy() return None # Test that evaluating coordinate functions for integrated orbits works, # for 2D orbits def test_coordinate_interpolation_2d(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014, toVerticalPotential MWPotential2014 = toVerticalPotential(MWPotential2014, 1.0) numpy.random.seed(1) nrand = 20 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(zs, vzs))) list_os = [Orbit([z, vz]) for z, vz in zip(zs, vzs)] # Before integration for ii in range(nrand): assert numpy.all(numpy.fabs(os.x()[ii] - list_os[ii].x()) < 1e-10), ( "Evaluating Orbits x does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vx()[ii] - list_os[ii].vx()) < 1e-10), ( "Evaluating Orbits vx does not agree with Orbit" ) # Integrate all times = numpy.linspace(0.0, 10.0, 1001) os.integrate(times, MWPotential2014) [o.integrate(times, MWPotential2014) for o in list_os] # Test exact times of integration for ii in range(nrand): assert numpy.all(numpy.fabs(os.x(times)[ii] - list_os[ii].x(times)) < 1e-10), ( "Evaluating Orbits x does not agree with Orbit" ) assert numpy.all( numpy.fabs(os.vx(times)[ii] - list_os[ii].vx(times)) < 1e-10 ), "Evaluating Orbits vx does not agree with Orbit" # Also a single time in the array ... assert numpy.all( numpy.fabs(os.x(times[1])[ii] - list_os[ii].x(times[1])) < 1e-10 ), "Evaluating Orbits x does not agree with Orbit" assert numpy.all( numpy.fabs(os.vx(times[1])[ii] - list_os[ii].vx(times[1])) < 1e-10 ), "Evaluating Orbits vx does not agree with Orbit" # Test actual interpolated itimes = times[:-2] + (times[1] - times[0]) / 2.0 for ii in range(nrand): assert numpy.all( numpy.fabs(os.x(itimes)[ii] - list_os[ii].x(itimes)) < 1e-10 ), "Evaluating Orbits x does not agree with Orbit" assert numpy.all( numpy.fabs(os.vx(itimes)[ii] - list_os[ii].vx(itimes)) < 1e-10 ), "Evaluating Orbits vx does not agree with Orbit" # Also a single time in the array ... assert numpy.all( numpy.fabs(os.x(itimes[1])[ii] - list_os[ii].x(itimes[1])) < 1e-10 ), "Evaluating Orbits x does not agree with Orbit" assert numpy.all( numpy.fabs(os.vx(itimes[1])[ii] - list_os[ii].vx(itimes[1])) < 1e-10 ), "Evaluating Orbits vx does not agree with Orbit" return None # Test interpolation with backwards orbit integration def test_backinterpolation(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 20 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] # Integrate all times = numpy.linspace(0.0, -10.0, 1001) os.integrate(times, MWPotential2014) [o.integrate(times, MWPotential2014) for o in list_os] # Test actual interpolated itimes = times[:-2] + (times[1] - times[0]) / 2.0 for ii in range(nrand): assert numpy.all( numpy.fabs(os.R(itimes)[ii] - list_os[ii].R(itimes)) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" # Also a single time in the array ... assert numpy.all( numpy.fabs(os.R(itimes[1])[ii] - list_os[ii].R(itimes[1])) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" return None # Test that evaluating coordinate functions for integrated orbits works for # a single orbit def test_coordinate_interpolation_oneorbit(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 1 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] # Before integration for ii in range(nrand): # .time is special, just a single array assert numpy.all(numpy.fabs(os.time() - list_os[ii].time()) < 1e-10), ( "Evaluating Orbits time does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.R()[ii] - list_os[ii].R()) < 1e-10), ( "Evaluating Orbits R does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.r()[ii] - list_os[ii].r()) < 1e-10), ( "Evaluating Orbits r does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vR()[ii] - list_os[ii].vR()) < 1e-10), ( "Evaluating Orbits vR does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vT()[ii] - list_os[ii].vT()) < 1e-10), ( "Evaluating Orbits vT does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.z()[ii] - list_os[ii].z()) < 1e-10), ( "Evaluating Orbits z does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.vz()[ii] - list_os[ii].vz()) < 1e-10), ( "Evaluating Orbits vz does not agree with Orbit" ) assert numpy.all( numpy.fabs( ((os.phi()[ii] - list_os[ii].phi() + numpy.pi) % (2.0 * numpy.pi)) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" # Integrate all times = numpy.linspace(0.0, 10.0, 1001) os.integrate(times, MWPotential2014) [o.integrate(times, MWPotential2014) for o in list_os] # Test exact times of integration for ii in range(nrand): # .time is special, just a single array assert numpy.all( numpy.fabs(os.time(times) - list_os[ii].time(times)) < 1e-10 ), "Evaluating Orbits time does not agree with Orbit" assert numpy.all(numpy.fabs(os.R(times)[ii] - list_os[ii].R(times)) < 1e-10), ( "Evaluating Orbits R does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.r(times)[ii] - list_os[ii].r(times)) < 1e-10), ( "Evaluating Orbits r does not agree with Orbit" ) assert numpy.all( numpy.fabs(os.vR(times)[ii] - list_os[ii].vR(times)) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(times)[ii] - list_os[ii].vT(times)) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all(numpy.fabs(os.z(times)[ii] - list_os[ii].z(times)) < 1e-10), ( "Evaluating Orbits z does not agree with Orbit" ) assert numpy.all( numpy.fabs(os.vz(times)[ii] - list_os[ii].vz(times)) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs( ( (os.phi(times)[ii] - list_os[ii].phi(times) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" # Also a single time in the array ... # .time is special, just a single array assert numpy.all( numpy.fabs(os.time(times[1]) - list_os[ii].time(times[1])) < 1e-10 ), "Evaluating Orbits time does not agree with Orbit" assert numpy.all( numpy.fabs(os.R(times[1])[ii] - list_os[ii].R(times[1])) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(times[1])[ii] - list_os[ii].r(times[1])) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(times[1])[ii] - list_os[ii].vR(times[1])) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(times[1])[ii] - list_os[ii].vT(times[1])) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z(times[1])[ii] - list_os[ii].z(times[1])) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz(times[1])[ii] - list_os[ii].vz(times[1])) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs( ( (os.phi(times[1])[ii] - list_os[ii].phi(times[1]) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" # Test actual interpolated itimes = times[:-2] + (times[1] - times[0]) / 2.0 for ii in range(nrand): assert numpy.all( numpy.fabs(os.R(itimes)[ii] - list_os[ii].R(itimes)) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(itimes)[ii] - list_os[ii].r(itimes)) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(itimes)[ii] - list_os[ii].vR(itimes)) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(itimes)[ii] - list_os[ii].vT(itimes)) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z(itimes)[ii] - list_os[ii].z(itimes)) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz(itimes)[ii] - list_os[ii].vz(itimes)) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs( ( (os.phi(itimes)[ii] - list_os[ii].phi(itimes) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" # Also a single time in the array ... assert numpy.all( numpy.fabs(os.R(itimes[1])[ii] - list_os[ii].R(itimes[1])) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(itimes[1])[ii] - list_os[ii].r(itimes[1])) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(itimes[1])[ii] - list_os[ii].vR(itimes[1])) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(itimes[1])[ii] - list_os[ii].vT(itimes[1])) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z(itimes[1])[ii] - list_os[ii].z(itimes[1])) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz(itimes[1])[ii] - list_os[ii].vz(itimes[1])) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs( ( (os.phi(itimes[1])[ii] - list_os[ii].phi(itimes[1]) + numpy.pi) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" return None # Test that an error is raised when evaluating an orbit outside of the # integration range def test_interpolate_outsiderange(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 3 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) # Integrate all times = numpy.linspace(0.0, 10.0, 1001) os.integrate(times, MWPotential2014) with pytest.raises(ValueError) as excinfo: os.R(11.0) with pytest.raises(ValueError) as excinfo: os.R(-1.0) # Also for arrays that partially overlap with pytest.raises(ValueError) as excinfo: os.R(numpy.linspace(5.0, 11.0, 1001)) with pytest.raises(ValueError) as excinfo: os.R(numpy.linspace(-5.0, 5.0, 1001)) def test_output_shape(): # Test that the output shape is correct and that the shaped output is correct from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = (3, 1, 2) Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vxvv = numpy.rollaxis(numpy.array([Rs, vRs, vTs, zs, vzs, phis]), 0, 4) os = Orbit(vxvv) list_os = [ [ [ Orbit( [ Rs[ii, jj, kk], vRs[ii, jj, kk], vTs[ii, jj, kk], zs[ii, jj, kk], vzs[ii, jj, kk], phis[ii, jj, kk], ] ) for kk in range(nrand[2]) ] for jj in range(nrand[1]) ] for ii in range(nrand[0]) ] # Before integration for ii in range(nrand[0]): for jj in range(nrand[1]): for kk in range(nrand[2]): # .time is special, just a single array assert numpy.all( numpy.fabs(os.time() - list_os[ii][jj][kk].time()) < 1e-10 ), "Evaluating Orbits time does not agree with Orbit" assert numpy.all( numpy.fabs(os.R()[ii, jj, kk] - list_os[ii][jj][kk].R()) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r()[ii, jj, kk] - list_os[ii][jj][kk].r()) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR()[ii, jj, kk] - list_os[ii][jj][kk].vR()) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT()[ii, jj, kk] - list_os[ii][jj][kk].vT()) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z()[ii, jj, kk] - list_os[ii][jj][kk].z()) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz()[ii, jj, kk] - list_os[ii][jj][kk].vz()) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs( ( ( os.phi()[ii, jj, kk] - list_os[ii][jj][kk].phi() + numpy.pi ) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all( numpy.fabs(os.x()[ii, jj, kk] - list_os[ii][jj][kk].x()) < 1e-10 ), "Evaluating Orbits x does not agree with Orbit" assert numpy.all( numpy.fabs(os.y()[ii, jj, kk] - list_os[ii][jj][kk].y()) < 1e-10 ), "Evaluating Orbits y does not agree with Orbit" assert numpy.all( numpy.fabs(os.vx()[ii, jj, kk] - list_os[ii][jj][kk].vx()) < 1e-10 ), "Evaluating Orbits vx does not agree with Orbit" assert numpy.all( numpy.fabs(os.vy()[ii, jj, kk] - list_os[ii][jj][kk].vy()) < 1e-10 ), "Evaluating Orbits vy does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi()[ii, jj, kk] - list_os[ii][jj][kk].vphi()) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" assert numpy.all( numpy.fabs(os.ra()[ii, jj, kk] - list_os[ii][jj][kk].ra()) < 1e-10 ), "Evaluating Orbits ra does not agree with Orbit" assert numpy.all( numpy.fabs(os.dec()[ii, jj, kk] - list_os[ii][jj][kk].dec()) < 1e-10 ), "Evaluating Orbits dec does not agree with Orbit" assert numpy.all( numpy.fabs(os.dist()[ii, jj, kk] - list_os[ii][jj][kk].dist()) < 1e-10 ), "Evaluating Orbits dist does not agree with Orbit" assert numpy.all( numpy.fabs(os.ll()[ii, jj, kk] - list_os[ii][jj][kk].ll()) < 1e-10 ), "Evaluating Orbits ll does not agree with Orbit" assert numpy.all( numpy.fabs(os.bb()[ii, jj, kk] - list_os[ii][jj][kk].bb()) < 1e-10 ), "Evaluating Orbits bb does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmra()[ii, jj, kk] - list_os[ii][jj][kk].pmra()) < 1e-10 ), "Evaluating Orbits pmra does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmdec()[ii, jj, kk] - list_os[ii][jj][kk].pmdec()) < 1e-10 ), "Evaluating Orbits pmdec does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmll()[ii, jj, kk] - list_os[ii][jj][kk].pmll()) < 1e-10 ), "Evaluating Orbits pmll does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmbb()[ii, jj, kk] - list_os[ii][jj][kk].pmbb()) < 1e-10 ), "Evaluating Orbits pmbb does not agree with Orbit" assert numpy.all( numpy.fabs(os.vra()[ii, jj, kk] - list_os[ii][jj][kk].vra()) < 1e-10 ), "Evaluating Orbits vra does not agree with Orbit" assert numpy.all( numpy.fabs(os.vdec()[ii, jj, kk] - list_os[ii][jj][kk].vdec()) < 1e-10 ), "Evaluating Orbits vdec does not agree with Orbit" assert numpy.all( numpy.fabs(os.vll()[ii, jj, kk] - list_os[ii][jj][kk].vll()) < 1e-10 ), "Evaluating Orbits vll does not agree with Orbit" assert numpy.all( numpy.fabs(os.vbb()[ii, jj, kk] - list_os[ii][jj][kk].vbb()) < 1e-10 ), "Evaluating Orbits vbb does not agree with Orbit" assert numpy.all( numpy.fabs(os.vlos()[ii, jj, kk] - list_os[ii][jj][kk].vlos()) < 1e-10 ), "Evaluating Orbits vlos does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioX()[ii, jj, kk] - list_os[ii][jj][kk].helioX()) < 1e-10 ), "Evaluating Orbits helioX does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioY()[ii, jj, kk] - list_os[ii][jj][kk].helioY()) < 1e-10 ), "Evaluating Orbits helioY does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioZ()[ii, jj, kk] - list_os[ii][jj][kk].helioZ()) < 1e-10 ), "Evaluating Orbits helioZ does not agree with Orbit" assert numpy.all( numpy.fabs(os.U()[ii, jj, kk] - list_os[ii][jj][kk].U()) < 1e-10 ), "Evaluating Orbits U does not agree with Orbit" assert numpy.all( numpy.fabs(os.V()[ii, jj, kk] - list_os[ii][jj][kk].V()) < 1e-10 ), "Evaluating Orbits V does not agree with Orbit" assert numpy.all( numpy.fabs(os.W()[ii, jj, kk] - list_os[ii][jj][kk].W()) < 1e-10 ), "Evaluating Orbits W does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord().ra[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord().ra ) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord().dec[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord().dec ) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord().distance[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord().distance ) .to(u.kpc) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" if _APY3: assert numpy.all( numpy.fabs( os.SkyCoord().pm_ra_cosdec[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord().pm_ra_cosdec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord().pm_dec[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord().pm_dec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord().radial_velocity[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord().radial_velocity ) .to(u.km / u.s) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" # Integrate all times = numpy.linspace(0.0, 10.0, 1001) os.integrate(times, MWPotential2014) for ii in range(nrand[0]): for jj in range(nrand[1]): for kk in range(nrand[2]): list_os[ii][jj][kk].integrate(times, MWPotential2014) # Test exact times of integration for ii in range(nrand[0]): for jj in range(nrand[1]): for kk in range(nrand[2]): # .time is special, just a single array assert numpy.all( numpy.fabs(os.time(times) - list_os[ii][jj][kk].time(times)) < 1e-10 ), "Evaluating Orbits time does not agree with Orbit" assert numpy.all( numpy.fabs(os.R(times)[ii, jj, kk] - list_os[ii][jj][kk].R(times)) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(times)[ii, jj, kk] - list_os[ii][jj][kk].r(times)) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(times)[ii, jj, kk] - list_os[ii][jj][kk].vR(times)) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(times)[ii, jj, kk] - list_os[ii][jj][kk].vT(times)) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z(times)[ii, jj, kk] - list_os[ii][jj][kk].z(times)) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz(times)[ii, jj, kk] - list_os[ii][jj][kk].vz(times)) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs( ( ( os.phi(times)[ii, jj, kk] - list_os[ii][jj][kk].phi(times) + numpy.pi ) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all( numpy.fabs(os.x(times)[ii, jj, kk] - list_os[ii][jj][kk].x(times)) < 1e-10 ), "Evaluating Orbits x does not agree with Orbit" assert numpy.all( numpy.fabs(os.y(times)[ii, jj, kk] - list_os[ii][jj][kk].y(times)) < 1e-10 ), "Evaluating Orbits y does not agree with Orbit" assert numpy.all( numpy.fabs(os.vx(times)[ii, jj, kk] - list_os[ii][jj][kk].vx(times)) < 1e-10 ), "Evaluating Orbits vx does not agree with Orbit" assert numpy.all( numpy.fabs(os.vy(times)[ii, jj, kk] - list_os[ii][jj][kk].vy(times)) < 1e-10 ), "Evaluating Orbits vy does not agree with Orbit" assert numpy.all( numpy.fabs( os.vphi(times)[ii, jj, kk] - list_os[ii][jj][kk].vphi(times) ) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" assert numpy.all( numpy.fabs(os.ra(times)[ii, jj, kk] - list_os[ii][jj][kk].ra(times)) < 1e-10 ), "Evaluating Orbits ra does not agree with Orbit" assert numpy.all( numpy.fabs( os.dec(times)[ii, jj, kk] - list_os[ii][jj][kk].dec(times) ) < 1e-10 ), "Evaluating Orbits dec does not agree with Orbit" assert numpy.all( numpy.fabs( os.dist(times)[ii, jj, kk] - list_os[ii][jj][kk].dist(times) ) < 1e-10 ), "Evaluating Orbits dist does not agree with Orbit" assert numpy.all( numpy.fabs(os.ll(times)[ii, jj, kk] - list_os[ii][jj][kk].ll(times)) < 1e-10 ), "Evaluating Orbits ll does not agree with Orbit" assert numpy.all( numpy.fabs(os.bb(times)[ii, jj, kk] - list_os[ii][jj][kk].bb(times)) < 1e-10 ), "Evaluating Orbits bb does not agree with Orbit" assert numpy.all( numpy.fabs( os.pmra(times)[ii, jj, kk] - list_os[ii][jj][kk].pmra(times) ) < 1e-10 ), "Evaluating Orbits pmra does not agree with Orbit" assert numpy.all( numpy.fabs( os.pmdec(times)[ii, jj, kk] - list_os[ii][jj][kk].pmdec(times) ) < 1e-10 ), "Evaluating Orbits pmdec does not agree with Orbit" assert numpy.all( numpy.fabs( os.pmll(times)[ii, jj, kk] - list_os[ii][jj][kk].pmll(times) ) < 1e-10 ), "Evaluating Orbits pmll does not agree with Orbit" assert numpy.all( numpy.fabs( os.pmbb(times)[ii, jj, kk] - list_os[ii][jj][kk].pmbb(times) ) < 1e-10 ), "Evaluating Orbits pmbb does not agree with Orbit" assert numpy.all( numpy.fabs( os.vra(times)[ii, jj, kk] - list_os[ii][jj][kk].vra(times) ) < 1e-10 ), "Evaluating Orbits vra does not agree with Orbit" assert numpy.all( numpy.fabs( os.vdec(times)[ii, jj, kk] - list_os[ii][jj][kk].vdec(times) ) < 1e-10 ), "Evaluating Orbits vdec does not agree with Orbit" assert numpy.all( numpy.fabs( os.vll(times)[ii, jj, kk] - list_os[ii][jj][kk].vll(times) ) < 1e-10 ), "Evaluating Orbits vll does not agree with Orbit" assert numpy.all( numpy.fabs( os.vbb(times)[ii, jj, kk] - list_os[ii][jj][kk].vbb(times) ) < 1e-10 ), "Evaluating Orbits vbb does not agree with Orbit" assert numpy.all( numpy.fabs( os.vlos(times)[ii, jj, kk] - list_os[ii][jj][kk].vlos(times) ) < 1e-9 ), "Evaluating Orbits vlos does not agree with Orbit" assert numpy.all( numpy.fabs( os.helioX(times)[ii, jj, kk] - list_os[ii][jj][kk].helioX(times) ) < 1e-10 ), "Evaluating Orbits helioX does not agree with Orbit" assert numpy.all( numpy.fabs( os.helioY(times)[ii, jj, kk] - list_os[ii][jj][kk].helioY(times) ) < 1e-10 ), "Evaluating Orbits helioY does not agree with Orbit" assert numpy.all( numpy.fabs( os.helioZ(times)[ii, jj, kk] - list_os[ii][jj][kk].helioZ(times) ) < 1e-10 ), "Evaluating Orbits helioZ does not agree with Orbit" assert numpy.all( numpy.fabs(os.U(times)[ii, jj, kk] - list_os[ii][jj][kk].U(times)) < 1e-10 ), "Evaluating Orbits U does not agree with Orbit" assert numpy.all( numpy.fabs(os.V(times)[ii, jj, kk] - list_os[ii][jj][kk].V(times)) < 1e-10 ), "Evaluating Orbits V does not agree with Orbit" assert numpy.all( numpy.fabs(os.W(times)[ii, jj, kk] - list_os[ii][jj][kk].W(times)) < 1e-10 ), "Evaluating Orbits W does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).ra[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord(times).ra ) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).dec[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord(times).dec ) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).distance[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord(times).distance ) .to(u.kpc) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" if _APY3: assert numpy.all( numpy.fabs( os.SkyCoord(times).pm_ra_cosdec[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord(times).pm_ra_cosdec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).pm_dec[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord(times).pm_dec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).radial_velocity[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord(times).radial_velocity ) .to(u.km / u.s) .value < 1e-9 ), "Evaluating Orbits SkyCoord does not agree with Orbit" return None def test_output_reshape(): # Test that the output shape is correct and that the shaped output is correct from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = (3, 1, 2) Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vxvv = numpy.rollaxis(numpy.array([Rs, vRs, vTs, zs, vzs, phis]), 0, 4) os = Orbit(vxvv) # NOW RESHAPE # First try a shape that doesn't work to test the error with pytest.raises(ValueError) as excinfo: os.reshape((4, 2, 1)) # then do one that should work and also setup the list of indiv orbits # with the new shape newshape = (3, 2, 1) os.reshape(newshape) Rs = Rs.reshape(newshape) vRs = vRs.reshape(newshape) vTs = vTs.reshape(newshape) zs = zs.reshape(newshape) vzs = vzs.reshape(newshape) phis = phis.reshape(newshape) nrand = newshape list_os = [ [ [ Orbit( [ Rs[ii, jj, kk], vRs[ii, jj, kk], vTs[ii, jj, kk], zs[ii, jj, kk], vzs[ii, jj, kk], phis[ii, jj, kk], ] ) for kk in range(nrand[2]) ] for jj in range(nrand[1]) ] for ii in range(nrand[0]) ] # Before integration for ii in range(nrand[0]): for jj in range(nrand[1]): for kk in range(nrand[2]): # .time is special, just a single array assert numpy.all( numpy.fabs(os.time() - list_os[ii][jj][kk].time()) < 1e-10 ), "Evaluating Orbits time does not agree with Orbit" assert numpy.all( numpy.fabs(os.R()[ii, jj, kk] - list_os[ii][jj][kk].R()) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r()[ii, jj, kk] - list_os[ii][jj][kk].r()) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR()[ii, jj, kk] - list_os[ii][jj][kk].vR()) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT()[ii, jj, kk] - list_os[ii][jj][kk].vT()) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z()[ii, jj, kk] - list_os[ii][jj][kk].z()) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz()[ii, jj, kk] - list_os[ii][jj][kk].vz()) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs( ( ( os.phi()[ii, jj, kk] - list_os[ii][jj][kk].phi() + numpy.pi ) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all( numpy.fabs(os.x()[ii, jj, kk] - list_os[ii][jj][kk].x()) < 1e-10 ), "Evaluating Orbits x does not agree with Orbit" assert numpy.all( numpy.fabs(os.y()[ii, jj, kk] - list_os[ii][jj][kk].y()) < 1e-10 ), "Evaluating Orbits y does not agree with Orbit" assert numpy.all( numpy.fabs(os.vx()[ii, jj, kk] - list_os[ii][jj][kk].vx()) < 1e-10 ), "Evaluating Orbits vx does not agree with Orbit" assert numpy.all( numpy.fabs(os.vy()[ii, jj, kk] - list_os[ii][jj][kk].vy()) < 1e-10 ), "Evaluating Orbits vy does not agree with Orbit" assert numpy.all( numpy.fabs(os.vphi()[ii, jj, kk] - list_os[ii][jj][kk].vphi()) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" assert numpy.all( numpy.fabs(os.ra()[ii, jj, kk] - list_os[ii][jj][kk].ra()) < 1e-10 ), "Evaluating Orbits ra does not agree with Orbit" assert numpy.all( numpy.fabs(os.dec()[ii, jj, kk] - list_os[ii][jj][kk].dec()) < 1e-10 ), "Evaluating Orbits dec does not agree with Orbit" assert numpy.all( numpy.fabs(os.dist()[ii, jj, kk] - list_os[ii][jj][kk].dist()) < 1e-10 ), "Evaluating Orbits dist does not agree with Orbit" assert numpy.all( numpy.fabs(os.ll()[ii, jj, kk] - list_os[ii][jj][kk].ll()) < 1e-10 ), "Evaluating Orbits ll does not agree with Orbit" assert numpy.all( numpy.fabs(os.bb()[ii, jj, kk] - list_os[ii][jj][kk].bb()) < 1e-10 ), "Evaluating Orbits bb does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmra()[ii, jj, kk] - list_os[ii][jj][kk].pmra()) < 1e-10 ), "Evaluating Orbits pmra does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmdec()[ii, jj, kk] - list_os[ii][jj][kk].pmdec()) < 1e-10 ), "Evaluating Orbits pmdec does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmll()[ii, jj, kk] - list_os[ii][jj][kk].pmll()) < 1e-10 ), "Evaluating Orbits pmll does not agree with Orbit" assert numpy.all( numpy.fabs(os.pmbb()[ii, jj, kk] - list_os[ii][jj][kk].pmbb()) < 1e-10 ), "Evaluating Orbits pmbb does not agree with Orbit" assert numpy.all( numpy.fabs(os.vra()[ii, jj, kk] - list_os[ii][jj][kk].vra()) < 1e-10 ), "Evaluating Orbits vra does not agree with Orbit" assert numpy.all( numpy.fabs(os.vdec()[ii, jj, kk] - list_os[ii][jj][kk].vdec()) < 1e-10 ), "Evaluating Orbits vdec does not agree with Orbit" assert numpy.all( numpy.fabs(os.vll()[ii, jj, kk] - list_os[ii][jj][kk].vll()) < 1e-10 ), "Evaluating Orbits vll does not agree with Orbit" assert numpy.all( numpy.fabs(os.vbb()[ii, jj, kk] - list_os[ii][jj][kk].vbb()) < 1e-10 ), "Evaluating Orbits vbb does not agree with Orbit" assert numpy.all( numpy.fabs(os.vlos()[ii, jj, kk] - list_os[ii][jj][kk].vlos()) < 1e-10 ), "Evaluating Orbits vlos does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioX()[ii, jj, kk] - list_os[ii][jj][kk].helioX()) < 1e-10 ), "Evaluating Orbits helioX does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioY()[ii, jj, kk] - list_os[ii][jj][kk].helioY()) < 1e-10 ), "Evaluating Orbits helioY does not agree with Orbit" assert numpy.all( numpy.fabs(os.helioZ()[ii, jj, kk] - list_os[ii][jj][kk].helioZ()) < 1e-10 ), "Evaluating Orbits helioZ does not agree with Orbit" assert numpy.all( numpy.fabs(os.U()[ii, jj, kk] - list_os[ii][jj][kk].U()) < 1e-10 ), "Evaluating Orbits U does not agree with Orbit" assert numpy.all( numpy.fabs(os.V()[ii, jj, kk] - list_os[ii][jj][kk].V()) < 1e-10 ), "Evaluating Orbits V does not agree with Orbit" assert numpy.all( numpy.fabs(os.W()[ii, jj, kk] - list_os[ii][jj][kk].W()) < 1e-10 ), "Evaluating Orbits W does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord().ra[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord().ra ) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord().dec[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord().dec ) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord().distance[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord().distance ) .to(u.kpc) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" if _APY3: assert numpy.all( numpy.fabs( os.SkyCoord().pm_ra_cosdec[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord().pm_ra_cosdec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord().pm_dec[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord().pm_dec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord().radial_velocity[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord().radial_velocity ) .to(u.km / u.s) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" # Integrate all times = numpy.linspace(0.0, 10.0, 1001) os.integrate(times, MWPotential2014) for ii in range(nrand[0]): for jj in range(nrand[1]): for kk in range(nrand[2]): list_os[ii][jj][kk].integrate(times, MWPotential2014) # Test exact times of integration for ii in range(nrand[0]): for jj in range(nrand[1]): for kk in range(nrand[2]): # .time is special, just a single array assert numpy.all( numpy.fabs(os.time(times) - list_os[ii][jj][kk].time(times)) < 1e-10 ), "Evaluating Orbits time does not agree with Orbit" assert numpy.all( numpy.fabs(os.R(times)[ii, jj, kk] - list_os[ii][jj][kk].R(times)) < 1e-10 ), "Evaluating Orbits R does not agree with Orbit" assert numpy.all( numpy.fabs(os.r(times)[ii, jj, kk] - list_os[ii][jj][kk].r(times)) < 1e-10 ), "Evaluating Orbits r does not agree with Orbit" assert numpy.all( numpy.fabs(os.vR(times)[ii, jj, kk] - list_os[ii][jj][kk].vR(times)) < 1e-10 ), "Evaluating Orbits vR does not agree with Orbit" assert numpy.all( numpy.fabs(os.vT(times)[ii, jj, kk] - list_os[ii][jj][kk].vT(times)) < 1e-10 ), "Evaluating Orbits vT does not agree with Orbit" assert numpy.all( numpy.fabs(os.z(times)[ii, jj, kk] - list_os[ii][jj][kk].z(times)) < 1e-10 ), "Evaluating Orbits z does not agree with Orbit" assert numpy.all( numpy.fabs(os.vz(times)[ii, jj, kk] - list_os[ii][jj][kk].vz(times)) < 1e-10 ), "Evaluating Orbits vz does not agree with Orbit" assert numpy.all( numpy.fabs( ( ( os.phi(times)[ii, jj, kk] - list_os[ii][jj][kk].phi(times) + numpy.pi ) % (2.0 * numpy.pi) ) - numpy.pi ) < 1e-10 ), "Evaluating Orbits phi does not agree with Orbit" assert numpy.all( numpy.fabs(os.x(times)[ii, jj, kk] - list_os[ii][jj][kk].x(times)) < 1e-10 ), "Evaluating Orbits x does not agree with Orbit" assert numpy.all( numpy.fabs(os.y(times)[ii, jj, kk] - list_os[ii][jj][kk].y(times)) < 1e-10 ), "Evaluating Orbits y does not agree with Orbit" assert numpy.all( numpy.fabs(os.vx(times)[ii, jj, kk] - list_os[ii][jj][kk].vx(times)) < 1e-10 ), "Evaluating Orbits vx does not agree with Orbit" assert numpy.all( numpy.fabs(os.vy(times)[ii, jj, kk] - list_os[ii][jj][kk].vy(times)) < 1e-10 ), "Evaluating Orbits vy does not agree with Orbit" assert numpy.all( numpy.fabs( os.vphi(times)[ii, jj, kk] - list_os[ii][jj][kk].vphi(times) ) < 1e-10 ), "Evaluating Orbits vphi does not agree with Orbit" assert numpy.all( numpy.fabs(os.ra(times)[ii, jj, kk] - list_os[ii][jj][kk].ra(times)) < 1e-10 ), "Evaluating Orbits ra does not agree with Orbit" assert numpy.all( numpy.fabs( os.dec(times)[ii, jj, kk] - list_os[ii][jj][kk].dec(times) ) < 1e-10 ), "Evaluating Orbits dec does not agree with Orbit" assert numpy.all( numpy.fabs( os.dist(times)[ii, jj, kk] - list_os[ii][jj][kk].dist(times) ) < 1e-10 ), "Evaluating Orbits dist does not agree with Orbit" assert numpy.all( numpy.fabs(os.ll(times)[ii, jj, kk] - list_os[ii][jj][kk].ll(times)) < 1e-10 ), "Evaluating Orbits ll does not agree with Orbit" assert numpy.all( numpy.fabs(os.bb(times)[ii, jj, kk] - list_os[ii][jj][kk].bb(times)) < 1e-10 ), "Evaluating Orbits bb does not agree with Orbit" assert numpy.all( numpy.fabs( os.pmra(times)[ii, jj, kk] - list_os[ii][jj][kk].pmra(times) ) < 1e-10 ), "Evaluating Orbits pmra does not agree with Orbit" assert numpy.all( numpy.fabs( os.pmdec(times)[ii, jj, kk] - list_os[ii][jj][kk].pmdec(times) ) < 1e-10 ), "Evaluating Orbits pmdec does not agree with Orbit" assert numpy.all( numpy.fabs( os.pmll(times)[ii, jj, kk] - list_os[ii][jj][kk].pmll(times) ) < 1e-10 ), "Evaluating Orbits pmll does not agree with Orbit" assert numpy.all( numpy.fabs( os.pmbb(times)[ii, jj, kk] - list_os[ii][jj][kk].pmbb(times) ) < 1e-10 ), "Evaluating Orbits pmbb does not agree with Orbit" assert numpy.all( numpy.fabs( os.vra(times)[ii, jj, kk] - list_os[ii][jj][kk].vra(times) ) < 1e-10 ), "Evaluating Orbits vra does not agree with Orbit" assert numpy.all( numpy.fabs( os.vdec(times)[ii, jj, kk] - list_os[ii][jj][kk].vdec(times) ) < 1e-10 ), "Evaluating Orbits vdec does not agree with Orbit" assert numpy.all( numpy.fabs( os.vll(times)[ii, jj, kk] - list_os[ii][jj][kk].vll(times) ) < 1e-10 ), "Evaluating Orbits vll does not agree with Orbit" assert numpy.all( numpy.fabs( os.vbb(times)[ii, jj, kk] - list_os[ii][jj][kk].vbb(times) ) < 1e-10 ), "Evaluating Orbits vbb does not agree with Orbit" assert numpy.all( numpy.fabs( os.vlos(times)[ii, jj, kk] - list_os[ii][jj][kk].vlos(times) ) < 1e-9 ), "Evaluating Orbits vlos does not agree with Orbit" assert numpy.all( numpy.fabs( os.helioX(times)[ii, jj, kk] - list_os[ii][jj][kk].helioX(times) ) < 1e-10 ), "Evaluating Orbits helioX does not agree with Orbit" assert numpy.all( numpy.fabs( os.helioY(times)[ii, jj, kk] - list_os[ii][jj][kk].helioY(times) ) < 1e-10 ), "Evaluating Orbits helioY does not agree with Orbit" assert numpy.all( numpy.fabs( os.helioZ(times)[ii, jj, kk] - list_os[ii][jj][kk].helioZ(times) ) < 1e-10 ), "Evaluating Orbits helioZ does not agree with Orbit" assert numpy.all( numpy.fabs(os.U(times)[ii, jj, kk] - list_os[ii][jj][kk].U(times)) < 1e-10 ), "Evaluating Orbits U does not agree with Orbit" assert numpy.all( numpy.fabs(os.V(times)[ii, jj, kk] - list_os[ii][jj][kk].V(times)) < 1e-10 ), "Evaluating Orbits V does not agree with Orbit" assert numpy.all( numpy.fabs(os.W(times)[ii, jj, kk] - list_os[ii][jj][kk].W(times)) < 1e-10 ), "Evaluating Orbits W does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).ra[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord(times).ra ) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).dec[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord(times).dec ) .to(u.deg) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).distance[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord(times).distance ) .to(u.kpc) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" if _APY3: assert numpy.all( numpy.fabs( os.SkyCoord(times).pm_ra_cosdec[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord(times).pm_ra_cosdec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).pm_dec[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord(times).pm_dec ) .to(u.mas / u.yr) .value < 1e-10 ), "Evaluating Orbits SkyCoord does not agree with Orbit" assert numpy.all( numpy.fabs( os.SkyCoord(times).radial_velocity[ii, jj, kk] - list_os[ii][jj][kk].SkyCoord(times).radial_velocity ) .to(u.km / u.s) .value < 1e-9 ), "Evaluating Orbits SkyCoord does not agree with Orbit" return None def test_output_specialshapes(): # Test that the output shape is correct and that the shaped output is correct, for 'special' inputs (single objects, ...) from galpy.orbit import Orbit # vxvv= list of [R,vR,vT,z,...] should be shape == () and scalar output os = Orbit([1.0, 0.1, 1.0, 0.1, 0.0, 0.1]) assert os.shape == (), "Shape of Orbits with list of [R,vR,...] input is not empty" assert numpy.ndim(os.R()) == 0, ( "Orbits with list of [R,vR,...] input does not return scalar" ) # Similar for list [ra,dec,...] os = Orbit([1.0, 0.1, 1.0, 0.1, 0.0, 0.1], radec=True) assert os.shape == (), ( "Shape of Orbits with list of [ra,dec,...] input is not empty" ) assert numpy.ndim(os.R()) == 0, ( "Orbits with list of [ra,dec,...] input does not return scalar" ) # Also with units os = Orbit( [ 1.0 * u.deg, 0.1 * u.rad, 1.0 * u.pc, 0.1 * u.mas / u.yr, 0.0 * u.arcsec / u.yr, 0.1 * u.pc / u.Myr, ], radec=True, ) assert os.shape == (), ( "Shape of Orbits with list of [ra,dec,...] w/units input is not empty" ) assert numpy.ndim(os.R()) == 0, ( "Orbits with list of [ra,dec,...] w/units input does not return scalar" ) # Also from_name os = Orbit.from_name("LMC") assert os.shape == (), "Shape of Orbits with from_name single object is not empty" assert numpy.ndim(os.R()) == 0, ( "Orbits with from_name single object does not return scalar" ) # vxvv= list of list of [R,vR,vT,z,...] should be shape == (1,) and array output os = Orbit([[1.0, 0.1, 1.0, 0.1, 0.0, 0.1]]) assert os.shape == (1,), ( "Shape of Orbits with list of list of [R,vR,...] input is not (1,)" ) assert numpy.ndim(os.R()) == 1, ( "Orbits with list of list of [R,vR,...] input does not return array" ) # vxvv= array of [R,vR,vT,z,...] should be shape == () and scalar output os = Orbit(numpy.array([1.0, 0.1, 1.0, 0.1, 0.0, 0.1])) assert os.shape == (), "Shape of Orbits with array of [R,vR,...] input is not empty" assert numpy.ndim(os.R()) == 0, ( "Orbits with array of [R,vR,...] input does not return scalar" ) if _APY3: # vxvv= single SkyCoord should be shape == () and scalar output co = apycoords.SkyCoord( ra=1.0 * u.deg, dec=0.5 * u.rad, distance=2.0 * u.kpc, pm_ra_cosdec=-0.1 * u.mas / u.yr, pm_dec=10.0 * u.mas / u.yr, radial_velocity=10.0 * u.km / u.s, frame="icrs", ) os = Orbit(co) assert os.shape == co.shape, ( "Shape of Orbits with SkyCoord does not agree with shape of SkyCoord" ) # vxvv= single SkyCoord, but as array should be shape == (1,) and array output s = numpy.ones(1) co = apycoords.SkyCoord( ra=s * 1.0 * u.deg, dec=s * 0.5 * u.rad, distance=s * 2.0 * u.kpc, pm_ra_cosdec=-0.1 * u.mas / u.yr * s, pm_dec=10.0 * u.mas / u.yr * s, radial_velocity=10.0 * u.km / u.s * s, frame="icrs", ) os = Orbit(co) assert os.shape == co.shape, ( "Shape of Orbits with SkyCoord does not agree with shape of SkyCoord" ) # vxvv= None should be shape == (1,) and array output os = Orbit() assert os.shape == (), "Shape of Orbits with vxvv=None input is not empty" assert numpy.ndim(os.R()) == 0, ( "Orbits with with vxvv=None input does not return scalar" ) return None def test_call_issue256(): # Same as for Orbit instances: non-integrated orbit with t=/=0 should return error from galpy.orbit import Orbit o = Orbit(vxvv=[[5.0, -1.0, 0.8, 3, -0.1, 0]]) # no integration of the orbit with pytest.raises(ValueError) as excinfo: o.R(30) return None # Test that the energy, angular momentum, and Jacobi functions work as expected def test_energy_jacobi_angmom(): from galpy.orbit import Orbit numpy.random.seed(1) nrand = 10 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) # 6D os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] _check_energy_jacobi_angmom(os, list_os) # 5D os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs))) list_os = [ Orbit([R, vR, vT, z, vz]) for R, vR, vT, z, vz in zip(Rs, vRs, vTs, zs, vzs) ] _check_energy_jacobi_angmom(os, list_os) # 4D os = Orbit(list(zip(Rs, vRs, vTs, phis))) list_os = [Orbit([R, vR, vT, phi]) for R, vR, vT, phi in zip(Rs, vRs, vTs, phis)] _check_energy_jacobi_angmom(os, list_os) # 3D os = Orbit(list(zip(Rs, vRs, vTs))) list_os = [Orbit([R, vR, vT]) for R, vR, vT in zip(Rs, vRs, vTs)] _check_energy_jacobi_angmom(os, list_os) # 2D os = Orbit(list(zip(zs, vzs))) list_os = [Orbit([z, vz]) for z, vz in zip(zs, vzs)] _check_energy_jacobi_angmom(os, list_os) return None def _check_energy_jacobi_angmom(os, list_os): nrand = len(os) from galpy.potential import ( DehnenBarPotential, DoubleExponentialDiskPotential, MWPotential2014, SpiralArmsPotential, ) sp = SpiralArmsPotential() dp = DehnenBarPotential() lp = DoubleExponentialDiskPotential(normalize=1.0) if os.dim() == 1: from galpy.potential import toVerticalPotential MWPotential2014 = toVerticalPotential(MWPotential2014, 1.0) lp = toVerticalPotential(lp, 1.0) # Before integration for ii in range(nrand): assert numpy.all( numpy.fabs( os.E(pot=MWPotential2014)[ii] / list_os[ii].E(pot=MWPotential2014) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits E does not agree with Orbit" if os.dim() == 3: assert numpy.all( numpy.fabs( os.ER(pot=MWPotential2014)[ii] / list_os[ii].ER(pot=MWPotential2014) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits ER does not agree with Orbit" assert numpy.all( numpy.fabs( os.Ez(pot=MWPotential2014)[ii] / list_os[ii].Ez(pot=MWPotential2014) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Ez does not agree with Orbit" if os.phasedim() % 2 == 0 and os.dim() != 1: assert numpy.all( numpy.fabs(os.L()[ii] / list_os[ii].L() - 1.0) < 10.0**-10.0 ), "Evaluating Orbits L does not agree with Orbit" if os.dim() != 1: assert numpy.all( numpy.fabs(os.Lz()[ii] / list_os[ii].Lz() - 1.0) < 10.0**-10.0 ), "Evaluating Orbits Lz does not agree with Orbit" if os.phasedim() % 2 == 0 and os.dim() != 1: assert numpy.all( numpy.fabs( os.Jacobi(pot=MWPotential2014)[ii] / list_os[ii].Jacobi(pot=MWPotential2014) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Jacobi does not agree with Orbit" # Also explicitly set OmegaP assert numpy.all( numpy.fabs( os.Jacobi(pot=MWPotential2014, OmegaP=0.6)[ii] / list_os[ii].Jacobi(pot=MWPotential2014, OmegaP=0.6) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Jacobi does not agree with Orbit" # Potential for which array evaluation definitely does not work for ii in range(nrand): assert numpy.all( numpy.fabs(os.E(pot=lp)[ii] / list_os[ii].E(pot=lp) - 1.0) < 10.0**-10.0 ), "Evaluating Orbits E does not agree with Orbit" if os.dim() == 3: assert numpy.all( numpy.fabs(os.ER(pot=lp)[ii] / list_os[ii].ER(pot=lp) - 1.0) < 10.0**-10.0 ), "Evaluating Orbits ER does not agree with Orbit" assert numpy.all( numpy.fabs(os.Ez(pot=lp)[ii] / list_os[ii].Ez(pot=lp) - 1.0) < 10.0**-10.0 ), "Evaluating Orbits Ez does not agree with Orbit" if os.phasedim() % 2 == 0 and os.dim() != 1: assert numpy.all( numpy.fabs(os.L()[ii] / list_os[ii].L() - 1.0) < 10.0**-10.0 ), "Evaluating Orbits L does not agree with Orbit" if os.dim() != 1: assert numpy.all( numpy.fabs(os.Lz()[ii] / list_os[ii].Lz() - 1.0) < 10.0**-10.0 ), "Evaluating Orbits Lz does not agree with Orbit" if os.phasedim() % 2 == 0 and os.dim() != 1: assert numpy.all( numpy.fabs(os.Jacobi(pot=lp)[ii] / list_os[ii].Jacobi(pot=lp) - 1.0) < 10.0**-10.0 ), "Evaluating Orbits Jacobi does not agree with Orbit" # Also explicitly set OmegaP assert numpy.all( numpy.fabs( os.Jacobi(pot=lp, OmegaP=0.6)[ii] / list_os[ii].Jacobi(pot=lp, OmegaP=0.6) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Jacobi does not agree with Orbit" if os.phasedim() == 6: # Also in 3D assert numpy.all( numpy.fabs( os.Jacobi(pot=lp, OmegaP=[0.0, 0.0, 0.6])[ii] / list_os[ii].Jacobi(pot=lp, OmegaP=0.6) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Jacobi does not agree with Orbit" assert numpy.all( numpy.fabs( os.Jacobi(pot=lp, OmegaP=numpy.array([0.0, 0.0, 0.6]))[ii] / list_os[ii].Jacobi(pot=lp, OmegaP=0.6) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Jacobi does not agree with Orbit" # o.E before integration gives AttributeError with pytest.raises(AttributeError): os.E() with pytest.raises(AttributeError): os.Jacobi() # Integrate all times = numpy.linspace(0.0, 10.0, 1001) os.integrate(times, MWPotential2014) [o.integrate(times, MWPotential2014) for o in list_os] for ii in range(nrand): # Don't have to specify the potential or set to None assert numpy.all( numpy.fabs( os.E(times)[ii] / list_os[ii].E(times, pot=MWPotential2014) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits E does not agree with Orbit" if os.dim() == 3: assert numpy.all( numpy.fabs( os.ER(times, pot=None)[ii] / list_os[ii].ER(times, pot=MWPotential2014) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits ER does not agree with Orbit" assert numpy.all( numpy.fabs( os.Ez(times)[ii] / list_os[ii].Ez(times, pot=MWPotential2014) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Ez does not agree with Orbit" if os.phasedim() % 2 == 0 and os.dim() != 1: assert numpy.all( numpy.fabs(os.L(times)[ii] / list_os[ii].L(times) - 1.0) < 10.0**-10.0 ), "Evaluating Orbits L does not agree with Orbit" if os.dim() != 1: assert numpy.all( numpy.fabs(os.Lz(times)[ii] / list_os[ii].Lz(times) - 1.0) < 10.0**-10.0 ), "Evaluating Orbits Lz does not agree with Orbit" if os.phasedim() % 2 == 0 and os.dim() != 1: assert numpy.all( numpy.fabs(os.Jacobi(times)[ii] / list_os[ii].Jacobi(times) - 1.0) < 10.0**-10.0 ), "Evaluating Orbits Jacobi does not agree with Orbit" # Also explicitly set OmegaP assert numpy.all( numpy.fabs( os.Jacobi(times, pot=MWPotential2014, OmegaP=0.6)[ii] / list_os[ii].Jacobi(times, pot=MWPotential2014, OmegaP=0.6) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Jacobi does not agree with Orbit" if os.phasedim() == 6: # Also in 3D assert numpy.all( numpy.fabs( os.Jacobi(times, pot=MWPotential2014, OmegaP=[0.0, 0.0, 0.6])[ii] / list_os[ii].Jacobi(times, pot=MWPotential2014, OmegaP=0.6) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Jacobi does not agree with Orbit" assert numpy.all( numpy.fabs( os.Jacobi( times, pot=MWPotential2014, OmegaP=numpy.array([0.0, 0.0, 0.6]) )[ii] / list_os[ii].Jacobi(times, pot=MWPotential2014, OmegaP=0.6) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Jacobi does not agree with Orbit" # Don't do non-axi for odd-D Orbits or 1D if os.phasedim() % 2 == 1 or os.dim() == 1: return None # Add bar and spiral for ii in range(nrand): assert numpy.all( numpy.fabs( os.E(pot=MWPotential2014 + dp + sp)[ii] / list_os[ii].E(pot=MWPotential2014 + dp + sp) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits E does not agree with Orbit" if os.dim() == 3: assert numpy.all( numpy.fabs( os.ER(pot=MWPotential2014 + dp + sp)[ii] / list_os[ii].ER(pot=MWPotential2014 + dp + sp) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits ER does not agree with Orbit" assert numpy.all( numpy.fabs( os.Ez(pot=MWPotential2014 + dp + sp)[ii] / list_os[ii].Ez(pot=MWPotential2014 + dp + sp) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Ez does not agree with Orbit" if os.phasedim() % 2 == 0 and os.dim() != 1: assert numpy.all( numpy.fabs(os.L()[ii] / list_os[ii].L() - 1.0) < 10.0**-10.0 ), "Evaluating Orbits L does not agree with Orbit" if os.dim() != 1: assert numpy.all( numpy.fabs(os.Lz()[ii] / list_os[ii].Lz() - 1.0) < 10.0**-10.0 ), "Evaluating Orbits Lz does not agree with Orbit" if os.phasedim() % 2 == 0 and os.dim() != 1: assert numpy.all( numpy.fabs( os.Jacobi(pot=MWPotential2014 + dp + sp)[ii] / list_os[ii].Jacobi(pot=MWPotential2014 + dp + sp) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Jacobi does not agree with Orbit" # Also explicitly set OmegaP assert numpy.all( numpy.fabs( os.Jacobi(pot=MWPotential2014 + dp + sp, OmegaP=0.6)[ii] / list_os[ii].Jacobi(pot=MWPotential2014 + dp + sp, OmegaP=0.6) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits Jacobi does not agree with Orbit" return None # Test that L cannot be computed for (a) linearOrbits and (b) 5D orbits def test_angmom_errors(): from galpy.orbit import Orbit o = Orbit([[1.0, 0.1]]) with pytest.raises(AttributeError): o.L() o = Orbit([[1.0, 0.1, 1.1, 0.1, -0.2]]) with pytest.raises(AttributeError): o.L() return None # Test that we can still get outputs when there aren't enough points for an actual interpolation # Test whether Orbits evaluation methods sound warning when called with # unitless time when orbit is integrated with unitfull times def test_orbits_method_integrate_t_asQuantity_warning(): from astropy import units from test_orbit import check_integrate_t_asQuantity_warning from galpy.orbit import Orbit from galpy.potential import MWPotential2014 # Setup and integrate orbit ts = numpy.linspace(0.0, 10.0, 1001) * units.Gyr o = Orbit([[1.1, 0.1, 1.1, 0.1, 0.1, 0.2], [1.1, 0.1, 1.1, 0.1, 0.1, 0.2]]) o.integrate(ts, MWPotential2014) # Now check check_integrate_t_asQuantity_warning(o, "R") return None # Test new orbits formed from __call__ def test_newOrbits(): from galpy.orbit import Orbit o = Orbit([[1.0, 0.1, 1.1, 0.1, 0.0, 0.0], [1.1, 0.3, 0.9, -0.2, 0.3, 2.0]]) ts = numpy.linspace(0.0, 1.0, 21) # v. quick orbit integration lp = potential.LogarithmicHaloPotential(normalize=1.0) o.integrate(ts, lp) no = o(ts[-1]) # new Orbits assert numpy.all(no.R() == o.R(ts[-1])), ( "New Orbits formed from calling an old orbit does not have the correct R" ) assert numpy.all(no.vR() == o.vR(ts[-1])), ( "New Orbits formed from calling an old orbit does not have the correct vR" ) assert numpy.all(no.vT() == o.vT(ts[-1])), ( "New Orbits formed from calling an old orbit does not have the correct vT" ) assert numpy.all(no.z() == o.z(ts[-1])), ( "New Orbits formed from calling an old orbit does not have the correct z" ) assert numpy.all(no.vz() == o.vz(ts[-1])), ( "New Orbits formed from calling an old orbit does not have the correct vz" ) assert numpy.all(no.phi() == o.phi(ts[-1])), ( "New Orbits formed from calling an old orbit does not have the correct phi" ) assert not no._roSet, ( "New Orbits formed from calling an old orbit does not have the correct roSet" ) assert not no._voSet, ( "New Orbits formed from calling an old orbit does not have the correct roSet" ) # Also test this for multiple time outputs nos = o(ts[-2:]) # new Orbits assert numpy.all(numpy.fabs(nos.R() - o.R(ts[-2:])) < 10.0**-10.0), ( "New Orbits formed from calling an old orbit does not have the correct R" ) assert numpy.all(numpy.fabs(nos.vR() - o.vR(ts[-2:])) < 10.0**-10.0), ( "New Orbits formed from calling an old orbit does not have the correct vR" ) assert numpy.all(numpy.fabs(nos.vT() - o.vT(ts[-2:])) < 10.0**-10.0), ( "New Orbits formed from calling an old orbit does not have the correct vT" ) assert numpy.all(numpy.fabs(nos.z() - o.z(ts[-2:])) < 10.0**-10.0), ( "New Orbits formed from calling an old orbit does not have the correct z" ) assert numpy.all(numpy.fabs(nos.vz() - o.vz(ts[-2:])) < 10.0**-10.0), ( "New Orbits formed from calling an old orbit does not have the correct vz" ) assert numpy.all(numpy.fabs(nos.phi() - o.phi(ts[-2:])) < 10.0**-10.0), ( "New Orbits formed from calling an old orbit does not have the correct phi" ) assert not nos._roSet, ( "New Orbits formed from calling an old orbit does not have the correct roSet" ) assert not nos._voSet, ( "New Orbits formed from calling an old orbit does not have the correct roSet" ) return None # Test new orbits formed from __call__, before integration def test_newOrbit_b4integration(): from galpy.orbit import Orbit o = Orbit([[1.0, 0.1, 1.1, 0.1, 0.0, 0.0], [1.1, 0.3, 0.9, -0.2, 0.3, 2.0]]) no = o() # New Orbits formed before integration assert numpy.all(numpy.fabs(no.R() - o.R()) < 10.0**-10.0), ( "New Orbits formed from calling an old orbit does not have the correct R" ) assert numpy.all(numpy.fabs(no.vR() - o.vR()) < 10.0**-10.0), ( "New Orbits formed from calling an old orbit does not have the correct vR" ) assert numpy.all(numpy.fabs(no.vT() - o.vT()) < 10.0**-10.0), ( "New Orbits formed from calling an old orbit does not have the correct vT" ) assert numpy.all(numpy.fabs(no.z() - o.z()) < 10.0**-10.0), ( "New Orbits formed from calling an old orbit does not have the correct z" ) assert numpy.all(numpy.fabs(no.vz() - o.vz()) < 10.0**-10.0), ( "New Orbits formed from calling an old orbit does not have the correct vz" ) assert numpy.all(numpy.fabs(no.phi() - o.phi()) < 10.0**-10.0), ( "New Orbits formed from calling an old orbit does not have the correct phi" ) assert not no._roSet, ( "New Orbits formed from calling an old orbit does not have the correct roSet" ) assert not no._voSet, ( "New Orbits formed from calling an old orbit does not have the correct roSet" ) return None # Test that we can still get outputs when there aren't enough points for an actual interpolation def test_badinterpolation(): from galpy.orbit import Orbit o = Orbit([[1.0, 0.1, 1.1, 0.1, 0.0, 0.0], [1.1, 0.3, 0.9, -0.2, 0.3, 2.0]]) ts = numpy.linspace( 0.0, 1.0, 3 ) # v. quick orbit integration, w/ not enough points for interpolation lp = potential.LogarithmicHaloPotential(normalize=1.0) o.integrate(ts, lp) no = o(ts[-1]) # new orbit assert numpy.all(no.R() == o.R(ts[-1])), ( "New orbit formed from calling an old orbit does not have the correct R" ) assert numpy.all(no.vR() == o.vR(ts[-1])), ( "New orbit formed from calling an old orbit does not have the correct vR" ) assert numpy.all(no.vT() == o.vT(ts[-1])), ( "New orbit formed from calling an old orbit does not have the correct vT" ) assert numpy.all(no.z() == o.z(ts[-1])), ( "New orbit formed from calling an old orbit does not have the correct z" ) assert numpy.all(no.vz() == o.vz(ts[-1])), ( "New orbit formed from calling an old orbit does not have the correct vz" ) assert numpy.all(no.phi() == o.phi(ts[-1])), ( "New orbit formed from calling an old orbit does not have the correct phi" ) assert not no._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not no._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) # Also test this for multiple time outputs nos = o(ts[-2:]) # new Orbits # First t assert numpy.all(numpy.fabs(nos.R() - o.R(ts[-2:])) < 10.0**-10.0), ( "New orbit formed from calling an old orbit does not have the correct R" ) assert numpy.all(numpy.fabs(nos.vR() - o.vR(ts[-2:])) < 10.0**-10.0), ( "New orbit formed from calling an old orbit does not have the correct vR" ) assert numpy.all(numpy.fabs(nos.vT() - o.vT(ts[-2:])) < 10.0**-10.0), ( "New orbit formed from calling an old orbit does not have the correct vT" ) assert numpy.all(numpy.fabs(nos.z() - o.z(ts[-2:])) < 10.0**-10.0), ( "New orbit formed from calling an old orbit does not have the correct z" ) assert numpy.all(numpy.fabs(nos.vz() - o.vz(ts[-2:])) < 10.0**-10.0), ( "New orbit formed from calling an old orbit does not have the correct vz" ) assert numpy.all(numpy.fabs(nos.phi() - o.phi(ts[-2:])) < 10.0**-10.0), ( "New orbit formed from calling an old orbit does not have the correct phi" ) assert not nos._roSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) assert not nos._voSet, ( "New orbit formed from calling an old orbit does not have the correct roSet" ) # Try point in between, shouldn't work with pytest.raises(LookupError) as exc_info: no = o(0.6) return None # Check plotting routines def test_plotting(): from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential o = Orbit( [Orbit([1.0, 0.1, 1.1, 0.1, 0.2, 2.0]), Orbit([1.0, 0.1, 1.1, 0.1, 0.2, 2.0])] ) oa = Orbit([Orbit([1.0, 0.1, 1.1, 0.1, 0.2]), Orbit([1.0, 0.1, 1.1, 0.1, 0.2])]) # Interesting shape os = Orbit( numpy.array( [ [[1.0, 0.1, 1.1, -0.1, -0.2, 0.0], [1.0, 0.2, 1.2, 0.0, -0.1, 1.0]], [[1.0, -0.2, 0.9, 0.2, 0.2, 2.0], [1.2, -0.4, 1.1, -0.1, 0.0, -2.0]], [[1.0, 0.2, 0.9, 0.3, -0.2, 0.1], [1.2, 0.4, 1.1, -0.2, 0.05, 4.0]], ] ) ) times = numpy.linspace(0.0, 7.0, 251) lp = LogarithmicHaloPotential(normalize=1.0, q=0.8) # Integrate o.integrate(times, lp) oa.integrate(times, lp) os.integrate(times, lp) # Some plots # Energy o.plotE() o.plotE(normed=True) o.plotE(pot=lp, d1="R") o.plotE(pot=lp, d1="vR") o.plotE(pot=lp, d1="vT") o.plotE(pot=lp, d1="z") o.plotE(pot=lp, d1="vz") o.plotE(pot=lp, d1="phi") oa.plotE() oa.plotE(pot=lp, d1="R") oa.plotE(pot=lp, d1="vR") oa.plotE(pot=lp, d1="vT") oa.plotE(pot=lp, d1="z") oa.plotE(pot=lp, d1="vz") os.plotE() os.plotE(pot=lp, d1="R") os.plotE(pot=lp, d1="vR") os.plotE(pot=lp, d1="vT") os.plotE(pot=lp, d1="z") os.plotE(pot=lp, d1="vz") # Vertical energy o.plotEz() o.plotEz(normed=True) o.plotEz(pot=lp, d1="R") o.plotEz(pot=lp, d1="vR") o.plotEz(pot=lp, d1="vT") o.plotEz(pot=lp, d1="z") o.plotEz(pot=lp, d1="vz") o.plotEz(pot=lp, d1="phi") oa.plotEz() oa.plotEz(normed=True) oa.plotEz(pot=lp, d1="R") oa.plotEz(pot=lp, d1="vR") oa.plotEz(pot=lp, d1="vT") oa.plotEz(pot=lp, d1="z") oa.plotEz(pot=lp, d1="vz") os.plotEz() os.plotEz(normed=True) os.plotEz(pot=lp, d1="R") os.plotEz(pot=lp, d1="vR") os.plotEz(pot=lp, d1="vT") os.plotEz(pot=lp, d1="z") os.plotEz(pot=lp, d1="vz") # Radial energy o.plotER() o.plotER(normed=True) # Radial energy oa.plotER() oa.plotER(normed=True) os.plotER() os.plotER(normed=True) # Jacobi o.plotJacobi() o.plotJacobi(normed=True) o.plotJacobi(pot=lp, d1="R", OmegaP=1.0) o.plotJacobi(pot=lp, d1="vR") o.plotJacobi(pot=lp, d1="vT") o.plotJacobi(pot=lp, d1="z") o.plotJacobi(pot=lp, d1="vz") o.plotJacobi(pot=lp, d1="phi") oa.plotJacobi() oa.plotJacobi(pot=lp, d1="R", OmegaP=1.0) oa.plotJacobi(pot=lp, d1="vR") oa.plotJacobi(pot=lp, d1="vT") oa.plotJacobi(pot=lp, d1="z") oa.plotJacobi(pot=lp, d1="vz") os.plotJacobi() os.plotJacobi(pot=lp, d1="R", OmegaP=1.0) os.plotJacobi(pot=lp, d1="vR") os.plotJacobi(pot=lp, d1="vT") os.plotJacobi(pot=lp, d1="z") os.plotJacobi(pot=lp, d1="vz") # Plot the orbit itself o.plot() # defaults oa.plot() os.plot() o.plot(d1="vR") o.plotR() o.plotvR(d1="vT") o.plotvT(d1="z") o.plotz(d1="vz") o.plotvz(d1="phi") o.plotphi(d1="vR") o.plotx(d1="vx") o.plotvx(d1="y") o.ploty(d1="vy") o.plotvy(d1="x") # Remaining attributes o.plot(d1="ra", d2="dec") o.plot(d2="ra", d1="dec") o.plot(d1="pmra", d2="pmdec") o.plot(d2="pmra", d1="pmdec") o.plot(d1="ll", d2="bb") o.plot(d2="ll", d1="bb") o.plot(d1="pmll", d2="pmbb") o.plot(d2="pmll", d1="pmbb") o.plot(d1="vlos", d2="dist") o.plot(d2="vlos", d1="dist") o.plot(d1="helioX", d2="U") o.plot(d2="helioX", d1="U") o.plot(d1="helioY", d2="V") o.plot(d2="helioY", d1="V") o.plot(d1="helioZ", d2="W") o.plot(d2="helioZ", d1="W") o.plot(d2="r", d1="R") o.plot(d2="R", d1="r") # Some more energies etc. o.plot(d1="E", d2="R") o.plot(d1="Enorm", d2="R") o.plot(d1="Ez", d2="R") o.plot(d1="Eznorm", d2="R") o.plot(d1="ER", d2="R") o.plot(d1="ERnorm", d2="R") o.plot(d1="Jacobi", d2="R") o.plot(d1="Jacobinorm", d2="R") # callables o.plot(d1=lambda t: t, d2=lambda t: o.R(t)) # Expressions o.plot(d1="t", d2="r*R/vR") os.plot(d1="t", d2="r*R/vR") return None def test_plotSOS(): from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential # 3D o = Orbit( [Orbit([1.0, 0.1, 1.1, 0.1, 0.2, 2.0]), Orbit([1.0, 0.1, 1.1, 0.1, 0.2, 2.0])] ) pot = potential.MWPotential2014 o.plotSOS(pot) o.plotSOS(pot, use_physical=True, ro=8.0, vo=220.0) # 2D o = Orbit([Orbit([1.0, 0.1, 1.1, 2.0]), Orbit([1.0, 0.1, 1.1, 2.0])]) pot = LogarithmicHaloPotential(normalize=1.0).toPlanar() o.plotSOS(pot) o.plotSOS(pot, use_physical=True, ro=8.0, vo=220.0) return None def test_plotBruteSOS(): from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential # 3D o = Orbit( [Orbit([1.0, 0.1, 1.1, 0.1, 0.2, 2.0]), Orbit([1.0, 0.1, 1.1, 0.1, 0.2, 2.0])] ) pot = potential.MWPotential2014 o.plotBruteSOS(numpy.linspace(0.0, 20.0 * numpy.pi, 100001), pot) o.plotBruteSOS( numpy.linspace(0.0, 20.0 * numpy.pi, 100001), pot, use_physical=True, ro=8.0, vo=220.0, ) # 2D o = Orbit([Orbit([1.0, 0.1, 1.1, 2.0]), Orbit([1.0, 0.1, 1.1, 2.0])]) pot = LogarithmicHaloPotential(normalize=1.0).toPlanar() o.plotBruteSOS(numpy.linspace(0.0, 20.0 * numpy.pi, 100001), pot) o.plotBruteSOS( numpy.linspace(0.0, 20.0 * numpy.pi, 100001), pot, use_physical=True, ro=8.0, vo=220.0, ) return None def test_integrate_method_warning(): """Test Orbits.integrate raises an error if method is invalid""" from galpy.orbit import Orbit from galpy.potential import MWPotential2014 o = Orbit( [ Orbit(vxvv=[1.0, 0.1, 0.1, 0.5, 0.1, 0.0]), Orbit(vxvv=[1.0, 0.1, 0.1, 0.5, 0.1, 0.0]), ] ) t = numpy.arange(0.0, 10.0, 0.001) with pytest.raises(ValueError): o.integrate(t, MWPotential2014, method="rk4") # Test that fallback onto Python integrators works for Orbits def test_integrate_Cfallback_symplec(): from test_potential import BurkertPotentialNoC from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0]), Orbit([0.9, 0.3, 1.0]), Orbit([1.2, -0.3, 0.7]), ] orbits = Orbit(orbits_list) # Integrate as Orbits pot = BurkertPotentialNoC() pot.normalize(1.0) orbits.integrate(times, pot, method="symplec4_c") # Integrate as multiple Orbits for o in orbits_list: o.integrate(times, pot, method="symplec4_c") # Compare for ii in range(len(orbits)): assert ( numpy.amax(numpy.fabs(orbits_list[ii].R(times) - orbits.R(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vR(times) - orbits.vR(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vT(times) - orbits.vT(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None def test_integrate_Cfallback_nonsymplec(): from test_potential import BurkertPotentialNoC from galpy.orbit import Orbit times = numpy.linspace(0.0, 10.0, 1001) orbits_list = [ Orbit([1.0, 0.1, 1.0]), Orbit([0.9, 0.3, 1.0]), Orbit([1.2, -0.3, 0.7]), ] orbits = Orbit(orbits_list) # Integrate as Orbits pot = BurkertPotentialNoC() pot.normalize(1.0) orbits.integrate(times, pot, method="dop853_c") # Integrate as multiple Orbits for o in orbits_list: o.integrate(times, pot, method="dop853_c") # Compare for ii in range(len(orbits)): assert ( numpy.amax(numpy.fabs(orbits_list[ii].R(times) - orbits.R(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vR(times) - orbits.vR(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) assert ( numpy.amax(numpy.fabs(orbits_list[ii].vT(times) - orbits.vT(times)[ii])) < 1e-10 ), ( "Integration of multiple orbits as Orbits does not agree with integrating multiple orbits" ) return None # Test flippingg an orbit def setup_orbits_flip(tp, ro, vo, zo, solarmotion, axi=False): from galpy.orbit import Orbit if isinstance(tp, potential.linearPotential): o = Orbit( [[1.0, 1.0], [0.2, -0.3]], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion ) elif isinstance(tp, potential.planarPotential): if axi: o = Orbit( [[1.0, 1.1, 1.1], [1.1, -0.1, 0.9]], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion, ) else: o = Orbit( [[1.0, 1.1, 1.1, 0.0], [1.1, -1.2, -0.9, 2.0]], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion, ) else: if axi: o = Orbit( [[1.0, 1.1, 1.1, 0.1, 0.1], [1.1, -0.7, 1.4, -0.1, 0.3]], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion, ) else: o = Orbit( [[1.0, 1.1, 1.1, 0.1, 0.1, 0.0], [0.6, -0.4, -1.0, -0.3, -0.5, 2.0]], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion, ) return o def test_flip(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) plp = lp.toPlanar() llp = lp.toVertical(1.0) for ii in range(5): # Scales to test that these are properly propagated to the new Orbit ro, vo, zo, solarmotion = 10.0, 300.0, 0.01, "schoenrich" if ii == 0: # axi, full o = setup_orbits_flip(lp, ro, vo, zo, solarmotion, axi=True) elif ii == 1: # track azimuth, full o = setup_orbits_flip(lp, ro, vo, zo, solarmotion, axi=False) elif ii == 2: # axi, planar o = setup_orbits_flip(plp, ro, vo, zo, solarmotion, axi=True) elif ii == 3: # track azimuth, full o = setup_orbits_flip(plp, ro, vo, zo, solarmotion, axi=False) elif ii == 4: # linear orbit o = setup_orbits_flip(llp, ro, vo, None, None, axi=False) of = o.flip() # First check that the scales have been propagated properly assert numpy.fabs(o._ro - of._ro) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.fabs(o._vo - of._vo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert (o._zo is None) * (of._zo is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert (o._solarmotion is None) * (of._solarmotion is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) else: assert numpy.fabs(o._zo - of._zo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.all( numpy.fabs(o._solarmotion - of._solarmotion) < 10.0**-15.0 ), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._roSet == of._roSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._voSet == of._voSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert numpy.all(numpy.abs(o.x() - of.x()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vx() + of.vx()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) else: assert numpy.all(numpy.abs(o.R() - of.R()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vR() + of.vR()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vT() + of.vT()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) if ii % 2 == 1: assert numpy.all(numpy.abs(o.phi() - of.phi()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) if ii < 2: assert numpy.all(numpy.abs(o.z() - of.z()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vz() + of.vz()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) return None # Test flippingg an orbit inplace def test_flip_inplace(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) plp = lp.toPlanar() llp = lp.toVertical(1.0) for ii in range(5): # Scales (not really necessary for this test) ro, vo, zo, solarmotion = 10.0, 300.0, 0.01, "schoenrich" if ii == 0: # axi, full o = setup_orbits_flip(lp, ro, vo, zo, solarmotion, axi=True) elif ii == 1: # track azimuth, full o = setup_orbits_flip(lp, ro, vo, zo, solarmotion, axi=False) elif ii == 2: # axi, planar o = setup_orbits_flip(plp, ro, vo, zo, solarmotion, axi=True) elif ii == 3: # track azimuth, full o = setup_orbits_flip(plp, ro, vo, zo, solarmotion, axi=False) elif ii == 4: # linear orbit o = setup_orbits_flip(llp, ro, vo, None, None, axi=False) of = o() of.flip(inplace=True) # First check that the scales have been propagated properly assert numpy.fabs(o._ro - of._ro) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.fabs(o._vo - of._vo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert (o._zo is None) * (of._zo is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert (o._solarmotion is None) * (of._solarmotion is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) else: assert numpy.fabs(o._zo - of._zo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.all( numpy.fabs(o._solarmotion - of._solarmotion) < 10.0**-15.0 ), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._roSet == of._roSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._voSet == of._voSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert numpy.all(numpy.abs(o.x() - of.x()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vx() + of.vx()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) else: assert numpy.all(numpy.abs(o.R() - of.R()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vR() + of.vR()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vT() + of.vT()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) if ii % 2 == 1: assert numpy.all(numpy.abs(o.phi() - of.phi()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) if ii < 2: assert numpy.all(numpy.abs(o.z() - of.z()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vz() + of.vz()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) return None # Test flippingg an orbit inplace after orbit integration def test_flip_inplace_integrated(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) plp = lp.toPlanar() llp = lp.toVertical(1.0) ts = numpy.linspace(0.0, 1.0, 11) for ii in range(5): # Scales (not really necessary for this test) ro, vo, zo, solarmotion = 10.0, 300.0, 0.01, "schoenrich" if ii == 0: # axi, full o = setup_orbits_flip(lp, ro, vo, zo, solarmotion, axi=True) elif ii == 1: # track azimuth, full o = setup_orbits_flip(lp, ro, vo, zo, solarmotion, axi=False) elif ii == 2: # axi, planar o = setup_orbits_flip(plp, ro, vo, zo, solarmotion, axi=True) elif ii == 3: # track azimuth, full o = setup_orbits_flip(plp, ro, vo, zo, solarmotion, axi=False) elif ii == 4: # linear orbit o = setup_orbits_flip(llp, ro, vo, None, None, axi=False) of = o() if ii < 2 or ii == 3: o.integrate(ts, lp) of.integrate(ts, lp) elif ii == 2: o.integrate(ts, plp) of.integrate(ts, plp) else: o.integrate(ts, llp) of.integrate(ts, llp) of.flip(inplace=True) # Just check one time, allows code duplication! o = o(0.5) of = of(0.5) # First check that the scales have been propagated properly assert numpy.fabs(o._ro - of._ro) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.fabs(o._vo - of._vo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert (o._zo is None) * (of._zo is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert (o._solarmotion is None) * (of._solarmotion is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) else: assert numpy.fabs(o._zo - of._zo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.all( numpy.fabs(o._solarmotion - of._solarmotion) < 10.0**-15.0 ), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._roSet == of._roSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._voSet == of._voSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert numpy.all(numpy.abs(o.x() - of.x()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vx() + of.vx()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) else: assert numpy.all(numpy.abs(o.R() - of.R()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vR() + of.vR()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vT() + of.vT()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) if ii % 2 == 1: assert numpy.all(numpy.abs(o.phi() - of.phi()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) if ii < 2: assert numpy.all(numpy.abs(o.z() - of.z()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vz() + of.vz()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) return None # Test flippingg an orbit inplace after orbit integration, and after having # once evaluated the orbit before flipping inplace (#345) # only difference wrt previous test is a line that evaluates of before # flipping def test_flip_inplace_integrated_evaluated(): from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) plp = lp.toPlanar() llp = lp.toVertical(1.0) ts = numpy.linspace(0.0, 1.0, 11) for ii in range(5): # Scales (not really necessary for this test) ro, vo, zo, solarmotion = 10.0, 300.0, 0.01, "schoenrich" if ii == 0: # axi, full o = setup_orbits_flip(lp, ro, vo, zo, solarmotion, axi=True) elif ii == 1: # track azimuth, full o = setup_orbits_flip(lp, ro, vo, zo, solarmotion, axi=False) elif ii == 2: # axi, planar o = setup_orbits_flip(plp, ro, vo, zo, solarmotion, axi=True) elif ii == 3: # track azimuth, full o = setup_orbits_flip(plp, ro, vo, zo, solarmotion, axi=False) elif ii == 4: # linear orbit o = setup_orbits_flip(llp, ro, vo, None, None, axi=False) of = o() if ii < 2 or ii == 3: o.integrate(ts, lp) of.integrate(ts, lp) elif ii == 2: o.integrate(ts, plp) of.integrate(ts, plp) else: o.integrate(ts, llp) of.integrate(ts, llp) # Evaluate, make sure it is at an interpolated time! dumb = of.R(0.52) # Now flip of.flip(inplace=True) # Just check one time, allows code duplication! o = o(0.52) of = of(0.52) # First check that the scales have been propagated properly assert numpy.fabs(o._ro - of._ro) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.fabs(o._vo - of._vo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert (o._zo is None) * (of._zo is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert (o._solarmotion is None) * (of._solarmotion is None), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) else: assert numpy.fabs(o._zo - of._zo) < 10.0**-15.0, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert numpy.all( numpy.fabs(o._solarmotion - of._solarmotion) < 10.0**-15.0 ), ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._roSet == of._roSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) assert o._voSet == of._voSet, ( "o.flip() did not conserve physical scales and coordinate-transformation parameters" ) if ii == 4: assert numpy.all(numpy.abs(o.x() - of.x()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vx() + of.vx()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) else: assert numpy.all(numpy.abs(o.R() - of.R()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vR() + of.vR()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vT() + of.vT()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) if ii % 2 == 1: assert numpy.all(numpy.abs(o.phi() - of.phi()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) if ii < 2: assert numpy.all(numpy.abs(o.z() - of.z()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) assert numpy.all(numpy.abs(o.vz() + of.vz()) < 10.0**-10.0), ( "o.flip() did not work as expected" ) return None # test getOrbit def test_getOrbit(): from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) o = Orbit([[1.0, 0.1, 1.2, 0.3, 0.2, 2.0], [1.0, -0.1, 1.1, -0.3, 0.2, 5.0]]) times = numpy.linspace(0.0, 7.0, 251) o.integrate(times, lp) Rs = o.R(times) vRs = o.vR(times) vTs = o.vT(times) zs = o.z(times) vzs = o.vz(times) phis = o.phi(times) orbarray = o.getOrbit() assert numpy.all(numpy.fabs(Rs - orbarray[..., 0])) < 10.0**-16.0, ( "getOrbit does not work as expected for R" ) assert numpy.all(numpy.fabs(vRs - orbarray[..., 1])) < 10.0**-16.0, ( "getOrbit does not work as expected for vR" ) assert numpy.all(numpy.fabs(vTs - orbarray[..., 2])) < 10.0**-16.0, ( "getOrbit does not work as expected for vT" ) assert numpy.all(numpy.fabs(zs - orbarray[..., 3])) < 10.0**-16.0, ( "getOrbit does not work as expected for z" ) assert numpy.all(numpy.fabs(vzs - orbarray[..., 4])) < 10.0**-16.0, ( "getOrbit does not work as expected for vz" ) assert numpy.all(numpy.fabs(phis - orbarray[..., 5])) < 10.0**-16.0, ( "getOrbit does not work as expected for phi" ) return None # Test that the eccentricity, zmax, rperi, and rap calculated numerically by # Orbits agrees with that calculated numerically using Orbit def test_EccZmaxRperiRap_num_againstorbit_3d(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 10 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] # First test AttributeError when not integrated with pytest.raises(AttributeError): os.e() with pytest.raises(AttributeError): os.zmax() with pytest.raises(AttributeError): os.rperi() with pytest.raises(AttributeError): os.rap() # Integrate all times = numpy.linspace(0.0, 10.0, 1001) os.integrate(times, MWPotential2014) [o.integrate(times, MWPotential2014) for o in list_os] for ii in range(nrand): assert numpy.all(numpy.fabs(os.e()[ii] - list_os[ii].e()) < 1e-10), ( "Evaluating Orbits e does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.zmax()[ii] - list_os[ii].zmax()) < 1e-10), ( "Evaluating Orbits zmax does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.rperi()[ii] - list_os[ii].rperi()) < 1e-10), ( "Evaluating Orbits rperi does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.rap()[ii] - list_os[ii].rap()) < 1e-10), ( "Evaluating Orbits rap does not agree with Orbit" ) return None def test_EccZmaxRperiRap_num_againstorbit_2d(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 10 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, phis))) list_os = [Orbit([R, vR, vT, phi]) for R, vR, vT, phi in zip(Rs, vRs, vTs, phis)] # Integrate all times = numpy.linspace(0.0, 10.0, 1001) os.integrate(times, MWPotential2014) [o.integrate(times, MWPotential2014) for o in list_os] for ii in range(nrand): assert numpy.all(numpy.fabs(os.e()[ii] - list_os[ii].e()) < 1e-10), ( "Evaluating Orbits e does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.rperi()[ii] - list_os[ii].rperi()) < 1e-10), ( "Evaluating Orbits rperi does not agree with Orbit" ) assert numpy.all(numpy.fabs(os.rap()[ii] - list_os[ii].rap()) < 1e-10), ( "Evaluating Orbits rap does not agree with Orbit" ) return None # Test that the eccentricity, zmax, rperi, and rap calculated analytically by # Orbits agrees with that calculated analytically using Orbit def test_EccZmaxRperiRap_analytic_againstorbit_3d(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 10 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] # First test AttributeError when no potential and not integrated with pytest.raises(AttributeError): os.e(analytic=True) with pytest.raises(AttributeError): os.zmax(analytic=True) with pytest.raises(AttributeError): os.rperi(analytic=True) with pytest.raises(AttributeError): os.rap(analytic=True) for type in ["spherical", "staeckel", "adiabatic"]: for ii in range(nrand): assert numpy.all( numpy.fabs( os.e(pot=MWPotential2014, analytic=True, type=type)[ii] - list_os[ii].e(pot=MWPotential2014, analytic=True, type=type) ) < 1e-10 ), ( f"Evaluating Orbits e analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.zmax(pot=MWPotential2014, analytic=True, type=type)[ii] - list_os[ii].zmax(pot=MWPotential2014, analytic=True, type=type) ) < 1e-10 ), ( f"Evaluating Orbits zmax analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.rperi(pot=MWPotential2014, analytic=True, type=type)[ii] - list_os[ii].rperi(pot=MWPotential2014, analytic=True, type=type) ) < 1e-10 ), ( f"Evaluating Orbits rperi analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.rap(pot=MWPotential2014, analytic=True, type=type)[ii] - list_os[ii].rap(pot=MWPotential2014, analytic=True, type=type) ) < 1e-10 ), ( f"Evaluating Orbits rap analytically does not agree with Orbit for type={type}" ) return None def test_EccZmaxRperiRap_analytic_againstorbit_2d(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 10 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, phis))) list_os = [Orbit([R, vR, vT, phi]) for R, vR, vT, phi in zip(Rs, vRs, vTs, phis)] # No matter the type, should always be using adiabtic, not specified in # Orbit for type in ["spherical", "staeckel", "adiabatic"]: for ii in range(nrand): assert numpy.all( numpy.fabs( os.e(pot=MWPotential2014, analytic=True, type=type)[ii] - list_os[ii].e(pot=MWPotential2014, analytic=True) ) < 1e-10 ), ( f"Evaluating Orbits e analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.rperi(pot=MWPotential2014, analytic=True, type=type)[ii] - list_os[ii].rperi(pot=MWPotential2014, analytic=True, type=type) ) < 1e-10 ), ( f"Evaluating Orbits rperi analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.rap(pot=MWPotential2014, analytic=True, type=type)[ii] - list_os[ii].rap(pot=MWPotential2014, analytic=True) ) < 1e-10 ), ( f"Evaluating Orbits rap analytically does not agree with Orbit for type={type}" ) return None def test_rguiding(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 10 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] # First test that if potential is not given, error is raised with pytest.raises(RuntimeError): os.rguiding() # With small number, calculation is direct for ii in range(nrand): assert numpy.all( numpy.fabs( os.rguiding(pot=MWPotential2014)[ii] / list_os[ii].rguiding(pot=MWPotential2014) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits rguiding analytically does not agree with Orbit" # With large number, calculation is interpolated nrand = 1002 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] rgs = os.rguiding(pot=MWPotential2014) for ii in range(nrand): assert numpy.all( numpy.fabs(rgs[ii] / list_os[ii].rguiding(pot=MWPotential2014) - 1.0) < 10.0**-10.0 ), "Evaluating Orbits rguiding analytically does not agree with Orbit" # rguiding for non-axi potential fails with pytest.raises( RuntimeError, match="Potential given to rguiding is non-axisymmetric, but rguiding requires an axisymmetric potential", ) as exc_info: os.rguiding(pot=MWPotential2014 + potential.DehnenBarPotential()) return None def test_rE(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 10 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] # First test that if potential is not given, error is raised with pytest.raises(RuntimeError): os.rE() # With small number, calculation is direct for ii in range(nrand): assert numpy.all( numpy.fabs( os.rE(pot=MWPotential2014)[ii] / list_os[ii].rE(pot=MWPotential2014) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits rE analytically does not agree with Orbit" # With large number, calculation is interpolated nrand = 1002 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] rgs = os.rE(pot=MWPotential2014) for ii in range(nrand): assert numpy.all( numpy.fabs(rgs[ii] / list_os[ii].rE(pot=MWPotential2014) - 1.0) < 10.0**-10.0 ), "Evaluating Orbits rE analytically does not agree with Orbit" # rE for non-axi potential fails with pytest.raises( RuntimeError, match="Potential given to rE is non-axisymmetric, but rE requires an axisymmetric potential", ) as exc_info: os.rE(pot=MWPotential2014 + potential.DehnenBarPotential()) return None def test_LcE(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 10 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] # First test that if potential is not given, error is raised with pytest.raises(RuntimeError): os.LcE() # With small number, calculation is direct for ii in range(nrand): assert numpy.all( numpy.fabs( os.LcE(pot=MWPotential2014)[ii] / list_os[ii].LcE(pot=MWPotential2014) - 1.0 ) < 10.0**-10.0 ), "Evaluating Orbits LcE analytically does not agree with Orbit" # With large number, calculation is interpolated nrand = 1002 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] rgs = os.LcE(pot=MWPotential2014) for ii in range(nrand): assert numpy.all( numpy.fabs(rgs[ii] / list_os[ii].LcE(pot=MWPotential2014) - 1.0) < 10.0**-10.0 ), "Evaluating Orbits LcE analytically does not agree with Orbit" # LcE for non-axi potential fails with pytest.raises( RuntimeError, match="Potential given to LcE is non-axisymmetric, but LcE requires an axisymmetric potential", ) as exc_info: os.LcE(pot=MWPotential2014 + potential.DehnenBarPotential()) return None # Test that the actions, frequencies/periods, and angles calculated # analytically by Orbits agrees with that calculated analytically using Orbit def test_actionsFreqsAngles_againstorbit_3d(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 10 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, zs, vzs, phis))) list_os = [ Orbit([R, vR, vT, z, vz, phi]) for R, vR, vT, z, vz, phi in zip(Rs, vRs, vTs, zs, vzs, phis) ] # First test AttributeError when no potential and not integrated with pytest.raises(AttributeError): os.jr() with pytest.raises(AttributeError): os.jp() with pytest.raises(AttributeError): os.jz() with pytest.raises(AttributeError): os.wr() with pytest.raises(AttributeError): os.wp() with pytest.raises(AttributeError): os.wz() with pytest.raises(AttributeError): os.Or() with pytest.raises(AttributeError): os.Op() with pytest.raises(AttributeError): os.Oz() with pytest.raises(AttributeError): os.Tr() with pytest.raises(AttributeError): os.Tp() with pytest.raises(AttributeError): os.TrTp() with pytest.raises(AttributeError): os.Tz() # Tolerance for jr, jp, jz, diff. for isochroneApprox, because currently # not implemented in exactly the same way in Orbit and Orbits (Orbit uses # __call__ for the actions, Orbits uses actionsFreqsAngles, which is diff.) tol = {} tol["spherical"] = -12.0 tol["staeckel"] = -12.0 tol["adiabatic"] = -12.0 tol["isochroneApprox"] = -2.0 # For now we skip adiabatic here, because frequencies and angles not # implemented yet # for type in ['spherical','staeckel','adiabatic']: for type in ["spherical", "staeckel", "isochroneApprox"]: for ii in range(nrand): assert numpy.all( numpy.fabs( os.jr(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].jr( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 10.0 ** tol[type] ), ( f"Evaluating Orbits jr analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.jp(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].jp( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 10.0 ** tol[type] ), ( f"Evaluating Orbits jp analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.jz(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].jz( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 10.0 ** tol[type] ), ( f"Evaluating Orbits jz analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.wr(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].wr( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits wr analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.wp(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].wp( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits wp analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.wz(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].wz( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits wz analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.Or(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].Or( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Or analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.Op(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].Op( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Op analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.Oz(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].Oz( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Oz analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.Tr(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].Tr( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Tr analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.Tp(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].Tp( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Tp analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.TrTp(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].TrTp( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits TrTp analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.Tz(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].Tz( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Tz analytically does not agree with Orbit for type={type}" ) if type == "isochroneApprox": break # otherwise takes too long return None # Test that the actions, frequencies/periods, and angles calculated # analytically by Orbits agrees with that calculated analytically using Orbit def test_actionsFreqsAngles_againstorbit_2d(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = 10 Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) os = Orbit(list(zip(Rs, vRs, vTs, phis))) list_os = [Orbit([R, vR, vT, phi]) for R, vR, vT, phi in zip(Rs, vRs, vTs, phis)] # First test AttributeError when no potential and not integrated with pytest.raises(AttributeError): os.jr() with pytest.raises(AttributeError): os.jp() with pytest.raises(AttributeError): os.jz() with pytest.raises(AttributeError): os.wr() with pytest.raises(AttributeError): os.wp() with pytest.raises(AttributeError): os.wz() with pytest.raises(AttributeError): os.Or() with pytest.raises(AttributeError): os.Op() with pytest.raises(AttributeError): os.Oz() with pytest.raises(AttributeError): os.Tr() with pytest.raises(AttributeError): os.Tp() with pytest.raises(AttributeError): os.TrTp() with pytest.raises(AttributeError): os.Tz() # Tolerance for jr, jp, jz, diff. for isochroneApprox, because currently # not implemented in exactly the same way in Orbit and Orbits (Orbit uses # __call__ for the actions, Orbits uses actionsFreqsAngles, which is diff.) tol = {} tol["spherical"] = -12.0 tol["staeckel"] = -12.0 tol["adiabatic"] = -12.0 tol["isochroneApprox"] = -2.0 # For now we skip adiabatic here, because frequencies and angles not # implemented yet # for type in ['spherical','staeckel','adiabatic']: for type in ["spherical", "staeckel", "isochroneApprox"]: for ii in range(nrand): assert numpy.all( numpy.fabs( os.jr(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].jr( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 10.0 ** tol[type] ), ( f"Evaluating Orbits jr analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.jp(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].jp( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 10.0 ** tol[type] ), ( f"Evaluating Orbits jp analytically does not agree with Orbit for type={type}" ) # zero, so don't divide, also doesn't work for isochroneapprox now if not type == "isochroneApprox": assert numpy.all( numpy.fabs( os.jz(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] - list_os[ii].jz( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) ) < 10.0 ** tol[type] ), ( f"Evaluating Orbits jz analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.wr(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].wr( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits wr analytically does not agree with Orbit for type={type}" ) # Think I may have fixed wp = NaN? # assert numpy.all(numpy.fabs(os.wp(pot=MWPotential2014,analytic=True,type=type,b=0.8)[ii]/list_os[ii].wp(pot=MWPotential2014,analytic=True,type=type,b=0.8)-1.) < 1e-10), 'Evaluating Orbits wp analytically does not agree with Orbit for type={}'.format(type) # assert numpy.all(numpy.fabs(os.wz(pot=MWPotential2014,analytic=True,type=type,b=0.8)[ii]/list_os[ii].wz(pot=MWPotential2014,analytic=True,type=type,b=0.8)-1.) < 1e-10), 'Evaluating Orbits wz analytically does not agree with Orbit for type={}'.format(type) assert numpy.all( numpy.fabs( os.Or(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].Or( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Or analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.Op(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].Op( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Op analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.Oz(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].Oz( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Oz analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.Tr(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].Tr( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Tr analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.Tp(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].Tp( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Tp analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.TrTp(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ ii ] / list_os[ii].TrTp( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits TrTp analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( os.Tz(pot=MWPotential2014, analytic=True, type=type, b=0.8)[ii] / list_os[ii].Tz( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Tz analytically does not agree with Orbit for type={type}" ) if type == "isochroneApprox": break # otherwise takes too long return None def test_actionsFreqsAngles_output_shape(): # Test that the output shape is correct and that the shaped output is correct for actionAngle methods from galpy.orbit import Orbit from galpy.potential import MWPotential2014 numpy.random.seed(1) nrand = (3, 1, 2) Rs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 vRs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vTs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) + 1.0 zs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vzs = 0.2 * (2.0 * numpy.random.uniform(size=nrand) - 1.0) phis = 2.0 * numpy.pi * (2.0 * numpy.random.uniform(size=nrand) - 1.0) vxvv = numpy.rollaxis(numpy.array([Rs, vRs, vTs, zs, vzs, phis]), 0, 4) os = Orbit(vxvv) list_os = [ [ [ Orbit( [ Rs[ii, jj, kk], vRs[ii, jj, kk], vTs[ii, jj, kk], zs[ii, jj, kk], vzs[ii, jj, kk], phis[ii, jj, kk], ] ) for kk in range(nrand[2]) ] for jj in range(nrand[1]) ] for ii in range(nrand[0]) ] # Tolerance for jr, jp, jz, diff. for isochroneApprox, because currently # not implemented in exactly the same way in Orbit and Orbits (Orbit uses # __call__ for the actions, Orbits uses actionsFreqsAngles, which is diff.) tol = {} tol["spherical"] = -12.0 tol["staeckel"] = -12.0 tol["adiabatic"] = -12.0 tol["isochroneApprox"] = -2.0 # For now we skip adiabatic here, because frequencies and angles not # implemented yet # for type in ['spherical','staeckel','adiabatic']: for type in ["spherical", "staeckel", "isochroneApprox"]: # Evaluate Orbits once to not be too slow... tjr = os.jr(pot=MWPotential2014, analytic=True, type=type, b=0.8) tjp = os.jp(pot=MWPotential2014, analytic=True, type=type, b=0.8) tjz = os.jz(pot=MWPotential2014, analytic=True, type=type, b=0.8) twr = os.wr(pot=MWPotential2014, analytic=True, type=type, b=0.8) twp = os.wp(pot=MWPotential2014, analytic=True, type=type, b=0.8) twz = os.wz(pot=MWPotential2014, analytic=True, type=type, b=0.8) tOr = os.Or(pot=MWPotential2014, analytic=True, type=type, b=0.8) tOp = os.Op(pot=MWPotential2014, analytic=True, type=type, b=0.8) tOz = os.Oz(pot=MWPotential2014, analytic=True, type=type, b=0.8) tTr = os.Tr(pot=MWPotential2014, analytic=True, type=type, b=0.8) tTp = os.Tp(pot=MWPotential2014, analytic=True, type=type, b=0.8) tTrTp = os.TrTp(pot=MWPotential2014, analytic=True, type=type, b=0.8) tTz = os.Tz(pot=MWPotential2014, analytic=True, type=type, b=0.8) for ii in range(nrand[0]): for jj in range(nrand[1]): for kk in range(nrand[2]): assert numpy.all( numpy.fabs( tjr[ii, jj, kk] / list_os[ii][jj][kk].jr( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 10.0 ** tol[type] ), ( f"Evaluating Orbits jr analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( tjp[ii, jj, kk] / list_os[ii][jj][kk].jp( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 10.0 ** tol[type] ), ( f"Evaluating Orbits jp analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( tjz[ii, jj, kk] / list_os[ii][jj][kk].jz( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 10.0 ** tol[type] ), ( f"Evaluating Orbits jz analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( twr[ii, jj, kk] / list_os[ii][jj][kk].wr( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits wr analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( twp[ii, jj, kk] / list_os[ii][jj][kk].wp( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits wp analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( twz[ii, jj, kk] / list_os[ii][jj][kk].wz( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits wz analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( tOr[ii, jj, kk] / list_os[ii][jj][kk].Or( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Or analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( tOp[ii, jj, kk] / list_os[ii][jj][kk].Op( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Op analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( tOz[ii, jj, kk] / list_os[ii][jj][kk].Oz( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Oz analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( tTr[ii, jj, kk] / list_os[ii][jj][kk].Tr( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Tr analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( tTp[ii, jj, kk] / list_os[ii][jj][kk].Tp( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Tp analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( tTrTp[ii, jj, kk] / list_os[ii][jj][kk].TrTp( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits TrTp analytically does not agree with Orbit for type={type}" ) assert numpy.all( numpy.fabs( tTz[ii, jj, kk] / list_os[ii][jj][kk].Tz( pot=MWPotential2014, analytic=True, type=type, b=0.8 ) - 1.0 ) < 1e-10 ), ( f"Evaluating Orbits Tz analytically does not agree with Orbit for type={type}" ) if type == "isochroneApprox": break # otherwise takes too long return None # Test that the delta parameter is properly dealt with when using the staeckel # approximation: when it changes, need to re-do the aA calcs. def test_actionsFreqsAngles_staeckeldelta(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 os = Orbit([None, None]) # Just twice the Sun! # First with delta jr = os.jr(delta=0.4, pot=MWPotential2014) # Now without, should be different jrn = os.jr(pot=MWPotential2014) assert numpy.all(numpy.fabs(jr - jrn) > 1e-4), ( "Action calculation in Orbits using Staeckel approximation not updated when going from specifying delta to not specifying it" ) # Again, now the other way around os = Orbit([None, None]) # Just twice the Sun! # First without delta jrn = os.jr(pot=MWPotential2014) # Now with, should be different jr = os.jr(delta=0.4, pot=MWPotential2014) assert numpy.all(numpy.fabs(jr - jrn) > 1e-4), ( "Action calculation in Orbits using Staeckel approximation not updated when going from specifying delta to not specifying it" ) return None # Test that actionAngleStaeckel for a spherical potential is the same # as actionAngleSpherical def test_actionsFreqsAngles_staeckeldeltaequalzero(): from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential os = Orbit([None, None]) # Just twice the Sun! lp = LogarithmicHaloPotential(normalize=1.0) assert numpy.all( numpy.fabs(os.jr(pot=lp, type="staeckel") - os.jr(pot=lp, type="spherical")) < 1e-8 ), ( "Action-angle function for staeckel method with spherical potential is not equal to actionAngleSpherical" ) assert numpy.all( numpy.fabs(os.jp(pot=lp, type="staeckel") - os.jp(pot=lp, type="spherical")) < 1e-8 ), ( "Action-angle function for staeckel method with spherical potential is not equal to actionAngleSpherical" ) assert numpy.all( numpy.fabs(os.jz(pot=lp, type="staeckel") - os.jz(pot=lp, type="spherical")) < 1e-8 ), ( "Action-angle function for staeckel method with spherical potential is not equal to actionAngleSpherical" ) assert numpy.all( numpy.fabs(os.wr(pot=lp, type="staeckel") - os.wr(pot=lp, type="spherical")) < 1e-8 ), ( "Action-angle function for staeckel method with spherical potential is not equal to actionAngleSpherical" ) assert numpy.all( numpy.fabs(os.wp(pot=lp, type="staeckel") - os.wp(pot=lp, type="spherical")) < 1e-8 ), ( "Action-angle function for staeckel method with spherical potential is not equal to actionAngleSpherical" ) assert numpy.all( numpy.fabs(os.wz(pot=lp, type="staeckel") - os.wz(pot=lp, type="spherical")) < 1e-8 ), ( "Action-angle function for staeckel method with spherical potential is not equal to actionAngleSpherical" ) assert numpy.all( numpy.fabs(os.Tr(pot=lp, type="staeckel") - os.Tr(pot=lp, type="spherical")) < 1e-8 ), ( "Action-angle function for staeckel method with spherical potential is not equal to actionAngleSpherical" ) assert numpy.all( numpy.fabs(os.Tp(pot=lp, type="staeckel") - os.Tp(pot=lp, type="spherical")) < 1e-8 ), ( "Action-angle function for staeckel method with spherical potential is not equal to actionAngleSpherical" ) assert numpy.all( numpy.fabs(os.Tz(pot=lp, type="staeckel") - os.Tz(pot=lp, type="spherical")) < 1e-8 ), ( "Action-angle function for staeckel method with spherical potential is not equal to actionAngleSpherical" ) return None # Test that the b / ip parameters are properly dealt with when using the # isochroneapprox approximation: when they change, need to re-do the aA calcs. def test_actionsFreqsAngles_isochroneapproxb(): from galpy.orbit import Orbit from galpy.potential import IsochronePotential, MWPotential2014 os = Orbit([None, None]) # Just twice the Sun! # First with one b jr = os.jr(type="isochroneapprox", b=0.8, pot=MWPotential2014) # Now with another b, should be different jrn = os.jr(type="isochroneapprox", b=1.8, pot=MWPotential2014) assert numpy.all(numpy.fabs(jr - jrn) > 1e-4), ( "Action calculation in Orbits using isochroneapprox approximation not updated when going from specifying b to not specifying it" ) # Again, now specifying ip os = Orbit([None, None]) # Just twice the Sun! # First with one jrn = os.jr( pot=MWPotential2014, type="isochroneapprox", ip=IsochronePotential(normalize=1.1, b=0.8), ) # Now with another one, should be different jr = os.jr( pot=MWPotential2014, type="isochroneapprox", ip=IsochronePotential(normalize=0.99, b=1.8), ) assert numpy.all(numpy.fabs(jr - jrn) > 1e-4), ( "Action calculation in Orbits using isochroneapprox approximation not updated when going from specifying delta to not specifying it" ) return None def test_actionsFreqsAngles_RuntimeError_1d(): from galpy.orbit import Orbit os = Orbit([[1.0, 0.1], [0.2, 0.3]]) with pytest.raises(RuntimeError): os.jz(analytic=True) return None def test_ChandrasekharDynamicalFrictionForce_constLambda(): # Test from test_potential for Orbits now! # # Test that the ChandrasekharDynamicalFrictionForce with constant Lambda # agrees with analytical solutions for circular orbits: # assuming that a mass remains on a circular orbit in an isothermal halo # with velocity dispersion sigma and for constant Lambda: # r_final^2 - r_initial^2 = -0.604 ln(Lambda) GM/sigma t # (e.g., B&T08, p. 648) from galpy.orbit import Orbit from galpy.util import conversion ro, vo = 8.0, 220.0 # Parameters GMs = 10.0**9.0 / conversion.mass_in_msol(vo, ro) const_lnLambda = 7.0 r_inits = [2.0, 2.5] dt = 2.0 / conversion.time_in_Gyr(vo, ro) # Compute lp = potential.LogarithmicHaloPotential(normalize=1.0, q=1.0) cdfc = potential.ChandrasekharDynamicalFrictionForce( GMs=GMs, const_lnLambda=const_lnLambda, dens=lp ) # don't provide sigmar, so it gets computed using galpy.df.jeans o = Orbit( [ Orbit([r_inits[0], 0.0, 1.0, 0.0, 0.0, 0.0]), Orbit([r_inits[1], 0.0, 1.0, 0.0, 0.0, 0.0]), ] ) ts = numpy.linspace(0.0, dt, 1001) o.integrate(ts, [lp, cdfc], method="leapfrog") # also tests fallback onto odeint r_pred = numpy.sqrt( numpy.array(o.r()) ** 2.0 - 0.604 * const_lnLambda * GMs * numpy.sqrt(2.0) * dt ) assert numpy.all(numpy.fabs(r_pred - numpy.array(o.r(ts[-1]))) < 0.015), ( "ChandrasekharDynamicalFrictionForce with constant lnLambda for circular orbits does not agree with analytical prediction" ) return None # Check that toPlanar works def test_toPlanar(): from galpy.orbit import Orbit obs = Orbit([[1.0, 0.1, 1.1, 0.3, 0.0, 2.0], [1.0, -0.2, 1.3, -0.3, 0.0, 5.0]]) obsp = obs.toPlanar() assert obsp.dim() == 2, "toPlanar does not generate an Orbit w/ dim=2 for FullOrbit" assert numpy.all(obsp.R() == obs.R()), ( "Planar orbit generated w/ toPlanar does not have the correct R" ) assert numpy.all(obsp.vR() == obs.vR()), ( "Planar orbit generated w/ toPlanar does not have the correct vR" ) assert numpy.all(obsp.vT() == obs.vT()), ( "Planar orbit generated w/ toPlanar does not have the correct vT" ) assert numpy.all(obsp.phi() == obs.phi()), ( "Planar orbit generated w/ toPlanar does not have the correct phi" ) obs = Orbit([[1.0, 0.1, 1.1, 0.3, 0.0], [1.0, -0.2, 1.3, -0.3, 0.0]]) obsp = obs.toPlanar() assert obsp.dim() == 2, "toPlanar does not generate an Orbit w/ dim=2 for RZOrbit" assert numpy.all(obsp.R() == obs.R()), ( "Planar orbit generated w/ toPlanar does not have the correct R" ) assert numpy.all(obsp.vR() == obs.vR()), ( "Planar orbit generated w/ toPlanar does not have the correct vR" ) assert numpy.all(obsp.vT() == obs.vT()), ( "Planar orbit generated w/ toPlanar does not have the correct vT" ) ro, vo, zo, solarmotion = 10.0, 300.0, 0.01, "schoenrich" obs = Orbit( [[1.0, 0.1, 1.1, 0.3, 0.0, 2.0], [1.0, -0.2, 1.3, -0.3, 0.0, 5.0]], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion, ) obsp = obs.toPlanar() assert obsp.dim() == 2, "toPlanar does not generate an Orbit w/ dim=2 for RZOrbit" assert numpy.all(obsp.R() == obs.R()), ( "Planar orbit generated w/ toPlanar does not have the correct R" ) assert numpy.all(obsp.vR() == obs.vR()), ( "Planar orbit generated w/ toPlanar does not have the correct vR" ) assert numpy.all(obsp.vT() == obs.vT()), ( "Planar orbit generated w/ toPlanar does not have the correct vT" ) assert numpy.fabs(obs._ro - obsp._ro) < 10.0**-15.0, ( "Planar orbit generated w/ toPlanar does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert numpy.fabs(obs._vo - obsp._vo) < 10.0**-15.0, ( "Planar orbit generated w/ toPlanar does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert numpy.fabs(obs._zo - obsp._zo) < 10.0**-15.0, ( "Planar orbit generated w/ toPlanar does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert numpy.all(numpy.fabs(obs._solarmotion - obsp._solarmotion) < 10.0**-15.0), ( "Planar orbit generated w/ toPlanar does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert obs._roSet == obsp._roSet, ( "Planar orbit generated w/ toPlanar does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert obs._voSet == obsp._voSet, ( "Planar orbit generated w/ toPlanar does not have the proper physical scale and coordinate-transformation parameters associated with it" ) obs = Orbit([[1.0, 0.1, 1.1, 0.3], [1.0, -0.2, 1.3, -0.3]]) try: obs.toPlanar() except AttributeError: pass else: raise AttributeError( "toPlanar() applied to a planar Orbit did not raise an AttributeError" ) return None # Check that toLinear works def test_toLinear(): from galpy.orbit import Orbit obs = Orbit([[1.0, 0.1, 1.1, 0.3, 0.0, 2.0], [1.0, -0.2, 1.3, -0.3, 0.0, 5.0]]) obsl = obs.toLinear() assert obsl.dim() == 1, "toLinear does not generate an Orbit w/ dim=1 for FullOrbit" assert numpy.all(obsl.x() == obs.z()), ( "Linear orbit generated w/ toLinear does not have the correct z" ) assert numpy.all(obsl.vx() == obs.vz()), ( "Linear orbit generated w/ toLinear does not have the correct vx" ) obs = Orbit([[1.0, 0.1, 1.1, 0.3, 0.0], [1.0, -0.2, 1.3, -0.3, 0.0]]) obsl = obs.toLinear() assert obsl.dim() == 1, "toLinear does not generate an Orbit w/ dim=1 for FullOrbit" assert numpy.all(obsl.x() == obs.z()), ( "Linear orbit generated w/ toLinear does not have the correct z" ) assert numpy.all(obsl.vx() == obs.vz()), ( "Linear orbit generated w/ toLinear does not have the correct vx" ) obs = Orbit([[1.0, 0.1, 1.1, 0.3], [1.0, -0.2, 1.3, -0.3]]) try: obs.toLinear() except AttributeError: pass else: raise AttributeError( "toLinear() applied to a planar Orbit did not raise an AttributeError" ) # w/ scales ro, vo = 10.0, 300.0 obs = Orbit( [[1.0, 0.1, 1.1, 0.3, 0.0, 2.0], [1.0, -0.2, 1.3, -0.3, 0.0, 5.0]], ro=ro, vo=vo ) obsl = obs.toLinear() assert obsl.dim() == 1, "toLinwar does not generate an Orbit w/ dim=1 for FullOrbit" assert numpy.all(obsl.x() == obs.z()), ( "Linear orbit generated w/ toLinear does not have the correct z" ) assert numpy.all(obsl.vx() == obs.vz()), ( "Linear orbit generated w/ toLinear does not have the correct vx" ) assert numpy.fabs(obs._ro - obsl._ro) < 10.0**-15.0, ( "Linear orbit generated w/ toLinear does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert numpy.fabs(obs._vo - obsl._vo) < 10.0**-15.0, ( "Linear orbit generated w/ toLinear does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert obsl._zo is None, ( "Linear orbit generated w/ toLinear does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert obsl._solarmotion is None, ( "Linear orbit generated w/ toLinear does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert obs._roSet == obsl._roSet, ( "Linear orbit generated w/ toLinear does not have the proper physical scale and coordinate-transformation parameters associated with it" ) assert obs._voSet == obsl._voSet, ( "Linear orbit generated w/ toLinear does not have the proper physical scale and coordinate-transformation parameters associated with it" ) return None # Check that the routines that should return physical coordinates are turned off by turn_physical_off def test_physical_output_off(): from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0) o = Orbit() ro = o._ro vo = o._vo # turn off o.turn_physical_off() # Test positions assert numpy.fabs(o.R() - o.R(use_physical=False)) < 10.0**-10.0, ( "o.R() output for Orbit setup with ro= does not work as expected when turned off" ) assert numpy.fabs(o.x() - o.x(use_physical=False)) < 10.0**-10.0, ( "o.x() output for Orbit setup with ro= does not work as expected when turned off" ) assert numpy.fabs(o.y() - o.y(use_physical=False)) < 10.0**-10.0, ( "o.y() output for Orbit setup with ro= does not work as expected when turned off" ) assert numpy.fabs(o.z() - o.z(use_physical=False)) < 10.0**-10.0, ( "o.z() output for Orbit setup with ro= does not work as expected when turned off" ) assert numpy.fabs(o.r() - o.r(use_physical=False)) < 10.0**-10.0, ( "o.r() output for Orbit setup with ro= does not work as expected when turned off" ) # Test velocities assert numpy.fabs(o.vR() - o.vR(use_physical=False)) < 10.0**-10.0, ( "o.vR() output for Orbit setup with vo= does not work as expected when turned off" ) assert numpy.fabs(o.vT() - o.vT(use_physical=False)) < 10.0**-10.0, ( "o.vT() output for Orbit setup with vo= does not work as expected" ) assert numpy.fabs(o.vphi() - o.vphi(use_physical=False)) < 10.0**-10.0, ( "o.vphi() output for Orbit setup with vo= does not work as expected when turned off" ) assert numpy.fabs(o.vx() - o.vx(use_physical=False)) < 10.0**-10.0, ( "o.vx() output for Orbit setup with vo= does not work as expected when turned off" ) assert numpy.fabs(o.vy() - o.vy(use_physical=False)) < 10.0**-10.0, ( "o.vy() output for Orbit setup with vo= does not work as expected when turned off" ) assert numpy.fabs(o.vz() - o.vz(use_physical=False)) < 10.0**-10.0, ( "o.vz() output for Orbit setup with vo= does not work as expected when turned off" ) # Test energies assert numpy.fabs(o.E(pot=lp) - o.E(pot=lp, use_physical=False)) < 10.0**-10.0, ( "o.E() output for Orbit setup with vo= does not work as expected when turned off" ) assert ( numpy.fabs(o.Jacobi(pot=lp) - o.Jacobi(pot=lp, use_physical=False)) < 10.0**-10.0 ), "o.E() output for Orbit setup with vo= does not work as expected when turned off" assert numpy.fabs(o.ER(pot=lp) - o.ER(pot=lp, use_physical=False)) < 10.0**-10.0, ( "o.ER() output for Orbit setup with vo= does not work as expected when turned off" ) assert numpy.fabs(o.Ez(pot=lp) - o.Ez(pot=lp, use_physical=False)) < 10.0**-10.0, ( "o.Ez() output for Orbit setup with vo= does not work as expected when turned off" ) # Test angular momentun assert numpy.all(numpy.fabs(o.L() - o.L(use_physical=False)) < 10.0**-10.0), ( "o.L() output for Orbit setup with ro=,vo= does not work as expected when turned off" ) # Test action-angle functions assert ( numpy.fabs( o.jr(pot=lp, type="staeckel", delta=0.5) - o.jr(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.jr() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.jp(pot=lp, type="staeckel", delta=0.5) - o.jp(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.jp() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.jz(pot=lp, type="staeckel", delta=0.5) - o.jz(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.jz() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tr(pot=lp, type="staeckel", delta=0.5) - o.Tr(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Tr() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tp(pot=lp, type="staeckel", delta=0.5) - o.Tp(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Tp() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tz(pot=lp, type="staeckel", delta=0.5) - o.Tz(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Tz() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Or(pot=lp, type="staeckel", delta=0.5) - o.Or(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Or() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Op(pot=lp, type="staeckel", delta=0.5) - o.Op(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Op() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Oz(pot=lp, type="staeckel", delta=0.5) - o.Oz(pot=lp, type="staeckel", delta=0.5, use_physical=False) ) < 10.0**-10.0 ), "o.Oz() output for Orbit setup with ro=,vo= does not work as expected" # Also test the times assert numpy.fabs(o.time(1.0) - 1.0) < 10.0**-10.0, ( "o.time() in physical coordinates does not work as expected when turned off" ) assert ( numpy.fabs(o.time(1.0, ro=ro, vo=vo) - ro / vo / 1.0227121655399913) < 10.0**-10.0 ), "o.time() in physical coordinates does not work as expected when turned off" return None # Check that the routines that should return physical coordinates are turned # back on by turn_physical_on def test_physical_output_on(): from astropy import units from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0) o = Orbit() ro = o._ro vo = o._vo o_orig = o() # turn off and on o.turn_physical_off() for ii in range(3): if ii == 0: o.turn_physical_on(ro=ro, vo=vo) elif ii == 1: o.turn_physical_on(ro=ro * units.kpc, vo=vo * units.km / units.s) else: o.turn_physical_on() # Test positions assert numpy.fabs(o.R() - o_orig.R(use_physical=True)) < 10.0**-10.0, ( "o.R() output for Orbit setup with ro= does not work as expected when turned back on" ) assert numpy.fabs(o.x() - o_orig.x(use_physical=True)) < 10.0**-10.0, ( "o.x() output for Orbit setup with ro= does not work as expected when turned back on" ) assert numpy.fabs(o.y() - o_orig.y(use_physical=True)) < 10.0**-10.0, ( "o.y() output for Orbit setup with ro= does not work as expected when turned back on" ) assert numpy.fabs(o.z() - o_orig.z(use_physical=True)) < 10.0**-10.0, ( "o.z() output for Orbit setup with ro= does not work as expected when turned back on" ) # Test velocities assert numpy.fabs(o.vR() - o_orig.vR(use_physical=True)) < 10.0**-10.0, ( "o.vR() output for Orbit setup with vo= does not work as expected when turned back on" ) assert numpy.fabs(o.vT() - o_orig.vT(use_physical=True)) < 10.0**-10.0, ( "o.vT() output for Orbit setup with vo= does not work as expected" ) assert numpy.fabs(o.vphi() - o_orig.vphi(use_physical=True)) < 10.0**-10.0, ( "o.vphi() output for Orbit setup with vo= does not work as expected when turned back on" ) assert numpy.fabs(o.vx() - o_orig.vx(use_physical=True)) < 10.0**-10.0, ( "o.vx() output for Orbit setup with vo= does not work as expected when turned back on" ) assert numpy.fabs(o.vy() - o_orig.vy(use_physical=True)) < 10.0**-10.0, ( "o.vy() output for Orbit setup with vo= does not work as expected when turned back on" ) assert numpy.fabs(o.vz() - o_orig.vz(use_physical=True)) < 10.0**-10.0, ( "o.vz() output for Orbit setup with vo= does not work as expected when turned back on" ) # Test energies assert ( numpy.fabs(o.E(pot=lp) - o_orig.E(pot=lp, use_physical=True)) < 10.0**-10.0 ), ( "o.E() output for Orbit setup with vo= does not work as expected when turned back on" ) assert ( numpy.fabs(o.Jacobi(pot=lp) - o_orig.Jacobi(pot=lp, use_physical=True)) < 10.0**-10.0 ), ( "o.E() output for Orbit setup with vo= does not work as expected when turned back on" ) assert ( numpy.fabs(o.ER(pot=lp) - o_orig.ER(pot=lp, use_physical=True)) < 10.0**-10.0 ), ( "o.ER() output for Orbit setup with vo= does not work as expected when turned back on" ) assert ( numpy.fabs(o.Ez(pot=lp) - o_orig.Ez(pot=lp, use_physical=True)) < 10.0**-10.0 ), ( "o.Ez() output for Orbit setup with vo= does not work as expected when turned back on" ) # Test angular momentun assert numpy.all( numpy.fabs(o.L() - o_orig.L(use_physical=True)) < 10.0**-10.0 ), ( "o.L() output for Orbit setup with ro=,vo= does not work as expected when turned back on" ) # Test action-angle functions assert ( numpy.fabs( o.jr(pot=lp, type="staeckel", delta=0.5) - o_orig.jr(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.jr() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.jp(pot=lp, type="staeckel", delta=0.5) - o_orig.jp(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.jp() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.jz(pot=lp, type="staeckel", delta=0.5) - o_orig.jz(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.jz() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tr(pot=lp, type="staeckel", delta=0.5) - o_orig.Tr(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.Tr() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tp(pot=lp, type="staeckel", delta=0.5) - o_orig.Tp(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.Tp() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Tz(pot=lp, type="staeckel", delta=0.5) - o_orig.Tz(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.Tz() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Or(pot=lp, type="staeckel", delta=0.5) - o_orig.Or(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.Or() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Op(pot=lp, type="staeckel", delta=0.5) - o_orig.Op(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.Op() output for Orbit setup with ro=,vo= does not work as expected" assert ( numpy.fabs( o.Oz(pot=lp, type="staeckel", delta=0.5) - o_orig.Oz(pot=lp, type="staeckel", delta=0.5, use_physical=True) ) < 10.0**-10.0 ), "o.Oz() output for Orbit setup with ro=,vo= does not work as expected" # Also test the times assert ( numpy.fabs(o.time(1.0) - o_orig.time(1.0, use_physical=True)) < 10.0**-10.0 ), ( "o_orig.time() in physical coordinates does not work as expected when turned back on" ) return None # Test that Orbits can be pickled def test_pickling(): import pickle from galpy.orbit import Orbit # Just test most common setup: 3D, 6 phase-D vxvvs = [[1.0, 0.1, 1.0, 0.1, -0.2, 1.5], [0.1, 3.0, 1.1, -0.3, 0.4, 2.0]] orbits = Orbit(vxvvs) pickled = pickle.dumps(orbits) orbits_unpickled = pickle.loads(pickled) # Tests assert orbits_unpickled.dim() == 3, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert orbits_unpickled.phasedim() == 6, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits_unpickled.R()[0] - 1.0) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits_unpickled.R()[1] - 0.1) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits_unpickled.vR()[0] - 0.1) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits_unpickled.vR()[1] - 3.0) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits_unpickled.vT()[0] - 1.0) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits_unpickled.vT()[1] - 1.1) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits_unpickled.z()[0] - 0.1) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits_unpickled.z()[1] + 0.3) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits_unpickled.vz()[0] + 0.2) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits_unpickled.vz()[1] - 0.4) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits_unpickled.phi()[0] - 1.5) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) assert numpy.fabs(orbits_unpickled.phi()[1] - 2.0) < 1e-10, ( "Orbits initialization with vxvv in 3D, 6 phase-D does not work as expected" ) return None def test_from_name_values(): from galpy.orbit import Orbit # test Vega and Lacaille 8760 o = Orbit.from_name("Vega", "Lacaille 8760") assert numpy.allclose(o.ra(), [279.23473479, 319.31362024]), ( "RA of Vega/Lacaille 8760 does not match SIMBAD value" ) assert numpy.allclose(o.dec(), [38.78368896, -38.86736390]), ( "DEC of Vega/Lacaille 8760 does not match SIMBAD value" ) assert numpy.allclose(o.dist(), [1 / 130.23, 1 / 251.9124]), ( "Parallax of Vega/Lacaille 8760 does not match SIMBAD value" ) assert numpy.allclose(o.pmra(), [200.94, -3258.996]), ( "PMRA of Vega/Lacaille 8760 does not match SIMBAD value" ) assert numpy.allclose(o.pmdec(), [286.23, -1145.862]), ( "PMDec of Vega/Lacaille 8760 does not match SIMBAD value" ) assert numpy.allclose(o.vlos(), [-13.50, 20.56]), ( "radial velocity of Vega/Lacaille 8760 does not match SIMBAD value" ) # test Vega and Lacaille 8760, as a list o = Orbit.from_name(["Vega", "Lacaille 8760"]) assert numpy.allclose(o.ra(), [279.23473479, 319.31362024]), ( "RA of Vega/Lacaille 8760 does not match SIMBAD value" ) assert numpy.allclose(o.dec(), [38.78368896, -38.86736390]), ( "DEC of Vega/Lacaille 8760 does not match SIMBAD value" ) assert numpy.allclose(o.dist(), [1 / 130.23, 1 / 251.9124]), ( "Parallax of Vega/Lacaille 8760 does not match SIMBAD value" ) assert numpy.allclose(o.pmra(), [200.94, -3258.996]), ( "PMRA of Vega/Lacaille 8760 does not match SIMBAD value" ) assert numpy.allclose(o.pmdec(), [286.23, -1145.862]), ( "PMDec of Vega/Lacaille 8760 does not match SIMBAD value" ) assert numpy.allclose(o.vlos(), [-13.50, 20.56]), ( "radial velocity of Vega/Lacaille 8760 does not match SIMBAD value" ) return None def test_from_name_name(): # Test that o.name gives the expected output from galpy.orbit import Orbit assert Orbit.from_name("LMC").name == "LMC", ( "Orbit.from_name does not appear to set the name attribute correctly" ) assert numpy.char.equal(Orbit.from_name(["LMC"]).name, numpy.char.array("LMC")), ( "Orbit.from_name does not appear to set the name attribute correctly" ) assert numpy.all( numpy.char.equal( Orbit.from_name(["LMC", "SMC"]).name, numpy.char.array(["LMC", "SMC"]) ) ), "Orbit.from_name does not appear to set the name attribute correctly" # Also slice assert Orbit.from_name(["LMC", "SMC", "Fornax"])[-1].name == "Fornax", ( "Orbit.from_name does not appear to set the name attribute correctly" ) assert numpy.all( numpy.char.equal( Orbit.from_name(["LMC", "SMC", "Fornax"])[:2].name, numpy.char.array(["LMC", "SMC"]), ) ), "Orbit.from_name does not appear to set the name attribute correctly" return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_potential.py0000644000175100001660000137471014761352023017104 0ustar00runnerdocker############################TESTS ON POTENTIALS################################ import os import sys PY3 = sys.version > "3" import numpy import pytest from scipy import optimize try: import pynbody _PYNBODY_LOADED = True except ImportError: _PYNBODY_LOADED = False from galpy import orbit, potential from galpy.util import _rotate_to_arbitrary_vector, coords # Test whether the normalization of the potential works def test_normalize_potential(): # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("specialTwoPowerSphericalPotential") pots.append("DehnenTwoPowerSphericalPotential") pots.append("DehnenCoreTwoPowerSphericalPotential") pots.append("HernquistTwoPowerSphericalPotential") pots.append("JaffeTwoPowerSphericalPotential") pots.append("NFWTwoPowerSphericalPotential") pots.append("specialMiyamotoNagaiPotential") pots.append("specialPowerSphericalPotential") pots.append("specialFlattenedPowerPotential") pots.append("specialMN3ExponentialDiskPotentialPD") pots.append("specialMN3ExponentialDiskPotentialSECH") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] if False: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) for p in pots: # if not 'NFW' in p: continue #For testing the test # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "isNonAxi") and tp.isNonAxi: continue # skip, bc vcirc not well defined if not hasattr(tp, "normalize"): continue tp.normalize(1.0) assert (tp.Rforce(1.0, 0.0) + 1.0) ** 2.0 < 10.0**-16.0, ( "Normalization of %s potential fails" % p ) assert (tp.vcirc(1.0) ** 2.0 - 1.0) ** 2.0 < 10.0**-16.0, ( "Normalization of %s potential fails" % p ) tp.normalize(0.5) if hasattr(tp, "toPlanar"): ptp = tp.toPlanar() else: ptp = tp assert (ptp.Rforce(1.0, 0.0) + 0.5) ** 2.0 < 10.0**-16.0, ( "Normalization of %s potential fails" % p ) assert (ptp.vcirc(1.0) ** 2.0 - 0.5) ** 2.0 < 10.0**-16.0, ( "Normalization of %s potential fails" % p ) # Also test SphericalShell and RingPotential's setup, bc not done elsewhere tp = potential.SphericalShellPotential(normalize=1.0) assert (tp.Rforce(1.0, 0.0) + 1.0) ** 2.0 < 10.0**-16.0, ( "Normalization of %s potential fails" % p ) assert (tp.vcirc(1.0) ** 2.0 - 1.0) ** 2.0 < 10.0**-16.0, ( "Normalization of %s potential fails" % p ) tp = potential.RingPotential(normalize=0.5) assert (tp.Rforce(1.0, 0.0) + 0.5) ** 2.0 < 10.0**-16.0, ( "Normalization of %s potential fails" % p ) assert (tp.vcirc(1.0) ** 2.0 - 0.5) ** 2.0 < 10.0**-16.0, ( "Normalization of %s potential fails" % p ) return None # Test whether the derivative of the potential is minus the force def test_forceAsDeriv_potential(): # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("specialTwoPowerSphericalPotential") pots.append("DehnenTwoPowerSphericalPotential") pots.append("DehnenCoreTwoPowerSphericalPotential") pots.append("HernquistTwoPowerSphericalPotential") pots.append("JaffeTwoPowerSphericalPotential") pots.append("NFWTwoPowerSphericalPotential") pots.append("specialMiyamotoNagaiPotential") pots.append("specialMN3ExponentialDiskPotentialPD") pots.append("specialMN3ExponentialDiskPotentialSECH") pots.append("specialPowerSphericalPotential") pots.append("specialFlattenedPowerPotential") pots.append("testMWPotential") pots.append("testplanarMWPotential") pots.append("testlinearMWPotential") pots.append("mockInterpRZPotential") if _PYNBODY_LOADED: pots.append("mockSnapshotRZPotential") pots.append("mockInterpSnapshotRZPotential") pots.append("mockCosmphiDiskPotentialnegcp") pots.append("mockCosmphiDiskPotentialnegp") pots.append("mockDehnenBarPotentialT1") pots.append("mockDehnenBarPotentialTm1") pots.append("mockDehnenBarPotentialTm1Omega0") pots.append("mockDehnenBarPotentialTm5") pots.append("mockEllipticalDiskPotentialT1") pots.append("mockEllipticalDiskPotentialTm1") pots.append("mockEllipticalDiskPotentialTm5") pots.append("mockSteadyLogSpiralPotentialT1") pots.append("mockSteadyLogSpiralPotentialTm1") pots.append("mockSteadyLogSpiralPotentialTm1Omega0") pots.append("mockSteadyLogSpiralPotentialTm5") pots.append("mockTransientLogSpiralPotential") pots.append("mockFlatEllipticalDiskPotential") # for evaluate w/ nonaxi lists pots.append("mockMovingObjectPotential") pots.append("mockMovingObjectPotentialExplPlummer") pots.append("oblateHernquistPotential") pots.append("oblateNFWPotential") pots.append("oblatenoGLNFWPotential") pots.append("oblateJaffePotential") pots.append("prolateHernquistPotential") pots.append("prolateNFWPotential") pots.append("prolateJaffePotential") pots.append("triaxialHernquistPotential") pots.append("triaxialNFWPotential") pots.append("triaxialJaffePotential") pots.append("zRotatedTriaxialNFWPotential") pots.append("yRotatedTriaxialNFWPotential") pots.append("fullyRotatedTriaxialNFWPotential") pots.append("fullyRotatednoGLTriaxialNFWPotential") pots.append("HernquistTwoPowerTriaxialPotential") pots.append("NFWTwoPowerTriaxialPotential") pots.append("JaffeTwoPowerTriaxialPotential") pots.append("mockSCFZeeuwPotential") pots.append("mockSCFNFWPotential") pots.append("mockSCFAxiDensity1Potential") pots.append("mockSCFAxiDensity2Potential") pots.append("mockSCFDensityPotential") pots.append("mockAxisymmetricFerrersPotential") pots.append("sech2DiskSCFPotential") pots.append("expwholeDiskSCFPotential") pots.append("nonaxiDiskSCFPotential") pots.append("rotatingSpiralArmsPotential") pots.append("specialSpiralArmsPotential") pots.append("DehnenSmoothDehnenBarPotential") pots.append("mockDehnenSmoothBarPotentialT1") pots.append("mockDehnenSmoothBarPotentialTm1") pots.append("mockDehnenSmoothBarPotentialTm5") pots.append("mockDehnenSmoothBarPotentialDecay") pots.append("SolidBodyRotationSpiralArmsPotential") pots.append("triaxialLogarithmicHaloPotential") pots.append("CorotatingRotationSpiralArmsPotential") pots.append("GaussianAmplitudeDehnenBarPotential") pots.append("nestedListPotential") pots.append("mockInterpSphericalPotential") pots.append("mockInterpSphericalPotentialwForce") pots.append("mockAdiabaticContractionMWP14WrapperPotential") pots.append("mockAdiabaticContractionMWP14ExplicitfbarWrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotentialwInclination") pots.append("mockRotatedAndTiltedTriaxialLogHaloPotentialwInclination") pots.append("mockRotatedTiltedOffsetMWP14WrapperPotential") pots.append("mockKuzminLikeWrapperPotential") pots.append("mockOffsetMWP14WrapperPotential") pots.append("mockTimeDependentAmplitudeWrapperPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] if False: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) Rs = numpy.array([0.5, 1.0, 2.0]) Zs = numpy.array([0.0, 0.125, -0.125, 0.25, -0.25]) phis = numpy.array( [0.0, 0.5, -0.5, 1.0, -1.0, numpy.pi, 0.5 + numpy.pi, 1.0 + numpy.pi] ) # tolerances in log10 tol = {} tol["default"] = -8.0 tol["DoubleExponentialDiskPotential"] = -6.0 # these are more difficult tol["RazorThinExponentialDiskPotential"] = -6.0 tol["AnyAxisymmetricRazorThinDiskPotential"] = -4.9 tol["mockInterpRZPotential"] = -4.0 tol["FerrersPotential"] = -7.0 for p in pots: # if not 'NFW' in p: continue #For testing the test # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "normalize"): tp.normalize(1.0) # Set tolerance if p in list(tol.keys()): ttol = tol[p] else: ttol = tol["default"] # Radial force for ii in range(len(Rs)): for jj in range(len(Zs)): dr = 10.0**-8.0 newR = Rs[ii] + dr dr = newR - Rs[ii] # Representable number if isinstance(tp, potential.linearPotential): mpotderivR = ( potential.evaluatelinearPotentials(tp, Rs[ii]) - potential.evaluatelinearPotentials(tp, Rs[ii] + dr) ) / dr tRforce = potential.evaluatelinearForces(tp, Rs[ii]) elif isinstance(tp, potential.planarPotential): mpotderivR = ( potential.evaluateplanarPotentials(tp, Rs[ii], phi=Zs[jj]) - potential.evaluateplanarPotentials( tp, Rs[ii] + dr, phi=Zs[jj] ) ) / dr tRforce = potential.evaluateplanarRforces(tp, Rs[ii], phi=Zs[jj]) else: mpotderivR = ( potential.evaluatePotentials(tp, Rs[ii], Zs[jj], phi=1.0) - potential.evaluatePotentials(tp, Rs[ii] + dr, Zs[jj], phi=1.0) ) / dr tRforce = potential.evaluateRforces(tp, Rs[ii], Zs[jj], phi=1.0) if tRforce**2.0 < 10.0**ttol: assert mpotderivR**2.0 < 10.0**ttol, ( f"Calculation of the Radial force as the Radial derivative of the {p} potential fails at (R,Z) = ({Rs[ii]:.3f},{Zs[jj]:.3f}); diff = {numpy.fabs(tRforce - mpotderivR):e}, rel. diff = {numpy.fabs((tRforce - mpotderivR) / tRforce):e}" ) else: assert (tRforce - mpotderivR) ** 2.0 / tRforce**2.0 < 10.0**ttol, ( f"Calculation of the Radial force as the Radial derivative of the {p} potential fails at (R,Z) = ({Rs[ii]:.3f},{Zs[jj]:.3f}); diff = {numpy.fabs(tRforce - mpotderivR):e}, rel. diff = {numpy.fabs((tRforce - mpotderivR) / tRforce):e}" ) # azimuthal torque, if it exists if isinstance(tp, potential.linearPotential): continue for ii in range(len(Rs)): for jj in range(len(phis)): dphi = 10.0**-8.0 newphi = phis[jj] + dphi dphi = newphi - phis[jj] # Representable number if isinstance(tp, potential.planarPotential): mpotderivphi = ( tp(Rs[ii], phi=phis[jj]) - tp(Rs[ii], phi=phis[jj] + dphi) ) / dphi tphitorque = potential.evaluateplanarphitorques( tp, Rs[ii], phi=phis[jj] ) else: mpotderivphi = ( tp(Rs[ii], 0.05, phi=phis[jj]) - tp(Rs[ii], 0.05, phi=phis[jj] + dphi) ) / dphi tphitorque = potential.evaluatephitorques( tp, Rs[ii], 0.05, phi=phis[jj] ) try: if tphitorque**2.0 < 10.0**ttol: assert mpotderivphi**2.0 < 10.0**ttol else: assert ( tphitorque - mpotderivphi ) ** 2.0 / tphitorque**2.0 < 10.0**ttol except AssertionError: if isinstance(tp, potential.planarPotential): raise AssertionError( f"Calculation of the azimuthal torque as the azimuthal derivative of the {p} potential fails at (R,phi) = ({Rs[ii]:.3f},{phis[jj]:.3f}); diff = {numpy.fabs(tphitorque - mpotderivphi):e}, rel. diff = {numpy.fabs((tphitorque - mpotderivphi) / tphitorque):e}" ) else: raise AssertionError( f"Calculation of the azimuthal torque as the azimuthal derivative of the {p} potential fails at (R,Z,phi) = ({Rs[ii]:.3f},0.05,{phis[jj]:.3f}); diff = {numpy.fabs(tphitorque - mpotderivphi):e}, rel. diff = {numpy.fabs((tphitorque - mpotderivphi) / tphitorque):e}" ) # Vertical force, if it exists if isinstance(tp, potential.planarPotential) or isinstance( tp, potential.linearPotential ): continue for ii in range(len(Rs)): for jj in range(len(Zs)): ##Excluding KuzminDiskPotential when z = 0 if Zs[jj] == 0 and isinstance(tp, potential.KuzminDiskPotential): continue dz = 10.0**-8.0 newZ = Zs[jj] + dz dz = newZ - Zs[jj] # Representable number mpotderivz = ( tp(Rs[ii], Zs[jj], phi=1.0) - tp(Rs[ii], Zs[jj] + dz, phi=1.0) ) / dz tzforce = potential.evaluatezforces(tp, Rs[ii], Zs[jj], phi=1.0) if tzforce**2.0 < 10.0**ttol: assert mpotderivz**2.0 < 10.0**ttol, ( f"Calculation of the vertical force as the vertical derivative of the {p} potential fails at (R,Z) = ({Rs[ii]:.3f},{Zs[jj]:.3f}); diff = {numpy.fabs(mpotderivz):e}, rel. diff = {numpy.fabs((tzforce - mpotderivz) / tzforce):e}" ) else: assert (tzforce - mpotderivz) ** 2.0 / tzforce**2.0 < 10.0**ttol, ( f"Calculation of the vertical force as the vertical derivative of the {p} potential fails at (R,Z) = ({Rs[ii]:.3f},{Zs[jj]:.3f}); diff = {numpy.fabs(mpotderivz):e}, rel. diff = {numpy.fabs((tzforce - mpotderivz) / tzforce):e}" ) # Test whether the second derivative of the potential is minus the derivative of the force def test_2ndDeriv_potential(): # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("specialTwoPowerSphericalPotential") pots.append("DehnenTwoPowerSphericalPotential") pots.append("DehnenCoreTwoPowerSphericalPotential") pots.append("HernquistTwoPowerSphericalPotential") pots.append("JaffeTwoPowerSphericalPotential") pots.append("NFWTwoPowerSphericalPotential") pots.append("specialMiyamotoNagaiPotential") pots.append("specialMN3ExponentialDiskPotentialPD") pots.append("specialMN3ExponentialDiskPotentialSECH") pots.append("specialPowerSphericalPotential") pots.append("specialFlattenedPowerPotential") pots.append("testMWPotential") pots.append("testplanarMWPotential") pots.append("testlinearMWPotential") pots.append("mockInterpRZPotential") pots.append("mockCosmphiDiskPotentialnegcp") pots.append("mockCosmphiDiskPotentialnegp") pots.append("mockDehnenBarPotentialT1") pots.append("mockDehnenBarPotentialTm1") pots.append("mockDehnenBarPotentialTm1Omega0") pots.append("mockDehnenBarPotentialTm5") pots.append("mockEllipticalDiskPotentialT1") pots.append("mockEllipticalDiskPotentialTm1") pots.append("mockEllipticalDiskPotentialTm5") pots.append("mockSteadyLogSpiralPotentialT1") pots.append("mockSteadyLogSpiralPotentialTm1") pots.append("mockSteadyLogSpiralPotentialTm1Omega0") pots.append("mockSteadyLogSpiralPotentialTm5") pots.append("mockTransientLogSpiralPotential") pots.append("mockFlatEllipticalDiskPotential") # for evaluate w/ nonaxi lists pots.append("oblateHernquistPotential") # in case these are ever implemented pots.append("oblateNFWPotential") pots.append("oblatenoGLNFWPotential") pots.append("oblateJaffePotential") pots.append("prolateHernquistPotential") pots.append("prolateNFWPotential") pots.append("prolateJaffePotential") pots.append("triaxialHernquistPotential") pots.append("triaxialNFWPotential") pots.append("triaxialJaffePotential") pots.append("HernquistTwoPowerTriaxialPotential") pots.append("NFWTwoPowerTriaxialPotential") pots.append("JaffeTwoPowerTriaxialPotential") pots.append("mockAxisymmetricFerrersPotential") pots.append("rotatingSpiralArmsPotential") pots.append("specialSpiralArmsPotential") pots.append("DehnenSmoothDehnenBarPotential") pots.append("mockDehnenSmoothBarPotentialT1") pots.append("mockDehnenSmoothBarPotentialTm1") pots.append("mockDehnenSmoothBarPotentialTm5") pots.append("mockDehnenSmoothBarPotentialDecay") pots.append("SolidBodyRotationSpiralArmsPotential") pots.append("triaxialLogarithmicHaloPotential") pots.append("CorotatingRotationSpiralArmsPotential") pots.append("GaussianAmplitudeDehnenBarPotential") pots.append("nestedListPotential") pots.append("mockInterpSphericalPotential") pots.append("mockInterpSphericalPotentialwForce") pots.append("mockAdiabaticContractionMWP14WrapperPotential") pots.append("mockAdiabaticContractionMWP14ExplicitfbarWrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotentialwInclination") pots.append("mockRotatedAndTiltedTriaxialLogHaloPotentialwInclination") pots.append("mockRotatedTiltedOffsetMWP14WrapperPotential") pots.append("mockOffsetMWP14WrapperPotential") pots.append("mockTimeDependentAmplitudeWrapperPotential") pots.append("mockKuzminLikeWrapperPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] if False: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) Rs = numpy.array([0.5, 1.0, 2.0]) Zs = numpy.array([0.0, 0.125, -0.125, 0.25, -0.25]) phis = numpy.array( [0.0, 0.5, -0.5, 1.0, -1.0, numpy.pi, 0.5 + numpy.pi, 1.0 + numpy.pi] ) # tolerances in log10 tol = {} tol["default"] = -8.0 tol["DoubleExponentialDiskPotential"] = -3.0 # these are more difficult tol["RazorThinExponentialDiskPotential"] = -6.0 tol["AnyAxisymmetricRazorThinDiskPotential"] = -4.5 tol["mockInterpRZPotential"] = -4.0 tol["DehnenBarPotential"] = -7.0 for p in pots: # if not 'NFW' in p: continue #For testing the test # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "normalize"): tp.normalize(1.0) # Set tolerance if p in list(tol.keys()): ttol = tol[p] else: ttol = tol["default"] # 2nd radial if hasattr(tp, "_R2deriv"): for ii in range(len(Rs)): for jj in range(len(Zs)): if ( p == "RazorThinExponentialDiskPotential" and numpy.fabs(Zs[jj]) > 0.0 ): continue # Not implemented dr = 10.0**-8.0 newR = Rs[ii] + dr dr = newR - Rs[ii] # Representable number if isinstance(tp, potential.linearPotential): mRforcederivR = ( tp.Rforce(Rs[ii]) - tp.Rforce(Rs[ii] + dr) ) / dr tR2deriv = tp.R2deriv(Rs[ii]) elif isinstance(tp, potential.planarPotential): mRforcederivR = ( tp.Rforce(Rs[ii], Zs[jj]) - tp.Rforce(Rs[ii] + dr, Zs[jj]) ) / dr tR2deriv = potential.evaluateplanarR2derivs( tp, Rs[ii], phi=Zs[jj] ) else: mRforcederivR = ( tp.Rforce(Rs[ii], Zs[jj], phi=1.0) - tp.Rforce(Rs[ii] + dr, Zs[jj], phi=1.0) ) / dr tR2deriv = potential.evaluateR2derivs( tp, Rs[ii], Zs[jj], phi=1.0 ) if tR2deriv**2.0 < 10.0**ttol: assert mRforcederivR**2.0 < 10.0**ttol, ( f"Calculation of the second Radial derivative of the potential as the Radial derivative of the {p} Radial force fails at (R,Z) = ({Rs[ii]:.3f},{Zs[jj]:.3f}); diff = {numpy.fabs(tR2deriv - mRforcederivR):e}, rel. diff = {numpy.fabs((tR2deriv - mRforcederivR) / tR2deriv):e}" ) else: assert ( tR2deriv - mRforcederivR ) ** 2.0 / tR2deriv**2.0 < 10.0**ttol, ( f"Calculation of the second Radial derivative of the potential as the Radial derivative of the {p} Radial force fails at (R,Z) = ({Rs[ii]:.3f},{Zs[jj]:.3f}); diff = {numpy.fabs(tR2deriv - mRforcederivR):e}, rel. diff = {numpy.fabs((tR2deriv - mRforcederivR) / tR2deriv):e}" ) # 2nd azimuthal if not isinstance(tp, potential.linearPotential) and hasattr(tp, "_phi2deriv"): for ii in range(len(Rs)): for jj in range(len(phis)): dphi = 10.0**-8.0 newphi = phis[jj] + dphi dphi = newphi - phis[jj] # Representable number if isinstance(tp, potential.planarPotential): mphitorquederivphi = ( tp.phitorque(Rs[ii], phi=phis[jj]) - tp.phitorque(Rs[ii], phi=phis[jj] + dphi) ) / dphi tphi2deriv = tp.phi2deriv(Rs[ii], phi=phis[jj]) else: mphitorquederivphi = ( tp.phitorque(Rs[ii], 0.05, phi=phis[jj]) - tp.phitorque(Rs[ii], 0.05, phi=phis[jj] + dphi) ) / dphi tphi2deriv = potential.evaluatephi2derivs( tp, Rs[ii], 0.05, phi=phis[jj] ) try: if tphi2deriv**2.0 < 10.0**ttol: assert mphitorquederivphi**2.0 < 10.0**ttol else: assert ( tphi2deriv - mphitorquederivphi ) ** 2.0 / tphi2deriv**2.0 < 10.0**ttol except AssertionError: if isinstance(tp, potential.planarPotential): raise AssertionError( f"Calculation of the second azimuthal derivative of the potential as the azimuthal derivative of the {p} azimuthal torque fails at (R,phi) = ({Rs[ii]:.3f},{phis[jj]:.3f}); diff = {numpy.fabs(tphi2deriv - mphitorquederivphi):e}, rel. diff = {numpy.fabs((tphi2deriv - mphitorquederivphi) / tphi2deriv):e}" ) else: raise AssertionError( f"Calculation of the second azimuthal derivative of the potential as the azimuthal derivative of the {p} azimuthal torque fails at (R,Z,phi) = ({Rs[ii]:.3f},0.05,{phis[jj]:.3f}); diff = {numpy.fabs(tphi2deriv - mphitorquederivphi):e}, rel. diff = {numpy.fabs((tphi2deriv - mphitorquederivphi) / tphi2deriv):e}" ) # mixed radial azimuthal: Isn't this the same as what's below?? if not isinstance(tp, potential.linearPotential) and hasattr(tp, "_Rphideriv"): for ii in range(len(Rs)): for jj in range(len(phis)): dphi = 10.0**-8.0 newphi = phis[jj] + dphi dphi = newphi - phis[jj] # Representable number if isinstance(tp, potential.planarPotential): mRforcederivphi = ( tp.Rforce(Rs[ii], phi=phis[jj]) - tp.Rforce(Rs[ii], phi=phis[jj] + dphi) ) / dphi tRphideriv = tp.Rphideriv(Rs[ii], phi=phis[jj]) else: mRforcederivphi = ( tp.Rforce(Rs[ii], 0.05, phi=phis[jj]) - tp.Rforce(Rs[ii], 0.05, phi=phis[jj] + dphi) ) / dphi tRphideriv = potential.evaluateRphiderivs( tp, Rs[ii], 0.05, phi=phis[jj] ) try: if tRphideriv**2.0 < 10.0**ttol: assert mRforcederivphi**2.0 < 10.0**ttol else: assert ( tRphideriv - mRforcederivphi ) ** 2.0 / tRphideriv**2.0 < 10.0**ttol except AssertionError: if isinstance(tp, potential.planarPotential): raise AssertionError( f"Calculation of the mixed radial, azimuthal derivative of the potential as the azimuthal derivative of the {p} Radial force fails at (R,phi) = ({Rs[ii]:.3f},{phis[jj]:.3f}); diff = {numpy.fabs(tRphideriv - mRforcederivphi):e}, rel. diff = {numpy.fabs((tRphideriv - mRforcederivphi) / tRphideriv):e}" ) else: raise AssertionError( f"Calculation of the mixed radial, azimuthal derivative of the potential as the azimuthal derivative of the {p} azimuthal torque fails at (R,Z,phi) = ({Rs[ii]:.3f},0.05,{phis[jj]:.3f}); diff = {numpy.fabs(tRphideriv - mRforcederivphi):e}, rel. diff = {numpy.fabs((tRphideriv - mRforcederivphi) / tRphideriv):e}" ) # 2nd vertical if ( not isinstance(tp, potential.planarPotential) and not isinstance(tp, potential.linearPotential) and hasattr(tp, "_z2deriv") ): for ii in range(len(Rs)): for jj in range(len(Zs)): if p == "RazorThinExponentialDiskPotential": continue # Not implemented, or badly defined if p == "TwoPowerSphericalPotential": continue # Not implemented, or badly defined if p == "specialTwoPowerSphericalPotential": continue # Not implemented, or badly defined if p == "DehnenTwoPowerSphericalPotential": continue # Not implemented, or badly defined if p == "DehnenCoreTwoPowerSphericalPotential": continue # Not implemented, or badly defined if p == "HernquistTwoPowerSphericalPotential": continue # Not implemented, or badly defined if p == "JaffeTwoPowerSphericalPotential": continue # Not implemented, or badly defined if p == "NFWTwoPowerSphericalPotential": continue # Not implemented, or badly defined # Excluding KuzminDiskPotential at z = 0 if p == "KuzminDiskPotential" and Zs[jj] == 0: continue dz = 10.0**-8.0 newz = Zs[jj] + dz dz = newz - Zs[jj] # Representable number mzforcederivz = ( tp.zforce(Rs[ii], Zs[jj], phi=1.0) - tp.zforce(Rs[ii], Zs[jj] + dz, phi=1.0) ) / dz tz2deriv = potential.evaluatez2derivs(tp, Rs[ii], Zs[jj], phi=1.0) if tz2deriv**2.0 < 10.0**ttol: assert mzforcederivz**2.0 < 10.0**ttol, ( f"Calculation of the second vertical derivative of the potential as the vertical derivative of the {p} vertical force fails at (R,Z) = ({Rs[ii]:.3f},{Zs[jj]:.3f}); diff = {numpy.fabs(tz2deriv - mzforcederivz):e}, rel. diff = {numpy.fabs((tz2deriv - mzforcederivz) / tz2deriv):e}" ) else: assert ( tz2deriv - mzforcederivz ) ** 2.0 / tz2deriv**2.0 < 10.0**ttol, ( f"Calculation of the second vertical derivative of the potential as the vertical derivative of the {p} vertical force fails at (R,Z) = ({Rs[ii]:.3f},{Zs[jj]:.3f}); diff = {numpy.fabs(tz2deriv - mzforcederivz):e}, rel. diff = {numpy.fabs((tz2deriv - mzforcederivz) / tz2deriv):e}" ) # mixed radial vertical if ( not isinstance(tp, potential.planarPotential) and not isinstance(tp, potential.linearPotential) and hasattr(tp, "_Rzderiv") ): for ii in range(len(Rs)): for jj in range(len(Zs)): # Excluding KuzminDiskPotential at z = 0 if p == "KuzminDiskPotential" and Zs[jj] == 0: continue # if p == 'RazorThinExponentialDiskPotential': continue #Not implemented, or badly defined dz = 10.0**-8.0 newz = Zs[jj] + dz dz = newz - Zs[jj] # Representable number mRforcederivz = ( tp.Rforce(Rs[ii], Zs[jj], phi=1.0) - tp.Rforce(Rs[ii], Zs[jj] + dz, phi=1.0) ) / dz tRzderiv = potential.evaluateRzderivs(tp, Rs[ii], Zs[jj], phi=1.0) if tRzderiv**2.0 < 10.0**ttol: assert mRforcederivz**2.0 < 10.0**ttol, ( f"Calculation of the mixed radial vertical derivative of the potential as the vertical derivative of the {p} radial force fails at (R,Z) = ({Rs[ii]:.3f},{Zs[jj]:.3f}); diff = {numpy.fabs(tRzderiv - mRforcederivz):e}, rel. diff = {numpy.fabs((tRzderiv - mRforcederivz) / tRzderiv):e}" ) else: assert ( tRzderiv - mRforcederivz ) ** 2.0 / tRzderiv**2.0 < 10.0**ttol, ( f"Calculation of the mixed radial vertical derivative of the potential as the vertical derivative of the {p} radial force fails at (R,Z) = ({Rs[ii]:.3f},{Zs[jj]:.3f}); diff = {numpy.fabs(tRzderiv - mRforcederivz):e}, rel. diff = {numpy.fabs((tRzderiv - mRforcederivz) / tRzderiv):e}" ) # mixed radial, azimuthal if not isinstance(tp, potential.linearPotential) and hasattr(tp, "_Rphideriv"): for ii in range(len(Rs)): for jj in range(len(phis)): # if p == 'RazorThinExponentialDiskPotential': continue #Not implemented, or badly defined dphi = 10.0**-8.0 newphi = phis[jj] + dphi dphi = newphi - phis[jj] # Representable number if isinstance(tp, potential.planarPotential): mRforcederivphi = ( tp.Rforce(Rs[ii], phi=phis[jj]) - tp.Rforce(Rs[ii], phi=phis[jj] + dphi) ) / dphi tRphideriv = potential.evaluateplanarPotentials( tp, Rs[ii], phi=phis[jj], dR=1, dphi=1 ) else: mRforcederivphi = ( tp.Rforce(Rs[ii], 0.1, phi=phis[jj]) - tp.Rforce(Rs[ii], 0.1, phi=phis[jj] + dphi) ) / dphi tRphideriv = potential.evaluatePotentials( tp, Rs[ii], 0.1, phi=phis[jj], dR=1, dphi=1 ) if tRphideriv**2.0 < 10.0**ttol: assert mRforcederivphi**2.0 < 10.0**ttol, ( f"Calculation of the mixed radial azimuthal derivative of the potential as the azimuthal derivative of the {p} radial force fails at (R,phi) = ({Rs[ii]:.3f},{phis[jj]:.3f}); diff = {numpy.fabs(tRphideriv - mRforcederivphi):e}, rel. diff = {numpy.fabs((tRphideriv - mRforcederivphi) / tRphideriv):e}" ) else: assert ( tRphideriv - mRforcederivphi ) ** 2.0 / tRphideriv**2.0 < 10.0**ttol, ( f"Calculation of the mixed radial azimuthal derivative of the potential as the azimuthal derivative of the {p} radial force fails at (R,phi) = ({Rs[ii]:.3f},{phis[jj]:.3f}); diff = {numpy.fabs(tRphideriv - mRforcederivphi):e}, rel. diff = {numpy.fabs((tRphideriv - mRforcederivphi) / tRphideriv):e}" ) # mixed azimuthal, vertical if ( not isinstance(tp, potential.planarPotential) and not isinstance(tp, potential.linearPotential) and hasattr(tp, "_phizderiv") ): for ii in range(len(Rs)): for jj in range(len(phis)): # if p == 'RazorThinExponentialDiskPotential': continue #Not implemented, or badly defined dphi = 10.0**-8.0 newphi = phis[jj] + dphi dphi = newphi - phis[jj] # Representable number mzforcederivphi = ( tp.zforce(Rs[ii], 0.1, phi=phis[jj]) - tp.zforce(Rs[ii], 0.1, phi=phis[jj] + dphi) ) / dphi tphizderiv = potential.evaluatephizderivs( tp, Rs[ii], 0.1, phi=phis[jj] ) if tphizderiv**2.0 < 10.0**ttol: assert mzforcederivphi**2.0 < 10.0**ttol, ( f"Calculation of the mixed azimuthal vertical derivative of the potential as the azimuthal derivative of the {p} vertical force fails at (R,phi) = ({Rs[ii]:.3f},{phis[jj]:.3f}); diff = {numpy.fabs(tphizderiv - mzforcederivphi):e}, rel. diff = {numpy.fabs((tphizderiv - mzforcederivphi) / tphizderiv):e}" ) else: assert ( tphizderiv - mzforcederivphi ) ** 2.0 / tphizderiv**2.0 < 10.0**ttol, ( f"Calculation of the mixed azimuthal vertical derivative of the potential as the azimuthal derivative of the {p} vertical force fails at (R,phi) = ({Rs[ii]:.3f},{phis[jj]:.3f}); diff = {numpy.fabs(tphizderiv - mzforcederivphi):e}, rel. diff = {numpy.fabs((tphizderiv - mzforcederivphi) / tphizderiv):e}" ) # Test whether the Poisson equation is satisfied if _dens and the relevant second derivatives are implemented def test_poisson_potential(): # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("specialTwoPowerSphericalPotential") pots.append("DehnenTwoPowerSphericalPotential") pots.append("DehnenCoreTwoPowerSphericalPotential") pots.append("HernquistTwoPowerSphericalPotential") pots.append("JaffeTwoPowerSphericalPotential") pots.append("NFWTwoPowerSphericalPotential") pots.append("specialMiyamotoNagaiPotential") pots.append("specialMN3ExponentialDiskPotentialPD") pots.append("specialMN3ExponentialDiskPotentialSECH") pots.append("specialFlattenedPowerPotential") pots.append("specialPowerSphericalPotential") pots.append("testMWPotential") pots.append("testplanarMWPotential") pots.append("testlinearMWPotential") pots.append("oblateHernquistPotential") # in cae these are ever implemented pots.append("oblateNFWPotential") pots.append("oblateJaffePotential") pots.append("prolateHernquistPotential") pots.append("prolateNFWPotential") pots.append("prolateJaffePotential") pots.append("triaxialHernquistPotential") pots.append("triaxialNFWPotential") pots.append("triaxialJaffePotential") pots.append("HernquistTwoPowerTriaxialPotential") pots.append("NFWTwoPowerTriaxialPotential") pots.append("JaffeTwoPowerTriaxialPotential") pots.append("rotatingSpiralArmsPotential") pots.append("specialSpiralArmsPotential") pots.append("DehnenSmoothDehnenBarPotential") pots.append("SolidBodyRotationSpiralArmsPotential") pots.append("triaxialLogarithmicHaloPotential") pots.append("CorotatingRotationSpiralArmsPotential") pots.append("GaussianAmplitudeDehnenBarPotential") pots.append("nestedListPotential") pots.append("mockInterpSphericalPotential") pots.append("mockInterpSphericalPotentialwForce") pots.append("mockAdiabaticContractionMWP14WrapperPotential") pots.append("mockAdiabaticContractionMWP14ExplicitfbarWrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotentialwInclination") pots.append("mockRotatedAndTiltedTriaxialLogHaloPotentialwInclination") pots.append("mockRotatedTiltedOffsetMWP14WrapperPotential") pots.append("mockOffsetMWP14WrapperPotential") pots.append("mockTimeDependentAmplitudeWrapperPotential") pots.append("mockKuzminLikeWrapperPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] if False: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) Rs = numpy.array([0.5, 1.0, 2.0]) Zs = numpy.array([0.0, 0.125, -0.125, 0.25, -0.25]) phis = numpy.array( [0.0, 0.5, -0.5, 1.0, -1.0, numpy.pi, 0.5 + numpy.pi, 1.0 + numpy.pi] ) # tolerances in log10 tol = {} tol["default"] = -8.0 tol["DoubleExponentialDiskPotential"] = -3.0 # these are more difficult tol["SpiralArmsPotential"] = -3 # these are more difficult tol["rotatingSpiralArmsPotential"] = -3 tol["specialSpiralArmsPotential"] = -4 tol["SolidBodyRotationSpiralArmsPotential"] = -2.9 # these are more difficult tol["nestedListPotential"] = -3 # these are more difficult # tol['RazorThinExponentialDiskPotential']= -6. for p in pots: # if not 'NFW' in p: continue #For testing the test # if 'Isochrone' in p: continue #For testing the test # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "normalize"): tp.normalize(1.0) # Set tolerance if p in list(tol.keys()): ttol = tol[p] else: ttol = tol["default"] # 2nd radial if ( not hasattr(tp, "_dens") or not hasattr(tp, "_R2deriv") or not hasattr(tp, "_Rforce") or not hasattr(tp, "phi2deriv") or not hasattr(tp, "_z2deriv") ): continue for ii in range(len(Rs)): for jj in range(len(Zs)): for kk in range(len(phis)): tpoissondens = tp.dens( Rs[ii], Zs[jj], phi=phis[kk], forcepoisson=True ) tdens = potential.evaluateDensities( tp, Rs[ii], Zs[jj], phi=phis[kk], forcepoisson=False ) if tdens**2.0 < 10.0**ttol: assert tpoissondens**2.0 < 10.0**ttol, ( f"Poisson equation relation between the derivatives of the potential and the implemented density is not satisfied for the {p} potential at (R,Z,phi) = ({Rs[ii]:.3f},{Zs[jj]:.3f},{phis[kk]:.3f}); diff = {numpy.fabs(tdens - tpoissondens):e}, rel. diff = {numpy.fabs((tdens - tpoissondens) / tdens):e}" ) else: assert ( tpoissondens - tdens ) ** 2.0 / tdens**2.0 < 10.0**ttol, ( f"Poisson equation relation between the derivatives of the potential and the implemented density is not satisfied for the {p} potential at (R,Z,phi) = ({Rs[ii]:.3f},{Zs[jj]:.3f},{phis[kk]:.3f}); diff = {numpy.fabs(tdens - tpoissondens):e}, rel. diff = {numpy.fabs((tdens - tpoissondens) / tdens):e}" ) return None # Test whether the (integrated) Poisson equation is satisfied if _surfdens and the relevant second derivatives are implemented def test_poisson_surfdens_potential(): # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("testMWPotential") """ pots.append('specialTwoPowerSphericalPotential') pots.append('DehnenTwoPowerSphericalPotential') pots.append('DehnenCoreTwoPowerSphericalPotential') pots.append('HernquistTwoPowerSphericalPotential') pots.append('JaffeTwoPowerSphericalPotential') pots.append('NFWTwoPowerSphericalPotential') pots.append('specialMiyamotoNagaiPotential') pots.append('specialMN3ExponentialDiskPotentialPD') pots.append('specialMN3ExponentialDiskPotentialSECH') pots.append('specialFlattenedPowerPotential') pots.append('specialPowerSphericalPotential') pots.append('testplanarMWPotential') pots.append('testlinearMWPotential') pots.append('oblateHernquistPotential') # in cae these are ever implemented pots.append('oblateNFWPotential') pots.append('oblateJaffePotential') pots.append('prolateHernquistPotential') pots.append('prolateNFWPotential') pots.append('prolateJaffePotential') pots.append('triaxialHernquistPotential') pots.append('triaxialNFWPotential') pots.append('triaxialJaffePotential') pots.append('HernquistTwoPowerTriaxialPotential') pots.append('NFWTwoPowerTriaxialPotential') pots.append('JaffeTwoPowerTriaxialPotential') pots.append('rotatingSpiralArmsPotential') pots.append('specialSpiralArmsPotential') pots.append('DehnenSmoothDehnenBarPotential') pots.append('SolidBodyRotationSpiralArmsPotential') pots.append('triaxialLogarithmicHaloPotential') pots.append('CorotatingRotationSpiralArmsPotential') pots.append('GaussianAmplitudeDehnenBarPotential') pots.append('nestedListPotential') """ pots.append("mockInterpSphericalPotential") pots.append("mockInterpSphericalPotentialwForce") pots.append("mockAdiabaticContractionMWP14WrapperPotential") pots.append("mockAdiabaticContractionMWP14ExplicitfbarWrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotentialwInclination") pots.append("mockRotatedAndTiltedTriaxialLogHaloPotentialwInclination") pots.append("mockRotatedTiltedOffsetMWP14WrapperPotential") pots.append("mockOffsetMWP14WrapperPotential") pots.append("mockKuzminLikeWrapperPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] if False: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") rmpots.append( "RazorThinExponentialDiskPotential" ) # R2deriv not implemented for |Z| > 0 for p in rmpots: pots.remove(p) Rs = numpy.array([0.5, 1.0, 2.0]) Zs = numpy.array([0.125, 0.25, 1.0, 10.0]) phis = numpy.array( [0.0, 0.5, -0.5, 1.0, -1.0, numpy.pi, 0.5 + numpy.pi, 1.0 + numpy.pi] ) # tolerances in log10 tol = {} tol["default"] = -8.0 tol["DoubleExponentialDiskPotential"] = -3.0 # these are more difficult tol[ "SphericalShellPotential" ] = -0 # Direct integration fails to deal with delta function! # tol['SpiralArmsPotential']= -3 #these are more difficult # tol['rotatingSpiralArmsPotential']= -3 # tol['specialSpiralArmsPotential']= -4 # tol['SolidBodyRotationSpiralArmsPotential']= -2.9 #these are more difficult # tol['nestedListPotential']= -3 #these are more difficult # tol['RazorThinExponentialDiskPotential']= -6. for p in pots: # if not 'NFW' in p: continue #For testing the test # if 'Isochrone' in p: continue #For testing the test # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "normalize"): tp.normalize(1.0) # Set tolerance if p in list(tol.keys()): ttol = tol[p] else: ttol = tol["default"] # 2nd radial if ( not hasattr(tp, "_surfdens") or not hasattr(tp, "_R2deriv") or not hasattr(tp, "_Rforce") or not hasattr(tp, "phi2deriv") or not hasattr(tp, "_zforce") or ( tclass._surfdens == potential.Potential._surfdens and not p == "FlattenedPowerPotential" ) ): # make sure _surfdens is explicitly implemented continue for ii in range(len(Rs)): for kk in range(len(phis)): for jj in range(len(Zs)): tpoissondens = tp.surfdens( Rs[ii], Zs[jj], phi=phis[kk], forcepoisson=True ) tdens = potential.evaluateSurfaceDensities( tp, Rs[ii], Zs[jj], phi=phis[kk], forcepoisson=False ) if tdens**2.0 < 10.0**ttol: assert tpoissondens**2.0 < 10.0**ttol, ( f"Poisson equation relation between the derivatives of the potential and the implemented surface density is not satisfied for the {p} potential at (R,Z,phi) = ({Rs[ii]:.3f},{Zs[jj]:.3f},{phis[kk]:.3f}); diff = {numpy.fabs(tdens - tpoissondens):e}, rel. diff = {numpy.fabs((tdens - tpoissondens) / tdens):e}" ) else: assert ( tpoissondens - tdens ) ** 2.0 / tdens**2.0 < 10.0**ttol, ( f"Poisson equation relation between the derivatives of the potential and the implemented surface density is not satisfied for the {p} potential at (R,Z,phi) = ({Rs[ii]:.3f},{Zs[jj]:.3f},{phis[kk]:.3f}); diff = {numpy.fabs(tdens - tpoissondens):e}, rel. diff = {numpy.fabs((tdens - tpoissondens) / tdens):e}" ) if p == "mockRotatedAndTiltedTriaxialLogHaloPotentialwInclination": continue # takes a long time otherwise... skip after all z at one (R,phi) return None # Test whether the _evaluate function is correctly implemented in specifying derivatives def test_evaluateAndDerivs_potential(): # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("specialTwoPowerSphericalPotential") pots.append("DehnenTwoPowerSphericalPotential") pots.append("DehnenCoreTwoPowerSphericalPotential") pots.append("HernquistTwoPowerSphericalPotential") pots.append("JaffeTwoPowerSphericalPotential") pots.append("NFWTwoPowerSphericalPotential") pots.append("specialMiyamotoNagaiPotential") pots.append("specialMN3ExponentialDiskPotentialPD") pots.append("specialMN3ExponentialDiskPotentialSECH") pots.append("specialFlattenedPowerPotential") pots.append("specialPowerSphericalPotential") pots.append("mockCosmphiDiskPotentialnegcp") pots.append("mockCosmphiDiskPotentialnegp") pots.append("mockDehnenBarPotentialT1") pots.append("mockDehnenBarPotentialTm1") pots.append("mockDehnenBarPotentialTm1Omega0") pots.append("mockDehnenBarPotentialTm5") pots.append("mockEllipticalDiskPotentialT1") pots.append("mockEllipticalDiskPotentialTm1") pots.append("mockSteadyLogSpiralPotentialTm1Omega0") pots.append("mockEllipticalDiskPotentialTm5") pots.append("mockSteadyLogSpiralPotentialT1") pots.append("mockSteadyLogSpiralPotentialTm1") pots.append("mockSteadyLogSpiralPotentialTm5") pots.append("mockTransientLogSpiralPotential") pots.append("mockMovingObjectPotential") pots.append("oblateHernquistPotential") # in cae these are ever implemented pots.append("oblateNFWPotential") pots.append("oblateJaffePotential") pots.append("prolateHernquistPotential") pots.append("prolateNFWPotential") pots.append("prolateJaffePotential") pots.append("triaxialHernquistPotential") pots.append("triaxialNFWPotential") pots.append("triaxialJaffePotential") pots.append("mockSCFZeeuwPotential") pots.append("mockSCFNFWPotential") pots.append("mockSCFAxiDensity1Potential") pots.append("mockSCFAxiDensity2Potential") pots.append("mockSCFDensityPotential") pots.append("sech2DiskSCFPotential") pots.append("expwholeDiskSCFPotential") pots.append("nonaxiDiskSCFPotential") pots.append("rotatingSpiralArmsPotential") pots.append("specialSpiralArmsPotential") pots.append("SolidBodyRotationSpiralArmsPotential") pots.append("DehnenSmoothDehnenBarPotential") pots.append("mockDehnenSmoothBarPotentialT1") pots.append("mockDehnenSmoothBarPotentialTm1") pots.append("mockDehnenSmoothBarPotentialTm5") pots.append("mockDehnenSmoothBarPotentialDecay") pots.append("triaxialLogarithmicHaloPotential") pots.append("CorotatingRotationSpiralArmsPotential") pots.append("GaussianAmplitudeDehnenBarPotential") pots.append("nestedListPotential") pots.append("mockInterpSphericalPotential") pots.append("mockInterpSphericalPotentialwForce") pots.append("mockAdiabaticContractionMWP14WrapperPotential") pots.append("mockAdiabaticContractionMWP14ExplicitfbarWrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotentialwInclination") pots.append("mockRotatedAndTiltedTriaxialLogHaloPotentialwInclination") pots.append("mockRotatedTiltedOffsetMWP14WrapperPotential") pots.append("mockOffsetMWP14WrapperPotential") pots.append("mockTimeDependentAmplitudeWrapperPotential") pots.append("mockKuzminLikeWrapperPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] if False: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) # tolerances in log10 tol = {} tol["default"] = -12.0 # tol['DoubleExponentialDiskPotential']= -3. #these are more difficult # tol['RazorThinExponentialDiskPotential']= -6. for p in pots: # if 'Isochrone' in p: continue #For testing the test # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "normalize"): tp.normalize(1.0) # Set tolerance if p in list(tol.keys()): ttol = tol[p] else: ttol = tol["default"] # 1st radial if isinstance(tp, potential.linearPotential): continue elif isinstance(tp, potential.planarPotential): tevaldr = tp(1.2, phi=0.1, dR=1) trforce = tp.Rforce(1.2, phi=0.1) else: tevaldr = tp(1.2, 0.1, phi=0.1, dR=1) trforce = tp.Rforce(1.2, 0.1, phi=0.1) if not tevaldr is None: if tevaldr**2.0 < 10.0**ttol: assert trforce**2.0 < 10.0**ttol, ( "Calculation of radial derivative through _evaluate and Rforce inconsistent for the %s potential" % p ) else: assert (tevaldr + trforce) ** 2.0 / tevaldr**2.0 < 10.0**ttol, ( "Calculation of radial derivative through _evaluate and Rforce inconsistent for the %s potential" % p ) # 2nd radial hasR2 = True from galpy.potential import PotentialError if "RazorThin" in p: R2z = 0.0 else: R2z = 0.1 try: if isinstance(tp, potential.planarPotential): tp.R2deriv(1.2) else: tp.R2deriv(1.2, R2z) except PotentialError: hasR2 = False if hasR2: if isinstance(tp, potential.planarPotential): tevaldr2 = tp(1.2, phi=0.1, dR=2) tr2deriv = tp.R2deriv(1.2, phi=0.1) else: tevaldr2 = tp(1.2, R2z, phi=0.1, dR=2) tr2deriv = tp.R2deriv(1.2, R2z, phi=0.1) if not tevaldr2 is None: if tevaldr2**2.0 < 10.0**ttol: assert tr2deriv * 2.0 < 10.0**ttol, ( "Calculation of 2nd radial derivative through _evaluate and R2deriv inconsistent for the %s potential" % p ) else: assert (tevaldr2 - tr2deriv) ** 2.0 / tevaldr2**2.0 < 10.0**ttol, ( "Calculation of 2nd radial derivative through _evaluate and R2deriv inconsistent for the %s potential" % p ) # 1st phi if isinstance(tp, potential.planarPotential): tevaldphi = tp(1.2, phi=0.1, dphi=1) tphitorque = tp.phitorque(1.2, phi=0.1) else: tevaldphi = tp(1.2, 0.1, phi=0.1, dphi=1) tphitorque = tp.phitorque(1.2, 0.1, phi=0.1) if not tevaldphi is None: if tevaldphi**2.0 < 10.0**ttol: assert tphitorque**2.0 < 10.0**ttol, ( "Calculation of azimuthal derivative through _evaluate and phitorque inconsistent for the %s potential" % p ) else: assert (tevaldphi + tphitorque) ** 2.0 / tevaldphi**2.0 < 10.0**ttol, ( "Calculation of azimuthal derivative through _evaluate and phitorque inconsistent for the %s potential" % p ) # 2nd phi hasphi2 = True try: if isinstance(tp, potential.planarPotential): tp.phi2deriv(1.2, phi=0.1) else: tp.phi2deriv(1.2, 0.1, phi=0.1) except (PotentialError, AttributeError): hasphi2 = False if hasphi2 and hasattr(tp, "_phi2deriv"): if isinstance(tp, potential.planarPotential): tevaldphi2 = tp(1.2, phi=0.1, dphi=2) tphi2deriv = tp.phi2deriv(1.2, phi=0.1) else: tevaldphi2 = tp(1.2, 0.1, phi=0.1, dphi=2) tphi2deriv = tp.phi2deriv(1.2, 0.1, phi=0.1) if not tevaldphi2 is None: if tevaldphi2**2.0 < 10.0**ttol: assert tphi2deriv * 2.0 < 10.0**ttol, ( "Calculation of 2nd azimuthal derivative through _evaluate and phi2deriv inconsistent for the %s potential" % p ) else: assert ( tevaldphi2 - tphi2deriv ) ** 2.0 / tevaldphi2**2.0 < 10.0**ttol, ( "Calculation of 2nd azimuthal derivative through _evaluate and phi2deriv inconsistent for the %s potential" % p ) # Test that much higher derivatives are not implemented try: tp(1.2, 0.1, dR=4, dphi=10) except NotImplementedError: pass else: raise AssertionError( "Higher-order derivative request in potential __call__ does not raise NotImplementedError for %s" % p ) continue # mixed radial,vertical if isinstance(tp, potential.planarPotential): tevaldrz = tp(1.2, 0.1, phi=0.1, dR=1, dz=1) trzderiv = tp.Rzderiv(1.2, 0.1, phi=0.1) else: tevaldrz = tp(1.2, 0.1, phi=0.1, dR=1, dz=1) trzderiv = tp.Rzderiv(1.2, 0.1, phi=0.1) if not tevaldrz is None: if tevaldrz**2.0 < 10.0**ttol: assert trzderiv * 2.0 < 10.0**ttol, ( "Calculation of mixed radial,vertical derivative through _evaluate and z2deriv inconsistent for the %s potential" % p ) else: assert (tevaldrz - trzderiv) ** 2.0 / tevaldrz**2.0 < 10.0**ttol, ( "Calculation of mixed radial,vertical derivative through _evaluate and z2deriv inconsistent for the %s potential" % p ) return None # Test that potentials can be multiplied or divided by a number def test_amp_mult_divide(): # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("specialTwoPowerSphericalPotential") pots.append("DehnenTwoPowerSphericalPotential") pots.append("DehnenCoreTwoPowerSphericalPotential") pots.append("HernquistTwoPowerSphericalPotential") pots.append("JaffeTwoPowerSphericalPotential") pots.append("NFWTwoPowerSphericalPotential") pots.append("specialMiyamotoNagaiPotential") pots.append("specialMN3ExponentialDiskPotentialPD") pots.append("specialMN3ExponentialDiskPotentialSECH") pots.append("specialPowerSphericalPotential") pots.append("specialFlattenedPowerPotential") pots.append("testMWPotential") pots.append("testplanarMWPotential") pots.append("testlinearMWPotential") pots.append("mockInterpRZPotential") if _PYNBODY_LOADED: pots.append("mockSnapshotRZPotential") pots.append("mockInterpSnapshotRZPotential") pots.append("mockCosmphiDiskPotentialnegcp") pots.append("mockCosmphiDiskPotentialnegp") pots.append("mockDehnenBarPotentialT1") pots.append("mockDehnenBarPotentialTm1") pots.append("mockDehnenBarPotentialTm1Omega0") pots.append("mockDehnenBarPotentialTm5") pots.append("mockEllipticalDiskPotentialT1") pots.append("mockEllipticalDiskPotentialTm1") pots.append("mockSteadyLogSpiralPotentialTm1Omega0") pots.append("mockEllipticalDiskPotentialTm5") pots.append("mockSteadyLogSpiralPotentialT1") pots.append("mockSteadyLogSpiralPotentialTm1") pots.append("mockSteadyLogSpiralPotentialTm5") pots.append("mockTransientLogSpiralPotential") pots.append("mockFlatEllipticalDiskPotential") # for evaluate w/ nonaxi lists pots.append("mockMovingObjectPotential") pots.append("mockMovingObjectPotentialExplPlummer") pots.append("oblateHernquistPotential") pots.append("oblateNFWPotential") pots.append("oblatenoGLNFWPotential") pots.append("oblateJaffePotential") pots.append("prolateHernquistPotential") pots.append("prolateNFWPotential") pots.append("prolateJaffePotential") pots.append("triaxialHernquistPotential") pots.append("triaxialNFWPotential") pots.append("triaxialJaffePotential") pots.append("zRotatedTriaxialNFWPotential") pots.append("yRotatedTriaxialNFWPotential") pots.append("fullyRotatedTriaxialNFWPotential") pots.append("fullyRotatednoGLTriaxialNFWPotential") pots.append("HernquistTwoPowerTriaxialPotential") pots.append("NFWTwoPowerTriaxialPotential") pots.append("JaffeTwoPowerTriaxialPotential") pots.append("mockSCFZeeuwPotential") pots.append("mockSCFNFWPotential") pots.append("mockSCFAxiDensity1Potential") pots.append("mockSCFAxiDensity2Potential") pots.append("mockSCFDensityPotential") pots.append("mockAxisymmetricFerrersPotential") pots.append("sech2DiskSCFPotential") pots.append("expwholeDiskSCFPotential") pots.append("nonaxiDiskSCFPotential") pots.append("rotatingSpiralArmsPotential") pots.append("specialSpiralArmsPotential") pots.append("DehnenSmoothDehnenBarPotential") pots.append("mockDehnenSmoothBarPotentialT1") pots.append("mockDehnenSmoothBarPotentialTm1") pots.append("mockDehnenSmoothBarPotentialTm5") pots.append("mockDehnenSmoothBarPotentialDecay") pots.append("SolidBodyRotationSpiralArmsPotential") pots.append("triaxialLogarithmicHaloPotential") pots.append("CorotatingRotationSpiralArmsPotential") pots.append("GaussianAmplitudeDehnenBarPotential") pots.append("nestedListPotential") pots.append("mockInterpSphericalPotential") pots.append("mockInterpSphericalPotentialwForce") pots.append("mockAdiabaticContractionMWP14WrapperPotential") pots.append("mockAdiabaticContractionMWP14ExplicitfbarWrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotentialwInclination") pots.append("mockRotatedAndTiltedTriaxialLogHaloPotentialwInclination") pots.append("mockRotatedTiltedOffsetMWP14WrapperPotential") pots.append("mockOffsetMWP14WrapperPotential") pots.append("mockKuzminLikeWrapperPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] if False: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) R, Z, phi = 0.75, 0.2, 1.76 nums = numpy.random.uniform(size=len(pots)) # random set of amp changes for num, p in zip(nums, pots): # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "normalize"): tp.normalize(1.0) if isinstance(tp, potential.linearPotential): assert numpy.fabs(tp(R) * num - (num * tp)(R)) < 1e-10, ( "Multiplying a linearPotential with a number does not behave as expected" ) # Other way... assert numpy.fabs(tp(R) * num - (tp * num)(R)) < 1e-10, ( "Multiplying a linearPotential with a number does not behave as expected" ) assert numpy.fabs(tp(R) / num - (tp / num)(R)) < 1e-10, ( "Dividing a linearPotential with a number does not behave as expected" ) elif isinstance(tp, potential.planarPotential): assert numpy.fabs(tp(R, phi=phi) * num - (num * tp)(R, phi=phi)) < 1e-10, ( "Multiplying a planarPotential with a number does not behave as expected" ) # Other way... assert numpy.fabs(tp(R, phi=phi) * num - (tp * num)(R, phi=phi)) < 1e-10, ( "Multiplying a planarPotential with a number does not behave as expected" ) assert numpy.fabs(tp(R, phi=phi) / num - (tp / num)(R, phi=phi)) < 1e-10, ( "Dividing a planarPotential with a number does not behave as expected" ) else: assert ( numpy.fabs(tp(R, Z, phi=phi) * num - (num * tp)(R, Z, phi=phi)) < 1e-10 ), "Multiplying a Potential with a number does not behave as expected" # Other way... assert ( numpy.fabs(tp(R, Z, phi=phi) * num - (tp * num)(R, Z, phi=phi)) < 1e-10 ), "Multiplying a Potential with a number does not behave as expected" assert ( numpy.fabs(tp(R, Z, phi=phi) / num - (tp / num)(R, Z, phi=phi)) < 1e-10 ), "Dividing a Potential with a number does not behave as expected" return None # Test whether potentials that support array input do so correctly def test_potential_array_input(): # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("mockInterpSphericalPotential") pots.append("mockInterpSphericalPotentialwForce") pots.append("mockAdiabaticContractionMWP14WrapperPotential") pots.append("mockAdiabaticContractionMWP14ExplicitfbarWrapperPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] rmpots.append("FerrersPotential") rmpots.append("PerfectEllipsoidPotential") rmpots.append("TriaxialHernquistPotential") rmpots.append("TriaxialJaffePotential") rmpots.append("TriaxialNFWPotential") rmpots.append("TwoPowerTriaxialPotential") rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") rmpots.append("AnyAxisymmetricRazorThinDiskPotential") rmpots.append("AnySphericalPotential") rmpots.append("SphericalShellPotential") rmpots.append("HomogeneousSpherePotential") rmpots.append("TriaxialGaussianPotential") rmpots.append("PowerTriaxialPotential") # These cannot be setup without arguments rmpots.append("MovingObjectPotential") rmpots.append("SnapshotRZPotential") rmpots.append("InterpSnapshotRZPotential") # 2D ones that cannot use this test rmpots.append("CosmphiDiskPotential") rmpots.append("EllipticalDiskPotential") rmpots.append("LopsidedDiskPotential") rmpots.append("HenonHeilesPotential") rmpots.append("TransientLogSpiralPotential") rmpots.append("SteadyLogSpiralPotential") # 1D ones that cannot use this test rmpots.append("IsothermalDiskPotential") rmpots.append("KGPotential") for p in rmpots: pots.remove(p) rs = numpy.linspace(0.1, 2.0, 11) zs = numpy.linspace(-2.0, 2.0, 11) phis = numpy.linspace(0.0, numpy.pi, 11) ts = numpy.linspace(0.0, 10.0, 11) for p in pots: # if not 'NFW' in p: continue #For testing the test # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() # Potential itself tpevals = numpy.array( [tp(r, z, phi=phi, t=t) for (r, z, phi, t) in zip(rs, zs, phis, ts)] ) assert numpy.all( numpy.fabs(tp(rs, zs, phi=phis, t=ts) - tpevals) < 10.0**-10.0 ), f"{p} evaluation does not work as expected for array inputs" # Rforce tpevals = numpy.array( [tp.Rforce(r, z, phi=phi, t=t) for (r, z, phi, t) in zip(rs, zs, phis, ts)] ) assert numpy.all( numpy.fabs(tp.Rforce(rs, zs, phi=phis, t=ts) - tpevals) < 10.0**-10.0 ), f"{p} Rforce evaluation does not work as expected for array inputs" # zforce tpevals = numpy.array( [tp.zforce(r, z, phi=phi, t=t) for (r, z, phi, t) in zip(rs, zs, phis, ts)] ) assert numpy.all( numpy.fabs(tp.zforce(rs, zs, phi=phis, t=ts) - tpevals) < 10.0**-10.0 ), f"{p} zforce evaluation does not work as expected for array inputs" # phitorque tpevals = numpy.array( [ tp.phitorque(r, z, phi=phi, t=t) for (r, z, phi, t) in zip(rs, zs, phis, ts) ] ) assert numpy.all( numpy.fabs(tp.phitorque(rs, zs, phi=phis, t=ts) - tpevals) < 10.0**-10.0 ), f"{p} zforce evaluation does not work as expected for array inputs" # R2deriv if hasattr(tp, "_R2deriv"): tpevals = numpy.array( [ tp.R2deriv(r, z, phi=phi, t=t) for (r, z, phi, t) in zip(rs, zs, phis, ts) ] ) assert numpy.all( numpy.fabs(tp.R2deriv(rs, zs, phi=phis, t=ts) - tpevals) < 10.0**-10.0 ), f"{p} R2deriv evaluation does not work as expected for array inputs" # z2deriv if ( hasattr(tp, "_z2deriv") and not p == "TwoPowerSphericalPotential" ): # latter bc done through R2deriv tpevals = numpy.array( [ tp.z2deriv(r, z, phi=phi, t=t) for (r, z, phi, t) in zip(rs, zs, phis, ts) ] ) assert numpy.all( numpy.fabs(tp.z2deriv(rs, zs, phi=phis, t=ts) - tpevals) < 10.0**-10.0 ), f"{p} z2deriv evaluation does not work as expected for array inputs" # phi2deriv if hasattr(tp, "_R2deriv"): tpevals = numpy.array( [ tp.phi2deriv(r, z, phi=phi, t=t) for (r, z, phi, t) in zip(rs, zs, phis, ts) ] ) assert numpy.all( numpy.fabs(tp.phi2deriv(rs, zs, phi=phis, t=ts) - tpevals) < 10.0**-10.0 ), f"{p} phi2deriv evaluation does not work as expected for array inputs" # Rzderiv if hasattr(tp, "_Rzderiv"): tpevals = numpy.array( [ tp.Rzderiv(r, z, phi=phi, t=t) for (r, z, phi, t) in zip(rs, zs, phis, ts) ] ) assert numpy.all( numpy.fabs(tp.Rzderiv(rs, zs, phi=phis, t=ts) - tpevals) < 10.0**-10.0 ), f"{p} Rzderiv evaluation does not work as expected for array inputs" # Rphideriv if hasattr(tp, "_Rphideriv"): tpevals = numpy.array( [ tp.Rphideriv(r, z, phi=phi, t=t) for (r, z, phi, t) in zip(rs, zs, phis, ts) ] ) assert numpy.all( numpy.fabs(tp.Rphideriv(rs, zs, phi=phis, t=ts) - tpevals) < 10.0**-10.0 ), f"{p} Rphideriv evaluation does not work as expected for array inputs" # phizderiv if hasattr(tp, "_phizderiv"): tpevals = numpy.array( [ tp.phizderiv(r, z, phi=phi, t=t) for (r, z, phi, t) in zip(rs, zs, phis, ts) ] ) assert numpy.all( numpy.fabs(tp.phizderiv(rs, zs, phi=phis, t=ts) - tpevals) < 10.0**-10.0 ), f"{p} phizderiv evaluation does not work as expected for array inputs" # dens tpevals = numpy.array( [tp.dens(r, z, phi=phi, t=t) for (r, z, phi, t) in zip(rs, zs, phis, ts)] ) assert numpy.all( numpy.fabs(tp.dens(rs, zs, phi=phis, t=ts) - tpevals) < 10.0**-10.0 ), f"{p} dens evaluation does not work as expected for array inputs" return None # Test that 1D potentials created using toVertical can handle array input if # their 3D versions can def test_toVertical_array(): # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("mockInterpSphericalPotential") pots.append("mockInterpSphericalPotentialwForce") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] rmpots.append("FerrersPotential") rmpots.append("PerfectEllipsoidPotential") rmpots.append("TriaxialHernquistPotential") rmpots.append("TriaxialJaffePotential") rmpots.append("TriaxialNFWPotential") rmpots.append("TwoPowerTriaxialPotential") rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") rmpots.append("AnyAxisymmetricRazorThinDiskPotential") rmpots.append("AnySphericalPotential") rmpots.append("SphericalShellPotential") rmpots.append("HomogeneousSpherePotential") rmpots.append("TriaxialGaussianPotential") rmpots.append("PowerTriaxialPotential") # These cannot be setup without arguments rmpots.append("MovingObjectPotential") rmpots.append("SnapshotRZPotential") rmpots.append("InterpSnapshotRZPotential") for p in rmpots: pots.remove(p) xs = numpy.linspace(-2.0, 2.0, 11) ts = numpy.linspace(0.0, 10.0, 11) for p in pots: # if not 'NFW' in p: continue #For testing the test # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() # Only do 3D --> 1D potentials if not isinstance(tp, potential.Potential): continue tp = potential.toVerticalPotential(tp, 0.8, phi=0.2) # Potential itself tpevals = numpy.array([tp(x, t=t) for (x, t) in zip(xs, ts)]) assert numpy.all(numpy.fabs(tp(xs, t=ts) - tpevals) < 10.0**-10.0), ( f"{p} evaluation does not work as expected for array inputs for toVerticalPotential potentials" ) # force tpevals = numpy.array([tp.force(x, t=t) for (x, t) in zip(xs, ts)]) assert numpy.all(numpy.fabs(tp.force(xs, t=ts) - tpevals) < 10.0**-10.0), ( f"{p} force evaluation does not work as expected for array inputs for toVerticalPotential" ) # Also test Morgan's example pot = potential.toVerticalPotential(potential.MWPotential2014, 1.0) # Potential itself tpevals = numpy.array( [potential.evaluatelinearPotentials(pot, x, t=t) for (x, t) in zip(xs, ts)] ) assert numpy.all( numpy.fabs(potential.evaluatelinearPotentials(pot, xs, t=ts) - tpevals) < 10.0**-10.0 ), ( f"{p} evaluation does not work as expected for array inputs for toVerticalPotential potentials" ) # Rforce tpevals = numpy.array( [potential.evaluatelinearForces(pot, x, t=t) for (x, t) in zip(xs, ts)] ) assert numpy.all( numpy.fabs(potential.evaluatelinearForces(pot, xs, t=ts) - tpevals) < 10.0**-10.0 ), ( f"{p} force evaluation does not work as expected for array inputs for toVerticalPotential" ) return None # Test that all potentials can be evaluated at zero def test_potential_at_zero(): # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] # pots.append('specialTwoPowerSphericalPotential') # pots.append('DehnenTwoPowerSphericalPotential') # pots.append('DehnenCoreTwoPowerSphericalPotential') # pots.append('HernquistTwoPowerSphericalPotential') # pots.append('JaffeTwoPowerSphericalPotential') # pots.append('NFWTwoPowerSphericalPotential') # Difficult, and who cares? pots.append("specialMiyamotoNagaiPotential") pots.append("specialMN3ExponentialDiskPotentialPD") pots.append("specialMN3ExponentialDiskPotentialSECH") pots.append("specialPowerSphericalPotential") pots.append("specialFlattenedPowerPotential") pots.append("testMWPotential") pots.append("mockInterpRZPotential") if _PYNBODY_LOADED: pots.append("mockSnapshotRZPotential") pots.append("mockInterpSnapshotRZPotential") pots.append("oblateHernquistPotential") pots.append("oblateNFWPotential") pots.append("oblatenoGLNFWPotential") pots.append("oblateJaffePotential") pots.append("prolateHernquistPotential") pots.append("prolateNFWPotential") pots.append("prolateJaffePotential") pots.append("triaxialHernquistPotential") pots.append("triaxialNFWPotential") pots.append("triaxialJaffePotential") pots.append("zRotatedTriaxialNFWPotential") # Difficult bc of rotation pots.append("yRotatedTriaxialNFWPotential") # Difficult bc of rotation pots.append("fullyRotatedTriaxialNFWPotential") # Difficult bc of rotation pots.append("fullyRotatednoGLTriaxialNFWPotential") # Difficult bc of rotation pots.append("HernquistTwoPowerTriaxialPotential") pots.append("NFWTwoPowerTriaxialPotential") # pots.append('JaffeTwoPowerTriaxialPotential') # not finite pots.append("mockSCFZeeuwPotential") pots.append("mockSCFNFWPotential") pots.append("mockSCFAxiDensity1Potential") pots.append("mockSCFAxiDensity2Potential") pots.append("mockSCFDensityPotential") pots.append("sech2DiskSCFPotential") pots.append("expwholeDiskSCFPotential") pots.append("nonaxiDiskSCFPotential") pots.append("mockInterpSphericalPotential") pots.append("mockInterpSphericalPotentialwForce") pots.append("mockAdiabaticContractionMWP14WrapperPotential") pots.append("mockAdiabaticContractionMWP14ExplicitfbarWrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotentialwInclination") pots.append("mockRotatedAndTiltedTriaxialLogHaloPotentialwInclination") pots.append("mockRotatedTiltedOffsetMWP14WrapperPotential") pots.append("mockOffsetMWP14WrapperPotential") pots.append("mockKuzminLikeWrapperPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] # Remove some more potentials that we don't support for now TO DO rmpots.append("BurkertPotential") # Need to figure out... # rmpots.append('FerrersPotential') # Need to figure out... # rmpots.append('KuzminKutuzovStaeckelPotential') # Need to figure out... rmpots.append("RazorThinExponentialDiskPotential") # Need to figure out... rmpots.append("RingPotential") # Easy, but who cares? # rmpots.append('SoftenedNeedleBarPotential') # Not that hard, but haven't done it rmpots.append("SpiralArmsPotential") rmpots.append("TwoPowerSphericalPotential") # Need to figure out # rmpots.append('TwoPowerTriaxialPotential') # Need to figure out # 2D ones that cannot use this test rmpots.append("CosmphiDiskPotential") rmpots.append("EllipticalDiskPotential") rmpots.append("LopsidedDiskPotential") rmpots.append("HenonHeilesPotential") rmpots.append("TransientLogSpiralPotential") rmpots.append("SteadyLogSpiralPotential") # 1D ones that cannot use this test rmpots.append("IsothermalDiskPotential") rmpots.append("KGPotential") for p in rmpots: pots.remove(p) for p in pots: # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "normalize"): tp.normalize(1.0) assert not numpy.isnan( potential.evaluatePotentials(tp, 0, 0, phi=0.0, t=0.0) ), f"Potential {p} evaluated at zero gave NaN" # Also for arrays if ( p == "FerrersPotential" or p == "HomogeneousSpherePotential" or p == "PerfectEllipsoidPotential" or p == "SphericalShellPotential" or p == "AnyAxisymmetricRazorThinDiskPotential" or p == "AnySphericalPotential" or p == "mockRotatedAndTiltedMWP14WrapperPotential" or p == "mockRotatedAndTiltedMWP14WrapperPotentialwInclination" or p == "mockRotatedAndTiltedTriaxialLogHaloPotentialwInclination" or p == "mockRotatedTiltedOffsetMWP14WrapperPotential" or p == "mockOffsetMWP14WrapperPotential" or "riaxial" in p or "oblate" in p or "prolate" in p ): continue assert not numpy.any( numpy.isnan( potential.evaluatePotentials( tp, numpy.zeros(4), numpy.zeros(4), phi=0.0, t=0.0 ) ) ), f"Potential {p} evaluated at zero gave NaN" return None # Test that all potentials can be evaluated with large numbers and with infinity def test_potential_at_infinity(): # One of the main reasons for this test is the implementation of vesc, # which uses the potential at infinity. Import what vesc uses for infinity from galpy.potential.plotEscapecurve import _INF # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] # pots.append('specialTwoPowerSphericalPotential') pots.append("DehnenTwoPowerSphericalPotential") pots.append("DehnenCoreTwoPowerSphericalPotential") pots.append("HernquistTwoPowerSphericalPotential") pots.append("JaffeTwoPowerSphericalPotential") # pots.append('NFWTwoPowerSphericalPotential') # Difficult, and who cares? pots.append("specialMiyamotoNagaiPotential") pots.append("specialMN3ExponentialDiskPotentialPD") pots.append("specialMN3ExponentialDiskPotentialSECH") pots.append("specialPowerSphericalPotential") pots.append("specialFlattenedPowerPotential") pots.append("testMWPotential") pots.append("mockInterpRZPotential") # if _PYNBODY_LOADED: # pots.append('mockSnapshotRZPotential') # pots.append('mockInterpSnapshotRZPotential') pots.append("oblateHernquistPotential") pots.append("oblateNFWPotential") pots.append("oblatenoGLNFWPotential") pots.append("oblateJaffePotential") pots.append("prolateHernquistPotential") pots.append("prolateNFWPotential") pots.append("prolateJaffePotential") pots.append("triaxialHernquistPotential") pots.append("triaxialNFWPotential") pots.append("triaxialJaffePotential") # pots.append('zRotatedTriaxialNFWPotential') # Difficult bc of rotation # pots.append('yRotatedTriaxialNFWPotential') # Difficult bc of rotation # pots.append('fullyRotatedTriaxialNFWPotential') # Difficult bc of rotation # pots.append('fullyRotatednoGLTriaxialNFWPotential') # Difficult bc of rotation # pots.append('HernquistTwoPowerTriaxialPotential') # pots.append('NFWTwoPowerTriaxialPotential') # pots.append('JaffeTwoPowerTriaxialPotential') pots.append("mockSCFZeeuwPotential") pots.append("mockSCFNFWPotential") pots.append("mockSCFAxiDensity1Potential") pots.append("mockSCFAxiDensity2Potential") pots.append("mockSCFDensityPotential") pots.append("sech2DiskSCFPotential") pots.append("expwholeDiskSCFPotential") pots.append("nonaxiDiskSCFPotential") pots.append("mockInterpSphericalPotential") pots.append("mockInterpSphericalPotentialwForce") pots.append("mockAdiabaticContractionMWP14WrapperPotential") pots.append("mockAdiabaticContractionMWP14ExplicitfbarWrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotential") pots.append("mockRotatedAndTiltedMWP14WrapperPotentialwInclination") pots.append("mockRotatedAndTiltedTriaxialLogHaloPotentialwInclination") pots.append("mockRotatedTiltedOffsetMWP14WrapperPotential") pots.append("mockOffsetMWP14WrapperPotential") pots.append("mockKuzminLikeWrapperPotential") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] # Remove some more potentials that we don't support for now TO DO rmpots.append("FerrersPotential") # Need to figure out... rmpots.append("KuzminKutuzovStaeckelPotential") # Need to figure out... rmpots.append("RazorThinExponentialDiskPotential") # Need to figure out... rmpots.append("SoftenedNeedleBarPotential") # Not that hard, but haven't done it rmpots.append("SpiralArmsPotential") # Need to have 0 x cos = 0 rmpots.append("TwoPowerTriaxialPotential") # Need to figure out # 2D ones that cannot use this test rmpots.append("CosmphiDiskPotential") rmpots.append("EllipticalDiskPotential") rmpots.append("LopsidedDiskPotential") rmpots.append("HenonHeilesPotential") rmpots.append("TransientLogSpiralPotential") rmpots.append("SteadyLogSpiralPotential") # 1D ones that cannot use this test rmpots.append("IsothermalDiskPotential") rmpots.append("KGPotential") for p in rmpots: pots.remove(p) for p in pots: # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if hasattr(tp, "normalize"): tp.normalize(1.0) assert not numpy.isnan( potential.evaluatePotentials(tp, numpy.inf, 0, phi=0.0, t=0.0) ), f"Potential {p} evaluated at infinity gave NaN" assert not numpy.isnan( potential.evaluatePotentials(tp, _INF, 0, phi=0.0, t=0.0) ), f"Potential {p} evaluated at vesc _INF gave NaN" # Also for arrays if ( p == "HomogeneousSpherePotential" or p == "PerfectEllipsoidPotential" or p == "SphericalShellPotential" or p == "AnyAxisymmetricRazorThinDiskPotential" or p == "AnySphericalPotential" or p == "mockRotatedAndTiltedMWP14WrapperPotential" or p == "mockRotatedAndTiltedMWP14WrapperPotentialwInclination" or p == "mockRotatedAndTiltedTriaxialLogHaloPotentialwInclination" or p == "mockRotatedTiltedOffsetMWP14WrapperPotential" or p == "mockOffsetMWP14WrapperPotential" or "riaxial" in p or "oblate" in p or "prolate" in p ): continue assert not numpy.any( numpy.isnan( potential.evaluatePotentials( tp, numpy.inf * numpy.ones(4), numpy.zeros(4), phi=0.0, t=0.0 ) ) ), f"Potential {p} evaluated at infinity gave NaN" assert not numpy.any( numpy.isnan( potential.evaluatePotentials( tp, _INF * numpy.ones(4), numpy.zeros(4), phi=0.0, t=0.0 ) ) ), f"Potential {p} evaluated at vesc _INF gave NaN" return None # Test that the amplitude for potentials with a finite mass and amp=mass is # correct through the relation -r^2 F_r =~ GM at large r def test_finitemass_amp(): r_large = 10000.0 # KeplerPotential mass = 3.0 kp = potential.KeplerPotential(amp=mass) assert ( numpy.fabs( mass + r_large**2.0 * kp.rforce( r_large / numpy.sqrt(2.0), r_large / numpy.sqrt(2.0), ) ) < 1e-8 ), "Mass amp parameter of KeplerPotential does not not equal total mass" # IsochronePotential r_large = 1000000000.0 mass = 3.0 ip = potential.IsochronePotential(amp=mass, b=0.4) assert ( numpy.fabs( mass + r_large**2.0 * ip.rforce( r_large / numpy.sqrt(2.0), r_large / numpy.sqrt(2.0), ) ) < 1e-8 ), "Mass amp parameter of IsochronePotential does not not equal total mass" # PlummerPotential r_large = 10000.0 mass = 3.0 pp = potential.PlummerPotential(amp=mass, b=0.4) assert ( numpy.fabs( mass + r_large**2.0 * pp.rforce( r_large / numpy.sqrt(2.0), r_large / numpy.sqrt(2.0), ) ) < 1e-8 ), "Mass amp parameter of PlummerPotential does not not equal total mass" # SphericalShellPotential mass = 3.0 sp = potential.SphericalShellPotential(amp=mass, a=0.4) assert ( numpy.fabs( mass + r_large**2.0 * sp.rforce( r_large / numpy.sqrt(2.0), r_large / numpy.sqrt(2.0), ) ) < 1e-8 ), "Mass amp parameter of SphericalShellPotential does not not equal total mass" # RingPotential mass = 3.0 rp = potential.RingPotential(amp=mass, a=0.4) assert ( numpy.fabs( mass + r_large**2.0 * rp.rforce( r_large / numpy.sqrt(2.0), r_large / numpy.sqrt(2.0), ) ) < 1e-8 ), "Mass amp parameter of RingPotential does not not equal total mass" # KuzminDiskPotential r_large = 1000000000.0 mass = 3.0 kp = potential.KuzminDiskPotential(amp=mass, a=0.4) assert ( numpy.fabs( mass + r_large**2.0 * kp.rforce( r_large / numpy.sqrt(2.0), r_large / numpy.sqrt(2.0), ) ) < 1e-8 ), "Mass amp parameter of KuzminDiskPotential does not not equal total mass" # MiyamotoNagaiPotential r_large = 1000000000.0 mass = 3.0 mp = potential.MiyamotoNagaiPotential(amp=mass, a=0.4) assert ( numpy.fabs( mass + r_large**2.0 * mp.rforce( r_large / numpy.sqrt(2.0), r_large / numpy.sqrt(2.0), ) ) < 1e-8 ), "Mass amp parameter of MiyamotoNagaiPotential does not not equal total mass" return None # Test that the spherically radial force is correct def test_rforce(): # Spherical potentials: Rforce = rforce x R / r; zforce = rforce x z /r pp = potential.PlummerPotential(amp=2.0, b=2.0) R, z = 1.3, 0.4 r = numpy.sqrt(R * R + z * z) assert numpy.fabs(pp.Rforce(R, z) * r / R - pp.rforce(R, z)) < 10.0**-10.0, ( "rforce does not behave as expected for spherical potentials" ) assert ( numpy.fabs( potential.evaluateRforces(pp, R, z) * r / R - potential.evaluaterforces(pp, R, z) ) < 10.0**-10.0 ), "evaluaterforces does not behave as expected for spherical potentials" return None def test_rforce_dissipative(): # Use dynamical friction along a radial orbit at z=0 --> spherical pp = potential.PlummerPotential(amp=1.12, b=2.0) cdfc = potential.ChandrasekharDynamicalFrictionForce( GMs=0.01, const_lnLambda=8.0, dens=pp, sigmar=lambda r: 1.0 / numpy.sqrt(2.0) ) R, z, phi = 1.3, 0.0, 1.1 v = [0.1, 0.0, 0.0] r = numpy.sqrt(R * R + z * z) assert ( numpy.fabs( cdfc.Rforce(R, z, phi=phi, v=v) * r / R - cdfc.rforce(R, z, phi=phi, v=v) ) < 10.0**-10.0 ), ( "rforce does not behave as expected for spherical potentials for dissipative forces" ) assert ( numpy.fabs( potential.evaluateRforces([pp, cdfc], R, z, phi=phi, v=v) * r / R - potential.evaluaterforces([pp, cdfc], R, z, phi=phi, v=v) ) < 10.0**-10.0 ), ( "evaluaterforces does not behave as expected for spherical potentials for dissipative forces" ) assert ( numpy.fabs( potential.evaluateRforces(cdfc, R, z, phi=phi, v=v) * r / R - potential.evaluaterforces(cdfc, R, z, phi=phi, v=v) ) < 10.0**-10.0 ), ( "evaluaterforces does not behave as expected for spherical potentials for dissipative forces" ) return None # Test that the spherically second radial derivative is correct def test_r2deriv(): # Spherical potentials: Rforce = rforce x R / r; zforce = rforce x z /r # and R2deriv = r2deriv x (R/r)^2 - rforce x z^2/r^3 # and z2deriv = z2deriv x (z/r)^2 - rforce x R^2/R^3 # and Rzderiv = r2deriv x Rz/r^2 + rforce x Rz/r^3 pp = potential.PlummerPotential(amp=2.0, b=2.0) R, z = 1.3, 0.4 r = numpy.sqrt(R * R + z * z) assert ( numpy.fabs( pp.R2deriv(R, z) - pp.r2deriv(R, z) * (R / r) ** 2.0 + pp.rforce(R, z) * z**2.0 / r**3.0 ) < 10.0**-10.0 ), "r2deriv does not behave as expected for spherical potentials" assert ( numpy.fabs( pp.z2deriv(R, z) - pp.r2deriv(R, z) * (z / r) ** 2.0 + pp.rforce(R, z) * R**2.0 / r**3.0 ) < 10.0**-10.0 ), "r2deriv does not behave as expected for spherical potentials" assert ( numpy.fabs( pp.Rzderiv(R, z) - pp.r2deriv(R, z) * R * z / r**2.0 - pp.rforce(R, z) * R * z / r**3.0 ) < 10.0**-10.0 ), "r2deriv does not behave as expected for spherical potentials" assert ( numpy.fabs( potential.evaluateR2derivs([pp], R, z) - potential.evaluater2derivs([pp], R, z) * (R / r) ** 2.0 + potential.evaluaterforces([pp], R, z) * z**2.0 / r**3.0 ) < 10.0**-10.0 ), "r2deriv does not behave as expected for spherical potentials" assert ( numpy.fabs( potential.evaluatez2derivs([pp], R, z) - potential.evaluater2derivs([pp], R, z) * (z / r) ** 2.0 + potential.evaluaterforces([pp], R, z) * R**2.0 / r**3.0 ) < 10.0**-10.0 ), "r2deriv does not behave as expected for spherical potentials" assert ( numpy.fabs( potential.evaluateRzderivs([pp], R, z) - potential.evaluater2derivs([pp], R, z) * R * z / r**2.0 - potential.evaluaterforces([pp], R, z) * R * z / r**3.0 ) < 10.0**-10.0 ), "r2deriv does not behave as expected for spherical potentials" return None # Check that the masses are calculated correctly for spherical potentials def test_mass_spher(): # PowerPotential close to Kepler should be very steep pp = potential.PowerSphericalPotential(amp=2.0, alpha=2.999) kp = potential.KeplerPotential(amp=2.0) assert ( numpy.fabs( ((3.0 - 2.999) / (4.0 * numpy.pi) * pp.mass(10.0) - kp.mass(10.0)) / kp.mass(10.0) ) < 10.0**-2.0 ), ( "Mass for PowerSphericalPotential close to KeplerPotential is not close to KeplerPotential's mass" ) pp = potential.PowerSphericalPotential(amp=2.0) # mass = amp x r^(3-alpha) tR = 1.0 assert ( numpy.fabs( potential.mass(pp, tR, forceint=True) - pp._amp * tR ** (3.0 - pp.alpha) ) < 10.0**-10.0 ), "Mass for PowerSphericalPotential not as expected" tR = 2.0 assert ( numpy.fabs( potential.mass([pp], tR, forceint=True) - pp._amp * tR ** (3.0 - pp.alpha) ) < 10.0**-10.0 ), "Mass for PowerSphericalPotential not as expected" tR = 20.0 assert ( numpy.fabs(pp.mass(tR, forceint=True) - pp._amp * tR ** (3.0 - pp.alpha)) < 10.0**-9.0 ), "Mass for PowerSphericalPotential not as expected" # Test that for a cut-off potential, the mass far beyond the cut-off is # 2pi rc^(3-alpha) gamma(1.5-alpha/2) pp = potential.PowerSphericalPotentialwCutoff(amp=2.0) from scipy import special expecMass = ( 2.0 * pp._amp * numpy.pi * pp.rc ** (3.0 - pp.alpha) * special.gamma(1.5 - pp.alpha / 2.0) ) tR = 5.0 assert ( numpy.fabs((pp.mass(tR, forceint=True) - expecMass) / expecMass) < 10.0**-6.0 ), "Mass of PowerSphericalPotentialwCutoff far beyond the cut-off not as expected" tR = 15.0 assert ( numpy.fabs((pp.mass(tR, forceint=True) - expecMass) / expecMass) < 10.0**-6.0 ), "Mass of PowerSphericalPotentialwCutoff far beyond the cut-off not as expected" tR = 50.0 assert ( numpy.fabs((pp.mass(tR, forceint=True) - expecMass) / expecMass) < 10.0**-6.0 ), "Mass of PowerSphericalPotentialwCutoff far beyond the cut-off not as expected" # Jaffe and Hernquist both have finite masses, NFW diverges logarithmically jp = potential.JaffePotential(amp=2.0, a=0.1) hp = potential.HernquistPotential(amp=2.0, a=0.1) np = potential.NFWPotential(amp=2.0, a=0.1) tR = 10.0 # Limiting behavior jaffemass = jp._amp * (1.0 - jp.a / tR) hernmass = hp._amp / 2.0 * (1.0 - 2.0 * hp.a / tR) nfwmass = np._amp * (numpy.log(tR / np.a) - 1.0 + np.a / tR) assert ( numpy.fabs((jp.mass(tR, forceint=True) - jaffemass) / jaffemass) < 10.0**-3.0 ), "Limit mass for Jaffe potential not as expected" assert ( numpy.fabs((hp.mass(tR, forceint=True) - hernmass) / hernmass) < 10.0**-3.0 ), "Limit mass for Hernquist potential not as expected" assert numpy.fabs((np.mass(tR, forceint=True) - nfwmass) / nfwmass) < 10.0**-2.0, ( "Limit mass for NFW potential not as expected" ) tR = 200.0 # Limiting behavior, add z, to test that too jaffemass = jp._amp * (1.0 - jp.a / tR) hernmass = hp._amp / 2.0 * (1.0 - 2.0 * hp.a / tR) nfwmass = np._amp * (numpy.log(tR / np.a) - 1.0 + np.a / tR) assert ( numpy.fabs((jp.mass(tR, forceint=True) - jaffemass) / jaffemass) < 10.0**-6.0 ), "Limit mass for Jaffe potential not as expected" assert ( numpy.fabs((hp.mass(tR, forceint=True) - hernmass) / hernmass) < 10.0**-6.0 ), "Limit mass for Jaffe potential not as expected" assert numpy.fabs((np.mass(tR, forceint=True) - nfwmass) / nfwmass) < 10.0**-4.0, ( "Limit mass for NFW potential not as expected" ) # Burkert as an example of a SphericalPotential bp = potential.BurkertPotential(amp=2.0, a=3.0) assert numpy.fabs(bp.mass(4.2, forceint=True) - bp.mass(4.2)) < 1e-6, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) return None # Check that the masses are implemented correctly for spherical potentials def test_mass_spher_analytic(): # TwoPowerSphericalPotentials all have explicitly implemented masses dcp = potential.DehnenCoreSphericalPotential(amp=2.0) jp = potential.JaffePotential(amp=2.0) hp = potential.HernquistPotential(amp=2.0) np = potential.NFWPotential(amp=2.0) tp = potential.TwoPowerSphericalPotential(amp=2.0) dp = potential.DehnenSphericalPotential(amp=2.0) pp = potential.PlummerPotential(amp=2.0, b=1.3) tR = 2.0 assert numpy.fabs(dcp.mass(tR, forceint=True) - dcp.mass(tR)) < 10.0**-10.0, ( "Explicit mass does not agree with integral of the density for Dehnen Core potential" ) assert numpy.fabs(jp.mass(tR, forceint=True) - jp.mass(tR)) < 10.0**-10.0, ( "Explicit mass does not agree with integral of the density for Jaffe potential" ) assert numpy.fabs(hp.mass(tR, forceint=True) - hp.mass(tR)) < 10.0**-10.0, ( "Explicit mass does not agree with integral of the density for Hernquist potential" ) assert numpy.fabs(np.mass(tR, forceint=True) - np.mass(tR)) < 10.0**-10.0, ( "Explicit mass does not agree with integral of the density for NFW potential" ) assert numpy.fabs(tp.mass(tR, forceint=True) - tp.mass(tR)) < 10.0**-10.0, ( "Explicit mass does not agree with integral of the density for TwoPowerSpherical potential" ) assert numpy.fabs(dp.mass(tR, forceint=True) - dp.mass(tR)) < 10.0**-10.0, ( "Explicit mass does not agree with integral of the density for DehnenSphericalPotential potential, for not z is None" ) assert numpy.fabs(pp.mass(tR, forceint=True) - pp.mass(tR)) < 10.0**-10.0, ( "Explicit mass does not agree with integral of the density for Plummer potential" ) return None # Check that the masses within (0,R) and (-z,z) are calculated correctly for spherical potentials def test_mass_spher_z(): from scipy import integrate def sphermass(pot, R, z): return ( 4.0 * numpy.pi * integrate.dblquad( lambda rp, zp: rp * potential.evaluateDensities(pot, rp, 0.0, phi=0.0), 0.0, z, lambda z: z, lambda z: numpy.sqrt(R**2.0 + z**2.0), )[0] ) # TwoPowerSphericalPotential tp = potential.TwoPowerSphericalPotential(amp=2.0) assert numpy.fabs(tp.mass(4.2, 1.3) - sphermass(tp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # DehnenSphericalPotential dp = potential.DehnenSphericalPotential(amp=2.0) assert numpy.fabs(dp.mass(4.2, 1.3) - sphermass(dp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # PlummerPotential pp = potential.PlummerPotential(amp=2.0, b=1.3) assert numpy.fabs(pp.mass(4.2, 1.3) - sphermass(pp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # DehnenCoreSphericalPotential dcp = potential.DehnenCoreSphericalPotential(amp=2.0) assert numpy.fabs(dcp.mass(4.2, 1.3) - sphermass(dcp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # JaffePotential jp = potential.JaffePotential(amp=2.0) assert numpy.fabs(jp.mass(4.2, 1.3) - sphermass(jp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # HernquistPotential hp = potential.HernquistPotential(amp=2.0) assert numpy.fabs(hp.mass(4.2, 1.3) - sphermass(hp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # NFWPotential np = potential.NFWPotential(amp=2.0) assert numpy.fabs(np.mass(4.2, 1.3) - sphermass(np, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # SCF version of HernquistPotential hp = potential.SCFPotential.from_density( potential.HernquistPotential(amp=2.0), 1, 0, 1.0, symmetry="spherical" ) assert numpy.fabs(hp.mass(4.2, 1.3) - sphermass(hp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # AnySphericalPotential version of HernquistPotential hp = potential.AnySphericalPotential( dens=lambda r: potential.HernquistPotential(amp=2.0).dens(r, 0.0), ) assert numpy.fabs(hp.mass(4.2, 1.3) - sphermass(hp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # TwoPowerTriaxialPotential that is actually spherical ttp = potential.TwoPowerTriaxialPotential(amp=2.0, a=1.0, b=1.0, c=1.0) assert numpy.fabs(ttp.mass(4.2, 1.3) - sphermass(ttp, 4.2, 1.3)) < 1e-5, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # TriaxialHernquistPotential that is actually spherical thp = potential.TriaxialHernquistPotential(amp=2.0, a=1.0, b=1.0, c=1.0) assert numpy.fabs(thp.mass(4.2, 1.3) - sphermass(thp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # TriaxialJaffe potential that is actually spherical tjp = potential.TriaxialJaffePotential(amp=2.0, a=1.0, b=1.0, c=1.0) assert numpy.fabs(tjp.mass(4.2, 1.3) - sphermass(tjp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # TriaxialNFW potential that is actually spherical tnp = potential.TriaxialNFWPotential(amp=2.0, a=1.0, b=1.0, c=1.0) assert numpy.fabs(tnp.mass(4.2, 1.3) - sphermass(tnp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # PerfectEllipsoidPotential that is actually spherical pep = potential.PerfectEllipsoidPotential(amp=2.0, a=1.0, b=1.0, c=1.0) assert numpy.fabs(pep.mass(4.2, 1.3) - sphermass(pep, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # TriaxialGaussian potential that is actually spherical tgp = potential.TriaxialGaussianPotential(amp=2.0, sigma=1.0, b=1.0, c=1.0) assert numpy.fabs(tgp.mass(4.2, 1.3) - sphermass(tgp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) # Dummy EllipsoidalPotential for testing the general approach from galpy.potential.EllipsoidalPotential import EllipsoidalPotential class dummy(EllipsoidalPotential): def __init__( self, amp=1.0, b=1.0, c=1.0, zvec=None, pa=None, glorder=50, normalize=False, ro=None, vo=None, ): EllipsoidalPotential.__init__( self, amp=amp, b=b, c=c, zvec=zvec, pa=pa, glorder=glorder, ro=ro, vo=vo ) return None def _mdens(self, m): return m**-2.0 dp = dummy(amp=2.0, b=1.0, c=1.0) r = 1.9 assert numpy.fabs(dp.mass(4.2, 1.3) - sphermass(dp, 4.2, 1.3)) < 1e-10, ( "2D Mass computed with Potentials's general implementation incorrect for spherical potential" ) return None # Check that the masses are calculated correctly for axisymmetric potentials def test_mass_axi(): # For Miyamoto-Nagai, we know that mass integrated over everything should be equal to amp, so mp = potential.MiyamotoNagaiPotential(amp=1.0) assert numpy.fabs(mp.mass(200.0, 20.0) - 1.0) < 0.01, ( "Total mass of Miyamoto-Nagai potential w/ amp=1 is not equal to 1" ) # Also spherical assert numpy.fabs(mp.mass(200.0) - 1.0) < 0.01, ( "Total mass of Miyamoto-Nagai potential w/ amp=1 is not equal to 1" ) # For a double-exponential disk potential, the # mass(R,z) = amp x hR^2 x hz x (1-(1+R/hR)xe^(-R/hR)) x (1-e^(-Z/hz) dp = potential.DoubleExponentialDiskPotential(amp=2.0) def dblexpmass(r, z, dp): return ( 4.0 * numpy.pi * dp._amp * dp._hr**2.0 * dp._hz * (1.0 - (1.0 + r / dp._hr) * numpy.exp(-r / dp._hr)) * (1.0 - numpy.exp(-z / dp._hz)) ) tR, tz = 0.01, 0.01 assert numpy.fabs(dp.mass(tR, tz, forceint=True) - dblexpmass(tR, tz, dp)) < 5e-8, ( "Mass for DoubleExponentialDiskPotential incorrect" ) tR, tz = 0.1, 0.05 assert numpy.fabs(dp.mass(tR, tz, forceint=True) - dblexpmass(tR, tz, dp)) < 3e-7, ( "Mass for DoubleExponentialDiskPotential incorrect" ) tR, tz = 1.0, 0.1 assert numpy.fabs(dp.mass(tR, tz, forceint=True) - dblexpmass(tR, tz, dp)) < 1e-6, ( "Mass for DoubleExponentialDiskPotential incorrect" ) tR, tz = 5.0, 0.1 assert ( numpy.fabs( (dp.mass(tR, tz, forceint=True) - dblexpmass(tR, tz, dp)) / dblexpmass(tR, tz, dp) ) < 10.0**-5.0 ), "Mass for DoubleExponentialDiskPotential incorrect" tR, tz = 5.0, 1.0 assert ( numpy.fabs( (dp.mass(tR, tz, forceint=True) - dblexpmass(tR, tz, dp)) / dblexpmass(tR, tz, dp) ) < 10.0**-5.0 ), "Mass for DoubleExponentialDiskPotential incorrect" # Razor thin disk rp = potential.RazorThinExponentialDiskPotential(amp=2.0) def razexpmass(r, z, dp): return ( 2.0 * numpy.pi * rp._amp * rp._hr**2.0 * (1.0 - (1.0 + r / rp._hr) * numpy.exp(-r / rp._hr)) ) tR, tz = 0.01, 0.01 assert ( numpy.fabs((rp.mass(tR, tz) - razexpmass(tR, tz, rp)) / razexpmass(tR, tz, rp)) < 10.0**-10.0 ), "Mass for RazorThinExponentialDiskPotential incorrect" tR, tz = 0.1, 0.05 assert ( numpy.fabs((rp.mass(tR, tz) - razexpmass(tR, tz, rp)) / razexpmass(tR, tz, rp)) < 10.0**-10.0 ), "Mass for RazorThinExponentialDiskPotential incorrect" tR, tz = 1.0, 0.1 assert ( numpy.fabs((rp.mass(tR, tz) - razexpmass(tR, tz, rp)) / razexpmass(tR, tz, rp)) < 10.0**-10.0 ), "Mass for RazorThinExponentialDiskPotential incorrect" tR, tz = 5.0, 0.1 assert ( numpy.fabs((rp.mass(tR, tz) - razexpmass(tR, tz, rp)) / razexpmass(tR, tz, rp)) < 10.0**-10.0 ), "Mass for RazorThinExponentialDiskPotential incorrect" tR, tz = 5.0, 1.0 assert ( numpy.fabs((rp.mass(tR, tz) - razexpmass(tR, tz, rp)) / razexpmass(tR, tz, rp)) < 10.0**-10.0 ), "Mass for RazorThinExponentialDiskPotential incorrect" # Kuzmin disk, amp = mass kp = potential.KuzminDiskPotential(amp=2.0, a=3.0) assert numpy.fabs(kp.mass(1000.0, 20.0) - 2.0) < 1e-2, ( "Mass for KuzminDiskPotential incorrect" ) assert numpy.fabs(kp.mass(1000.0) - 2.0) < 1e-2, ( "Mass for KuzminDiskPotential incorrect" ) # Test that nonAxi raises error from galpy.orbit import Orbit mop = potential.MovingObjectPotential(Orbit([1.0, 0.1, 1.1, 0.1, 0.0, 0.0])) with pytest.raises(NotImplementedError) as excinfo: mop.mass(1.0, 0.0) # also for lists with pytest.raises(NotImplementedError) as excinfo: potential.mass(mop, 1.0, 0.0) with pytest.raises(NotImplementedError) as excinfo: potential.mass([mop], 1.0, 0.0) return None # Test that axisymmetric masses are correctly returned for negative z (issue #555) # they should just be the same as those for positive z def test_mass_axi_negz(): # Example from @sferrone in issue #555 R = 1.6303380979868902 z = 1.2732319411637634 assert ( numpy.fabs( potential.mass(potential.MWPotential2014, R, z) - potential.mass(potential.MWPotential2014, R, -z) ) < 1e-10 ), "Axisymmetric mass for negative z is not the same as for positive z" return None # Check that the masses are calculated correctly for spheroidal potentials def test_mass_spheroidal(): # PerfectEllipsoidPotential: total mass is amp, no matter what the axis ratio pep = potential.PerfectEllipsoidPotential(amp=2.0, a=3.0, b=1.3, c=1.9) assert numpy.fabs(pep.mass(1000.0) - 2.0) < 1e-2, ( "Total mass for PerfectEllipsoidPotential is incorrect" ) pep = potential.PerfectEllipsoidPotential(amp=2.0, a=3.0, b=1.0, c=1.9) assert numpy.fabs(pep.mass(1000.0) - 2.0) < 1e-2, ( "Total mass for PerfectEllipsoidPotential is incorrect" ) pep = potential.PerfectEllipsoidPotential(amp=2.0, a=3.0, b=1.0, c=1.0) assert numpy.fabs(pep.mass(1000.0) - 2.0) < 1e-2, ( "Total mass for PerfectEllipsoidPotential is incorrect" ) pep = potential.PerfectEllipsoidPotential(amp=2.0, a=3.0, b=0.7, c=0.5) assert numpy.fabs(pep.mass(1000.0) - 2.0) < 1e-2, ( "Total mass for PerfectEllipsoidPotential is incorrect" ) # For TwoPowerTriaxial, the masses should be bxc times that for the spherical version b = 0.7 c = 0.5 tpp = potential.TriaxialJaffePotential(amp=2.0, a=3.0, b=b, c=c) sp = potential.JaffePotential(amp=2.0, a=3.0) assert numpy.fabs(tpp.mass(1.3) / b / c - sp.mass(1.3)) < 1e-6, ( "TwoPowerTriaxialPotential mass incorrect" ) tpp = potential.TriaxialHernquistPotential(amp=2.0, a=3.0, b=b, c=c) sp = potential.HernquistPotential(amp=2.0, a=3.0) assert numpy.fabs(tpp.mass(1.3) / b / c - sp.mass(1.3)) < 1e-6, ( "TwoPowerTriaxialPotential mass incorrect" ) tpp = potential.TriaxialNFWPotential(amp=2.0, a=3.0, b=b, c=c) sp = potential.NFWPotential(amp=2.0, a=3.0) assert numpy.fabs(tpp.mass(1.3) / b / c - sp.mass(1.3)) < 1e-6, ( "TwoPowerTriaxialPotential mass incorrect" ) tpp = potential.TwoPowerTriaxialPotential( amp=2.0, a=3.0, b=b, c=c, alpha=1.1, beta=4.1 ) sp = potential.TwoPowerSphericalPotential(amp=2.0, a=3.0, alpha=1.1, beta=4.1) assert numpy.fabs(tpp.mass(1.3) / b / c - sp.mass(1.3)) < 1e-6, ( "TwoPowerTriaxialPotential mass incorrect" ) # For TriaxialGaussianPotential, total mass is amp, no matter b/c pep = potential.TriaxialGaussianPotential(amp=2.0, sigma=3.0, b=1.3, c=1.9) assert numpy.fabs(pep.mass(1000.0) - 2.0) < 1e-2, ( "Total mass for TriaxialGaussianPotential is incorrect" ) pep = potential.TriaxialGaussianPotential(amp=2.0, sigma=3.0, b=1.0, c=1.9) assert numpy.fabs(pep.mass(1000.0) - 2.0) < 1e-2, ( "Total mass for TriaxialGaussianPotential is incorrect" ) pep = potential.TriaxialGaussianPotential(amp=2.0, sigma=3.0, b=1.0, c=1.0) assert numpy.fabs(pep.mass(1000.0) - 2.0) < 1e-2, ( "Total mass for TriaxialGaussianPotential is incorrect" ) pep = potential.TriaxialGaussianPotential(amp=2.0, sigma=3.0, b=0.7, c=0.5) assert numpy.fabs(pep.mass(1000.0) - 2.0) < 1e-2, ( "Total mass for TriaxialGaussianPotential is incorrect" ) # Dummy EllipsoidalPotential for testing the general approach from galpy.potential.EllipsoidalPotential import EllipsoidalPotential class dummy(EllipsoidalPotential): def __init__( self, amp=1.0, b=1.0, c=1.0, zvec=None, pa=None, glorder=50, normalize=False, ro=None, vo=None, ): EllipsoidalPotential.__init__( self, amp=amp, b=b, c=c, zvec=zvec, pa=pa, glorder=glorder, ro=ro, vo=vo ) return None def _mdens(self, m): return m**-2.0 b = 1.2 c = 1.7 dp = dummy(amp=2.0, b=b, c=c) r = 1.9 assert numpy.fabs(dp.mass(r) / b / c - 4.0 * numpy.pi * 2.0 * r) < 1e-6, ( "General potential.EllipsoidalPotential mass incorrect" ) r = 3.9 assert numpy.fabs(dp.mass(r) / b / c - 4.0 * numpy.pi * 2.0 * r) < 1e-6, ( "General potential.EllipsoidalPotential mass incorrect" ) return None # Check that toVertical and toPlanar work def test_toVertical_toPlanar(): # Grab all of the potentials pots = [ p for p in dir(potential) if ( "Potential" in p and not "plot" in p and not "RZTo" in p and not "FullTo" in p and not "toPlanar" in p and not "evaluate" in p and not "Wrapper" in p and not "toVertical" in p ) ] pots.append("mockInterpSphericalPotential") pots.append("mockInterpSphericalPotentialwForce") rmpots = [ "Potential", "MWPotential", "MWPotential2014", "MovingObjectPotential", "interpRZPotential", "linearPotential", "planarAxiPotential", "planarPotential", "verticalPotential", "PotentialError", "SnapshotRZPotential", "InterpSnapshotRZPotential", "EllipsoidalPotential", "NumericalPotentialDerivativesMixin", "SphericalPotential", "interpSphericalPotential", ] if False: rmpots.append("DoubleExponentialDiskPotential") rmpots.append("RazorThinExponentialDiskPotential") for p in rmpots: pots.remove(p) for p in pots: # Setup instance of potential try: tclass = getattr(potential, p) except AttributeError: tclass = getattr(sys.modules[__name__], p) tp = tclass() if not hasattr(tp, "normalize"): continue # skip these tp.normalize(1.0) if isinstance(tp, potential.linearPotential) or isinstance( tp, potential.planarPotential ): continue tpp = tp.toPlanar() assert isinstance(tpp, potential.planarPotential), ( "Conversion into planar potential of potential %s fails" % p ) tlp = tp.toVertical(1.0, phi=2.0) assert isinstance(tlp, potential.linearPotential), ( "Conversion into linear potential of potential %s fails" % p ) def test_RZToplanarPotential(): lp = potential.LogarithmicHaloPotential(normalize=1.0) plp = potential.RZToplanarPotential(lp) assert isinstance(plp, potential.planarPotential), ( "Running an RZPotential through RZToplanarPotential does not produce a planarPotential" ) # Check that a planarPotential through RZToplanarPotential is still planar pplp = potential.RZToplanarPotential(plp) assert isinstance(pplp, potential.planarPotential), ( "Running a planarPotential through RZToplanarPotential does not produce a planarPotential" ) # Check that a list with a mix of planar and 3D potentials produces list of planar ppplp = potential.RZToplanarPotential([lp, plp]) for p in ppplp: assert isinstance(p, potential.planarPotential), ( "Running a list with a mix of planar and 3D potentials through RZToPlanarPotential does not produce a list of planar potentials" ) # Check that giving an object that is not a list or Potential instance produces an error with pytest.raises(potential.PotentialError) as excinfo: plp = potential.RZToplanarPotential("something else") # Check that given a list of objects that are not a Potential instances gives an error with pytest.raises(potential.PotentialError) as excinfo: plp = potential.RZToplanarPotential([3, 4, 45]) with pytest.raises(potential.PotentialError) as excinfo: plp = potential.RZToplanarPotential([lp, 3, 4, 45]) # Check that using a non-axisymmetric potential gives an error lpna = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9, b=0.8) with pytest.raises(potential.PotentialError) as excinfo: plp = potential.RZToplanarPotential(lpna) with pytest.raises(potential.PotentialError) as excinfo: plp = potential.RZToplanarPotential([lpna]) # Check that giving potential.ChandrasekharDynamicalFrictionForce # gives an error pp = potential.PlummerPotential(amp=1.12, b=2.0) cdfc = potential.ChandrasekharDynamicalFrictionForce( GMs=0.01, const_lnLambda=8.0, dens=pp, sigmar=lambda r: 1.0 / numpy.sqrt(2.0) ) with pytest.raises(NotImplementedError) as excinfo: plp = potential.RZToplanarPotential([pp, cdfc]) with pytest.raises(NotImplementedError) as excinfo: plp = potential.RZToplanarPotential(cdfc) return None def test_toPlanarPotential(): tnp = potential.TriaxialNFWPotential(normalize=1.0, b=0.5) ptnp = potential.toPlanarPotential(tnp) assert isinstance(ptnp, potential.planarPotential), ( "Running a non-axisymmetric Potential through toPlanarPotential does not produce a planarPotential" ) # Also for list ptnp = potential.toPlanarPotential([tnp]) assert isinstance(ptnp[0], potential.planarPotential), ( "Running a non-axisymmetric Potential through toPlanarPotential does not produce a planarPotential" ) # Check that a planarPotential through toPlanarPotential is still planar pptnp = potential.toPlanarPotential(tnp) assert isinstance(pptnp, potential.planarPotential), ( "Running a planarPotential through toPlanarPotential does not produce a planarPotential" ) # Check that running potential.NonInertialFrameforce through works nip = potential.NonInertialFrameForce(Omega=numpy.array([0.0, 1.0, 0.0])) assert isinstance(potential.toPlanarPotential(nip), potential.planarForce), ( "Running a potential.NonInertialFrameForce through toPlanarPotential does not produce a planarDissipativeForce" ) try: ptnp = potential.toPlanarPotential("something else") except potential.PotentialError: pass else: raise AssertionError( "Using toPlanarPotential with a string rather than an Potential or a planarPotential did not raise PotentialError" ) # Check that list of objects that are not potentials gives error with pytest.raises(potential.PotentialError) as excinfo: plp = potential.toPlanarPotential([3, 4, 45]) return None def test_RZToverticalPotential(): lp = potential.LogarithmicHaloPotential(normalize=1.0) plp = potential.RZToverticalPotential(lp, 1.2) assert isinstance(plp, potential.linearPotential), ( "Running an RZPotential through RZToverticalPotential does not produce a linearPotential" ) # Check that a verticalPotential through RZToverticalPotential is still vertical pplp = potential.RZToverticalPotential(plp, 1.2) assert isinstance(pplp, potential.linearPotential), ( "Running a linearPotential through RZToverticalPotential does not produce a linearPotential" ) # Also for list pplp = potential.RZToverticalPotential([plp], 1.2) assert isinstance(pplp[0], potential.linearPotential), ( "Running a linearPotential through RZToverticalPotential does not produce a linearPotential" ) # Check that giving an object that is not a list or Potential instance produces an error with pytest.raises(potential.PotentialError) as excinfo: plp = potential.RZToverticalPotential("something else", 1.2) # Check that given a list of objects that are not a Potential instances gives an error with pytest.raises(potential.PotentialError) as excinfo: plp = potential.RZToverticalPotential([3, 4, 45], 1.2) with pytest.raises(potential.PotentialError) as excinfo: plp = potential.RZToverticalPotential([lp, 3, 4, 45], 1.2) # Check that giving a planarPotential gives an error with pytest.raises(potential.PotentialError) as excinfo: plp = potential.RZToverticalPotential(lp.toPlanar(), 1.2) # Check that giving a list of planarPotential gives an error with pytest.raises(potential.PotentialError) as excinfo: plp = potential.RZToverticalPotential([lp.toPlanar()], 1.2) # Check that using a non-axisymmetric potential gives an error lpna = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9, b=0.8) with pytest.raises(potential.PotentialError) as excinfo: plp = potential.RZToverticalPotential(lpna, 1.2) with pytest.raises(potential.PotentialError) as excinfo: plp = potential.RZToverticalPotential([lpna], 1.2) # Check that giving potential.ChandrasekharDynamicalFrictionForce # gives an error pp = potential.PlummerPotential(amp=1.12, b=2.0) cdfc = potential.ChandrasekharDynamicalFrictionForce( GMs=0.01, const_lnLambda=8.0, dens=pp, sigmar=lambda r: 1.0 / numpy.sqrt(2.0) ) with pytest.raises(NotImplementedError) as excinfo: plp = potential.RZToverticalPotential([pp, cdfc], 1.2) with pytest.raises(NotImplementedError) as excinfo: plp = potential.RZToverticalPotential(cdfc, 1.2) return None def test_toVerticalPotential(): tnp = potential.TriaxialNFWPotential(normalize=1.0, b=0.5) ptnp = potential.toVerticalPotential(tnp, 1.2, phi=0.8) assert isinstance(ptnp, potential.linearPotential), ( "Running a non-axisymmetric Potential through toVerticalPotential does not produce a linearPotential" ) # Also for list ptnp = potential.toVerticalPotential([tnp], 1.2, phi=0.8) assert isinstance(ptnp[0], potential.linearPotential), ( "Running a non-axisymmetric Potential through toVerticalPotential does not produce a linearPotential" ) # Check that a linearPotential through toVerticalPotential is still vertical ptnp = potential.toVerticalPotential(tnp, 1.2, phi=0.8) pptnp = potential.toVerticalPotential(ptnp, 1.2, phi=0.8) assert isinstance(pptnp, potential.linearPotential), ( "Running a linearPotential through toVerticalPotential does not produce a linearPotential" ) # also for list pptnp = potential.toVerticalPotential([ptnp], 1.2, phi=0.8) assert isinstance(pptnp[0], potential.linearPotential), ( "Running a linearPotential through toVerticalPotential does not produce a linearPotential" ) try: ptnp = potential.toVerticalPotential("something else", 1.2, phi=0.8) except potential.PotentialError: pass else: raise AssertionError( "Using toVerticalPotential with a string rather than an Potential or a linearPotential did not raise PotentialError" ) # Check that giving a planarPotential gives an error with pytest.raises(potential.PotentialError) as excinfo: plp = potential.toVerticalPotential(tnp.toPlanar(), 1.2, phi=0.8) # Check that giving a list of planarPotential gives an error with pytest.raises(potential.PotentialError) as excinfo: plp = potential.toVerticalPotential([tnp.toPlanar()], 1.2, phi=0.8) # Check that giving a list of non-potentials gives error with pytest.raises(potential.PotentialError) as excinfo: plp = potential.toVerticalPotential([3, 4, 45], 1.2) # Check that giving potential.ChandrasekharDynamicalFrictionForce # gives an error pp = potential.PlummerPotential(amp=1.12, b=2.0) cdfc = potential.ChandrasekharDynamicalFrictionForce( GMs=0.01, const_lnLambda=8.0, dens=pp, sigmar=lambda r: 1.0 / numpy.sqrt(2.0) ) with pytest.raises(NotImplementedError) as excinfo: plp = potential.toVerticalPotential([pp, cdfc], 1.2, phi=0.8) with pytest.raises(NotImplementedError) as excinfo: plp = potential.toVerticalPotential(cdfc, 1.2, phi=0.8) # Check that running a non-axisymmetric potential through toVertical w/o # phi gives an error with pytest.raises(potential.PotentialError) as excinfo: ptnp = potential.toVerticalPotential(tnp, 1.2) return None # Sanity check the derivative of the rotation curve and the frequencies in the plane def test_dvcircdR_omegac_epifreq_rl_vesc(): # Derivative of rotation curve # LogarithmicHaloPotential: rotation everywhere flat lp = potential.LogarithmicHaloPotential(normalize=1.0) assert lp.dvcircdR(1.0) ** 2.0 < 10.0**-16.0, ( "LogarithmicHaloPotential's rotation curve is not flat at R=1" ) assert lp.dvcircdR(0.5) ** 2.0 < 10.0**-16.0, ( "LogarithmicHaloPotential's rotation curve is not flat at R=0.5" ) assert lp.dvcircdR(2.0) ** 2.0 < 10.0**-16.0, ( "LogarithmicHaloPotential's rotation curve is not flat at R=2" ) # Kepler potential, vc = vc_0(R/R0)^-0.5 -> dvcdR= -0.5 vc_0 (R/R0)**-1.5 kp = potential.KeplerPotential(normalize=1.0) assert (kp.dvcircdR(1.0) + 0.5) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's rotation curve is not what it should be at R=1" ) assert (kp.dvcircdR(0.5) + 0.5**-0.5) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's rotation curve is not what it should be at R=0.5" ) assert (kp.dvcircdR(2.0) + 0.5**2.5) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's rotation curve is not what it should be at R=2" ) # Rotational frequency assert (lp.omegac(1.0) - 1.0) ** 2.0 < 10.0**-16.0, ( "LogarithmicHalo's rotational frequency is off at R=1" ) assert (lp.omegac(0.5) - 2.0) ** 2.0 < 10.0**-16.0, ( "LogarithmicHalo's rotational frequency is off at R=0.5" ) assert (lp.omegac(2.0) - 0.5) ** 2.0 < 10.0**-16.0, ( "LogarithmicHalo's rotational frequency is off at R=2" ) assert (lp.toPlanar().omegac(2.0) - 0.5) ** 2.0 < 10.0**-16.0, ( "LogarithmicHalo's rotational frequency is off at R=2 through planarPotential" ) # Epicycle frequency, flat rotation curve assert (lp.epifreq(1.0) - numpy.sqrt(2.0) * lp.omegac(1.0)) ** 2.0 < 10.0**-16.0, ( "LogarithmicHalo's epicycle and rotational frequency are inconsistent with kappa = sqrt(2) Omega at R=1" ) assert (lp.epifreq(0.5) - numpy.sqrt(2.0) * lp.omegac(0.5)) ** 2.0 < 10.0**-16.0, ( "LogarithmicHalo's epicycle and rotational frequency are inconsistent with kappa = sqrt(2) Omega at R=0.5" ) assert (lp.epifreq(2.0) - numpy.sqrt(2.0) * lp.omegac(2.0)) ** 2.0 < 10.0**-16.0, ( "LogarithmicHalo's epicycle and rotational frequency are inconsistent with kappa = sqrt(2) Omega at R=2" ) assert ( lp.toPlanar().epifreq(2.0) - numpy.sqrt(2.0) * lp.omegac(2.0) ) ** 2.0 < 10.0**-16.0, ( "LogarithmicHalo's epicycle and rotational frequency are inconsistent with kappa = sqrt(2) Omega at R=, through planar2" ) # Epicycle frequency, Kepler assert (kp.epifreq(1.0) - kp.omegac(1.0)) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's epicycle and rotational frequency are inconsistent with kappa = Omega at R=1" ) assert (kp.epifreq(0.5) - kp.omegac(0.5)) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's epicycle and rotational frequency are inconsistent with kappa = Omega at R=0.5" ) assert (kp.epifreq(2.0) - kp.omegac(2.0)) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's epicycle and rotational frequency are inconsistent with kappa = Omega at R=2" ) # Check radius of circular orbit, Kepler assert (kp.rl(1.0) - 1.0) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's radius of a circular orbit is wrong at Lz=1." ) assert (kp.rl(0.5) - 1.0 / 4.0) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's radius of a circular orbit is wrong at Lz=0.5" ) assert (kp.rl(2.0) - 4.0) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's radius of a circular orbit is wrong at Lz=2." ) # Check radius of circular orbit, PowerSphericalPotential with close-to-flat rotation curve pp = potential.PowerSphericalPotential(alpha=1.8, normalize=1.0) assert (pp.rl(1.0) - 1.0) ** 2.0 < 10.0**-16.0, ( "PowerSphericalPotential's radius of a circular orbit is wrong at Lz=1." ) assert (pp.rl(0.5) - 0.5 ** (10.0 / 11.0)) ** 2.0 < 10.0**-16.0, ( "PowerSphericalPotential's radius of a circular orbit is wrong at Lz=0.5" ) assert (pp.rl(2.0) - 2.0 ** (10.0 / 11.0)) ** 2.0 < 10.0**-16.0, ( "PowerSphericalPotential's radius of a circular orbit is wrong at Lz=2." ) # Check radius of circular orbit, PowerSphericalPotential with steeper rotation curve pp = potential.PowerSphericalPotential(alpha=0.5, normalize=1.0) assert (pp.rl(1.0) - 1.0) ** 2.0 < 10.0**-16.0, ( "PowerSphericalPotential's radius of a circular orbit is wrong at Lz=1." ) assert (pp.rl(0.0625) - 0.0625 ** (4.0 / 7.0)) ** 2.0 < 10.0**-16.0, ( "PowerSphericalPotential's radius of a circular orbit is wrong at Lz=0.0625" ) assert (pp.rl(16.0) - 16.0 ** (4.0 / 7.0)) ** 2.0 < 10.0**-16.0, ( "PowerSphericalPotential's radius of a circular orbit is wrong at Lz=16." ) # Check radius in MWPotential2014 at very small lz, to test small lz behavior lz = 0.000001 assert ( numpy.fabs( potential.vcirc( potential.MWPotential2014, potential.rl(potential.MWPotential2014, lz) ) * potential.rl(potential.MWPotential2014, lz) - lz ) < 1e-12 ), ( "Radius of circular orbit at small Lz in MWPotential2014 does not work as expected" ) # Escape velocity of Kepler potential assert (kp.vesc(1.0) ** 2.0 - 2.0) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's escape velocity is wrong at R=1" ) assert (kp.vesc(0.5) ** 2.0 - 2.0 * kp.vcirc(0.5) ** 2.0) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's escape velocity is wrong at R=0.5" ) assert (kp.vesc(2.0) ** 2.0 - 2.0 * kp.vcirc(2.0) ** 2.0) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's escape velocity is wrong at R=2" ) assert ( kp.toPlanar().vesc(2.0) ** 2.0 - 2.0 * kp.vcirc(2.0) ** 2.0 ) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's escape velocity is wrong at R=2, through planar" ) # W/ different interface assert (kp.vcirc(1.0) - potential.vcirc(kp, 1.0)) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's circular velocity does not agree between kp.vcirc and vcirc(kp)" ) assert (kp.vcirc(1.0) - potential.vcirc(kp.toPlanar(), 1.0)) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's circular velocity does not agree between kp.vcirc and vcirc(kp.toPlanar)" ) assert (kp.vesc(1.0) - potential.vesc(kp, 1.0)) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's escape velocity does not agree between kp.vesc and vesc(kp)" ) assert (kp.vesc(1.0) - potential.vesc(kp.toPlanar(), 1.0)) ** 2.0 < 10.0**-16.0, ( "KeplerPotential's escape velocity does not agree between kp.vesc and vesc(kp.toPlanar)" ) return None def test_vcirc_phi_axi(): # Test that giving phi to vcirc for an axisymmetric potential doesn't # affect the answer kp = potential.KeplerPotential(normalize=1.0) phis = numpy.linspace(0.0, numpy.pi, 101) vcs = numpy.array([kp.vcirc(1.0, phi) for phi in phis]) assert numpy.all(numpy.fabs(vcs - 1.0) < 10.0**-8.0), ( "Setting phi= in vcirc for axisymmetric potential gives different answers for different phi" ) # One at a different radius R = 0.5 vcs = numpy.array([kp.vcirc(R, phi) for phi in phis]) assert numpy.all(numpy.fabs(vcs - kp.vcirc(R)) < 10.0**-8.0), ( "Setting phi= in vcirc for axisymmetric potential gives different answers for different phi" ) return None def test_vcirc_phi_nonaxi(): # Test that giving phi to vcirc for a non-axisymmetric potential does # affect the answer tnp = potential.TriaxialNFWPotential(b=0.4, normalize=1.0) # limited phi range phis = numpy.linspace(numpy.pi / 5.0, numpy.pi / 2.0, 5) vcs = numpy.array([tnp.vcirc(1.0, phi) for phi in phis]) assert numpy.all(numpy.fabs(vcs - 1.0) > 0.01), ( "Setting phi= in vcirc for axisymmetric potential does not give different answers for different phi" ) # One at a different radius R = 0.5 vcs = numpy.array([tnp.vcirc(R, phi) for phi in phis]) assert numpy.all(numpy.fabs(vcs - tnp.vcirc(R, phi=0.0)) > 0.01), ( "Setting phi= in vcirc for axisymmetric potential does not give different answers for different phi" ) return None def test_vcirc_vesc_special(): # Test some special cases of vcirc and vesc dp = potential.EllipticalDiskPotential() try: potential.plotRotcurve([dp]) except (AttributeError, potential.PotentialError): # should be raised pass else: raise AssertionError( "plotRotcurve for non-axisymmetric potential should have raised AttributeError, but didn't" ) try: potential.plotEscapecurve([dp]) except AttributeError: # should be raised pass else: raise AssertionError( "plotEscapecurve for non-axisymmetric potential should have raised AttributeError, but didn't" ) lp = potential.LogarithmicHaloPotential(normalize=1.0) assert numpy.fabs(potential.calcRotcurve(lp, 0.8) - lp.vcirc(0.8)) < 10.0**-16.0, ( "Circular velocity calculated with calcRotcurve not the same as that calculated with vcirc" ) assert ( numpy.fabs(potential.calcEscapecurve(lp, 0.8) - lp.vesc(0.8)) < 10.0**-16.0 ), ( "Escape velocity calculated with calcEscapecurve not the same as that calculated with vcirc" ) return None def test_lindbladR(): lp = potential.LogarithmicHaloPotential(normalize=1.0) assert numpy.fabs(lp.lindbladR(0.5, "corotation") - 2.0) < 10.0**-10.0, ( "Location of co-rotation resonance is wrong for LogarithmicHaloPotential" ) assert ( numpy.fabs( lp.omegac(lp.lindbladR(0.5, 2)) - 2.0 / (2.0 - numpy.sqrt(2.0)) * 0.5 ) < 10.0**-14.0 ), "Location of m=2 resonance is wrong for LogarithmicHaloPotential" assert ( numpy.fabs( lp.omegac(lp.lindbladR(0.5, -2)) + 2.0 / (-2.0 - numpy.sqrt(2.0)) * 0.5 ) < 10.0**-14.0 ), "Location of m=-2 resonance is wrong for LogarithmicHaloPotential" # Also through general interface assert ( numpy.fabs( lp.omegac(potential.lindbladR(lp, 0.5, -2)) + 2.0 / (-2.0 - numpy.sqrt(2.0)) * 0.5 ) < 10.0**-14.0 ), "Location of m=-2 resonance is wrong for LogarithmicHaloPotential" # Also for planar assert ( numpy.fabs( lp.omegac(lp.toPlanar().lindbladR(0.5, -2)) + 2.0 / (-2.0 - numpy.sqrt(2.0)) * 0.5 ) < 10.0**-14.0 ), "Location of m=-2 resonance is wrong for LogarithmicHaloPotential" # Test non-existent ones mp = potential.MiyamotoNagaiPotential(normalize=1.0, a=0.3) assert mp.lindbladR(3.0, 2) is None, ( "MiyamotoNagai w/ OmegaP=3 should not have a inner m=2 LindbladR" ) assert mp.lindbladR(6.0, "corotation") is None, ( "MiyamotoNagai w/ OmegaP=6 should not have a inner m=2 LindbladR" ) # Test error try: lp.lindbladR(0.5, "wrong resonance") except OSError: pass else: raise AssertionError( "lindbladR w/ wrong m input should have raised IOError, but didn't" ) return None def test_rE_flatvc(): # Test the rE function for the case of a flat rotation curve # Expected rE when vc(1)=1 is exp(E-1/2) (e.g., Dehnen 1999 epicycle) lp = potential.LogarithmicHaloPotential(normalize=1.0) def expected_rE(E): return numpy.exp(E - 0.5) Es = numpy.linspace(-10.0, 20.0, 101) rEs = numpy.array([lp.rE(E) for E in Es]) assert numpy.amax(numpy.fabs(rEs - expected_rE(Es))) < 1e-6, ( "rE method does not give the expected result for a flat rotation curve" ) # Also as function rEs = numpy.array([potential.rE(lp, E) for E in Es]) assert numpy.amax(numpy.fabs(rEs - expected_rE(Es))) < 1e-6, ( "rE method does not give the expected result for a flat rotation curve" ) return None def test_rE_powervc(): # Test the rE function for the case of a power-law rotation curve: v = r^beta # Expected rE when vc(1)=1 is (2 beta E / [1+beta])**(1./[2beta]) # (e.g., Dehnen 1999 epicycle) betas = [-0.45, -0.2, 0.6, 0.9] def expected_rE(E, beta): return (2.0 * beta * E / (1.0 + beta)) ** (1.0 / 2.0 / beta) for beta in betas: pp = PowerSphericalPotential(alpha=2.0 - 2.0 * beta, normalize=1.0) rmin, rmax = 1e-8, 1e5 Emin = pp.vcirc(rmin) ** 2.0 / 2.0 + pp(rmin, 0.0) Emax = pp.vcirc(rmax) ** 2.0 / 2.0 + pp(rmax, 0.0) Es = numpy.linspace(Emin, Emax, 101) # Test both method and function if beta < 0.0: rEs = numpy.array([pp.rE(E) for E in Es]) else: rEs = numpy.array([potential.rE(pp, E) for E in Es]) assert numpy.amax(numpy.fabs(rEs - expected_rE(Es, beta))) < 1e-8, ( "rE method does not give the expected result for a power-law rotation curve" ) return None def test_rE_MWPotential2014(): # Test the rE function for MWPotential2014 # No closed-form true answer, so just check that the expected relation holds def Ec(r): return potential.vcirc( potential.MWPotential2014, r ) ** 2.0 / 2.0 + potential.evaluatePotentials(potential.MWPotential2014, r, 0.0) rmin, rmax = 1e-8, 1e5 Emin = Ec(rmin) Emax = Ec(rmax) Es = numpy.linspace(Emin, Emax, 101) rEs = numpy.array([potential.rE(potential.MWPotential2014, E) for E in Es]) Ecs = numpy.array([Ec(rE) for rE in rEs]) assert numpy.amax(numpy.fabs(Ecs - Es)) < 1e-8, ( "rE method does not give the expected result for MWPotential2014" ) return None def test_LcE_flatvc(): # Test the LcE function for the case of a flat rotation curve # Expected LcE when vc(1)=1 is exp(E-1/2) (e.g., Dehnen 1999 epicycle) lp = potential.LogarithmicHaloPotential(normalize=1.0) def expected_LcE(E): return numpy.exp(E - 0.5) Es = numpy.linspace(-10.0, 20.0, 101) LcEs = numpy.array([lp.LcE(E) for E in Es]) assert numpy.amax(numpy.fabs(LcEs - expected_LcE(Es))) < 1e-6, ( "LcE method does not give the expected result for a flat rotation curve" ) # Also as function LcEs = numpy.array([potential.LcE(lp, E) for E in Es]) assert numpy.amax(numpy.fabs(LcEs - expected_LcE(Es))) < 1e-6, ( "LcE method does not give the expected result for a flat rotation curve" ) return None def test_LcE_powervc(): # Test the LcE function for the case of a power-law rotation curve: v = r^beta # Expected LcE when vc(1)=1 is (2 beta E / [1+beta])**([1.+beta]/[2beta]) # (e.g., Dehnen 1999 epicycle) betas = [-0.45, -0.2, 0.6, 0.9] def expected_LcE(E, beta): return (2.0 * beta * E / (1.0 + beta)) ** ((1.0 + beta) / 2.0 / beta) for beta in betas: pp = PowerSphericalPotential(alpha=2.0 - 2.0 * beta, normalize=1.0) rmin, rmax = 1e-8, 1e5 Emin = pp.vcirc(rmin) ** 2.0 / 2.0 + pp(rmin, 0.0) Emax = pp.vcirc(rmax) ** 2.0 / 2.0 + pp(rmax, 0.0) Es = numpy.linspace(Emin, Emax, 101) # Test both method and function if beta < 0.0: LcEs = numpy.array([pp.LcE(E) for E in Es]) else: LcEs = numpy.array([potential.LcE(pp, E) for E in Es]) assert numpy.amax(numpy.fabs(LcEs - expected_LcE(Es, beta))) < 1e-5, ( "LcE method does not give the expected result for a power-law rotation curve" ) return None def test_vterm(): lp = potential.LogarithmicHaloPotential(normalize=1.0) assert ( numpy.fabs(lp.vterm(30.0, deg=True) - 0.5 * (lp.omegac(0.5) - 1.0)) < 10.0**-10.0 ), "vterm for LogarithmicHaloPotential at l=30 is incorrect" assert ( numpy.fabs( lp.vterm(numpy.pi / 3.0, deg=False) - numpy.sqrt(3.0) / 2.0 * (lp.omegac(numpy.sqrt(3.0) / 2.0) - 1.0) ) < 10.0**-10.0 ), "vterm for LogarithmicHaloPotential at l=60 in rad is incorrect" # Also using general interface assert ( numpy.fabs(potential.vterm(lp, 30.0, deg=True) - 0.5 * (lp.omegac(0.5) - 1.0)) < 10.0**-10.0 ), "vterm for LogarithmicHaloPotential at l=30 is incorrect" assert ( numpy.fabs( potential.vterm(lp, numpy.pi / 3.0, deg=False) - numpy.sqrt(3.0) / 2.0 * (lp.omegac(numpy.sqrt(3.0) / 2.0) - 1.0) ) < 10.0**-10.0 ), "vterm for LogarithmicHaloPotential at l=60 in rad is incorrect" return None def test_flattening(): # Simple tests: LogarithmicHalo qs = [0.75, 1.0, 1.25] for q in qs: lp = potential.LogarithmicHaloPotential(normalize=1.0, q=q) assert (lp.flattening(1.0, 0.001) - q) ** 2.0 < 10.0**-16.0, ( "Flattening of LogarithmicHaloPotential w/ q= %f is not equal to q at (R,z) = (1.,0.001)" % q ) assert (lp.flattening(1.0, 0.1) - q) ** 2.0 < 10.0**-16.0, ( "Flattening of LogarithmicHaloPotential w/ q= %f is not equal to q at (R,z) = (1.,0.1)" % q ) assert (lp.flattening(0.5, 0.001) - q) ** 2.0 < 10.0**-16.0, ( "Flattening of LogarithmicHaloPotential w/ q= %f is not equal to q at (R,z) = (0.5,0.001)" % q ) assert (lp.flattening(0.5, 0.1) - q) ** 2.0 < 10.0**-16.0, ( "Flattening of LogarithmicHaloPotential w/ q= %f is not equal to q at (R,z) = (0.5,0.1)" % q ) # One test with the general interface assert (potential.flattening(lp, 0.5, 0.1) - q) ** 2.0 < 10.0**-16.0, ( "Flattening of LogarithmicHaloPotential w/ q= %f is not equal to q at (R,z) = (0.5,0.1), through potential.flattening" % q ) # Check some spherical potentials kp = potential.KeplerPotential(normalize=1.0) assert (kp.flattening(1.0, 0.02) - 1.0) ** 2.0 < 10.0**-16.0, ( "Flattening of KeplerPotential is not equal to 1 at (R,z) = (1.,0.02)" ) np = potential.NFWPotential(normalize=1.0, a=5.0) assert (np.flattening(1.0, 0.02) - 1.0) ** 2.0 < 10.0**-16.0, ( "Flattening of NFWPotential is not equal to 1 at (R,z) = (1.,0.02)" ) hp = potential.HernquistPotential(normalize=1.0, a=5.0) assert (hp.flattening(1.0, 0.02) - 1.0) ** 2.0 < 10.0**-16.0, ( "Flattening of HernquistPotential is not equal to 1 at (R,z) = (1.,0.02)" ) # Disk potentials should be oblate everywhere mp = potential.MiyamotoNagaiPotential(normalize=1.0, a=0.5, b=0.05) assert mp.flattening(1.0, 0.1) <= 1.0, ( "Flattening of MiyamotoNagaiPotential w/ a=0.5, b=0.05 is > 1 at (R,z) = (1.,0.1)" ) assert mp.flattening(1.0, 2.0) <= 1.0, ( "Flattening of MiyamotoNagaiPotential w/ a=0.5, b=0.05 is > 1 at (R,z) = (1.,2.)" ) assert mp.flattening(3.0, 3.0) <= 1.0, ( "Flattening of MiyamotoNagaiPotential w/ a=0.5, b=0.05 is > 1 at (R,z) = (3.,3.)" ) return None def test_verticalfreq(): # For spherical potentials, vertical freq should be equal to rotational freq lp = potential.LogarithmicHaloPotential(normalize=1.0, q=1.0) kp = potential.KeplerPotential(normalize=1.0) np = potential.NFWPotential(normalize=1.0) bp = potential.BurkertPotential(normalize=1.0) rs = numpy.linspace(0.2, 2.0, 21) for r in rs: assert numpy.fabs(lp.verticalfreq(r) - lp.omegac(r)) < 10.0**-10.0, ( "Verticalfreq for spherical potential does not equal rotational freq" ) assert numpy.fabs(kp.verticalfreq(r) - kp.omegac(r)) < 10.0**-10.0, ( "Verticalfreq for spherical potential does not equal rotational freq" ) # Through general interface assert numpy.fabs(potential.verticalfreq(np, r) - np.omegac(r)) < 10.0**-10.0, ( "Verticalfreq for spherical potential does not equal rotational freq" ) assert ( numpy.fabs(potential.verticalfreq([bp], r) - bp.omegac(r)) < 10.0**-10.0 ), "Verticalfreq for spherical potential does not equal rotational freq" # For Double-exponential disk potential, epi^2+vert^2-2*rot^2 =~ 0 at very large distances (no longer explicitly, because we don't use a Kepler potential anylonger) if True: dp = potential.DoubleExponentialDiskPotential(normalize=1.0, hr=0.05, hz=0.01) assert ( numpy.fabs( dp.epifreq(1.0) ** 2.0 + dp.verticalfreq(1.0) ** 2.0 - 2.0 * dp.omegac(1.0) ** 2.0 ) < 10.0**-4.0 ), "epi^2+vert^2-2*rot^2 !=~ 0 for dblexp potential, very far from center" # Closer to the center, this becomes the Poisson eqn. assert ( numpy.fabs( dp.epifreq(0.125) ** 2.0 + dp.verticalfreq(0.125) ** 2.0 - 2.0 * dp.omegac(0.125) ** 2.0 - 4.0 * numpy.pi * dp.dens(0.125, 0.0) ) / 4.0 / numpy.pi / dp.dens(0.125, 0.0) < 10.0**-3.0 ), "epi^2+vert^2-2*rot^2 !=~ dens for dblexp potential" return None def test_planar_nonaxi(): dp = potential.EllipticalDiskPotential() try: potential.evaluateplanarPotentials(dp, 1.0) except potential.PotentialError: pass else: raise AssertionError( "evaluateplanarPotentials for non-axisymmetric potential w/o specifying phi did not raise PotentialError" ) try: potential.evaluateplanarRforces(dp, 1.0) except potential.PotentialError: pass else: raise AssertionError( "evaluateplanarRforces for non-axisymmetric potential w/o specifying phi did not raise PotentialError" ) try: potential.evaluateplanarphitorques(dp, 1.0) except potential.PotentialError: pass else: raise AssertionError( "evaluateplanarphitorques for non-axisymmetric potential w/o specifying phi did not raise PotentialError" ) try: potential.evaluateplanarR2derivs(dp, 1.0) except potential.PotentialError: pass else: raise AssertionError( "evaluateplanarR2derivs for non-axisymmetric potential w/o specifying phi did not raise PotentialError" ) return None def test_ExpDisk_special(): # Test some special cases for the ExponentialDisk potentials # Test that array input works dp = potential.DoubleExponentialDiskPotential(normalize=1.0) rs = numpy.linspace(0.1, 2.11) zs = numpy.ones_like(rs) * 0.1 # Potential itself dpevals = numpy.array([dp(r, z) for (r, z) in zip(rs, zs)]) assert numpy.all(numpy.fabs(dp(rs, zs) - dpevals) < 10.0**-10.0), ( "DoubleExppnentialDiskPotential evaluation does not work as expected for array inputs" ) # Rforce # dpevals= numpy.array([dp.Rforce(r,z) for (r,z) in zip(rs,zs)]) # assert numpy.all(numpy.fabs(dp.Rforce(rs,zs)-dpevals) < 10.**-10.), \ # 'DoubleExppnentialDiskPotential Rforce evaluation does not work as expected for array inputs' # zforce # dpevals= numpy.array([dp.zforce(r,z) for (r,z) in zip(rs,zs)]) # assert numpy.all(numpy.fabs(dp.zforce(rs,zs)-dpevals) < 10.**-10.), \ # 'DoubleExppnentialDiskPotential zforce evaluation does not work as expected for array inputs' # R2deriv # dpevals= numpy.array([dp.R2deriv(r,z) for (r,z) in zip(rs,zs)]) # assert numpy.all(numpy.fabs(dp.R2deriv(rs,zs)-dpevals) < 10.**-10.), \ # 'DoubleExppnentialDiskPotential R2deriv evaluation does not work as expected for array inputs' # z2deriv # dpevals= numpy.array([dp.z2deriv(r,z) for (r,z) in zip(rs,zs)]) # assert numpy.all(numpy.fabs(dp.z2deriv(rs,zs)-dpevals) < 10.**-10.), \ # 'DoubleExppnentialDiskPotential z2deriv evaluation does not work as expected for array inputs' # Rzderiv # dpevals= numpy.array([dp.Rzderiv(r,z) for (r,z) in zip(rs,zs)]) # assert numpy.all(numpy.fabs(dp.Rzderiv(rs,zs)-dpevals) < 10.**-10.), \ # 'DoubleExppnentialDiskPotential Rzderiv evaluation does not work as expected for array inputs' # Check the PotentialError for z=/=0 evaluation of R2deriv of RazorThinDiskPotential rp = potential.RazorThinExponentialDiskPotential(normalize=1.0) try: rp.R2deriv(1.0, 0.1) except potential.PotentialError: pass else: raise AssertionError( "RazorThinExponentialDiskPotential's R2deriv did not raise AttributeError for z=/= 0 input" ) return None def test_DehnenBar_special(): # Test some special cases for the DehnenBar potentials # Test that array input works dp = potential.DehnenBarPotential() # Test from rs < rb through to rs > rb rs = numpy.linspace(0.1 * dp._rb, 2.11 * dp._rb) zs = numpy.ones_like(rs) * 0.1 phis = numpy.ones_like(rs) * 0.1 # Potential itself dpevals = numpy.array([dp(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential evaluation does not work as expected for array inputs" ) # R array, z not an array dpevals = numpy.array([dp(r, zs[0], phi) for (r, phi) in zip(rs, phis)]) assert numpy.all(numpy.fabs(dp(rs, zs[0], phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential evaluation does not work as expected for array inputs" ) # z array, R not an array dpevals = numpy.array([dp(rs[0], z, phi) for (z, phi) in zip(zs, phis)]) assert numpy.all(numpy.fabs(dp(rs[0], zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential evaluation does not work as expected for array inputs" ) # Rforce dpevals = numpy.array([dp.Rforce(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp.Rforce(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential Rforce evaluation does not work as expected for array inputs" ) # R array, z not an array dpevals = numpy.array([dp.Rforce(r, zs[0], phi) for (r, phi) in zip(rs, phis)]) assert numpy.all(numpy.fabs(dp.Rforce(rs, zs[0], phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential Rforce does not work as expected for array inputs" ) # z array, R not an array dpevals = numpy.array([dp.Rforce(rs[0], z, phi) for (z, phi) in zip(zs, phis)]) assert numpy.all(numpy.fabs(dp.Rforce(rs[0], zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential Rforce does not work as expected for array inputs" ) # zforce dpevals = numpy.array([dp.zforce(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp.zforce(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential zforce evaluation does not work as expected for array inputs" ) # R array, z not an array dpevals = numpy.array([dp.zforce(r, zs[0], phi) for (r, phi) in zip(rs, phis)]) assert numpy.all(numpy.fabs(dp.zforce(rs, zs[0], phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential zforce does not work as expected for array inputs" ) # z array, R not an array dpevals = numpy.array([dp.zforce(rs[0], z, phi) for (z, phi) in zip(zs, phis)]) assert numpy.all(numpy.fabs(dp.zforce(rs[0], zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential zforce does not work as expected for array inputs" ) # phitorque dpevals = numpy.array( [dp.phitorque(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)] ) assert numpy.all(numpy.fabs(dp.phitorque(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential zforce evaluation does not work as expected for array inputs" ) # R array, z not an array dpevals = numpy.array([dp.phitorque(r, zs[0], phi) for (r, phi) in zip(rs, phis)]) assert numpy.all( numpy.fabs(dp.phitorque(rs, zs[0], phis) - dpevals) < 10.0**-10.0 ), "DehnenBarPotential phitorque does not work as expected for array inputs" # z array, R not an array dpevals = numpy.array([dp.phitorque(rs[0], z, phi) for (z, phi) in zip(zs, phis)]) assert numpy.all( numpy.fabs(dp.phitorque(rs[0], zs, phis) - dpevals) < 10.0**-10.0 ), "DehnenBarPotential phitorque does not work as expected for array inputs" # R2deriv dpevals = numpy.array([dp.R2deriv(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp.R2deriv(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential R2deriv evaluation does not work as expected for array inputs" ) # R array, z not an array dpevals = numpy.array([dp.R2deriv(r, zs[0], phi) for (r, phi) in zip(rs, phis)]) assert numpy.all(numpy.fabs(dp.R2deriv(rs, zs[0], phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential R2deriv does not work as expected for array inputs" ) # z array, R not an array dpevals = numpy.array([dp.R2deriv(rs[0], z, phi) for (z, phi) in zip(zs, phis)]) assert numpy.all(numpy.fabs(dp.R2deriv(rs[0], zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential R2deriv does not work as expected for array inputs" ) # z2deriv dpevals = numpy.array([dp.z2deriv(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp.z2deriv(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential z2deriv evaluation does not work as expected for array inputs" ) # R array, z not an array dpevals = numpy.array([dp.z2deriv(r, zs[0], phi) for (r, phi) in zip(rs, phis)]) assert numpy.all(numpy.fabs(dp.z2deriv(rs, zs[0], phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential z2deriv does not work as expected for array inputs" ) # z array, R not an array dpevals = numpy.array([dp.z2deriv(rs[0], z, phi) for (z, phi) in zip(zs, phis)]) assert numpy.all(numpy.fabs(dp.z2deriv(rs[0], zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential z2deriv does not work as expected for array inputs" ) # phi2deriv dpevals = numpy.array( [dp.phi2deriv(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)] ) assert numpy.all(numpy.fabs(dp.phi2deriv(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential z2deriv evaluation does not work as expected for array inputs" ) # R array, z not an array dpevals = numpy.array([dp.phi2deriv(r, zs[0], phi) for (r, phi) in zip(rs, phis)]) assert numpy.all( numpy.fabs(dp.phi2deriv(rs, zs[0], phis) - dpevals) < 10.0**-10.0 ), "DehnenBarPotential phi2deriv does not work as expected for array inputs" # z array, R not an array dpevals = numpy.array([dp.phi2deriv(rs[0], z, phi) for (z, phi) in zip(zs, phis)]) assert numpy.all( numpy.fabs(dp.phi2deriv(rs[0], zs, phis) - dpevals) < 10.0**-10.0 ), "DehnenBarPotential phi2deriv does not work as expected for array inputs" # Rzderiv dpevals = numpy.array([dp.Rzderiv(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp.Rzderiv(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential Rzderiv evaluation does not work as expected for array inputs" ) # R array, z not an array dpevals = numpy.array([dp.Rzderiv(r, zs[0], phi) for (r, phi) in zip(rs, phis)]) assert numpy.all(numpy.fabs(dp.Rzderiv(rs, zs[0], phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential Rzderiv does not work as expected for array inputs" ) # z array, R not an array dpevals = numpy.array([dp.Rzderiv(rs[0], z, phi) for (z, phi) in zip(zs, phis)]) assert numpy.all(numpy.fabs(dp.Rzderiv(rs[0], zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential Rzderiv does not work as expected for array inputs" ) # Rphideriv dpevals = numpy.array( [dp.Rphideriv(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)] ) assert numpy.all(numpy.fabs(dp.Rphideriv(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential Rphideriv evaluation does not work as expected for array inputs" ) # R array, z not an array dpevals = numpy.array([dp.Rphideriv(r, zs[0], phi) for (r, phi) in zip(rs, phis)]) assert numpy.all( numpy.fabs(dp.Rphideriv(rs, zs[0], phis) - dpevals) < 10.0**-10.0 ), "DehnenBarPotential Rphideriv does not work as expected for array inputs" # z array, R not an array dpevals = numpy.array([dp.Rphideriv(rs[0], z, phi) for (z, phi) in zip(zs, phis)]) assert numpy.all( numpy.fabs(dp.Rphideriv(rs[0], zs, phis) - dpevals) < 10.0**-10.0 ), "DehnenBarPotential Rphideriv does not work as expected for array inputs" # phizderiv dpevals = numpy.array( [dp.phizderiv(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)] ) assert numpy.all(numpy.fabs(dp.phizderiv(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "DehnenBarPotential phizderiv evaluation does not work as expected for array inputs" ) # R array, z not an array dpevals = numpy.array([dp.phizderiv(r, zs[0], phi) for (r, phi) in zip(rs, phis)]) assert numpy.all( numpy.fabs(dp.phizderiv(rs, zs[0], phis) - dpevals) < 10.0**-10.0 ), "DehnenBarPotential phizderiv does not work as expected for array inputs" # z array, R not an array dpevals = numpy.array([dp.phizderiv(rs[0], z, phi) for (z, phi) in zip(zs, phis)]) assert numpy.all( numpy.fabs(dp.phizderiv(rs[0], zs, phis) - dpevals) < 10.0**-10.0 ), "DehnenBarPotential phizderiv does not work as expected for array inputs" return None def test_SpiralArm_special(): # Test some special cases for the DehnenBar potentials # Test that array input works dp = potential.SpiralArmsPotential() rs = numpy.linspace(0.1, 2.0, 11) zs = numpy.ones_like(rs) * 0.1 phis = numpy.ones_like(rs) * 0.1 # Potential itself dpevals = numpy.array([dp(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "SpiralArmsPotential evaluation does not work as expected for array inputs" ) # Rforce dpevals = numpy.array([dp.Rforce(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp.Rforce(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "SpiralArmsPotential Rforce evaluation does not work as expected for array inputs" ) # zforce dpevals = numpy.array([dp.zforce(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp.zforce(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "SpiralArmsPotential zforce evaluation does not work as expected for array inputs" ) # phitorque dpevals = numpy.array( [dp.phitorque(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)] ) assert numpy.all(numpy.fabs(dp.phitorque(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "SpiralArmsPotential zforce evaluation does not work as expected for array inputs" ) # R2deriv dpevals = numpy.array([dp.R2deriv(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp.R2deriv(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "SpiralArmsPotential R2deriv evaluation does not work as expected for array inputs" ) # z2deriv dpevals = numpy.array([dp.z2deriv(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp.z2deriv(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "SpiralArmsPotential z2deriv evaluation does not work as expected for array inputs" ) # phi2deriv dpevals = numpy.array( [dp.phi2deriv(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)] ) assert numpy.all(numpy.fabs(dp.phi2deriv(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "SpiralArmsPotential z2deriv evaluation does not work as expected for array inputs" ) # Rzderiv dpevals = numpy.array([dp.Rzderiv(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp.Rzderiv(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "SpiralArmsPotential Rzderiv evaluation does not work as expected for array inputs" ) # Rphideriv dpevals = numpy.array( [dp.Rphideriv(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)] ) assert numpy.all(numpy.fabs(dp.Rphideriv(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "SpiralArmsPotential Rzderiv evaluation does not work as expected for array inputs" ) # phizderiv dpevals = numpy.array( [dp.phizderiv(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)] ) assert numpy.all(numpy.fabs(dp.phizderiv(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "SpiralArmsPotential Rzderiv evaluation does not work as expected for array inputs" ) # dens dpevals = numpy.array([dp.dens(r, z, phi) for (r, z, phi) in zip(rs, zs, phis)]) assert numpy.all(numpy.fabs(dp.dens(rs, zs, phis) - dpevals) < 10.0**-10.0), ( "SpiralArmsPotential Rzderiv evaluation does not work as expected for array inputs" ) return None def test_MovingObject_density(): mp = mockMovingObjectPotential() # Just test that the density far away from the object is close to zero assert numpy.fabs(mp.dens(5.0, 0.0)) < 10.0**-8.0, ( "Density far away from MovingObject is not close to zero" ) return None # test specialSelf for TwoPowerSphericalPotential def test_TwoPowerSphericalPotentialSpecialSelf(): # TODO replace manual additions with an automatic method # that checks the signatures all methods in all potentials kw = dict(amp=1.0, a=1.0, normalize=False, ro=None, vo=None) Rs = numpy.array([0.5, 1.0, 2.0]) Zs = numpy.array([0.0, 0.125, -0.125]) pot = potential.TwoPowerSphericalPotential(alpha=0, beta=4, **kw) comp = potential.DehnenCoreSphericalPotential(**kw) assert all(pot._evaluate(Rs, Zs) == comp._evaluate(Rs, Zs)) assert all(pot._Rforce(Rs, Zs) == comp._Rforce(Rs, Zs)) assert all(pot._zforce(Rs, Zs) == comp._zforce(Rs, Zs)) pot = potential.TwoPowerSphericalPotential(alpha=1, beta=4, **kw) comp = potential.HernquistPotential(**kw) assert all(pot._evaluate(Rs, Zs) == comp._evaluate(Rs, Zs)) assert all(pot._Rforce(Rs, Zs) == comp._Rforce(Rs, Zs)) assert all(pot._zforce(Rs, Zs) == comp._zforce(Rs, Zs)) pot = potential.TwoPowerSphericalPotential(alpha=2, beta=4, **kw) comp = potential.JaffePotential(**kw) assert all(pot._evaluate(Rs, Zs) == comp._evaluate(Rs, Zs)) assert all(pot._Rforce(Rs, Zs) == comp._Rforce(Rs, Zs)) assert all(pot._zforce(Rs, Zs) == comp._zforce(Rs, Zs)) pot = potential.TwoPowerSphericalPotential(alpha=1, beta=3, **kw) comp = potential.NFWPotential(**kw) assert all(pot._evaluate(Rs, Zs) == comp._evaluate(Rs, Zs)) assert all(pot._Rforce(Rs, Zs) == comp._Rforce(Rs, Zs)) assert all(pot._zforce(Rs, Zs) == comp._zforce(Rs, Zs)) return None def test_DehnenSphericalPotentialSpecialSelf(): # TODO replace manual additions with an automatic method # that checks the signatures all methods in all potentials kw = dict(amp=1.0, a=1.0, normalize=False, ro=None, vo=None) Rs = numpy.array([0.5, 1.0, 2.0]) Zs = numpy.array([0.0, 0.125, -0.125]) pot = potential.DehnenSphericalPotential(alpha=0, **kw) comp = potential.DehnenCoreSphericalPotential(**kw) assert all(pot._evaluate(Rs, Zs) == comp._evaluate(Rs, Zs)) assert all(pot._Rforce(Rs, Zs) == comp._Rforce(Rs, Zs)) assert all(pot._zforce(Rs, Zs) == comp._zforce(Rs, Zs)) assert all(pot._R2deriv(Rs, Zs) == comp._R2deriv(Rs, Zs)) assert all(pot._Rzderiv(Rs, Zs) == comp._Rzderiv(Rs, Zs)) pot = potential.DehnenSphericalPotential(alpha=1, **kw) comp = potential.HernquistPotential(**kw) assert all(pot._evaluate(Rs, Zs) == comp._evaluate(Rs, Zs)) assert all(pot._Rforce(Rs, Zs) == comp._Rforce(Rs, Zs)) assert all(pot._zforce(Rs, Zs) == comp._zforce(Rs, Zs)) pot = potential.DehnenSphericalPotential(alpha=2, **kw) comp = potential.JaffePotential(**kw) assert all(pot._evaluate(Rs, Zs) == comp._evaluate(Rs, Zs)) assert all(pot._Rforce(Rs, Zs) == comp._Rforce(Rs, Zs)) assert all(pot._zforce(Rs, Zs) == comp._zforce(Rs, Zs)) return None # Test that MWPotential is what it's supposed to be def test_MWPotential2014(): pot = potential.MWPotential2014 V0, R0 = 220.0, 8.0 # Check the parameters of the bulge assert pot[0].rc == 1.9 / R0, "MWPotential2014's bulge cut-off radius is incorrect" assert pot[0].alpha == 1.8, ( "MWPotential2014's bulge power-law exponent is incorrect" ) assert numpy.fabs(pot[0].Rforce(1.0, 0.0) + 0.05) < 10.0**-14.0, ( "MWPotential2014's bulge amplitude is incorrect" ) # Check the parameters of the disk assert numpy.fabs(pot[1]._a - 3.0 / R0) < 10.0**-14.0, ( "MWPotential2014's disk scale length is incorrect" ) assert numpy.fabs(pot[1]._b - 0.28 / R0) < 10.0**-14.0, ( "MWPotential2014's disk scale height is incorrect" ) assert numpy.fabs(pot[1].Rforce(1.0, 0.0) + 0.60) < 10.0**-14.0, ( "MWPotential2014's disk amplitude is incorrect" ) # Check the parameters of the halo assert numpy.fabs(pot[2].a - 16.0 / R0) < 10.0**-14.0, ( "MWPotential2014's halo scale radius is incorrect" ) assert numpy.fabs(pot[2].Rforce(1.0, 0.0) + 0.35) < 10.0**-14.0, ( "MWPotential2014's halo amplitude is incorrect" ) return None # Test that the McMillan17 potential is what it's supposed to be def test_McMillan17(): from galpy.potential.mwpotentials import McMillan17 from galpy.util import conversion ro, vo = McMillan17[0]._ro, McMillan17[0]._vo # Check some numbers from Table 3 of McMillan17: vertical force at the Sun assert ( numpy.fabs( -potential.evaluatezforces(McMillan17, 1.0, 1.1 / 8.21, use_physical=False) * conversion.force_in_2piGmsolpc2(vo, ro) - 73.9 ) < 0.2 ), "Vertical force at the Sun in McMillan17 does not agree with what it should be" # Halo density at the Sun assert ( numpy.fabs( potential.evaluateDensities(McMillan17[1], 1.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.0101 ) < 1e-4 ), "Halo density at the Sun in McMillan17 does not agree with what it should be" # Halo concentration assert ( numpy.fabs(McMillan17[1].conc(overdens=94.0, wrtcrit=True, H=70.4) - 15.4) < 1e-1 ), "Halo concentration in McMillan17 does not agree with what it is supposed to be" # Let's compute the mass of the NFWPotenial and add the paper's number for the mass in stars and gas. The following is the total mass in units of $10^11\,M_\odot$: assert ( numpy.fabs( (McMillan17[1].mass(50.0 / 8.21, quantity=False)) / 10.0**11.0 + 0.543 + 0.122 - 5.1 ) < 1e-1 ), "Mass within 50 kpc in McMillan17 does not agree with what it is supposed to be" # Mass of the bulge is slightly off assert ( numpy.fabs((McMillan17[2].mass(50.0 / 8.21, quantity=False)) / 10.0**9.0 - 9.23) < 4e-1 ), "Bulge mass in McMillan17 does not agree with what it is supposed to be" # Mass in stars, compute bulge+disk and subtract what's supposed to be gas assert ( numpy.fabs( ( McMillan17[0].mass(50.0 / 8.21, quantity=False) + McMillan17[2].mass(50.0 / 8.21, quantity=False) ) / 10.0**10.0 - 1.22 - 5.43 ) < 1e-1 ), "Stellar massi n McMillan17 does not agree with what it is supposed to be" return None # Test that the Cautun20 potential is what it's supposed to be def test_Cautun20(): from galpy.potential.mwpotentials import Cautun20 from galpy.util import conversion ro, vo = Cautun20[0]._ro, Cautun20[0]._vo # Check the rotation velocity at a few distances # at the Sun assert numpy.fabs(potential.vcirc(Cautun20, 1.0, quantity=False) - 230.1) < 1e-1, ( "Total circular velocity at the Sun in Cautun20 does not agree with what it should be" ) assert ( numpy.fabs(potential.vcirc(Cautun20[0], 1.0, quantity=False) - 157.6) < 1e-1 ), ( "Halo circular velocity at the Sun in Cautun20 does not agree with what it should be" ) assert ( numpy.fabs(potential.vcirc(Cautun20[1], 1.0, quantity=False) - 151.2) < 1e-1 ), ( "Disc circular velocity at the Sun in Cautun20 does not agree with what it should be" ) assert ( numpy.fabs(potential.vcirc(Cautun20[2], 1.0, quantity=False) - 70.8) < 1e-1 ), ( "Bulge circular velocity at the Sun in Cautun20 does not agree with what it should be" ) # at 50 kpc assert ( numpy.fabs(potential.vcirc(Cautun20, 50.0 / ro, quantity=False) - 184.3) < 1e-1 ), ( "Total circular velocity at 50 kpc in Cautun20 does not agree with what it should be" ) assert ( numpy.fabs(potential.vcirc(Cautun20[0], 50.0 / ro, quantity=False) - 166.9) < 1e-1 ), ( "Halo circular velocity at 50 kpc in Cautun20 does not agree with what it should be" ) assert ( numpy.fabs(potential.vcirc(Cautun20[1], 50.0 / ro, quantity=False) - 68.9) < 1e-1 ), ( "Disc circular velocity at 50 kpc in Cautun20 does not agree with what it should be" ) assert ( numpy.fabs(potential.vcirc(Cautun20[2], 50.0 / ro, quantity=False) - 28.3) < 1e-1 ), ( "Bulge circular velocity at 50 kpc in Cautun20 does not agree with what it should be" ) # check the enclosed halo mass assert ( numpy.fabs((Cautun20[0].mass(50.0 / ro, quantity=False)) / 10.0**11 - 3.23) < 1e-2 ), ( "DM halo mass within 50 kpc in Cautun20 does not agree with what it is supposed to be" ) assert ( numpy.fabs((Cautun20[0].mass(200.0 / ro, quantity=False)) / 10.0**11 - 9.03) < 1e-2 ), ( "DM halo mass within 50 kpc in Cautun20 does not agree with what it is supposed to be" ) # check the CGM density assert ( numpy.fabs( potential.evaluateDensities(Cautun20[3], 1.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) * 1.0e5 - 9.34 ) < 1e-2 ), "CGM density at the Sun in Cautun20 does not agree with what it should be" assert ( numpy.fabs( potential.evaluateDensities(Cautun20[3], 50.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) * 1.0e6 - 6.49 ) < 1e-2 ), "CGM density at 50 kpc in Cautun20 does not agree with what it should be" # Halo density at the Sun assert ( numpy.fabs( potential.evaluateDensities(Cautun20[0], 1.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) * 1.0e3 - 8.8 ) < 5e-2 ), "Halo density at the Sun in Cautun20 does not agree with what it should be" return None # Test that the Irrgang13 potentials are what they are supposed to be def test_Irrgang13(): from galpy.potential.mwpotentials import Irrgang13I, Irrgang13II, Irrgang13III # Model I ro, vo = Irrgang13I[0]._ro, Irrgang13I[0]._vo # Check some numbers from Table 1 of Irrgang13: circular velocity at the Sun assert ( numpy.fabs(potential.vcirc(Irrgang13I, 1.0, quantity=False) - 242.0) < 1e-2 ), ( "Circular velocity at the Sun in Irrgang13I does not agree with what it should be" ) # Mass of the bulge assert numpy.fabs(Irrgang13I[0].mass(100.0, quantity=False) / 1e9 - 9.5) < 1e-2, ( "Mass of the bulge in Irrgang13I does not agree with what it should be" ) # Mass of the disk assert ( numpy.fabs(Irrgang13I[1].mass(100.0, 10.0, quantity=False) / 1e10 - 6.6) < 1e-2 ), "Mass of the disk in Irrgang13I does not agree with what it should be" # Mass of the halo (go to edge in Irrgang13I) assert ( numpy.fabs(Irrgang13I[2].mass(200.0 / ro, quantity=False) / 1e12 - 1.8) < 1e-1 ), "Mass of the halo in Irrgang13I does not agree with what it should be" # Escape velocity at the Sun assert numpy.fabs(potential.vesc(Irrgang13I, 1.0, quantity=False) - 616.4) < 1e0, ( "Escape velocity at the Sun in Irrgang13I does not agree with what it should be" ) # Oort A assert ( numpy.fabs( 0.5 * ( potential.vcirc(Irrgang13I, 1.0, use_physical=False) - potential.dvcircdR(Irrgang13I, 1.0, use_physical=False) ) * vo / ro - 15.06 ) < 1e-1 ), "Oort A in Irrgang13I does not agree with what it should be" # Oort B assert ( numpy.fabs( -0.5 * ( potential.vcirc(Irrgang13I, 1.0, use_physical=False) + potential.dvcircdR(Irrgang13I, 1.0, use_physical=False) ) * vo / ro + 13.74 ) < 1e-1 ), "Oort B in Irrgang13I does not agree with what it should be" # Model II ro, vo = Irrgang13II[0]._ro, Irrgang13II[0]._vo # Check some numbers from Table 2 of Irrgang13: circular velocity at the Sun assert ( numpy.fabs(potential.vcirc(Irrgang13II, 1.0, quantity=False) - 240.4) < 3e-2 ), ( "Circular velocity at the Sun in Irrgang13II does not agree with what it should be" ) # Mass of the bulge assert numpy.fabs(Irrgang13II[0].mass(100.0, quantity=False) / 1e9 - 4.1) < 1e-1, ( "Mass of the bulge in Irrgang13II does not agree with what it should be" ) # Mass of the disk assert ( numpy.fabs(Irrgang13II[1].mass(100.0, 10.0, quantity=False) / 1e10 - 6.6) < 1e-1 ), "Mass of the disk in Irrgang13II does not agree with what it should be" # Mass of the halo (go to edge in Irrgang13II) assert numpy.fabs(Irrgang13II[2].mass(100.0, quantity=False) / 1e12 - 1.6) < 1e-1, ( "Mass of the halo in Irrgang13II does not agree with what it should be" ) # Escape velocity at the Sun assert numpy.fabs(potential.vesc(Irrgang13II, 1.0, quantity=False) - 575.9) < 1e0, ( "Escape velocity at the Sun in Irrgang13II does not agree with what it should be" ) # Oort A assert ( numpy.fabs( 0.5 * ( potential.vcirc(Irrgang13II, 1.0, use_physical=False) - potential.dvcircdR(Irrgang13II, 1.0, use_physical=False) ) * vo / ro - 15.11 ) < 1e-1 ), "Oort A in Irrgang13II does not agree with what it should be" # Oort B assert ( numpy.fabs( -0.5 * ( potential.vcirc(Irrgang13II, 1.0, use_physical=False) + potential.dvcircdR(Irrgang13II, 1.0, use_physical=False) ) * vo / ro + 13.68 ) < 1e-1 ), "Oort B in Irrgang13II does not agree with what it should be" # Model III ro, vo = Irrgang13III[0]._ro, Irrgang13III[0]._vo # Check some numbers from Table 3 of Irrgang13: circular velocity at the Sun assert ( numpy.fabs(potential.vcirc(Irrgang13III, 1.0, quantity=False) - 239.7) < 3e-2 ), ( "Circular velocity at the Sun in Irrgang13III does not agree with what it should be" ) # Mass of the bulge assert ( numpy.fabs(Irrgang13III[0].mass(100.0, quantity=False) / 1e9 - 10.2) < 1e-1 ), "Mass of the bulge in Irrgang13III does not agree with what it should be" # Mass of the disk assert ( numpy.fabs(Irrgang13III[1].mass(100.0, 10.0, quantity=False) / 1e10 - 7.2) < 1e-1 ), "Mass of the disk in Irrgang13III does not agree with what it should be" # Escape velocity at the Sun assert ( numpy.fabs(potential.vesc(Irrgang13III, 1.0, quantity=False) - 811.5) < 1e0 ), ( "Escape velocity at the Sun in Irrgang13III does not agree with what it should be" ) # Oort A assert ( numpy.fabs( 0.5 * ( potential.vcirc(Irrgang13III, 1.0, use_physical=False) - potential.dvcircdR(Irrgang13III, 1.0, use_physical=False) ) * vo / ro - 14.70 ) < 1e-1 ), "Oort A in Irrgang13III does not agree with what it should be" # Oort B assert ( numpy.fabs( -0.5 * ( potential.vcirc(Irrgang13III, 1.0, use_physical=False) + potential.dvcircdR(Irrgang13III, 1.0, use_physical=False) ) * vo / ro + 14.08 ) < 1e-1 ), "Oort B in Irrgang13III does not agree with what it should be" return None # Test that the Dehnen & Binney (1998) models are what they are supposed to be def test_DehnenBinney98(): from galpy.potential.mwpotentials import ( DehnenBinney98I, DehnenBinney98II, DehnenBinney98III, DehnenBinney98IV, ) check_DehnenBinney98_model(DehnenBinney98I, model="model 1") check_DehnenBinney98_model(DehnenBinney98II, model="model 2") check_DehnenBinney98_model(DehnenBinney98III, model="model 3") check_DehnenBinney98_model(DehnenBinney98IV, model="model 4") return None def check_DehnenBinney98_model(pot, model="model 1"): from galpy.util import conversion truth = { "model 1": {"SigmaR0": 43.3, "vc": 222.0, "Fz": 68.0, "A": 14.4, "B": -13.3}, "model 2": {"SigmaR0": 52.1, "vc": 217.0, "Fz": 72.2, "A": 14.3, "B": -12.9}, "model 3": {"SigmaR0": 52.7, "vc": 217.0, "Fz": 72.5, "A": 14.1, "B": -13.1}, "model 4": {"SigmaR0": 50.7, "vc": 220.0, "Fz": 72.1, "A": 13.8, "B": -13.6}, } phys_kwargs = conversion.get_physical(pot) ro = phys_kwargs.get("ro") vo = phys_kwargs.get("vo") assert ( numpy.fabs(pot[1].surfdens(1.0, 10.0 / ro) - truth[model]["SigmaR0"]) < 0.2 ), ( f"Surface density at R0 in Dehnen & Binney (1998) {model} does not agree with paper value" ) assert numpy.fabs(potential.vcirc(pot, 1.0) - truth[model]["vc"]) < 0.5, ( f"Circular velocity at R0 in Dehnen & Binney (1998) {model} does not agree with paper value" ) assert ( numpy.fabs( -potential.evaluatezforces(pot, 1.0, 1.1 / ro, use_physical=False) * conversion.force_in_2piGmsolpc2(vo, ro) - truth[model]["Fz"] ) < 0.2 ), ( f"Vertical force at R0 in Dehnen & Binney (1998) {model} does not agree with paper value" ) assert ( numpy.fabs( 0.5 * ( potential.vcirc(pot, 1.0, use_physical=False) - potential.dvcircdR(pot, 1.0, use_physical=False) ) * vo / ro - truth[model]["A"] ) < 0.05 ), f"Oort A in Dehnen & Binney (1998) {model} does not agree with paper value" assert ( numpy.fabs( -0.5 * ( potential.vcirc(pot, 1.0, use_physical=False) + potential.dvcircdR(pot, 1.0, use_physical=False) ) * vo / ro - truth[model]["B"] ) < 0.05 ), f"Oort A in Dehnen & Binney (1998) {model} does not agree with paper value" return None # Test that the virial setup of NFW works def test_NFW_virialsetup_wrtmeanmatter(): H, Om, overdens, wrtcrit = 71.0, 0.32, 201.0, False ro, vo = 220.0, 8.0 conc, mvir = 12.0, 1.1 np = potential.NFWPotential( conc=conc, mvir=mvir, vo=vo, ro=ro, H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit, ) assert ( numpy.fabs(conc - np.conc(H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit)) < 10.0**-6.0 ), "NFWPotential virial setup's concentration does not work" assert ( numpy.fabs( mvir - np.mvir(H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit) / 10.0**12.0 ) < 10.0**-6.0 ), "NFWPotential virial setup's virial mass does not work" return None def test_NFW_virialsetup_wrtcrit(): H, Om, overdens, wrtcrit = 71.0, 0.32, 201.0, True ro, vo = 220.0, 8.0 conc, mvir = 12.0, 1.1 np = potential.NFWPotential( conc=conc, mvir=mvir, vo=vo, ro=ro, H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit, ) assert ( numpy.fabs(conc - np.conc(H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit)) < 10.0**-6.0 ), "NFWPotential virial setup's concentration does not work" assert ( numpy.fabs( mvir - np.mvir(H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit) / 10.0**12.0 ) < 10.0**-6.0 ), "NFWPotential virial setup's virial mass does not work" return None def test_TriaxialNFW_virialsetup_wrtmeanmatter(): H, Om, overdens, wrtcrit = 71.0, 0.32, 201.0, False ro, vo = 220.0, 8.0 conc, mvir = 12.0, 1.1 np = potential.NFWPotential( conc=conc, mvir=mvir, vo=vo, ro=ro, H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit, ) tnp = potential.TriaxialNFWPotential( b=0.3, c=0.7, conc=conc, mvir=mvir, vo=vo, ro=ro, H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit, ) assert numpy.fabs(np.a - tnp.a) < 10.0**-10.0, ( "TriaxialNFWPotential virial setup's concentration does not work" ) assert numpy.fabs(np._amp - tnp._amp * 4.0 * numpy.pi * tnp.a**3) < 10.0**-6.0, ( "TriaxialNFWPotential virial setup's virial mass does not work" ) return None def test_TriaxialNFW_virialsetup_wrtcrit(): H, Om, overdens, wrtcrit = 71.0, 0.32, 201.0, True ro, vo = 220.0, 8.0 conc, mvir = 12.0, 1.1 np = potential.NFWPotential( conc=conc, mvir=mvir, vo=vo, ro=ro, H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit, ) tnp = potential.TriaxialNFWPotential( b=0.3, c=0.7, conc=conc, mvir=mvir, vo=vo, ro=ro, H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit, ) assert numpy.fabs(np.a - tnp.a) < 10.0**-10.0, ( "TriaxialNFWPotential virial setup's concentration does not work" ) assert numpy.fabs(np._amp - tnp._amp * 4.0 * numpy.pi * tnp.a**3) < 10.0**-6.0, ( "TriaxialNFWPotential virial setup's virial mass does not work" ) return None # Test that setting up an NFW potential with rmax,vmax works as expected def test_NFW_rmaxvmaxsetup(): rmax, vmax = 1.2, 3.23 np = potential.NFWPotential(rmax=rmax, vmax=vmax) assert numpy.fabs(np.rmax() - rmax) < 10.0**-10.0, ( "NFWPotential setup with rmax,vmax does not work as expected" ) assert numpy.fabs(np.vmax() - vmax) < 10.0**-10.0, ( "NFWPotential setup with rmax,vmax does not work as expected" ) return None def test_conc_attributeerror(): pp = potential.PowerSphericalPotential(normalize=1.0) # This potential doesn't have a scale, so we cannot calculate the concentration try: pp.conc(220.0, 8.0) except AttributeError: pass else: raise AssertionError( "conc function for potential w/o scale did not raise AttributeError" ) return None def test_mvir_attributeerror(): mp = potential.MiyamotoNagaiPotential(normalize=1.0) # Don't think I will ever implement the virial radius for this try: mp.mvir(220.0, 8.0) except AttributeError: pass else: raise AssertionError( "mvir function for potential w/o rvir did not raise AttributeError" ) return None # Test that virial quantities are correctly computed when specifying a different (ro,vo) pair from Potential setup (see issue #290) def test_NFW_virialquantities_diffrovo(): from galpy.util import conversion H, Om, overdens, wrtcrit = 71.0, 0.32, 201.0, False ro_setup, vo_setup = 220.0, 8.0 ros = [7.0, 8.0, 9.0] vos = [220.0, 230.0, 240.0] for ro, vo in zip(ros, vos): np = potential.NFWPotential(amp=2.0, a=3.0, ro=ro_setup, vo=vo_setup) # Computing the overdensity in physical units od = ( np.mvir(ro=ro, vo=vo, H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit) / 4.0 / numpy.pi * 3.0 / np.rvir(ro=ro, vo=vo, H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit) ** 3.0 ) * (10.0**6.0 / H**2.0 * 8.0 * numpy.pi / 3.0 / Om * (4.302 * 10.0**-6.0)) assert numpy.fabs(od - overdens) < 0.1, ( "NFWPotential's virial quantities computed in physical units with different (ro,vo) from setup are incorrect" ) od = ( np.mvir( ro=ro, vo=vo, H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit, use_physical=False, ) / 4.0 / numpy.pi * 3.0 / np.rvir( ro=ro, vo=vo, H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit, use_physical=False, ) ** 3.0 ) * conversion.dens_in_meanmatterdens(vo, ro, H=H, Om=Om) assert numpy.fabs(od - overdens) < 0.01, ( "NFWPotential's virial quantities computed in internal units with different (ro,vo) from setup are incorrect" ) # Also test concentration assert ( numpy.fabs( np.conc(ro=ro, vo=vo, H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit) - np.rvir(ro=ro, vo=vo, H=H, Om=Om, overdens=overdens, wrtcrit=wrtcrit) / np._scale / ro ) < 0.01 ), ( "NFWPotential's concentration computed for different (ro,vo) from setup is incorrect" ) return None # Test that rmax and vmax are correctly determined for an NFW potential def test_NFW_rmaxvmax(): # Setup with rmax,vmax rmax, vmax = 1.2, 3.23 np = potential.NFWPotential(rmax=rmax, vmax=vmax) # Now determine rmax and vmax numerically rmax_opt = optimize.minimize_scalar(lambda r: -np.vcirc(r), bracket=[0.01, 100.0])[ "x" ] assert numpy.fabs(rmax_opt - rmax) < 10.0**-7.0, ( "NFW rmax() function does not behave as expected" ) assert numpy.fabs(np.vcirc(rmax_opt) - vmax) < 10.0**-8.0, ( "NFW rmax() function does not behave as expected" ) assert numpy.fabs(np.vcirc(rmax_opt) - np.vmax()) < 10.0**-8.0, ( "NFW vmax() function does not behave as expected" ) return None def test_LinShuReductionFactor(): # Test that the LinShuReductionFactor is implemented correctly, by comparing to figure 1 in Lin & Shu (1966) from galpy.potential import ( LinShuReductionFactor, LogarithmicHaloPotential, epifreq, omegac, ) lp = LogarithmicHaloPotential(normalize=1.0) # work in flat rotation curve # nu^2 = 0.2, x=4 for m=2,sigmar=0.1 # w/ nu = m(OmegaP-omegac)/epifreq, x=sr^2*k^2/epifreq^2 R, m, sr = 0.9, 2.0, 0.1 tepi, tomegac = epifreq(lp, R), omegac(lp, R) OmegaP = tepi * numpy.sqrt(0.2) / m + tomegac # leads to nu^2 = 0.2 k = numpy.sqrt(4.0) * tepi / sr assert ( numpy.fabs(LinShuReductionFactor(lp, R, sr, m=m, k=k, OmegaP=OmegaP) - 0.18) < 0.01 ), "LinShuReductionFactor does not agree w/ Figure 1 from Lin & Shu (1966)" # nu^2 = 0.8, x=10 OmegaP = tepi * numpy.sqrt(0.8) / m + tomegac # leads to nu^2 = 0.8 k = numpy.sqrt(10.0) * tepi / sr assert ( numpy.fabs(LinShuReductionFactor(lp, R, sr, m=m, k=k, OmegaP=OmegaP) - 0.04) < 0.01 ), "LinShuReductionFactor does not agree w/ Figure 1 from Lin & Shu (1966)" # Similar test, but using a nonaxiPot= input from galpy.potential import SteadyLogSpiralPotential sp = SteadyLogSpiralPotential(m=2.0, omegas=OmegaP, alpha=k * R) assert numpy.fabs(LinShuReductionFactor(lp, R, sr, nonaxiPot=sp) - 0.04) < 0.01, ( "LinShuReductionFactor does not agree w/ Figure 1 from Lin & Shu (1966)" ) # Test exception try: LinShuReductionFactor(lp, R, sr) except OSError: pass else: raise AssertionError( "LinShuReductionFactor w/o nonaxiPot set or k=,m=,OmegaP= set did not raise IOError" ) return None def test_nemoaccname(): # There is no real good way to test this (I think), so I'm just testing to # what I think is the correct output now to make sure this isn't # accidentally changed # Log lp = potential.LogarithmicHaloPotential(normalize=1.0) assert lp.nemo_accname() == "LogPot", "Logarithmic potential's NEMO name incorrect" # NFW np = potential.NFWPotential(normalize=1.0) assert np.nemo_accname() == "NFW", "NFW's NEMO name incorrect" # Miyamoto-Nagai mp = potential.MiyamotoNagaiPotential(normalize=1.0) assert mp.nemo_accname() == "MiyamotoNagai", "MiyamotoNagai's NEMO name incorrect" # Power-spherical w/ cut-off pp = potential.PowerSphericalPotentialwCutoff(normalize=1.0) assert pp.nemo_accname() == "PowSphwCut", ( "Power-spherical potential w/ cuto-ff's NEMO name incorrect" ) # MN3ExponentialDiskPotential mp = potential.MN3ExponentialDiskPotential(normalize=1.0) assert mp.nemo_accname() == "MiyamotoNagai+MiyamotoNagai+MiyamotoNagai", ( "MN3ExponentialDiskPotential's NEMO name incorrect" ) # Plummer pp = potential.PlummerPotential(normalize=1.0) assert pp.nemo_accname() == "Plummer", "PlummerPotential's NEMO name incorrect" # Hernquist hp = potential.HernquistPotential(normalize=1.0) assert hp.nemo_accname() == "Dehnen", "HernquistPotential's NEMO name incorrect" return None def test_nemoaccnamepars_attributeerror(): # Use BurkertPotential (unlikely that I would implement that one in NEMO soon) bp = potential.BurkertPotential(normalize=1.0) try: bp.nemo_accname() except AttributeError: pass else: raise AssertionError( "nemo_accname for potential w/o accname does not raise AttributeError" ) try: bp.nemo_accpars(220.0, 8.0) except AttributeError: pass else: raise AssertionError( "nemo_accpars for potential w/o accname does not raise AttributeError" ) return None def test_nemoaccnames(): # Just test MWPotential2014 and a single potential # MWPotential2014 assert ( potential.nemo_accname(potential.MWPotential2014) == "PowSphwCut+MiyamotoNagai+NFW" ), "MWPotential2014's NEMO name is incorrect" # Power-spherical w/ cut-off pp = potential.PowerSphericalPotentialwCutoff(normalize=1.0) assert potential.nemo_accname(pp) == "PowSphwCut", ( "Power-spherical potential w/ cut-off's NEMO name incorrect" ) return None def test_nemoaccpars(): # Log lp = potential.LogarithmicHaloPotential( amp=2.0, core=3.0, q=27.0 ) # completely ridiculous, but tests scalings vo, ro = 2.0, 3.0 vo /= 1.0227121655399913 ap = lp.nemo_accpars(vo, ro).split(",") assert numpy.fabs(float(ap[0]) - 0) < 10.0**-8.0, ( "Logarithmic potential's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[1]) - 8.0) < 10.0**-8.0, ( "Logarithmic potential's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[2]) - 729.0) < 10.0**-8.0, ( "Logarithmic potential's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[3]) - 1.0) < 10.0**-8.0, ( "Logarithmic potential's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[4]) - 27.0) < 10.0**-8.0, ( "Logarithmic potential's NEMO accpars incorrect" ) # Miyamoto-Nagai mp = potential.MiyamotoNagaiPotential(amp=3.0, a=2.0, b=5.0) vo, ro = 7.0, 9.0 vo /= 1.0227121655399913 ap = mp.nemo_accpars(vo, ro).split(",") assert numpy.fabs(float(ap[0]) - 0) < 10.0**-8.0, ( "MiyamotoNagai's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[1]) - 1323.0) < 10.0**-5.0, ( "MiyamotoNagai's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[2]) - 18.0) < 10.0**-8.0, ( "MiyamotoNagai's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[3]) - 45.0) < 10.0**-8.0, ( "MiyamotoNagai's NEMO accpars incorrect" ) # Power-spherical w/ cut-off pp = potential.PowerSphericalPotentialwCutoff(amp=3.0, alpha=4.0, rc=5.0) vo, ro = 7.0, 9.0 vo /= 1.0227121655399913 ap = pp.nemo_accpars(vo, ro).split(",") assert numpy.fabs(float(ap[0]) - 0) < 10.0**-8.0, ( "Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[1]) - 11907.0) < 10.0**-4.0, ( "Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[2]) - 4.0) < 10.0**-8.0, ( "Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[3]) - 45.0) < 10.0**-8.0, ( "Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) # NFW np = potential.NFWPotential(amp=1.0 / 0.2162165954, a=1.0 / 16) vo, ro = 3.0, 4.0 vo /= 1.0227121655399913 ap = np.nemo_accpars(vo, ro).split(",") assert numpy.fabs(float(ap[0]) - 0) < 10.0**-8.0, "NFW's NEMO accpars incorrect" assert numpy.fabs(float(ap[1]) - 0.25) < 10.0**-8.0, "NFW's NEMO accpars incorrect" assert numpy.fabs(float(ap[2]) - 12.0) < 10.0**-8.0, "NFW's NEMO accpars incorrect" # MN3ExponentialDiskPotential mn = potential.MN3ExponentialDiskPotential(normalize=1.0, hr=2.0, hz=0.5) vo, ro = 3.0, 4.0 ap = mn.nemo_accpars(vo, ro).replace("#", ",").split(",") assert numpy.fabs(float(ap[0]) - 0) < 10.0**-8.0, ( "MN3ExponentialDiskPotential 's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[4]) - 0) < 10.0**-8.0, ( "MN3ExponentialDiskPotential 's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[8]) - 0) < 10.0**-8.0, ( "MN3ExponentialDiskPotential 's NEMO accpars incorrect" ) # Test ratios assert ( numpy.fabs(float(ap[1]) / float(ap[5]) - mn._mn3[0]._amp / mn._mn3[1]._amp) < 10.0**-8.0 ), "MN3ExponentialDiskPotential 's NEMO accpars incorrect" assert ( numpy.fabs(float(ap[1]) / float(ap[9]) - mn._mn3[0]._amp / mn._mn3[2]._amp) < 10.0**-8.0 ), "MN3ExponentialDiskPotential 's NEMO accpars incorrect" assert ( numpy.fabs(float(ap[2]) / float(ap[6]) - mn._mn3[0]._a / mn._mn3[1]._a) < 10.0**-8.0 ), "MN3ExponentialDiskPotential 's NEMO accpars incorrect" assert ( numpy.fabs(float(ap[2]) / float(ap[10]) - mn._mn3[0]._a / mn._mn3[2]._a) < 10.0**-8.0 ), "MN3ExponentialDiskPotential 's NEMO accpars incorrect" assert numpy.fabs(float(ap[3]) / float(ap[7]) - 1.0) < 10.0**-8.0, ( "MN3ExponentialDiskPotential 's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[3]) / float(ap[11]) - 1.0) < 10.0**-8.0, ( "MN3ExponentialDiskPotential 's NEMO accpars incorrect" ) # Plummer pp = potential.PlummerPotential(amp=3.0, b=5.0) vo, ro = 7.0, 9.0 vo /= 1.0227121655399913 ap = pp.nemo_accpars(vo, ro).split(",") assert numpy.fabs(float(ap[0]) - 0) < 10.0**-8.0, "Plummer's NEMO accpars incorrect" assert numpy.fabs(float(ap[1]) - 1323.0) < 10.0**-5.0, ( "Plummer's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[2]) - 45.0) < 10.0**-8.0, ( "Plummer's NEMO accpars incorrect" ) # Hernquist hp = potential.HernquistPotential(amp=2.0, a=1.0 / 4.0) vo, ro = 3.0, 4.0 vo /= 1.0227121655399913 ap = hp.nemo_accpars(vo, ro).split(",") assert numpy.fabs(float(ap[0]) - 0) < 10.0**-8.0, ( "Hernquist's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[1]) - 1.0) < 10.0**-8.0, ( "Hernquist's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[2]) - 9.0 * 4) < 10.0**-7.0, ( "Hernquist's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[3]) - 1.0) < 10.0**-8.0, ( "Hernquist's NEMO accpars incorrect" ) return None def test_nemoaccparss(): # Just combine a few of the above ones # Miyamoto + PowerSpherwCut mp = potential.MiyamotoNagaiPotential(amp=3.0, a=2.0, b=5.0) pp = potential.PowerSphericalPotentialwCutoff(amp=3.0, alpha=4.0, rc=5.0) vo, ro = 7.0, 9.0 vo /= 1.0227121655399913 ap = potential.nemo_accpars(mp, vo, ro).split(",") assert numpy.fabs(float(ap[0]) - 0) < 10.0**-8.0, ( "MiyamotoNagai's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[1]) - 1323.0) < 10.0**-5.0, ( "MiyamotoNagai's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[2]) - 18.0) < 10.0**-8.0, ( "MiyamotoNagai's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[3]) - 45.0) < 10.0**-8.0, ( "MiyamotoNagai's NEMO accpars incorrect" ) # PowSpherwCut ap = potential.nemo_accpars(pp, vo, ro).split(",") assert numpy.fabs(float(ap[0]) - 0) < 10.0**-8.0, ( "Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[1]) - 11907.0) < 10.0**-4.0, ( "Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[2]) - 4.0) < 10.0**-8.0, ( "Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[3]) - 45.0) < 10.0**-8.0, ( "Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) # Combined apc = potential.nemo_accpars([mp, pp], vo, ro).split("#") ap = apc[0].split(",") # should be MN assert numpy.fabs(float(ap[0]) - 0) < 10.0**-8.0, ( "Miyamoto+Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[1]) - 1323.0) < 10.0**-5.0, ( "Miyamoto+Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[2]) - 18.0) < 10.0**-8.0, ( "Miyamoto+Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[3]) - 45.0) < 10.0**-8.0, ( "Miyamoto+Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) ap = apc[1].split(",") # should be PP assert numpy.fabs(float(ap[0]) - 0) < 10.0**-8.0, ( "Miyamoto+Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[1]) - 11907.0) < 10.0**-4.0, ( "Miyamoto+Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[2]) - 4.0) < 10.0**-8.0, ( "Miyamoto+Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) assert numpy.fabs(float(ap[3]) - 45.0) < 10.0**-8.0, ( "Miyamoto+Power-spherical potential w/ cut-off's NEMO accpars incorrect" ) return None def test_MN3ExponentialDiskPotential_inputs(): # Test the inputs of the MN3ExponentialDiskPotential # IOError for hz so large that b is negative try: mn = potential.MN3ExponentialDiskPotential(amp=1.0, hz=50.0) except OSError: pass else: raise AssertionError( "MN3ExponentialDiskPotential with ridiculous hz should have given IOError, but didn't" ) # Warning when b/Rd > 3 or (b/Rd > 1.35 and posdens) # Turn warnings into errors to test for them import warnings from galpy.util import galpyWarning with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) mn = MN3ExponentialDiskPotential(normalize=1.0, hz=1.438, hr=1.0) # Should raise warning bc of MN3ExponentialDiskPotential, # might raise others raisedWarning = False for wa in w: raisedWarning = "MN3ExponentialDiskPotential" in str(wa.message) if raisedWarning: break assert raisedWarning, ( "MN3ExponentialDiskPotential w/o posdens, but with b/Rd > 3 did not raise galpyWarning" ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) mn = MN3ExponentialDiskPotential(normalize=1.0, hr=1.0, hz=0.7727, posdens=True) raisedWarning = False for wa in w: raisedWarning = "MN3ExponentialDiskPotential" in str(wa.message) if raisedWarning: break assert raisedWarning, ( "MN3ExponentialDiskPotential w/o posdens, but with b/Rd > 1.35 did not raise galpyWarning" ) return None def test_MN3ExponentialDiskPotential_hz(): # Test that we correctly convert from hz/Rd to b/Rd # exp mn = potential.MN3ExponentialDiskPotential(amp=1.0, hr=1.0, hz=1.0, sech=False) assert numpy.fabs(mn._brd - 1.875) < 0.05, ( "b/Rd not computed correctly for exponential profile" ) mn = potential.MN3ExponentialDiskPotential(amp=1.0, hr=2.0, hz=1.0, sech=False) assert numpy.fabs(mn._brd - 0.75) < 0.05, ( "b/Rd not computed correctly for exponential profile" ) # sech mn = potential.MN3ExponentialDiskPotential(amp=1.0, hr=1.0, hz=2.0, sech=True) assert numpy.fabs(mn._brd - 2.1) < 0.05, ( "b/Rd not computed correctly for sech^2 profile" ) mn = potential.MN3ExponentialDiskPotential(amp=1.0, hr=2.0, hz=2.0, sech=True) assert numpy.fabs(mn._brd - 0.9) < 0.05, ( "b/Rd not computed correctly for sech^2 profile" ) return None def test_MN3ExponentialDiskPotential_approx(): # Test that the 3MN approximation works to the advertised level # Zero thickness mn = potential.MN3ExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.001, sech=False) dp = potential.DoubleExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.001) dpmass = dp.mass(4.0, 5.0 * 0.001) assert numpy.fabs(mn.mass(4.0, 5.0 * 0.001) - dpmass) / dpmass < 0.005, ( "MN3ExponentialDiskPotential does not approximate the enclosed mass as advertised" ) # Finite thickness mn = potential.MN3ExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.62, sech=False) dp = potential.DoubleExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.62) dpmass = dp.mass(4.0, 5.0 * 0.6) assert numpy.fabs(mn.mass(4.0, 10.0 * 0.6) - dpmass) / dpmass < 0.01, ( "MN3ExponentialDiskPotential does not approximate the enclosed mass as advertised" ) # Finite thickness w/ sech mn = potential.MN3ExponentialDiskPotential(amp=0.5, hr=1.0, hz=1.24, sech=True) dp = potential.DoubleExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.62) dpmass = dp.mass(4.0, 5.0 * 0.6) assert numpy.fabs(mn.mass(4.0, 20.0 * 0.6) - dpmass) / dpmass < 0.01, ( "MN3ExponentialDiskPotential does not approximate the enclosed mass as advertised" ) # At 10 Rd # Zero thickness mn = potential.MN3ExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.001, sech=False) dp = potential.DoubleExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.001) dpmass = dp.mass(10.0, 5.0 * 0.001) assert numpy.fabs(mn.mass(10.0, 5.0 * 0.001) - dpmass) / dpmass < 0.04, ( "MN3ExponentialDiskPotential does not approximate the enclosed mass as advertised" ) # Finite thickness mn = potential.MN3ExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.62, sech=False) dp = potential.DoubleExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.62) dpmass = dp.mass(10.0, 5.0 * 0.6) assert numpy.fabs(mn.mass(10.0, 10.0 * 0.6) - dpmass) / dpmass < 0.04, ( "MN3ExponentialDiskPotential does not approximate the enclosed mass as advertised" ) # Finite thickness w/ sech mn = potential.MN3ExponentialDiskPotential(amp=0.5, hr=1.0, hz=1.24, sech=True) dp = potential.DoubleExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.62) dpmass = dp.mass(10.0, 5.0 * 0.6) assert numpy.fabs(mn.mass(10.0, 20.0 * 0.6) - dpmass) / dpmass < 0.04, ( "MN3ExponentialDiskPotential does not approximate the enclosed mass as advertised" ) # For posdens the deviations are larger # Zero thickness mn = potential.MN3ExponentialDiskPotential( amp=1.0, hr=1.0, hz=0.001, sech=False, posdens=True ) dp = potential.DoubleExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.001) dpmass = dp.mass(4.0, 5.0 * 0.001) assert numpy.fabs(mn.mass(4.0, 5.0 * 0.001) - dpmass) / dpmass < 0.015, ( "MN3ExponentialDiskPotential does not approximate the enclosed mass as advertised" ) # Finite thickness mn = potential.MN3ExponentialDiskPotential( amp=1.0, hr=1.0, hz=0.62, sech=False, posdens=True ) dp = potential.DoubleExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.62) dpmass = dp.mass(4.0, 5.0 * 0.6) assert numpy.fabs(mn.mass(4.0, 10.0 * 0.6) - dpmass) / dpmass < 0.015, ( "MN3ExponentialDiskPotential does not approximate the enclosed mass as advertised" ) # At 10 Rd # Zero thickness mn = potential.MN3ExponentialDiskPotential( amp=1.0, hr=1.0, hz=0.001, sech=False, posdens=True ) dp = potential.DoubleExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.001) dpmass = dp.mass(10.0, 5.0 * 0.001) assert numpy.fabs(mn.mass(10.0, 5.0 * 0.001) - dpmass) / dpmass > 0.04, ( "MN3ExponentialDiskPotential does not approximate the enclosed mass as advertised" ) assert numpy.fabs(mn.mass(10.0, 5.0 * 0.001) - dpmass) / dpmass < 0.07, ( "MN3ExponentialDiskPotential does not approximate the enclosed mass as advertised" ) # Finite thickness mn = potential.MN3ExponentialDiskPotential( amp=1.0, hr=1.0, hz=0.62, sech=False, posdens=True ) dp = potential.DoubleExponentialDiskPotential(amp=1.0, hr=1.0, hz=0.62) dpmass = dp.mass(10.0, 5.0 * 0.6) assert numpy.fabs(mn.mass(10.0, 10.0 * 0.6) - dpmass) / dpmass < 0.08, ( "MN3ExponentialDiskPotential does not approximate the enclosed mass as advertised" ) assert numpy.fabs(mn.mass(10.0, 10.0 * 0.6) - dpmass) / dpmass > 0.03, ( "MN3ExponentialDiskPotential does not approximate the enclosed mass as advertised" ) return None def test_TwoPowerTriaxialPotential_vs_TwoPowerSphericalPotential(): # Test that TwoPowerTriaxialPotential with spherical parameters is the same # as TwoPowerSphericalPotential tol = -4.0 # tough general case rs = numpy.linspace(0.001, 25.0, 1001) tnp = potential.TwoPowerTriaxialPotential( normalize=1.0, b=1.0, c=1.0, a=1.5, alpha=1.5, beta=3.5 ) np = potential.TwoPowerSphericalPotential(normalize=1.0, a=1.5, alpha=1.5, beta=3.5) assert numpy.all( numpy.fabs( numpy.array( [numpy.sqrt(tnp.Rforce(r, 0.0) / np.Rforce(r, 0.0)) for r in rs] ) - 1.0 ) < 10.0**tol ), ( "Vcirc not the same for TwoPowerSphericalPotential and spherical version of TwoPowerTriaxialPotential" ) # Also do specific cases tol = -8.0 # much better # Hernquist tnp = potential.TriaxialHernquistPotential(normalize=1.0, b=1.0, c=1.0, a=1.5) np = potential.HernquistPotential(normalize=1.0, a=1.5) assert numpy.all( numpy.fabs( numpy.array( [numpy.sqrt(tnp.Rforce(r, 0.0) / np.Rforce(r, 0.0)) for r in rs] ) - 1.0 ) < 10.0**tol ), "Vcirc not the same for Hernquist and spherical version of TriaxialHernquist" # NFW tnp = potential.TriaxialNFWPotential(normalize=1.0, b=1.0, c=1.0, a=1.5) np = potential.NFWPotential(normalize=1.0, a=1.5) assert numpy.all( numpy.fabs( numpy.array( [numpy.sqrt(tnp.Rforce(r, 0.0) / np.Rforce(r, 0.0)) for r in rs] ) - 1.0 ) < 10.0**tol ), "Vcirc not the same for NFW and spherical version of TriaxialNFW" # Jaffe tnp = potential.TriaxialJaffePotential(normalize=1.0, b=1.0, c=1.0, a=1.5) np = potential.JaffePotential(normalize=1.0, a=1.5) assert numpy.all( numpy.fabs( numpy.array( [numpy.sqrt(tnp.Rforce(r, 0.0) / np.Rforce(r, 0.0)) for r in rs] ) - 1.0 ) < 10.0**tol ), "Vcirc not the same for Jaffe and spherical version of TriaxialJaffe" return None # Test that TwoPowerTriaxial setup raises an error for bad values of alpha # and beta def test_TwoPowerTriaxialPotential_alphahigherror(): with pytest.raises(IOError) as excinfo: dummy = potential.TwoPowerTriaxialPotential(alpha=3.5) return None def test_TwoPowerTriaxialPotential_betalowerror(): with pytest.raises(IOError) as excinfo: dummy = potential.TwoPowerTriaxialPotential(beta=1.0) return None # Test that DehnenSphericalPotential setup raises an error for bad values of alpha def test_DehnenSphericalPotential_alphalowhigherror(): with pytest.raises(IOError) as excinfo: dummy = potential.DehnenSphericalPotential(alpha=-0.5) with pytest.raises(IOError) as excinfo: dummy = potential.DehnenSphericalPotential(alpha=3.5) return None # Test that FerrersPotential raises a value error for n < 0 def test_FerrersPotential_nNegative(): with pytest.raises(ValueError) as excinfo: dummy = potential.FerrersPotential(n=-1.0) return None # Test that SphericalShellPotential raises a value error for normalize=True and a > 1 def test_SphericalShellPotential_normalizer0(): with pytest.raises(ValueError) as excinfo: dummy = potential.SphericalShellPotential(normalize=1.0, a=2.0) return None # Test that RingPotential raises a value error for normalize=True and a > 1 def test_RingPotential_normalizer0(): with pytest.raises(ValueError) as excinfo: dummy = potential.RingPotential(normalize=1.0, a=2.0) return None def test_planeRotatedNFWPotential(): # Test that the rotation according to pa works as expected tnp = potential.TriaxialNFWPotential( normalize=1.0, a=1.5, b=0.5, pa=30.0 / 180.0 * numpy.pi ) # Compute the potential at a fixed radius, minimum should be at pa! Rs = 0.8 phis = numpy.linspace(0.0, numpy.pi, 1001) pot = numpy.array([tnp(Rs, 0.0, phi=phi) for phi in phis]) minphi = numpy.argmin(pot) minphi_pred = numpy.argmin(numpy.fabs(phis - 30.0 / 180.0 * numpy.pi)) assert minphi == minphi_pred, ( "Flattened NFW potential rotated around the z axis does not behave as expected" ) # Same for density, but max instead dens = numpy.array([tnp.dens(Rs, 0.0, phi=phi) for phi in phis]) minphi = numpy.argmax(dens) minphi_pred = numpy.argmin(numpy.fabs(phis - 30.0 / 180.0 * numpy.pi)) assert minphi == minphi_pred, ( "Flattened NFW potential rotated around the z axis does not behave as expected" ) # Also do a negative angle tnp = potential.TriaxialNFWPotential( normalize=1.0, a=1.5, b=0.5, pa=-60.0 / 180.0 * numpy.pi ) # Compute the potential at a fixed radius, minimum should be at pa! Rs = 0.8 phis = numpy.linspace(0.0, numpy.pi, 1001) pot = numpy.array([tnp(Rs, 0.0, phi=phi) for phi in phis]) minphi = numpy.argmin(pot) minphi_pred = numpy.argmin(numpy.fabs(phis - 120.0 / 180.0 * numpy.pi)) assert minphi == minphi_pred, ( "Flattened NFW potential rotated around the z axis does not behave as expected" ) # Same for density, but max instead dens = numpy.array([tnp.dens(Rs, 0.0, phi=phi) for phi in phis]) minphi = numpy.argmax(dens) minphi_pred = numpy.argmin(numpy.fabs(phis - 120.0 / 180.0 * numpy.pi)) assert minphi == minphi_pred, ( "Flattened NFW potential rotated around the z axis does not behave as expected" ) return None def test_zaxisRotatedNFWPotential(): from galpy.util import coords # Test that the rotation according to zvec works as expected pa = 30.0 / 180.0 * numpy.pi tnp = potential.TriaxialNFWPotential( normalize=1.0, a=1.5, c=0.5, zvec=[0.0, -numpy.sin(pa), numpy.cos(pa)] ) # Compute the potential at a fixed radius in the y/z plane, # minimum should be at pa! Rs = 0.8 phis = numpy.linspace(0.0, numpy.pi, 1001) xs = numpy.zeros_like(phis) ys = Rs * numpy.cos(phis) zs = Rs * numpy.sin(phis) tR, tphi, tz = coords.rect_to_cyl(xs, ys, zs) pot = numpy.array([tnp(r, z, phi=phi) for r, z, phi in zip(tR, tz, tphi)]) minphi = numpy.argmin(pot) minphi_pred = numpy.argmin(numpy.fabs(phis - 30.0 / 180.0 * numpy.pi)) assert minphi == minphi_pred, ( "Flattened NFW potential with rotated z axis does not behave as expected" ) # Same for density, but max instead dens = numpy.array([tnp.dens(r, z, phi=phi) for r, z, phi in zip(tR, tz, tphi)]) minphi = numpy.argmax(dens) minphi_pred = numpy.argmin(numpy.fabs(phis - 30.0 / 180.0 * numpy.pi)) assert minphi == minphi_pred, ( "Flattened NFW potential with rotated z axis does not behave as expected" ) # Another one pa = -60.0 / 180.0 * numpy.pi tnp = potential.TriaxialNFWPotential( normalize=1.0, a=1.5, c=0.5, zvec=[-numpy.sin(pa), 0.0, numpy.cos(pa)] ) # Compute the potential at a fixed radius in the z/z plane, # minimum should be at pa! Rs = 0.8 phis = numpy.linspace(0.0, numpy.pi, 1001) xs = Rs * numpy.cos(phis) ys = numpy.zeros_like(phis) zs = Rs * numpy.sin(phis) tR, tphi, tz = coords.rect_to_cyl(xs, ys, zs) pot = numpy.array([tnp(r, z, phi=phi) for r, z, phi in zip(tR, tz, tphi)]) minphi = numpy.argmin(pot) minphi_pred = numpy.argmin(numpy.fabs(phis - 120.0 / 180.0 * numpy.pi)) assert minphi == minphi_pred, ( "Flattened NFW potential with rotated z axis does not behave as expected" ) # Same for density, but max instead dens = numpy.array([tnp.dens(r, z, phi=phi) for r, z, phi in zip(tR, tz, tphi)]) minphi = numpy.argmax(dens) minphi_pred = numpy.argmin(numpy.fabs(phis - 120.0 / 180.0 * numpy.pi)) assert minphi == minphi_pred, ( "Flattened NFW potential with rotated z axis does not behave as expected" ) return None def test_nonaxierror_function(): # Test that the code throws an exception when calling a non-axisymmetric # potential without phi tnp = potential.TriaxialNFWPotential(amp=1.0, b=0.7, c=0.9) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluatePotentials(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluateDensities(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluateRforces(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluatezforces(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluatephitorques(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluateR2derivs(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluatez2derivs(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluateRzderivs(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluatephi2derivs(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluateRphiderivs(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluatephizderivs(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluaterforces(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluater2derivs(tnp, 1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: potential.evaluateSurfaceDensities(tnp, 1.0, 0.1) return None # Test that _isNonAxi raises a PotentialError if input is missing an isNonAxi attribute def test_nonaxi_missingattr(): # Check that giving an object that is not a list or Potential instance produces an error with pytest.raises(potential.PotentialError) as excinfo: _ = _isNonAxi("something else") # Check that given a list of objects that are not a Potential instances gives an error with pytest.raises(potential.PotentialError) as excinfo: _ = _isNonAxi([3, 4, 45]) # Check that using an a actual potential w/o nonaxi attribute gives an error pot = potential.Potential() del pot.isNonAxi with pytest.raises(potential.PotentialError) as excinfo: _ = _isNonAxi(pot) with pytest.raises(potential.PotentialError) as excinfo: _ = _isNonAxi([pot, pot, pot]) def test_SoftenedNeedleBarPotential_density(): # Some simple tests of the density of the SoftenedNeedleBarPotential # For a spherical softening kernel, density should be symmetric to y/z sbp = potential.SoftenedNeedleBarPotential( normalize=1.0, a=1.0, c=0.1, b=0.0, pa=0.0 ) assert ( numpy.fabs( sbp.dens(2.0, 0.0, phi=numpy.pi / 4.0) - sbp.dens(numpy.sqrt(2.0), numpy.sqrt(2.0), phi=0.0) ) < 10.0**-13.0 ), ( "SoftenedNeedleBarPotential with spherical softening kernel does not appear to have a spherically symmetric density" ) # Another one assert ( numpy.fabs( sbp.dens(4.0, 0.0, phi=numpy.pi / 4.0) - sbp.dens(2.0 * numpy.sqrt(2.0), 2.0 * numpy.sqrt(2.0), phi=0.0) ) < 10.0**-13.0 ), ( "SoftenedNeedleBarPotential with spherical softening kernel does not appear to have a spherically symmetric density" ) # For a flattened softening kernel, the density at (y,z) should be higher than at (z,y) sbp = potential.SoftenedNeedleBarPotential( normalize=1.0, a=1.0, c=0.1, b=0.3, pa=0.0 ) assert sbp.dens(2.0, 0.0, phi=numpy.pi / 4.0) > sbp.dens( numpy.sqrt(2.0), numpy.sqrt(2.0), phi=0.0 ), ( "SoftenedNeedleBarPotential with flattened softening kernel does not appear to have a consistent" ) # Another one assert sbp.dens(4.0, 0.0, phi=numpy.pi / 4.0) > sbp.dens( 2.0 * numpy.sqrt(2.0), 2.0 * numpy.sqrt(2.0), phi=0.0 ), ( "SoftenedNeedleBarPotential with flattened softening kernel does not appear to have a consistent" ) return None def test_DiskSCFPotential_SigmaDerivs(): # Test that the derivatives of Sigma are correctly implemented in DiskSCF # Very rough finite difference checks dscfp = potential.DiskSCFPotential( dens=lambda R, z: 1.0, # doesn't matter Sigma=[ {"type": "exp", "h": 1.0 / 3.0, "amp": 1.0}, {"type": "expwhole", "h": 1.0 / 3.0, "amp": 1.0, "Rhole": 0.5}, ], hz=[{"type": "exp", "h": 1.0 / 27.0}, {"type": "sech2", "h": 1.0 / 27.0}], a=1.0, N=2, L=2, ) # Sigma exp testRs = numpy.linspace(0.3, 1.5, 101) dR = 10.0**-8.0 assert numpy.all( numpy.fabs( ( (dscfp._Sigma[0](testRs + dR) - dscfp._Sigma[0](testRs)) / dR - dscfp._dSigmadR[0](testRs) ) / dscfp._dSigmadR[0](testRs) ) < 10.0**-7.0 ), ( "Derivative dSigmadR does not agree with finite-difference derivative of Sigma for exponential profile in DiskSCFPotential" ) assert numpy.all( numpy.fabs( ( (dscfp._dSigmadR[0](testRs + dR) - dscfp._dSigmadR[0](testRs)) / dR - dscfp._d2SigmadR2[0](testRs) ) / dscfp._d2SigmadR2[0](testRs) ) < 10.0**-7.0 ), ( "Derivative d2SigmadR2 does not agree with finite-difference derivative of dSigmadR for exponential profile in DiskSCFPotential" ) # Sigma expwhole dR = 10.0**-8.0 assert numpy.all( numpy.fabs( ( (dscfp._Sigma[1](testRs + dR) - dscfp._Sigma[1](testRs)) / dR - dscfp._dSigmadR[1](testRs) ) / dscfp._dSigmadR[1](testRs) ) < 10.0**-4.0 ), ( "Derivative dSigmadR does not agree with finite-difference derivative of Sigma for exponential-with-hole profile in DiskSCFPotential" ) assert numpy.all( numpy.fabs( ( (dscfp._dSigmadR[1](testRs + dR) - dscfp._dSigmadR[1](testRs)) / dR - dscfp._d2SigmadR2[1](testRs) ) / dscfp._d2SigmadR2[1](testRs) ) < 10.0**-4.0 ), ( "Derivative d2SigmadR2 does not agree with finite-difference derivative of dSigmadR for exponential-with-hole profile in DiskSCFPotential" ) return None def test_DiskSCFPotential_verticalDerivs(): # Test that the derivatives of Sigma are correctly implemented in DiskSCF # Very rough finite difference checks dscfp = potential.DiskSCFPotential( dens=lambda R, z: 1.0, # doesn't matter Sigma=[ {"type": "exp", "h": 1.0 / 3.0, "amp": 1.0}, {"type": "expwhole", "h": 1.0 / 3.0, "amp": 1.0, "Rhole": 0.5}, ], hz=[{"type": "exp", "h": 1.0 / 27.0}, {"type": "sech2", "h": 1.0 / 27.0}], a=1.0, N=2, L=2, ) # Vertical exp testzs = numpy.linspace(0.1 / 27.0, 3.0 / 27, 101) dz = 10.0**-8.0 assert numpy.all( numpy.fabs( ( (dscfp._Hz[0](testzs + dz) - dscfp._Hz[0](testzs)) / dz - dscfp._dHzdz[0](testzs) ) / dscfp._dHzdz[0](testzs) ) < 10.0**-5.5 ), ( "Derivative dHzdz does not agree with finite-difference derivative of Hz for exponential profile in DiskSCFPotential" ) assert numpy.all( numpy.fabs( ( (dscfp._dHzdz[0](testzs + dz) - dscfp._dHzdz[0](testzs)) / dz - dscfp._hz[0](testzs) ) / dscfp._hz[0](testzs) ) < 10.0**-6.0 ), ( "Derivative hz does not agree with finite-difference derivative of dHzdz for exponential profile in DiskSCFPotential" ) # Vertical sech^2 dz = 10.0**-8.0 assert numpy.all( numpy.fabs( ( (dscfp._Hz[1](testzs + dz) - dscfp._Hz[1](testzs)) / dz - dscfp._dHzdz[1](testzs) ) / dscfp._dHzdz[1](testzs) ) < 10.0**-5.5 ), ( "Derivative dSigmadz does not agree with finite-difference derivative of Sigma for sech2 profile in DiskSCFPotential" ) assert numpy.all( numpy.fabs( ( (dscfp._dHzdz[1](testzs + dz) - dscfp._dHzdz[1](testzs)) / dz - dscfp._hz[1](testzs) ) / dscfp._hz[1](testzs) ) < 10.0**-6.0 ), ( "Derivative hz does not agree with finite-difference derivative of dHzdz for sech2 profile in DiskSCFPotential" ) return None def test_DiskSCFPotential_nhzNeqnsigmaError(): with pytest.raises(ValueError) as excinfo: dummy = potential.DiskSCFPotential( dens=lambda R, z: numpy.exp(-3.0 * R) * 1.0 / numpy.cosh(z / 2.0 * 27.0) ** 2.0 / 4.0 * 27.0, Sigma={"h": 1.0 / 3.0, "type": "exp", "amp": 1.0}, hz=[{"type": "sech2", "h": 1.0 / 27.0}, {"type": "sech2", "h": 1.0 / 27.0}], a=1.0, N=5, L=5, ) return None def test_DiskSCFPotential_againstDoubleExp(): # Test that the DiskSCFPotential approx. of a dbl-exp disk agrees with # DoubleExponentialDiskPotential dp = potential.DoubleExponentialDiskPotential(amp=13.5, hr=1.0 / 3.0, hz=1.0 / 27.0) dscfp = potential.DiskSCFPotential( dens=lambda R, z: dp.dens(R, z), Sigma_amp=1.0, Sigma=lambda R: numpy.exp(-3.0 * R), dSigmadR=lambda R: -3.0 * numpy.exp(-3.0 * R), d2SigmadR2=lambda R: 9.0 * numpy.exp(-3.0 * R), hz={"type": "exp", "h": 1.0 / 27.0}, a=1.0, N=10, L=10, ) testRs = numpy.linspace(0.3, 1.5, 101) testzs = numpy.linspace(0.1 / 27.0, 3.0 / 27, 101) testR = 0.9 * numpy.ones_like(testzs) testz = 1.5 / 27.0 * numpy.ones_like(testRs) # Test potential assert numpy.all( numpy.fabs((dp(testRs, testz) - dscfp(testRs, testz)) / dscfp(testRs, testz)) < 10.0**-2.5 ), ( "DiskSCFPotential for double-exponential disk does not agree with DoubleExponentialDiskPotential" ) assert numpy.all( numpy.fabs((dp(testR, testzs) - dscfp(testR, testzs)) / dscfp(testRs, testz)) < 10.0**-2.5 ), ( "DiskSCFPotential for double-exponential disk does not agree with DoubleExponentialDiskPotential" ) # Rforce assert numpy.all( numpy.fabs( ( numpy.array([dp.Rforce(r, z) for (r, z) in zip(testRs, testz)]) - dscfp.Rforce(testRs, testz) ) / dscfp.Rforce(testRs, testz) ) < 10.0**-2.0 ), ( "DiskSCFPotential for double-exponential disk does not agree with DoubleExponentialDiskPotential" ) assert numpy.all( numpy.fabs( ( numpy.array([dp.Rforce(r, z) for (r, z) in zip(testR, testzs)]) - dscfp.Rforce(testR, testzs) ) / dscfp.Rforce(testRs, testz) ) < 10.0**-2.0 ), ( "DiskSCFPotential for double-exponential disk does not agree with DoubleExponentialDiskPotential" ) # zforce assert numpy.all( numpy.fabs( ( numpy.array([dp.zforce(r, z) for (r, z) in zip(testRs, testz)]) - dscfp.zforce(testRs, testz) ) / dscfp.zforce(testRs, testz) ) < 10.0**-1.5 ), ( "DiskSCFPotential for double-exponential disk does not agree with DoubleExponentialDiskPotential" ) # Following has rel. large difference at high z assert numpy.all( numpy.fabs( ( numpy.array([dp.zforce(r, z) for (r, z) in zip(testR, testzs)]) - dscfp.zforce(testR, testzs) ) / dscfp.zforce(testRs, testz) ) < 10.0**-1.0 ), ( "DiskSCFPotential for double-exponential disk does not agree with DoubleExponentialDiskPotential" ) return None def test_DiskSCFPotential_againstDoubleExp_dens(): # Test that the DiskSCFPotential approx. of a dbl-exp disk agrees with # DoubleExponentialDiskPotential dp = potential.DoubleExponentialDiskPotential(amp=13.5, hr=1.0 / 3.0, hz=1.0 / 27.0) dscfp = potential.DiskSCFPotential( dens=lambda R, z: dp.dens(R, z), Sigma={"type": "exp", "h": 1.0 / 3.0, "amp": 1.0}, hz={"type": "exp", "h": 1.0 / 27.0}, a=1.0, N=10, L=10, ) testRs = numpy.linspace(0.3, 1.5, 101) testzs = numpy.linspace(0.1 / 27.0, 3.0 / 27, 101) testR = 0.9 * numpy.ones_like(testzs) testz = 1.5 / 27.0 * numpy.ones_like(testRs) # Test density assert numpy.all( numpy.fabs( (dp.dens(testRs, testz) - dscfp.dens(testRs, testz)) / dscfp.dens(testRs, testz) ) < 10.0**-1.25 ), ( "DiskSCFPotential for double-exponential disk does not agree with DoubleExponentialDiskPotential" ) # difficult at high z assert numpy.all( numpy.fabs( (dp.dens(testR, testzs) - dscfp.dens(testR, testzs)) / dscfp.dens(testRs, testz) ) < 10.0**-1.0 ), ( "DiskSCFPotential for double-exponential disk does not agree with DoubleExponentialDiskPotential" ) return None def test_WrapperPotential_dims(): # Test that WrapperPotentials get assigned to Potential/planarPotential # correctly, based on input pot= from galpy.potential.WrapperPotential import ( WrapperPotential, parentWrapperPotential, planarWrapperPotential, ) dp = potential.DehnenBarPotential() # 3D pot should be Potential, Wrapper, parentWrapper, not planarX dwp = potential.DehnenSmoothWrapperPotential(pot=dp) assert isinstance(dwp, potential.Potential), ( "WrapperPotential for 3D pot= is not an instance of Potential" ) assert not isinstance(dwp, potential.planarPotential), ( "WrapperPotential for 3D pot= is an instance of planarPotential" ) assert isinstance(dwp, parentWrapperPotential), ( "WrapperPotential for 3D pot= is not an instance of parentWrapperPotential" ) assert isinstance(dwp, WrapperPotential), ( "WrapperPotential for 3D pot= is not an instance of WrapperPotential" ) assert not isinstance(dwp, planarWrapperPotential), ( "WrapperPotential for 3D pot= is an instance of planarWrapperPotential" ) # 2D pot should be Potential, Wrapper, parentWrapper, not planarX dwp = potential.DehnenSmoothWrapperPotential(pot=dp.toPlanar()) assert isinstance(dwp, potential.planarPotential), ( "WrapperPotential for 3D pot= is not an instance of planarPotential" ) assert not isinstance(dwp, potential.Potential), ( "WrapperPotential for 3D pot= is an instance of Potential" ) assert isinstance(dwp, parentWrapperPotential), ( "WrapperPotential for 3D pot= is not an instance of parentWrapperPotential" ) assert isinstance(dwp, planarWrapperPotential), ( "WrapperPotential for 3D pot= is not an instance of planarWrapperPotential" ) assert not isinstance(dwp, WrapperPotential), ( "WrapperPotential for 3D pot= is an instance of WrapperPotential" ) return None def test_Wrapper_potinputerror(): # Test that setting up a WrapperPotential with anything other than a # (list of) planar/Potentials raises an error with pytest.raises(ValueError) as excinfo: potential.DehnenSmoothWrapperPotential(pot=1) return None def test_Wrapper_incompatibleunitserror(): # Test that setting up a WrapperPotential with a potential with # incompatible units to the wrapper itself raises an error # 3D ro, vo = 8.0, 220.0 hp = potential.HernquistPotential(amp=0.55, a=1.3, ro=ro, vo=vo) with pytest.raises(AssertionError) as excinfo: potential.DehnenSmoothWrapperPotential(pot=hp, ro=1.1 * ro, vo=vo) with pytest.raises(AssertionError) as excinfo: potential.DehnenSmoothWrapperPotential(pot=hp, ro=ro, vo=vo * 1.1) with pytest.raises(AssertionError) as excinfo: potential.DehnenSmoothWrapperPotential(pot=hp, ro=1.1 * ro, vo=vo * 1.1) # 2D hp = potential.HernquistPotential(amp=0.55, a=1.3, ro=ro, vo=vo).toPlanar() with pytest.raises(AssertionError) as excinfo: potential.DehnenSmoothWrapperPotential(pot=hp, ro=1.1 * ro, vo=vo) with pytest.raises(AssertionError) as excinfo: potential.DehnenSmoothWrapperPotential(pot=hp, ro=ro, vo=vo * 1.1) with pytest.raises(AssertionError) as excinfo: potential.DehnenSmoothWrapperPotential(pot=hp, ro=1.1 * ro, vo=vo * 1.1) return None def test_Wrapper_Force_error(): # Test that applying a wrapper to a DissipativeForce does not currently work def M(t): return 1.0 # Initialize potentials and time-varying potentials df = potential.ChandrasekharDynamicalFrictionForce(GMs=1.0) with pytest.raises(RuntimeError) as excinfo: df_wrap = potential.TimeDependentAmplitudeWrapperPotential(A=M, amp=1, pot=df) assert ( "WrapperPotential cannot currently wrap non-Potential Force objects" == excinfo.value.args[0] ) # Also test for list with pytest.raises(RuntimeError) as excinfo: df_wrap = potential.TimeDependentAmplitudeWrapperPotential( A=M, amp=1, pot=potential.MWPotential2014 + df ) assert ( "WrapperPotential cannot currently wrap non-Potential Force objects" == excinfo.value.args[0] ) return None def test_WrapperPotential_unittransfer_3d(): # Test that units are properly transferred between a potential and its # wrapper from galpy.util import conversion ro, vo = 9.0, 230.0 hp = potential.HernquistPotential(amp=0.55, a=1.3, ro=ro, vo=vo) hpw = potential.DehnenSmoothWrapperPotential(pot=hp) hpw_phys = conversion.get_physical(hpw, include_set=True) assert hpw_phys["roSet"], "ro not set when wrapping a potential with ro set" assert hpw_phys["voSet"], "vo not set when wrapping a potential with vo set" assert numpy.fabs(hpw_phys["ro"] - ro) < 1e-10, ( "ro not properly transferred to wrapper when wrapping a potential with ro set" ) assert numpy.fabs(hpw_phys["vo"] - vo) < 1e-10, ( "vo not properly transferred to wrapper when wrapping a potential with vo set" ) # Just set ro hp = potential.HernquistPotential(amp=0.55, a=1.3, ro=ro) hpw = potential.DehnenSmoothWrapperPotential(pot=hp) hpw_phys = conversion.get_physical(hpw, include_set=True) assert hpw_phys["roSet"], "ro not set when wrapping a potential with ro set" assert not hpw_phys["voSet"], "vo not set when wrapping a potential with vo set" assert numpy.fabs(hpw_phys["ro"] - ro) < 1e-10, ( "ro not properly transferred to wrapper when wrapping a potential with ro set" ) # Just set vo hp = potential.HernquistPotential(amp=0.55, a=1.3, vo=vo) hpw = potential.DehnenSmoothWrapperPotential(pot=hp) hpw_phys = conversion.get_physical(hpw, include_set=True) assert not hpw_phys["roSet"], "ro not set when wrapping a potential with ro set" assert hpw_phys["voSet"], "vo not set when wrapping a potential with vo set" assert numpy.fabs(hpw_phys["vo"] - vo) < 1e-10, ( "vo not properly transferred to wrapper when wrapping a potential with vo set" ) return None def test_WrapperPotential_unittransfer_2d(): # Test that units are properly transferred between a potential and its # wrapper from galpy.util import conversion ro, vo = 9.0, 230.0 hp = potential.HernquistPotential(amp=0.55, a=1.3, ro=ro, vo=vo).toPlanar() hpw = potential.DehnenSmoothWrapperPotential(pot=hp) hpw_phys = conversion.get_physical(hpw, include_set=True) assert hpw_phys["roSet"], "ro not set when wrapping a potential with ro set" assert hpw_phys["voSet"], "vo not set when wrapping a potential with vo set" assert numpy.fabs(hpw_phys["ro"] - ro) < 1e-10, ( "ro not properly transferred to wrapper when wrapping a potential with ro set" ) assert numpy.fabs(hpw_phys["vo"] - vo) < 1e-10, ( "vo not properly transferred to wrapper when wrapping a potential with vo set" ) # Just set ro hp = potential.HernquistPotential(amp=0.55, a=1.3, ro=ro).toPlanar() hpw = potential.DehnenSmoothWrapperPotential(pot=hp) hpw_phys = conversion.get_physical(hpw, include_set=True) assert hpw_phys["roSet"], "ro not set when wrapping a potential with ro set" assert not hpw_phys["voSet"], "vo not set when wrapping a potential with vo set" assert numpy.fabs(hpw_phys["ro"] - ro) < 1e-10, ( "ro not properly transferred to wrapper when wrapping a potential with ro set" ) # Just set vo hp = potential.HernquistPotential(amp=0.55, a=1.3, vo=vo).toPlanar() hpw = potential.DehnenSmoothWrapperPotential(pot=hp) hpw_phys = conversion.get_physical(hpw, include_set=True) assert not hpw_phys["roSet"], "ro not set when wrapping a potential with ro set" assert hpw_phys["voSet"], "vo not set when wrapping a potential with vo set" assert numpy.fabs(hpw_phys["vo"] - vo) < 1e-10, ( "vo not properly transferred to wrapper when wrapping a potential with vo set" ) return None def test_WrapperPotential_serialization(): import pickle from galpy.potential.WrapperPotential import WrapperPotential dp = potential.DehnenBarPotential() dwp = potential.DehnenSmoothWrapperPotential(pot=dp) pickled_dwp = pickle.dumps(dwp) unpickled_dwp = pickle.loads(pickled_dwp) assert isinstance(unpickled_dwp, WrapperPotential), ( "Deserialized WrapperPotential is not an instance of WrapperPotential" ) testRs = numpy.linspace(0.1, 1, 100) testzs = numpy.linspace(-1, 1, 100) testphis = numpy.linspace(0, 2 * numpy.pi, 100) testts = numpy.linspace(0, 1, 100) for R, z, phi, t in zip(testRs, testzs, testphis, testts): assert dwp(R, z, phi, t) == unpickled_dwp(R, z, phi, t), ( "Deserialized WrapperPotential does not agree with original WrapperPotential" ) def test_WrapperPotential_print(): dp = potential.DehnenBarPotential() dwp = potential.DehnenSmoothWrapperPotential(pot=dp) assert print(dwp) is None, "Printing a 3D wrapper potential fails" dp = potential.DehnenBarPotential().toPlanar() dwp = potential.DehnenSmoothWrapperPotential(pot=dp) assert print(dwp) is None, "Printing a 2D wrapper potential fails" return None def test_dissipative_ignoreInPotentialDensity2ndDerivs(): # Test that dissipative forces are ignored when they are included in lists # given to evaluatePotentials, evaluateDensities, and evaluate2ndDerivs lp = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9, b=0.8) cdfc = potential.ChandrasekharDynamicalFrictionForce( GMs=0.01, const_lnLambda=8.0, dens=lp, sigmar=lambda r: 1.0 / numpy.sqrt(2.0) ) R, z = 2.0, 0.4 assert ( numpy.fabs( potential.evaluatePotentials([lp, cdfc], R, z, phi=1.0) - potential.evaluatePotentials([lp, cdfc], R, z, phi=1.0) ) < 1e-10 ), "Dissipative forces not ignored in evaluatePotentials" assert ( numpy.fabs( potential.evaluateDensities([lp, cdfc], R, z, phi=1.0) - potential.evaluateDensities([lp, cdfc], R, z, phi=1.0) ) < 1e-10 ), "Dissipative forces not ignored in evaluateDensities" assert ( numpy.fabs( potential.evaluateR2derivs([lp, cdfc], R, z, phi=1.0) - potential.evaluateR2derivs([lp, cdfc], R, z, phi=1.0) ) < 1e-10 ), "Dissipative forces not ignored in evaluateR2derivs" assert ( numpy.fabs( potential.evaluatez2derivs([lp, cdfc], R, z, phi=1.0) - potential.evaluatez2derivs([lp, cdfc], R, z, phi=1.0) ) < 1e-10 ), "Dissipative forces not ignored in evaluatez2derivs" assert ( numpy.fabs( potential.evaluateRzderivs([lp, cdfc], R, z, phi=1.0) - potential.evaluateRzderivs([lp, cdfc], R, z, phi=1.0) ) < 1e-10 ), "Dissipative forces not ignored in evaluateRzderivs" assert ( numpy.fabs( potential.evaluatephi2derivs([lp, cdfc], R, z, phi=1.0) - potential.evaluatephi2derivs([lp, cdfc], R, z, phi=1.0) ) < 1e-10 ), "Dissipative forces not ignored in evaluatephi2derivs" assert ( numpy.fabs( potential.evaluateRphiderivs([lp, cdfc], R, z, phi=1.0) - potential.evaluateRphiderivs([lp, cdfc], R, z, phi=1.0) ) < 1e-10 ), "Dissipative forces not ignored in evaluateRphiderivs" assert ( numpy.fabs( potential.evaluatephizderivs([lp, cdfc], R, z, phi=1.0) - potential.evaluatephizderivs([lp, cdfc], R, z, phi=1.0) ) < 1e-10 ), "Dissipative forces not ignored in evaluatephizderivs" assert ( numpy.fabs( potential.evaluater2derivs([lp, cdfc], R, z, phi=1.0) - potential.evaluater2derivs([lp, cdfc], R, z, phi=1.0) ) < 1e-10 ), "Dissipative forces not ignored in evaluater2derivs" return None def test_dissipative_noVelocityError(): # Test that calling evaluateXforces for a dissipative potential # without including velocity produces an error lp = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9, b=0.8) cdfc = potential.ChandrasekharDynamicalFrictionForce( GMs=0.01, const_lnLambda=8.0, dens=lp, sigmar=lambda r: 1.0 / numpy.sqrt(2.0) ) R, z, phi = 2.0, 0.4, 1.1 with pytest.raises(potential.PotentialError) as excinfo: dummy = potential.evaluateRforces([lp, cdfc], R, z, phi=phi) with pytest.raises(potential.PotentialError) as excinfo: dummy = potential.evaluatephitorques([lp, cdfc], R, z, phi=phi) with pytest.raises(potential.PotentialError) as excinfo: dummy = potential.evaluatezforces([lp, cdfc], R, z, phi=phi) with pytest.raises(potential.PotentialError) as excinfo: dummy = potential.evaluaterforces([lp, cdfc], R, z, phi=phi) return None def test_dissipative_noVelocityError_2d(): # Test that calling evaluateXforces for a dissipative potential # without including velocity produces an error in 2D lp = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9, b=0.8).toPlanar() cdfc = potential.ChandrasekharDynamicalFrictionForce( GMs=0.01, const_lnLambda=8.0, dens=lp, sigmar=lambda r: 1.0 / numpy.sqrt(2.0) ).toPlanar() R, z, phi = 2.0, 0.4, 1.1 with pytest.raises(potential.PotentialError) as excinfo: dummy = potential.evaluateplanarRforces(lp + cdfc, R, phi=phi) with pytest.raises(potential.PotentialError) as excinfo: dummy = potential.evaluateplanarphitorques(lp + cdfc, R, phi=phi) return None def test_NonInertialFrameForce_2d(): lp = potential.LogarithmicHaloPotential(normalize=1.0) nip = potential.NonInertialFrameForce(Omega=0.5) # Total radial force on circular orbit at R=2. in the non-inertial frame should be zero assert ( numpy.fabs( potential.evaluateplanarRforces( potential.toPlanarPotential(lp + nip), 2.0, phi=0.0, v=[0.0, 0.0], ) ) < 1e-10 ), "Non-inertial frame force does not cancel radial force on circular orbit" # Also splitting them up assert ( numpy.fabs( potential.evaluateplanarRforces( potential.toPlanarPotential(lp), 2.0, phi=0.0 ) + potential.evaluateplanarRforces( potential.toPlanarPotential(nip), 2.0, phi=0.0, v=[0.0, 0.0], ) ) < 1e-10 ), "Non-inertial frame force does not cancel radial force on circular orbit" assert ( numpy.fabs( potential.evaluateplanarRforces( potential.toPlanarPotential(lp), 2.0, phi=0.0 ) + potential.toPlanarPotential(nip).Rforce( 2.0, phi=0.0, v=[0.0, 0.0], ) ) < 1e-10 ), "Non-inertial frame force does not cancel radial force on circular orbit" # also the total azimuthal force should be zero assert ( numpy.fabs( potential.evaluateplanarphitorques( potential.toPlanarPotential(lp + nip), 2.0, phi=0.0, v=[0.0, 0.0], ) ) < 1e-10 ), "Non-inertial frame force does not cancel phi torque on circular orbit" # Also splitting them up assert ( numpy.fabs( potential.evaluateplanarphitorques( potential.toPlanarPotential(lp), 2.0, phi=0.0 ) + potential.evaluateplanarphitorques( potential.toPlanarPotential(nip), 2.0, phi=0.0, v=[0.0, 0.0], ) ) < 1e-10 ), "Non-inertial frame force does not cancel phi torque on circular orbit" assert ( numpy.fabs( potential.evaluateplanarphitorques( potential.toPlanarPotential(lp), 2.0, phi=0.0 ) + potential.toPlanarPotential(nip).phitorque( 2.0, phi=0.0, v=[0.0, 0.0], ) ) < 1e-10 ), "Non-inertial frame force does not cancel phi torque on circular orbit" return None def test_RingPotential_correctPotentialIntegral(): # Test that the RingPotential's potential is correct, by comparing it to a # direct integral solution of the Poisson equation from scipy import integrate, special # Direct solution def pot(R, z, amp=1.0, a=0.75): return ( -amp * integrate.quad( lambda k: special.jv(0, k * R) * special.jv(0, k * a) * numpy.exp(-k * numpy.fabs(z)), 0.0, numpy.inf, )[0] ) rp = potential.RingPotential(amp=3.0, a=0.75) # Just check a bunch of (R,z)s; z=0 the direct integration doesn't work well, so we don't check that Rs, zs = [1.2, 1.2, 0.2, 0.2], [0.1, -1.1, -0.1, 1.1] for R, z in zip(Rs, zs): assert numpy.fabs(pot(R, z, amp=3.0) - rp(R, z)) < 1e-8, ( f"RingPotential potential evaluation does not agree with direct integration at (R,z) = ({R},{z})" ) return None def test_DehnenSmoothWrapper_decay(): # Test that DehnenSmoothWrapperPotential with decay=True is the opposite # of decay=False lp = potential.LogarithmicHaloPotential(normalize=1.0) pot_grow = potential.DehnenSmoothWrapperPotential(pot=lp, tform=4.0, tsteady=3.0) pot_decay = potential.DehnenSmoothWrapperPotential( pot=lp, tform=4.0, tsteady=3.0, decay=True ) ts = numpy.linspace(0.0, 10.0, 1001) assert ( numpy.amax( numpy.fabs( lp(2.0, 0.0, ts) - [pot_grow(2.0, 0.0, t=t) + pot_decay(2.0, 0.0, t=t) for t in ts] ) ) < 1e-10 ), ( "DehnenSmoothWrapper with decay=True is not the opposite of the same with decay=False" ) assert ( numpy.amax( numpy.fabs( lp.Rforce(2.0, 0.0, ts) - [ pot_grow.Rforce(2.0, 0.0, t=t) + pot_decay.Rforce(2.0, 0.0, t=t) for t in ts ] ) ) < 1e-10 ), ( "DehnenSmoothWrapper with decay=True is not the opposite of the same with decay=False" ) return None def test_AdiabaticContractionWrapper(): # Some basic tests of adiabatic contraction dm1 = AdiabaticContractionWrapperPotential( pot=potential.MWPotential2014[2], baryonpot=potential.MWPotential2014[:2], f_bar=None, method="cautun", ) dm2 = AdiabaticContractionWrapperPotential( pot=potential.MWPotential2014[2], baryonpot=potential.MWPotential2014[:2], f_bar=0.157, method="cautun", ) dm3 = AdiabaticContractionWrapperPotential( pot=potential.MWPotential2014[2], baryonpot=potential.MWPotential2014[:2], f_bar=0.157, method="blumenthal", ) dm4 = AdiabaticContractionWrapperPotential( pot=potential.MWPotential2014[2], baryonpot=potential.MWPotential2014[:2], f_bar=0.157, method="gnedin", ) # at large r, the contraction should be almost negligible (1% for Cautun) r = 50.0 assert ( numpy.fabs(dm1.vcirc(r) / potential.MWPotential2014[2].vcirc(r) - 1.02) < 1e-2 ), '"cautun" adiabatic contraction at large distances' assert ( numpy.fabs(dm2.vcirc(r) / potential.MWPotential2014[2].vcirc(r) - 0.97) < 1e-2 ), '"cautun" adiabatic contraction at large distances' assert ( numpy.fabs(dm3.vcirc(r) / potential.MWPotential2014[2].vcirc(r) - 0.98) < 1e-2 ), '"blumenthal" adiabatic contraction at large distances' assert ( numpy.fabs(dm4.vcirc(r) / potential.MWPotential2014[2].vcirc(r) - 0.98) < 1e-2 ), '"gnedin" adiabatic contraction at large distances' # For MWPotential2014, contraction at 1 kpc should be about 4 in mass for # Cautun (their Fig. 2; Mstar ~ 7e10 Msun) r = 1.0 / dm1._ro assert ( numpy.fabs(dm1.mass(r) / potential.MWPotential2014[2].mass(r) - 3.40) < 1e-2 ), '"cautun" adiabatic contraction does not agree at R ~ 1 kpc' assert ( numpy.fabs(dm2.mass(r) / potential.MWPotential2014[2].mass(r) - 3.18) < 1e-2 ), '"cautun" adiabatic contraction does not agree at R ~ 1 kpc' assert ( numpy.fabs(dm3.mass(r) / potential.MWPotential2014[2].mass(r) - 4.22) < 1e-2 ), '"blumenthal" adiabatic contraction does not agree at R ~ 1 kpc' assert ( numpy.fabs(dm4.mass(r) / potential.MWPotential2014[2].mass(r) - 4.04) < 1e-2 ), '"gnedin" adiabatic contraction does not agree at R ~ 1 kpc' # At 10 kpc, it should be more like 2 r = 10.0 / dm1._ro assert ( numpy.fabs(dm1.mass(r) / potential.MWPotential2014[2].mass(r) - 1.78) < 1e-2 ), '"cautun" adiabatic contraction does not agree at R ~ 10 kpc' assert ( numpy.fabs(dm2.mass(r) / potential.MWPotential2014[2].mass(r) - 1.64) < 1e-2 ), '"cautun" adiabatic contraction does not agree at R ~ 10 kpc' assert ( numpy.fabs(dm3.mass(r) / potential.MWPotential2014[2].mass(r) - 1.67) < 1e-2 ), '"blumenthal" adiabatic contraction does not agree at R ~ 10 kpc' assert ( numpy.fabs(dm4.mass(r) / potential.MWPotential2014[2].mass(r) - 1.43) < 1e-2 ), '"gnedin" adiabatic contraction does not agree at R ~ 10 kpc' return None def test_RotateAndTiltWrapper(): # some tests of the rotate and tilt wrapper zvec = numpy.array([numpy.sqrt(1 / 3.0), numpy.sqrt(1 / 3.0), numpy.sqrt(1 / 3.0)]) zvec /= numpy.sqrt(numpy.sum(zvec**2)) rot = _rotate_to_arbitrary_vector(numpy.array([[0.0, 0.0, 1.0]]), zvec, inv=True)[0] galaxy_pa = 0.3 pa_rot = numpy.array( [ [numpy.cos(galaxy_pa), numpy.sin(galaxy_pa), 0.0], [-numpy.sin(galaxy_pa), numpy.cos(galaxy_pa), 0.0], [0.0, 0.0, 1.0], ] ) rot = numpy.dot(pa_rot, rot) xyz_test = numpy.array([0.5, 0.5, 0.5]) Rphiz_test = coords.rect_to_cyl(xyz_test[0], xyz_test[1], xyz_test[2]) txyz_test = numpy.dot(rot, xyz_test) tRphiz_test = coords.rect_to_cyl(txyz_test[0], txyz_test[1], txyz_test[2]) testpot = potential.RotateAndTiltWrapperPotential( zvec=zvec, galaxy_pa=galaxy_pa, pot=potential.MWPotential2014 ) # test against the transformed potential and a MWPotential evaluated at the transformed coords assert ( evaluatePotentials(testpot, Rphiz_test[0], Rphiz_test[2], phi=Rphiz_test[1]) - evaluatePotentials( potential.MWPotential2014, tRphiz_test[0], tRphiz_test[2], phi=tRphiz_test[1], ) ) < 1e-6, ( "Evaluating potential at same relative position in a Rotated and tilted MWPotential2014 and non-Rotated does not give same result" ) # Also a triaxial NFW NFW_wrapped = potential.RotateAndTiltWrapperPotential( zvec=zvec, galaxy_pa=galaxy_pa, pot=potential.TriaxialNFWPotential(amp=1.0, b=0.7, c=0.5), ) NFW_rot = potential.TriaxialNFWPotential( amp=1.0, zvec=zvec, pa=galaxy_pa, b=0.7, c=0.5 ) assert ( evaluatePotentials(NFW_wrapped, Rphiz_test[0], Rphiz_test[2], phi=Rphiz_test[1]) - evaluatePotentials(NFW_rot, Rphiz_test[0], Rphiz_test[2], phi=Rphiz_test[1]) ) < 1e-6, ( "Wrapped and Internally rotated NFW potentials do not match when evaluated at the same point" ) # Try not specifying galaxy_pa, shouldn be =0 NFW_wrapped = potential.RotateAndTiltWrapperPotential( zvec=zvec, pot=potential.TriaxialNFWPotential(amp=1.0, b=0.7, c=0.5) ) NFW_rot = potential.TriaxialNFWPotential(amp=1.0, zvec=zvec, pa=0.0, b=0.7, c=0.5) assert ( evaluatePotentials(NFW_wrapped, Rphiz_test[0], Rphiz_test[2], phi=Rphiz_test[1]) - evaluatePotentials(NFW_rot, Rphiz_test[0], Rphiz_test[2], phi=Rphiz_test[1]) ) < 1e-6, ( "Wrapped and Internally rotated NFW potentials do not match when evaluated at the same point" ) # Try not specifying zvec, should be =[0,0,1] NFW_wrapped = potential.RotateAndTiltWrapperPotential( galaxy_pa=galaxy_pa, pot=potential.TriaxialNFWPotential(amp=1.0, b=0.7, c=0.5) ) NFW_rot = potential.TriaxialNFWPotential( amp=1.0, zvec=[0.0, 0.0, 1.0], pa=galaxy_pa, b=0.7, c=0.5 ) assert ( evaluatePotentials(NFW_wrapped, Rphiz_test[0], Rphiz_test[2], phi=Rphiz_test[1]) - evaluatePotentials(NFW_rot, Rphiz_test[0], Rphiz_test[2], phi=Rphiz_test[1]) ) < 1e-6, ( "Wrapped and Internally rotated NFW potentials do not match when evaluated at the same point" ) # make sure the offset works as intended # triaxial NFW at x,y,z = [20.,0.,3.] NFW_wrapped = potential.RotateAndTiltWrapperPotential( zvec=zvec, galaxy_pa=galaxy_pa, offset=[20.0, 0.0, 3.0], pot=potential.TriaxialNFWPotential(amp=1.0, b=0.7, c=0.5), ) NFW_rot = potential.TriaxialNFWPotential( amp=1.0, zvec=zvec, pa=galaxy_pa, b=0.7, c=0.5 ) assert ( evaluatePotentials(NFW_wrapped, 0.0, 0.0, phi=0.0) - evaluatePotentials(NFW_rot, 20.0, -3.0, phi=numpy.pi) ) < 1e-6, ( "Wrapped + Offset and Internally rotated NFW potentials do not match when evaluated at the same point" ) def test_RotateAndTiltWrapper_pa_inclination_rotation_matrix(): # Test that the formula for the rotation matrix given in the documentation agrees with the code def rotation_matrix_docs(galaxy_pa, inclination, sky_pa): return numpy.dot( numpy.array( [ [numpy.cos(galaxy_pa), numpy.sin(galaxy_pa), 0.0], [-numpy.sin(galaxy_pa), numpy.cos(galaxy_pa), 0.0], [0.0, 0.0, 1.0], ] ), numpy.dot( numpy.array( [ [1.0, 0.0, 0.0], [0.0, numpy.cos(inclination), numpy.sin(inclination)], [0.0, -numpy.sin(inclination), numpy.cos(inclination)], ] ), numpy.array( [ [numpy.sin(sky_pa), -numpy.cos(sky_pa), 0.0], [numpy.cos(sky_pa), numpy.sin(sky_pa), 0.0], [0.0, 0.0, 1], ] ), ), ) galaxy_pa, inclination, sky_pa = 0.3, -0.2, 0.1 rtwp = potential.RotateAndTiltWrapperPotential( pot=potential.MWPotential2014, galaxy_pa=galaxy_pa, inclination=inclination, sky_pa=sky_pa, ) assert numpy.all( numpy.fabs(rtwp._rot - rotation_matrix_docs(galaxy_pa, inclination, sky_pa)) < 1e-10 ), ( "Rotation matrix in RotateAndTiltWrapperPotential does not agree with the formula in the documentation" ) galaxy_pa, inclination, sky_pa = -0.3, 1.2, 2.1 rtwp = potential.RotateAndTiltWrapperPotential( pot=potential.MWPotential2014, galaxy_pa=galaxy_pa, inclination=inclination, sky_pa=sky_pa, ) assert numpy.all( numpy.fabs(rtwp._rot - rotation_matrix_docs(galaxy_pa, inclination, sky_pa)) < 1e-10 ), ( "Rotation matrix in RotateAndTiltWrapperPotential does not agree with the formula in the documentation" ) return None def test_integration_RotateAndTiltWrapper(): ## test a quick orbit integration to hit the C code (also test pure python) # two potentials, one offset offset = [3.0, 2.0, 1.0] mwpot = potential.MWPotential2014 mwpot_wrapped = potential.RotateAndTiltWrapperPotential( pot=potential.MWPotential2014, offset=offset ) # initialise orbit ro = 8.0 orb = orbit.Orbit(ro=ro) # another, offset by the same as the potential init = orb.vxvv[0] R, vR, vT, z, vz, phi = init x, y, z = coords.cyl_to_rect(R, phi, z) vx, vy, vz = coords.cyl_to_rect_vec(vR, vT, vz, phi) tx, ty, tz = x - offset[0], y - offset[1], z - offset[2] tR, tphi, tz = coords.rect_to_cyl(tx, ty, tz) tvR, tvT, tvz = coords.rect_to_cyl_vec(vx, vy, vz, tR, tphi, tz, cyl=True) orb_t = orbit.Orbit([tR, tvR, tvT, tz, tvz, tphi], ro=ro) # integrate ts = numpy.linspace(0.0, 1.0, 1000) orb.integrate(ts, pot=mwpot, method="dop853") orb_t.integrate(ts, pot=mwpot_wrapped, method="dop853") # translate other orbit to match first one: orb_vxvv = orb_t.getOrbit() R, vR, vT, z, vz, phi = ( orb_vxvv[:, 0], orb_vxvv[:, 1], orb_vxvv[:, 2], orb_vxvv[:, 3], orb_vxvv[:, 4], orb_vxvv[:, 5], ) x, y, z = coords.cyl_to_rect(R, phi, z) vx, vy, vz = coords.cyl_to_rect_vec(vR, vT, vz, phi) tx, ty, tz = x + offset[0], y + offset[1], z + offset[2] tR, tphi, tz = coords.rect_to_cyl(tx, ty, tz) # check equal Rphi = numpy.dstack([orb.R(ts), orb.z(ts)])[0] Rphi_t = numpy.dstack([tR * ro, tz * ro])[0] assert numpy.all(numpy.fabs(Rphi - Rphi_t) < 10.0**-10), ( "Pure python orbit integration in an offset potential does not work as expected" ) # reinitialise orbits, just to be sure orb = orbit.Orbit(ro=ro) init = orb.vxvv[0] R, vR, vT, z, vz, phi = init offset = [3.0, 2.0, 1.0] x, y, z = coords.cyl_to_rect(R, phi, z) vx, vy, vz = coords.cyl_to_rect_vec(vR, vT, vz, phi) tx, ty, tz = x - offset[0], y - offset[1], z - offset[2] tR, tphi, tz = coords.rect_to_cyl(tx, ty, tz) tvR, tvT, tvz = coords.rect_to_cyl_vec(vx, vy, vz, tR, tphi, tz, cyl=True) orb_t = orbit.Orbit([tR, tvR, tvT, tz, tvz, tphi], ro=ro) # integrate, use C orb.integrate(ts, pot=mwpot, method="dop853_c") orb_t.integrate(ts, pot=mwpot_wrapped, method="dop853_c") orb_vxvv = orb_t.getOrbit() R, vR, vT, z, vz, phi = ( orb_vxvv[:, 0], orb_vxvv[:, 1], orb_vxvv[:, 2], orb_vxvv[:, 3], orb_vxvv[:, 4], orb_vxvv[:, 5], ) x, y, z = coords.cyl_to_rect(R, phi, z) vx, vy, vz = coords.cyl_to_rect_vec(vR, vT, vz, phi) tx, ty, tz = x + offset[0], y + offset[1], z + offset[2] tR, tphi, tz = coords.rect_to_cyl(tx, ty, tz) # check equal Rphi = numpy.dstack([orb.R(ts), orb.z(ts)])[0] Rphi_t = numpy.dstack([tR * ro, tz * ro])[0] assert numpy.all(numpy.fabs(Rphi - Rphi_t) < 10.0**-10), ( "C orbit integration in an offset potential does not work as expected" ) return None def test_vtermnegl_issue314(): # Test related to issue 314: vterm for negative l rp = potential.RazorThinExponentialDiskPotential(normalize=1.0, hr=3.0 / 8.0) assert numpy.fabs(rp.vterm(0.5) + rp.vterm(-0.5)) < 10.0**-8.0, ( "vterm for negative l does not behave as expected" ) return None def test_Ferrers_Rzderiv_issue319(): # Test that the Rz derivative works for the FerrersPotential (issue 319) fp = potential.FerrersPotential(normalize=1.0) from test_SpiralArmsPotential import deriv as derivative rzderiv = fp.Rzderiv(0.5, 0.2, phi=1.0) rzderiv_finitediff = derivative( lambda x: -fp.zforce(x, 0.2, phi=1.0), 0.5, dx=10.0**-8.0 ) assert numpy.fabs(rzderiv - rzderiv_finitediff) < 10.0**-7.0, ( "Rzderiv for FerrersPotential does not agree with finite-difference calculation" ) return None def test_rtide(): # Test that rtide is being calculated properly in select potentials lp = potential.LogarithmicHaloPotential() assert abs(1.0 - lp.rtide(1.0, 0.0, M=1.0) / 0.793700525984) < 10.0**-12.0, ( "Calculation of rtide in logarithmic potential fails" ) pmass = potential.PlummerPotential(b=0.0) assert abs(1.0 - pmass.rtide(1.0, 0.0, M=1.0) / 0.693361274351) < 10.0**-12.0, ( "Calculation of rtide in point-mass potential fails" ) # Also test function interface assert ( abs(1.0 - potential.rtide([lp], 1.0, 0.0, M=1.0) / 0.793700525984) < 10.0**-12.0 ), "Calculation of rtide in logarithmic potential fails" pmass = potential.PlummerPotential(b=0.0) assert ( abs(1.0 - potential.rtide([pmass], 1.0, 0.0, M=1.0) / 0.693361274351) < 10.0**-12.0 ), "Calculation of rtide in point-mass potential fails" return None def test_rtide_noMError(): # Test the running rtide without M= input raises error lp = potential.LogarithmicHaloPotential() with pytest.raises(potential.PotentialError) as excinfo: dummy = lp.rtide(1.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: dummy = potential.rtide([lp], 1.0, 0.0) return None def test_ttensor(): pmass = potential.KeplerPotential(normalize=1.0) tij = pmass.ttensor(1.0, 0.0, 0.0) # Full tidal tensor here should be diag(2,-1,-1) assert numpy.all(numpy.fabs(tij - numpy.diag([2, -1, -1])) < 1e-10), ( "Calculation of tidal tensor in point-mass potential fails" ) # Also test eigenvalues tij = pmass.ttensor(1.0, 0.0, 0.0, eigenval=True) assert numpy.all(numpy.fabs(tij - numpy.array([2, -1, -1])) < 1e-10), ( "Calculation of tidal tensor in point-mass potential fails" ) # Also test function interface tij = potential.ttensor([pmass], 1.0, 0.0, 0.0) # Full tidal tensor here should be diag(2,-1,-1) assert numpy.all(numpy.fabs(tij - numpy.diag([2, -1, -1])) < 1e-10), ( "Calculation of tidal tensor in point-mass potential fails" ) # Also test eigenvalues tij = potential.ttensor([pmass], 1.0, 0.0, 0.0, eigenval=True) assert numpy.all(numpy.fabs(tij - numpy.array([2, -1, -1])) < 1e-10), ( "Calculation of tidal tensor in point-mass potential fails" ) # Also Test symmetry when y!=0 and z!=0 tij = potential.ttensor([pmass], 1.0, 1.0, 1.0) assert numpy.all(numpy.fabs(tij[0][1] - tij[1][0]) < 1e-10), ( "Calculation of tidal tensor in point-mass potential fails" ) assert numpy.all(numpy.fabs(tij[0][2] - tij[2][0]) < 1e-10), ( "Calculation of tidal tensor in point-mass potential fails" ) assert numpy.all(numpy.fabs(tij[1][2] - tij[2][1]) < 1e-10), ( "Calculation of tidal tensor in point-mass potential fails" ) return None def test_ttensor_trace(): # Test that the trace of the tidal tensor == -4piG density for a bunch of # potentials pots = [ potential.KeplerPotential(normalize=1.0), potential.LogarithmicHaloPotential(normalize=3.0, q=0.8), potential.MiyamotoNagaiPotential(normalize=0.5, a=3.0, b=0.5), ] R, z, phi = 1.3, -0.2, 2.0 for pot in pots: assert ( numpy.fabs( numpy.trace(pot.ttensor(R, z, phi=phi)) + 4.0 * numpy.pi * pot.dens(R, z, phi=phi) ) < 1e-10 ), "Trace of the tidal tensor not equal 4piG density" # Also test a list assert ( numpy.fabs( numpy.trace(potential.ttensor(potential.MWPotential2014, R, z, phi=phi)) + 4.0 * numpy.pi * potential.evaluateDensities(potential.MWPotential2014, R, z, phi=phi) ) < 1e-10 ), "Trace of the tidal tensor not equal 4piG density" return None def test_ttensor_nonaxi(): # Test that computing the tidal tensor for a non-axi potential raises error lp = potential.LogarithmicHaloPotential(normalize=1.0, b=0.8, q=0.7) with pytest.raises(potential.PotentialError) as excinfo: dummy = lp.ttensor(1.0, 0.0, 0.0) with pytest.raises(potential.PotentialError) as excinfo: dummy = potential.ttensor(lp, 1.0, 0.0, 0.0) return None # Test that zvc_range returns the range over which the zvc is defined for a # given E,Lz def test_zvc_range(): E, Lz = -1.25, 0.6 Rmin, Rmax = potential.zvc_range(potential.MWPotential2014, E, Lz) assert ( numpy.fabs( potential.evaluatePotentials(potential.MWPotential2014, Rmin, 0.0) + Lz**2.0 / 2.0 / Rmin**2.0 - E ) < 1e-8 ), "zvc_range does not return radius at which Phi_eff(R,0) = E" assert ( numpy.fabs( potential.evaluatePotentials(potential.MWPotential2014, Rmax, 0.0) + Lz**2.0 / 2.0 / Rmax**2.0 - E ) < 1e-8 ), "zvc_range does not return radius at which Phi_eff(R,0) = E" R_a_little_less = Rmin - 1e-4 assert ( potential.evaluatePotentials(potential.MWPotential2014, R_a_little_less, 0.0) + Lz**2.0 / 2.0 / R_a_little_less**2.0 > E ), "zvc_range does not give the minimum R for which Phi_eff(R,0) < E" R_a_little_more = Rmax + 1e-4 assert ( potential.evaluatePotentials(potential.MWPotential2014, R_a_little_more, 0.0) + Lz**2.0 / 2.0 / R_a_little_more**2.0 > E ), "zvc_range does not give the maximum R for which Phi_eff(R,0) < E" # Another one for good measure E, Lz = -2.25, 0.2 Rmin, Rmax = potential.zvc_range(potential.MWPotential2014, E, Lz) assert ( numpy.fabs( potential.evaluatePotentials(potential.MWPotential2014, Rmin, 0.0) + Lz**2.0 / 2.0 / Rmin**2.0 - E ) < 1e-8 ), "zvc_range does not return radius at which Phi_eff(R,0) = E" assert ( numpy.fabs( potential.evaluatePotentials(potential.MWPotential2014, Rmax, 0.0) + Lz**2.0 / 2.0 / Rmax**2.0 - E ) < 1e-8 ), "zvc_range does not return radius at which Phi_eff(R,0) = E" R_a_little_less = Rmin - 1e-4 assert ( potential.evaluatePotentials(potential.MWPotential2014, R_a_little_less, 0.0) + Lz**2.0 / 2.0 / R_a_little_less**2.0 > E ), "zvc_range does not give the minimum R for which Phi_eff(R,0) < E" R_a_little_more = Rmax + 1e-4 assert ( potential.evaluatePotentials(potential.MWPotential2014, R_a_little_more, 0.0) + Lz**2.0 / 2.0 / R_a_little_more**2.0 > E ), "zvc_range does not give the maximum R for which Phi_eff(R,0) < E" # Also one for a single potential pot = potential.PlummerPotential(normalize=True) E, Lz = -1.9, 0.2 Rmin, Rmax = pot.zvc_range(E, Lz) assert ( numpy.fabs( potential.evaluatePotentials(pot, Rmin, 0.0) + Lz**2.0 / 2.0 / Rmin**2.0 - E ) < 1e-8 ), "zvc_range does not return radius at which Phi_eff(R,0) = E" assert ( numpy.fabs( potential.evaluatePotentials(pot, Rmax, 0.0) + Lz**2.0 / 2.0 / Rmax**2.0 - E ) < 1e-8 ), "zvc_range does not return radius at which Phi_eff(R,0) = E" R_a_little_less = Rmin - 1e-4 assert ( potential.evaluatePotentials(pot, R_a_little_less, 0.0) + Lz**2.0 / 2.0 / R_a_little_less**2.0 > E ), "zvc_range does not give the minimum R for which Phi_eff(R,0) < E" R_a_little_more = Rmax + 1e-4 assert ( potential.evaluatePotentials(pot, R_a_little_more, 0.0) + Lz**2.0 / 2.0 / R_a_little_more**2.0 > E ), "zvc_range does not give the maximum R for which Phi_eff(R,0) < E" return None # Test that we get [NaN,NaN] when there are no orbits for this combination of E and Lz def test_zvc_range_undefined(): # Set up circular orbit at Rc, then ask for Lz > Lzmax(E) Rc = 0.6653 E = ( potential.evaluatePotentials(potential.MWPotential2014, Rc, 0.0) + potential.vcirc(potential.MWPotential2014, Rc) ** 2.0 / 2.0 ) Lzmax = Rc * potential.vcirc(potential.MWPotential2014, Rc) assert numpy.all( numpy.isnan(potential.zvc_range(potential.MWPotential2014, E, Lzmax + 1e-4)) ), ( "zvc_range does not return [NaN,NaN] when no orbits exist at this combination of (E,Lz)" ) return None def test_zvc_at_rminmax(): E, Lz = -1.25, 0.6 Rmin, Rmax = potential.zvc_range(potential.MWPotential2014, E, Lz) assert numpy.fabs(potential.zvc(potential.MWPotential2014, Rmin, E, Lz)) < 1e-8, ( "zvc at minimum from zvc_range is not at zero height" ) assert numpy.fabs(potential.zvc(potential.MWPotential2014, Rmax, E, Lz)) < 1e-8, ( "zvc at maximum from zvc_range is not at zero height" ) # Another one for good measure E, Lz = -2.25, 0.2 Rmin, Rmax = potential.zvc_range(potential.MWPotential2014, E, Lz) assert numpy.fabs(potential.zvc(potential.MWPotential2014, Rmin, E, Lz)) < 1e-8, ( "zvc at minimum from zvc_range is not at zero height" ) assert numpy.fabs(potential.zvc(potential.MWPotential2014, Rmax, E, Lz)) < 1e-8, ( "zvc at maximum from zvc_range is not at zero height" ) # Also for a single potential pot = potential.PlummerPotential(normalize=True) E, Lz = -1.9, 0.2 Rmin, Rmax = pot.zvc_range(E, Lz) assert numpy.fabs(pot.zvc(Rmin, E, Lz)) < 1e-8, ( "zvc at minimum from zvc_range is not at zero height" ) assert numpy.fabs(pot.zvc(Rmax, E, Lz)) < 1e-8, ( "zvc at maximum from zvc_range is not at zero height" ) return None def test_zvc(): E, Lz = -1.25, 0.6 Rmin, Rmax = potential.zvc_range(potential.MWPotential2014, E, Lz) Rtrial = 0.5 * (Rmin + Rmax) ztrial = potential.zvc(potential.MWPotential2014, Rtrial, E, Lz) assert ( numpy.fabs( potential.evaluatePotentials(potential.MWPotential2014, Rtrial, ztrial) + Lz**2.0 / 2.0 / Rtrial**2.0 - E ) < 1e-8 ), "zvc does not return the height at which Phi_eff(R,z) = E" Rtrial = Rmin + 0.25 * (Rmax - Rmin) ztrial = potential.zvc(potential.MWPotential2014, Rtrial, E, Lz) assert ( numpy.fabs( potential.evaluatePotentials(potential.MWPotential2014, Rtrial, ztrial) + Lz**2.0 / 2.0 / Rtrial**2.0 - E ) < 1e-8 ), "zvc does not return the height at which Phi_eff(R,z) = E" Rtrial = Rmin + 0.75 * (Rmax - Rmin) ztrial = potential.zvc(potential.MWPotential2014, Rtrial, E, Lz) assert ( numpy.fabs( potential.evaluatePotentials(potential.MWPotential2014, Rtrial, ztrial) + Lz**2.0 / 2.0 / Rtrial**2.0 - E ) < 1e-8 ), "zvc does not return the height at which Phi_eff(R,z) = E" # Another one for good measure E, Lz = -2.25, 0.2 Rmin, Rmax = potential.zvc_range(potential.MWPotential2014, E, Lz) Rtrial = 0.5 * (Rmin + Rmax) ztrial = potential.zvc(potential.MWPotential2014, Rtrial, E, Lz) assert ( numpy.fabs( potential.evaluatePotentials(potential.MWPotential2014, Rtrial, ztrial) + Lz**2.0 / 2.0 / Rtrial**2.0 - E ) < 1e-8 ), "zvc does not return the height at which Phi_eff(R,z) = E" Rtrial = Rmin + 0.25 * (Rmax - Rmin) ztrial = potential.zvc(potential.MWPotential2014, Rtrial, E, Lz) assert ( numpy.fabs( potential.evaluatePotentials(potential.MWPotential2014, Rtrial, ztrial) + Lz**2.0 / 2.0 / Rtrial**2.0 - E ) < 1e-8 ), "zvc does not return the height at which Phi_eff(R,z) = E" Rtrial = Rmin + 0.75 * (Rmax - Rmin) ztrial = potential.zvc(potential.MWPotential2014, Rtrial, E, Lz) assert ( numpy.fabs( potential.evaluatePotentials(potential.MWPotential2014, Rtrial, ztrial) + Lz**2.0 / 2.0 / Rtrial**2.0 - E ) < 1e-8 ), "zvc does not return the height at which Phi_eff(R,z) = E" # Also for a single potential pot = potential.PlummerPotential(normalize=True) E, Lz = -1.9, 0.2 Rmin, Rmax = pot.zvc_range(E, Lz) Rtrial = 0.5 * (Rmin + Rmax) ztrial = pot.zvc(Rtrial, E, Lz) assert ( numpy.fabs( potential.evaluatePotentials(pot, Rtrial, ztrial) + Lz**2.0 / 2.0 / Rtrial**2.0 - E ) < 1e-8 ), "zvc does not return the height at which Phi_eff(R,z) = E" Rtrial = Rmin + 0.25 * (Rmax - Rmin) ztrial = pot.zvc(Rtrial, E, Lz) assert ( numpy.fabs( potential.evaluatePotentials(pot, Rtrial, ztrial) + Lz**2.0 / 2.0 / Rtrial**2.0 - E ) < 1e-8 ), "zvc does not return the height at which Phi_eff(R,z) = E" Rtrial = Rmin + 0.75 * (Rmax - Rmin) ztrial = pot.zvc(Rtrial, E, Lz) assert ( numpy.fabs( potential.evaluatePotentials(pot, Rtrial, ztrial) + Lz**2.0 / 2.0 / Rtrial**2.0 - E ) < 1e-8 ), "zvc does not return the height at which Phi_eff(R,z) = E" return None # Test that zvc outside of zvc_range is NaN def test_zvc_undefined(): E, Lz = -1.25, 0.6 Rmin, Rmax = potential.zvc_range(potential.MWPotential2014, E, Lz) assert numpy.isnan(potential.zvc(potential.MWPotential2014, Rmin - 1e-4, E, Lz)), ( "zvc at R < Rmin is not NaN" ) assert numpy.isnan(potential.zvc(potential.MWPotential2014, Rmax + 1e-4, E, Lz)), ( "zvc at R > Rmax is not NaN" ) # Another one for good measure E, Lz = -2.25, 0.2 Rmin, Rmax = potential.zvc_range(potential.MWPotential2014, E, Lz) assert numpy.isnan(potential.zvc(potential.MWPotential2014, Rmin - 1e-4, E, Lz)), ( "zvc at R < Rmin is not NaN" ) assert numpy.isnan(potential.zvc(potential.MWPotential2014, Rmax + 1e-4, E, Lz)), ( "zvc at R > Rmax is not NaN" ) return None # Check that we get the correct ValueError if no solution can be found def test_zvc_valueerror(): E, Lz = -1.25 + 100, 0.6 with pytest.raises(ValueError) as excinfo: potential.zvc(potential.MWPotential2014, 0.7, E + 100, Lz) return None def test_rhalf(): # Test some known cases a = numpy.pi # Hernquist, r12= (1+sqrt(2))a hp = potential.HernquistPotential(amp=1.0, a=a) assert numpy.fabs(hp.rhalf() - (1.0 + numpy.sqrt(2.0)) * a) < 1e-10, ( "Half-mass radius of the Hernquist potential incorrect" ) # DehnenSpherical, r12= a/(2^(1/(3-alpha)-1) alpha = 1.34 hp = potential.DehnenSphericalPotential(amp=1.0, a=a, alpha=alpha) assert numpy.fabs(hp.rhalf() - a / (2 ** (1.0 / (3.0 - alpha)) - 1.0)) < 1e-10, ( "Half-mass radius of the DehnenSpherical potential incorrect" ) # Plummer, r12= b/sqrt(1/0.5^(2/3)-1) pp = potential.PlummerPotential(amp=1.0, b=a) assert ( numpy.fabs(potential.rhalf(pp) - a / numpy.sqrt(0.5 ** (-2.0 / 3.0) - 1.0)) < 1e-10 ), "Half-mass radius of the Plummer potential incorrect" return None def test_tdyn(): # Spherical: tdyn = 2piR/vc a = numpy.pi # Hernquist hp = potential.HernquistPotential(amp=1.0, a=a) R = 1.4 assert numpy.fabs(hp.tdyn(R) - 2.0 * numpy.pi * R / hp.vcirc(R)) < 1e-10, ( "Dynamical time of the Hernquist potential incorrect" ) # DehnenSpherical alpha = 1.34 hp = potential.DehnenSphericalPotential(amp=1.0, a=a, alpha=alpha) assert ( numpy.fabs(potential.tdyn(hp, R) - 2.0 * numpy.pi * R / hp.vcirc(R)) < 1e-10 ), "Dynamical time of the DehnenSpherical potential incorrect" # Axi, this approx. holds hp = potential.MiyamotoNagaiPotential(amp=1.0, a=a, b=a / 5.0) R = 3.4 assert numpy.fabs(hp.tdyn(R) / (2.0 * numpy.pi * R / hp.vcirc(R)) - 1.0) < 0.03, ( "Dynamical time of the Miyamoto-Nagai potential incorrect" ) return None def test_NumericalPotentialDerivativesMixin(): # Test that the NumericalPotentialDerivativesMixin works as expected def get_mixin_first_instance(cls, *args, **kwargs): # Function to return instance of a class for Potential cls where # the NumericalPotentialDerivativesMixin comes first, so all derivs # are numerical (should otherwise always be used second!) class NumericalPot(potential.NumericalPotentialDerivativesMixin, cls): def __init__(self, *args, **kwargs): potential.NumericalPotentialDerivativesMixin.__init__(self, kwargs) cls.__init__(self, *args, **kwargs) return NumericalPot(*args, **kwargs) # Function to check all numerical derivatives def check_numerical_derivs(Pot, NumPot, tol=1e-6, tol2=1e-5): # tol: tolerance for forces, tol2: tolerance for 2nd derivatives # Check wide range of R,z,phi Rs = numpy.array([0.5, 1.0, 2.0]) Zs = numpy.array([0.0, 0.125, -0.125, 0.25, -0.25]) phis = numpy.array( [0.0, 0.5, -0.5, 1.0, -1.0, numpy.pi, 0.5 + numpy.pi, 1.0 + numpy.pi] ) for ii in range(len(Rs)): for jj in range(len(Zs)): for kk in range(len(phis)): # Forces assert ( numpy.fabs( ( Pot.Rforce(Rs[ii], Zs[jj], phi=phis[kk]) - NumPot.Rforce(Rs[ii], Zs[jj], phi=phis[kk]) ) / Pot.Rforce(Rs[ii], Zs[jj], phi=phis[kk]) ) < tol ), ( f"NumericalPotentialDerivativesMixin applied to {Pot.__class__.__name__} Potential does not give the correct Rforce" ) assert ( numpy.fabs( ( Pot.zforce(Rs[ii], Zs[jj], phi=phis[kk]) - NumPot.zforce(Rs[ii], Zs[jj], phi=phis[kk]) ) / Pot.zforce(Rs[ii], Zs[jj], phi=phis[kk]) ** (Zs[jj] > 0.0) ) < tol ), ( f"NumericalPotentialDerivativesMixin applied to {Pot.__class__.__name__} Potential does not give the correct zforce" ) assert ( numpy.fabs( ( Pot.phitorque(Rs[ii], Zs[jj], phi=phis[kk]) - NumPot.phitorque(Rs[ii], Zs[jj], phi=phis[kk]) ) / Pot.phitorque(Rs[ii], Zs[jj], phi=phis[kk]) ** Pot.isNonAxi ) < tol ), ( f"NumericalPotentialDerivativesMixin applied to {Pot.__class__.__name__} Potential does not give the correct phitorque" ) # Second derivatives assert ( numpy.fabs( ( Pot.R2deriv(Rs[ii], Zs[jj], phi=phis[kk]) - NumPot.R2deriv(Rs[ii], Zs[jj], phi=phis[kk]) ) / Pot.R2deriv(Rs[ii], Zs[jj], phi=phis[kk]) ) < tol2 ), ( f"NumericalPotentialDerivativesMixin applied to {Pot.__class__.__name__} Potential does not give the correct R2deriv" ) assert ( numpy.fabs( ( Pot.z2deriv(Rs[ii], Zs[jj], phi=phis[kk]) - NumPot.z2deriv(Rs[ii], Zs[jj], phi=phis[kk]) ) / Pot.z2deriv(Rs[ii], Zs[jj], phi=phis[kk]) ) < tol2 ), ( f"NumericalPotentialDerivativesMixin applied to {Pot.__class__.__name__} Potential does not give the correct z2deriv" ) assert ( numpy.fabs( ( Pot.phi2deriv(Rs[ii], Zs[jj], phi=phis[kk]) - NumPot.phi2deriv(Rs[ii], Zs[jj], phi=phis[kk]) ) / Pot.phi2deriv(Rs[ii], Zs[jj], phi=phis[kk]) ** Pot.isNonAxi ) < tol2 ), ( f"NumericalPotentialDerivativesMixin applied to {Pot.__class__.__name__} Potential does not give the correct phi2deriv" ) assert ( numpy.fabs( ( Pot.Rzderiv(Rs[ii], Zs[jj], phi=phis[kk]) - NumPot.Rzderiv(Rs[ii], Zs[jj], phi=phis[kk]) ) / Pot.Rzderiv(Rs[ii], Zs[jj], phi=phis[kk]) ** (Zs[jj] > 0.0) ) < tol2 ), ( f"NumericalPotentialDerivativesMixin applied to {Pot.__class__.__name__} Potential does not give the correct Rzderiv" ) assert ( numpy.fabs( ( Pot.Rphideriv(Rs[ii], Zs[jj], phi=phis[kk]) - NumPot.Rphideriv(Rs[ii], Zs[jj], phi=phis[kk]) ) / Pot.Rphideriv(Rs[ii], Zs[jj], phi=phis[kk]) ** Pot.isNonAxi ) < tol2 ), ( f"NumericalPotentialDerivativesMixin applied to {Pot.__class__.__name__} Potential does not give the correct Rphideriv" ) assert ( numpy.fabs( ( Pot.phizderiv(Rs[ii], Zs[jj], phi=phis[kk]) - NumPot.phizderiv(Rs[ii], Zs[jj], phi=phis[kk]) ) / Pot.phizderiv(Rs[ii], Zs[jj], phi=phis[kk]) ** (Pot.isNonAxi * (Zs[jj] != 0.0)) ) < tol2 ), ( f"NumericalPotentialDerivativesMixin applied to {Pot.__class__.__name__} Potential does not give the correct phizderiv" ) return None # Now check some potentials # potential.MiyamotoNagaiPotential mp = potential.MiyamotoNagaiPotential(amp=1.0, a=0.5, b=0.05) num_mp = get_mixin_first_instance( potential.MiyamotoNagaiPotential, amp=1.0, a=0.5, b=0.05 ) check_numerical_derivs(mp, num_mp) # potential.DehnenBarPotential dp = potential.DehnenBarPotential() num_dp = get_mixin_first_instance(potential.DehnenBarPotential) check_numerical_derivs(dp, num_dp) return None # Test that we don't get the "FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated" numpy warning for the SCF potential; issue #347 def test_scf_tupleindexwarning(): import warnings with warnings.catch_warnings(record=True): warnings.simplefilter("error", FutureWarning) p = mockSCFZeeuwPotential() p.Rforce(1.0, 0.0) # another one reported by Nil, now problem is with array input with warnings.catch_warnings(record=True): warnings.simplefilter("error", FutureWarning) p = mockSCFZeeuwPotential() p.Rforce(numpy.atleast_1d(1.0), numpy.atleast_1d(0.0)) return None # Test that conversion between xi and R works as expected def test_scf_xiToR(): from galpy.potential.SCFPotential import _RToxi, _xiToR a = numpy.pi r = 1.4 assert numpy.fabs(_xiToR(_RToxi(r, a=a), a=a) - r) < 1e-10, ( "_RToxi and _xiToR aren't each other's inverse in r <-> xi conversion used in SCF potential" ) xi = 1.3 assert numpy.fabs(_RToxi(_xiToR(xi, a=a), a=a) - xi) < 1e-10, ( "_RToxi and _xiToR aren't each other's inverse in r <-> xi conversion used in SCF potential" ) # Also for arrays r = numpy.linspace(0.1, 5.3, 21) assert numpy.all(numpy.fabs(_xiToR(_RToxi(r, a=a), a=a) - r) < 1e-10), ( "_RToxi and _xiToR aren't each other's inverse in r <-> xi conversion used in SCF potential" ) xi = numpy.linspace(-0.9, 0.9, 21) assert numpy.all(numpy.fabs(_RToxi(_xiToR(xi, a=a), a=a) - xi) < 1e-10), ( "_RToxi and _xiToR aren't each other's inverse in r <-> xi conversion used in SCF potential" ) # Check 0 and inf r = 0 assert numpy.fabs(_RToxi(r, a=a) + 1) < 1e-10, ( "_RToxi and _xiToR aren't each other's inverse in r <-> xi conversion used in SCF potential" ) xi = -1.0 assert numpy.fabs(_xiToR(xi, a=a)) < 1e-10, ( "_RToxi and _xiToR aren't each other's inverse in r <-> xi conversion used in SCF potential" ) r = numpy.inf assert numpy.fabs(_RToxi(r, a=a) - 1) < 1e-10, ( "_RToxi and _xiToR aren't each other's inverse in r <-> xi conversion used in SCF potential" ) xi = 1.0 assert numpy.isinf(_xiToR(xi, a=a)), ( "_RToxi and _xiToR aren't each other's inverse in r <-> xi conversion used in SCF potential" ) # Also for arrays with zero and inf r = numpy.concatenate((numpy.linspace(0.0, 5.3, 21), [numpy.inf])) assert numpy.all(numpy.fabs(_xiToR(_RToxi(r, a=a), a=a)[:-1] - r[:-1]) < 1e-10), ( "_RToxi and _xiToR aren't each other's inverse in r <-> xi conversion used in SCF potential" ) assert numpy.fabs(_RToxi(r, a=a)[-1] - 1.0) < 1e-10, ( "_RToxi and _xiToR aren't each other's inverse in r <-> xi conversion used in SCF potential" ) xi = numpy.linspace(-1, 1, 21) assert numpy.all(numpy.fabs(_RToxi(_xiToR(xi, a=a), a=a) - xi) < 1e-10), ( "_RToxi and _xiToR aren't each other's inverse in r <-> xi conversion used in SCF potential" ) return None # Test that attempting to multiply or divide a potential by something other than a number raises an error def test_mult_divide_error(): # 3D pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9) with pytest.raises(TypeError) as excinfo: pot * [1.0, 2.0] with pytest.raises(TypeError) as excinfo: [1.0, 2.0] * pot with pytest.raises(TypeError) as excinfo: pot / [1.0, 2.0] # 2D pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() with pytest.raises(TypeError) as excinfo: pot * [1.0, 2.0] with pytest.raises(TypeError) as excinfo: [1.0, 2.0] * pot with pytest.raises(TypeError) as excinfo: pot / [1.0, 2.0] # 1D pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toVertical(1.1) with pytest.raises(TypeError) as excinfo: pot * [1.0, 2.0] with pytest.raises(TypeError) as excinfo: [1.0, 2.0] * pot with pytest.raises(TypeError) as excinfo: pot / [1.0, 2.0] return None # Test that arithmetically adding potentials returns lists of potentials def test_add_potentials(): assert ( potential.MWPotential2014 == potential.MWPotential2014[0] + potential.MWPotential2014[1] + potential.MWPotential2014[2] ), ( "Potential addition of components of MWPotential2014 does not give MWPotential2014" ) # 3D pot1 = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9) pot2 = potential.MiyamotoNagaiPotential(normalize=0.2, a=0.4, b=0.1) pot3 = potential.HernquistPotential(normalize=0.4, a=0.1) assert pot1 + pot2 == [pot1, pot2] assert pot1 + pot2 + pot3 == [pot1, pot2, pot3] assert (pot1 + pot2) + pot3 == [pot1, pot2, pot3] assert pot1 + (pot2 + pot3) == [pot1, pot2, pot3] # 2D pot1 = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() pot2 = potential.MiyamotoNagaiPotential(normalize=0.2, a=0.4, b=0.1).toPlanar() pot3 = potential.HernquistPotential(normalize=0.4, a=0.1).toPlanar() assert pot1 + pot2 == [pot1, pot2] assert pot1 + pot2 + pot3 == [pot1, pot2, pot3] assert (pot1 + pot2) + pot3 == [pot1, pot2, pot3] assert pot1 + (pot2 + pot3) == [pot1, pot2, pot3] # 1D pot1 = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toVertical(1.1) pot2 = potential.MiyamotoNagaiPotential(normalize=0.2, a=0.4, b=0.1).toVertical(1.1) pot3 = potential.HernquistPotential(normalize=0.4, a=0.1).toVertical(1.1) assert pot1 + pot2 == [pot1, pot2] assert pot1 + pot2 + pot3 == [pot1, pot2, pot3] assert (pot1 + pot2) + pot3 == [pot1, pot2, pot3] assert pot1 + (pot2 + pot3) == [pot1, pot2, pot3] return None # Test that attempting to multiply or divide a potential by something other # than a number raises a TypeError (test both left and right) def test_add_potentials_error(): # 3D pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9) with pytest.raises(TypeError) as excinfo: 3 + pot with pytest.raises(TypeError) as excinfo: pot + 3 # 2D pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toPlanar() with pytest.raises(TypeError) as excinfo: 3 + pot with pytest.raises(TypeError) as excinfo: pot + 3 # 1D pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9).toVertical(1.1) with pytest.raises(TypeError) as excinfo: 3 + pot with pytest.raises(TypeError) as excinfo: pot + 3 return None # Test that adding potentials with incompatible unit systems raises an error def test_add_potentials_unitserror(): # 3D ro, vo = 8.0, 220.0 pot = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9, ro=ro, vo=vo) potro = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9, ro=ro * 1.1, vo=vo) potvo = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9, ro=ro, vo=vo * 1.1) potrovo = potential.LogarithmicHaloPotential( normalize=1.0, q=0.9, ro=ro * 1.1, vo=vo * 1.1 ) with pytest.raises(AssertionError) as excinfo: pot + potro with pytest.raises(AssertionError) as excinfo: pot + potvo with pytest.raises(AssertionError) as excinfo: pot + potrovo with pytest.raises(AssertionError) as excinfo: potro + pot with pytest.raises(AssertionError) as excinfo: potvo + pot with pytest.raises(AssertionError) as excinfo: potrovo + pot # 2D pot = potential.LogarithmicHaloPotential( normalize=1.0, q=0.9, ro=ro, vo=vo ).toPlanar() potro = potential.LogarithmicHaloPotential( normalize=1.0, q=0.9, ro=ro * 1.1, vo=vo ).toPlanar() potvo = potential.LogarithmicHaloPotential( normalize=1.0, q=0.9, ro=ro, vo=vo * 1.1 ).toPlanar() potrovo = potential.LogarithmicHaloPotential( normalize=1.0, q=0.9, ro=ro * 1.1, vo=vo * 1.1 ).toPlanar() with pytest.raises(AssertionError) as excinfo: pot + potro with pytest.raises(AssertionError) as excinfo: pot + potvo with pytest.raises(AssertionError) as excinfo: pot + potrovo with pytest.raises(AssertionError) as excinfo: potro + pot with pytest.raises(AssertionError) as excinfo: potvo + pot with pytest.raises(AssertionError) as excinfo: potrovo + pot # 1D pot = potential.LogarithmicHaloPotential( normalize=1.0, q=0.9, ro=ro, vo=vo ).toVertical(1.1) potro = potential.LogarithmicHaloPotential( normalize=1.0, q=0.9, ro=ro * 1.1, vo=vo ).toVertical(1.1) potvo = potential.LogarithmicHaloPotential( normalize=1.0, q=0.9, ro=ro, vo=vo * 1.1 ).toVertical(1.1) potrovo = potential.LogarithmicHaloPotential( normalize=1.0, q=0.9, ro=ro * 1.1, vo=vo * 1.1 ).toVertical(1.1) with pytest.raises(AssertionError) as excinfo: pot + potro with pytest.raises(AssertionError) as excinfo: pot + potvo with pytest.raises(AssertionError) as excinfo: pot + potrovo with pytest.raises(AssertionError) as excinfo: potro + pot with pytest.raises(AssertionError) as excinfo: potvo + pot with pytest.raises(AssertionError) as excinfo: potrovo + pot return None # Test unit handling of interpolated Spherical potentials def test_interSphericalPotential_unithandling(): pot = potential.HernquistPotential(amp=1.0, a=2.0, ro=8.3, vo=230.0) # Test that setting up the interpolated potential with inconsistent units # raises a RuntimeError with pytest.raises(RuntimeError): ipot = potential.interpSphericalPotential( rforce=pot, rgrid=numpy.geomspace(0.01, 5.0, 201), ro=7.5 ) with pytest.raises(RuntimeError): ipot = potential.interpSphericalPotential( rforce=pot, rgrid=numpy.geomspace(0.01, 5.0, 201), vo=210.0 ) # Check that units are properly transferred ipot = potential.interpSphericalPotential( rforce=pot, rgrid=numpy.geomspace(0.01, 5.0, 201) ) assert ipot._roSet, ( "ro of interpSphericalPotential not set, even though that of parent was set" ) assert ipot._ro == pot._ro, ( "ro of interpSphericalPotential does not agree with that of the parent potential" ) assert ipot._voSet, ( "vo of interpSphericalPotential not set, even though that of parent was set" ) assert ipot._vo == pot._vo, ( "vo of interpSphericalPotential does not agree with that of the parent potential" ) return None # Test that the amplitude of the isothermal disk potential is set correctly (issue #400) def test_isodisk_amplitude_issue400(): # Morgan's example z = numpy.linspace(-0.1, 0.1, 10001) pot = potential.IsothermalDiskPotential(amp=0.1, sigma=20.5 / 220.0) # Density at z=0 should be 0.1, no density or 2nd deriv for 1D at this # point, so manually compute z = numpy.linspace(-2e-4, 2e-4, 5) dens_at_0 = 1.0 / (numpy.pi * 4) * numpy.gradient(numpy.gradient(pot(z), z), z)[2] assert numpy.fabs(dens_at_0 - 0.1) < 1e-7, ( "Density at z=0 for IsothermalDiskPotential is not correct" ) return None def test_TimeDependentAmplitudeWrapperPotential_against_DehnenSmooth(): # Test that TimeDependentAmplitudeWrapperPotential acts the same as DehnenSmooth # Test = LogPot + DehnenBar grown smoothly # Both using the DehnenSmoothWrapper and the new TimeDependentAmplitudeWrapperPotential from galpy.orbit import Orbit lp = potential.LogarithmicHaloPotential() dbp = potential.DehnenBarPotential(tform=-100000.0, tsteady=1.0) dp = potential.DehnenSmoothWrapperPotential(pot=dbp) tp = potential.TimeDependentAmplitudeWrapperPotential(pot=dbp, A=dp._smooth) # Orbit of the Sun o = Orbit() ts = numpy.linspace(0.0, -20.0, 1001) o.integrate(ts, lp + dp) ott = o() ott.integrate(ts, lp + tp) tol = 1e-10 assert numpy.amax(numpy.fabs(o.x(ts) - ott.x(ts))) < tol, ( "Integrating an orbit in a growing DehnenSmoothWrapper does not agree between DehnenSmooth and TimeDependentWrapper" ) assert numpy.amax(numpy.fabs(o.y(ts) - ott.y(ts))) < tol, ( "Integrating an orbit in a growing DehnenSmoothWrapper does not agree between DehnenSmooth and TimeDependentWrapper" ) assert numpy.amax(numpy.fabs(o.z(ts) - ott.z(ts))) < tol, ( "Integrating an orbit in a growing DehnenSmoothWrapper does not agree between DehnenSmooth and TimeDependentWrapper" ) assert numpy.amax(numpy.fabs(o.vx(ts) - ott.vx(ts))) < tol, ( "Integrating an orbit in a growing DehnenSmoothWrapper does not agree between DehnenSmooth and TimeDependentWrapper" ) assert numpy.amax(numpy.fabs(o.vy(ts) - ott.vy(ts))) < tol, ( "Integrating an orbit in a growing DehnenSmoothWrapper does not agree between DehnenSmooth and TimeDependentWrapper" ) assert numpy.amax(numpy.fabs(o.vz(ts) - ott.vz(ts))) < tol, ( "Integrating an orbit in a growing DehnenSmoothWrapper does not agree between DehnenSmooth and TimeDependentWrapper" ) return None def test_TimeDependentAmplitudeWrapperPotential_against_DehnenSmooth_2d(): # Test that TimeDependentAmplitudeWrapperPotential acts the same as DehnenSmooth # Test = LogPot + DehnenBar grown smoothly # Both using the DehnenSmoothWrapper and the new TimeDependentAmplitudeWrapperPotential from galpy.orbit import Orbit lp = potential.LogarithmicHaloPotential() dbp = potential.DehnenBarPotential(tform=-100000.0, tsteady=1.0) dp = potential.DehnenSmoothWrapperPotential(pot=dbp) tp = potential.TimeDependentAmplitudeWrapperPotential(pot=dbp, A=dp._smooth) # Orbit of the Sun o = Orbit().toPlanar() ts = numpy.linspace(0.0, -20.0, 1001) o.integrate(ts, lp + dp) ott = o() ott.integrate(ts, lp + tp) tol = 1e-10 assert numpy.amax(numpy.fabs(o.x(ts) - ott.x(ts))) < tol, ( "Integrating an orbit in a growing DehnenSmoothWrapper does not agree between DehnenSmooth and TimeDependentWrapper" ) assert numpy.amax(numpy.fabs(o.y(ts) - ott.y(ts))) < tol, ( "Integrating an orbit in a growing DehnenSmoothWrapper does not agree between DehnenSmooth and TimeDependentWrapper" ) assert numpy.amax(numpy.fabs(o.vx(ts) - ott.vx(ts))) < tol, ( "Integrating an orbit in a growing DehnenSmoothWrapper does not agree between DehnenSmooth and TimeDependentWrapper" ) assert numpy.amax(numpy.fabs(o.vy(ts) - ott.vy(ts))) < tol, ( "Integrating an orbit in a growing DehnenSmoothWrapper does not agree between DehnenSmooth and TimeDependentWrapper" ) return None def test_TimeDependentAmplitudeWrapperPotential_against_DehnenSmooth_2d_dxdv(): # Test that TimeDependentAmplitudeWrapperPotential acts the same as DehnenSmooth # Test = LogPot + DehnenBar grown smoothly # Both using the DehnenSmoothWrapper and the new TimeDependentAmplitudeWrapperPotential from galpy.orbit import Orbit lp = potential.LogarithmicHaloPotential() dbp = potential.DehnenBarPotential(tform=-100000.0, tsteady=1.0) dp = potential.DehnenSmoothWrapperPotential(pot=dbp) tp = potential.TimeDependentAmplitudeWrapperPotential(pot=dbp, A=dp._smooth) # Orbit of the Sun o = Orbit().toPlanar() ts = numpy.linspace(0.0, -20.0, 1001) o.integrate_dxdv([1.0, 0.0, 0.0, 0.0], ts, lp + dp, rectIn=True, rectOut=True) ott = o() ott.integrate_dxdv([1.0, 0.0, 0.0, 0.0], ts, lp + tp, rectIn=True, rectOut=True) tol = 1e-10 assert numpy.amax(numpy.fabs(o.getOrbit_dxdv() - ott.getOrbit_dxdv())) < tol, ( "Integrating an orbit with dxdv in a growing DehnenSmoothWrapper does not agree between DehnenSmooth and TimeDependentWrapper" ) return None def test_TimeDependentAmplitudeWrapperPotential_inputerrors(): # TypeError when A not supplied lp = potential.LogarithmicHaloPotential() with pytest.raises( TypeError, match="A= input to TimeDependentAmplitudeWrapperPotential should be a function", ): tp = TimeDependentAmplitudeWrapperPotential(pot=lp) # TypeError when supplying a function with no argument with pytest.raises( TypeError, match="A= input to TimeDependentAmplitudeWrapperPotential should be a function that can be called with a single parameter", ): tp = TimeDependentAmplitudeWrapperPotential(pot=lp, A=lambda: 1.0) # TypeError when supplying a function with more than 1 argument with pytest.raises( TypeError, match="A= input to TimeDependentAmplitudeWrapperPotential should be a function that can be called with a single parameter", ): tp = TimeDependentAmplitudeWrapperPotential(pot=lp, A=lambda x, y: x + y) # But having additional arguments have defaults should be allowed tp = TimeDependentAmplitudeWrapperPotential(pot=lp, A=lambda x, y=1.0: x + y) # Return value should be a number with pytest.raises( TypeError, match=r"A= function needs to return a number \(specifically, a numbers.Number\)", ): tp = TimeDependentAmplitudeWrapperPotential(pot=lp, A=lambda t: (t, t + 1)) with pytest.raises( TypeError, match=r"A= function needs to return a number \(specifically, a numbers.Number\)", ): tp = TimeDependentAmplitudeWrapperPotential( pot=lp, A=lambda t: numpy.array([t]) ) return None def test_KuzminLikeWrapperPotential_against_KuzminDisk(): # Test that the KuzminLikeWrapperPotential applied to a KeplerPotential gives the # same potential as the KuzminDiskPotential from galpy.potential import ( KeplerPotential, KuzminDiskPotential, KuzminLikeWrapperPotential, ) kp = KeplerPotential(amp=1.0) kdp = KuzminDiskPotential(amp=1.0, a=1.3) kwp = KuzminLikeWrapperPotential(pot=kp, a=1.3) # Check that the potentials are the same in all ways R, z = 1.1, 0.2 tol = 1e-10 assert numpy.fabs(kdp(R, z) - kwp(R, z)) < tol, ( "KuzminLikeWrapperPotential does not give the same potential as KuzminDiskPotential" ) assert numpy.fabs(kdp.Rforce(R, z) - kwp.Rforce(R, z)) < tol, ( "KuzminLikeWrapperPotential does not give the same Rforce as KuzminDiskPotential" ) assert numpy.fabs(kdp.zforce(R, z) - kwp.zforce(R, z)) < tol, ( "KuzminLikeWrapperPotential does not give the same zforce as KuzminDiskPotential" ) assert numpy.fabs(kdp.R2deriv(R, z) - kwp.R2deriv(R, z)) < tol, ( "KuzminLikeWrapperPotential does not give the same R2deriv as KuzminDiskPotential" ) assert numpy.fabs(kdp.z2deriv(R, z) - kwp.z2deriv(R, z)) < tol, ( "KuzminLikeWrapperPotential does not give the same z2deriv as KuzminDiskPotential" ) assert numpy.fabs(kdp.Rzderiv(R, z) - kwp.Rzderiv(R, z)) < tol, ( "KuzminLikeWrapperPotential does not give the same Rzderiv as KuzminDiskPotential" ) return None def test_KuzminLikeWrapperPotential_against_MiyamotoNagai(): # Test that the KuzminLikeWrapperPotential applied to a KeplerPotential with non-zero # b gives the same potential as the MiyamotoNagaiPotential from galpy.potential import ( KeplerPotential, KuzminLikeWrapperPotential, MiyamotoNagaiPotential, ) kp = KeplerPotential(amp=1.0) mnp = MiyamotoNagaiPotential(amp=1.0, a=1.3, b=0.2) kwp = KuzminLikeWrapperPotential(pot=kp, a=1.3, b=0.2) # Check that the potentials are the same in all ways R, z = 1.1, 0.2 tol = 1e-10 assert numpy.fabs(mnp(R, z) - kwp(R, z)) < tol, ( "KuzminLikeWrapperPotential does not give the same potential as MiyamotoNagaiPotential" ) assert numpy.fabs(mnp.Rforce(R, z) - kwp.Rforce(R, z)) < tol, ( "KuzminLikeWrapperPotential does not give the same Rforce as MiyamotoNagaiPotential" ) assert numpy.fabs(mnp.zforce(R, z) - kwp.zforce(R, z)) < tol, ( "KuzminLikeWrapperPotential does not give the same zforce as MiyamotoNagaiPotential" ) assert numpy.fabs(mnp.R2deriv(R, z) - kwp.R2deriv(R, z)) < tol, ( "KuzminLikeWrapperPotential does not give the same R2deriv as MiyamotoNagaiPotential" ) assert numpy.fabs(mnp.z2deriv(R, z) - kwp.z2deriv(R, z)) < tol, ( "KuzminLikeWrapperPotential does not give the same z2deriv as MiyamotoNagaiPotential" ) assert numpy.fabs(mnp.Rzderiv(R, z) - kwp.Rzderiv(R, z)) < tol, ( "KuzminLikeWrapperPotential does not give the same Rzderiv as MiyamotoNagaiPotential" ) return None def test_KuzminLikeWrapperPotential_NonAxiError(): # Test that the KuzminLikeWrapperPotential applied to a non-axisymmetric potential # raises a RuntimeError from galpy.potential import KuzminLikeWrapperPotential, LogarithmicHaloPotential with pytest.raises(RuntimeError) as excinfo: kwp = KuzminLikeWrapperPotential( pot=LogarithmicHaloPotential(q=0.8, b=0.9), a=1.3 ) assert ( "KuzminLikeWrapperPotential only works for spherical or axisymmetric potentials" == excinfo.value.args[0] ) return None def test_phiforce_deprecation(): # Test that phiforce is being deprecated correctly for phitorque import warnings # Check that we've removed phiforce in the correct version from packaging.version import parse as parse_version deprecation_version = parse_version("1.8.3") from galpy import __version__ as galpy_version galpy_version = parse_version(galpy_version) should_be_removed = galpy_version > deprecation_version # Now test lp = potential.LogarithmicHaloPotential() # Method with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", FutureWarning) try: lp.phiforce(1.0, 0.1) except AttributeError: if not should_be_removed: raise AssertionError( "phiforce stopped working before it is supposed to have been removed" ) else: if should_be_removed: raise AssertionError( "phiforce not removed when it was supposed to be removed" ) raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "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 reused for the actual phi force component" ) if raisedWarning: break assert raisedWarning, ( "phiforce deprecation did not raise the expected warning" ) # Function with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", FutureWarning) try: potential.evaluatephiforces(lp, 1.0, 0.1) except AttributeError: if not should_be_removed: raise AssertionError( "phiforce stopped working before it is supposed to have been removed" ) else: if should_be_removed: raise AssertionError( "phiforce not removed when it was supposed to be removed" ) raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "evaluatephiforces has been renamed evaluatephitorques, 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 reused for the actual phi force component" ) if raisedWarning: break assert raisedWarning, ( "phiforce deprecation did not raise the expected warning" ) def test_phiforce_deprecation_2d(): # Test that phiforce is being deprecated correctly for phitorque import warnings # Check that we've removed phiforce in the correct version from packaging.version import parse as parse_version deprecation_version = parse_version("1.8.3") from galpy import __version__ as galpy_version galpy_version = parse_version(galpy_version) should_be_removed = galpy_version > deprecation_version # Now test lp = potential.LogarithmicHaloPotential().toPlanar() # Method with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", FutureWarning) try: lp.phiforce(1.0, 0.1) except AttributeError: if not should_be_removed: raise AssertionError( "phiforce stopped working before it is supposed to have been removed" ) else: if should_be_removed: raise AssertionError( "phiforce not removed when it was supposed to be removed" ) raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "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 reused for the actual phi force component" ) if raisedWarning: break assert raisedWarning, ( "phiforce deprecation did not raise the expected warning" ) # Function with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", FutureWarning) try: potential.evaluateplanarphiforces(lp, 1.0, 0.1) except AttributeError: if not should_be_removed: raise AssertionError( "phiforce stopped working before it is supposed to have been removed" ) else: if should_be_removed: raise AssertionError( "phiforce not removed when it was supposed to be removed" ) raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "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 reused for the actual phi force component" ) if raisedWarning: break assert raisedWarning, ( "phiforce deprecation did not raise the expected warning" ) # Test that Pot is required to be a positional argument for Potential functions def test_potential_Pot_is_positional(): from galpy import potential from galpy.potential import MWPotential2014 for func in [ potential.evaluatePotentials, potential.evaluateRforces, potential.evaluatezforces, potential.evaluateR2derivs, potential.evaluatez2derivs, potential.evaluateRzderivs, potential.evaluaterforces, potential.evaluatephitorques, potential.evaluateDensities, potential.evaluateSurfaceDensities, potential.flattening, potential.rtide, potential.ttensor, ]: with pytest.raises(TypeError) as excinfo: func(Pot=MWPotential2014, R=1.0, z=0.0) assert "required positional argument: 'Pot'" in excinfo.value.args[0] for func in [ potential.omegac, potential.epifreq, potential.verticalfreq, potential.rhalf, potential.tdyn, ]: with pytest.raises(TypeError) as excinfo: func(Pot=MWPotential2014, R=1.0) assert "required positional argument: 'Pot'" in excinfo.value.args[0] # Special cases with pytest.raises(TypeError) as excinfo: potential.lindbladR(Pot=MWPotential2014, OmegaP=1.0) assert "required positional argument: 'Pot'" in excinfo.value.args[0] with pytest.raises(TypeError) as excinfo: potential.rl(Pot=MWPotential2014, lz=1.0) assert "required positional argument: 'Pot'" in excinfo.value.args[0] with pytest.raises(TypeError) as excinfo: potential.rE(Pot=MWPotential2014, E=1.0) assert "required positional argument: 'Pot'" in excinfo.value.args[0] with pytest.raises(TypeError) as excinfo: potential.LcE(Pot=MWPotential2014, E=1.0) assert "required positional argument: 'Pot'" in excinfo.value.args[0] with pytest.raises(TypeError) as excinfo: potential.vterm(Pot=MWPotential2014, l=1.0) assert "required positional argument: 'Pot'" in excinfo.value.args[0] with pytest.raises(TypeError) as excinfo: potential.zvc_range(Pot=MWPotential2014, E=1.0, Lz=1.0) assert "required positional argument: 'Pot'" in excinfo.value.args[0] with pytest.raises(TypeError) as excinfo: potential.zvc(Pot=MWPotential2014, R=1.0, E=1.0, Lz=1.0) assert "required positional argument: 'Pot'" in excinfo.value.args[0] with pytest.raises(TypeError) as excinfo: potential.rhalf(Pot=MWPotential2014) assert "required positional argument: 'Pot'" in excinfo.value.args[0] return None # Test that Pot is required to be a positional argument for Potential functions def test_potential_Pot_is_positional_planar(): from galpy import potential from galpy.potential import MWPotential2014 for func in [ potential.evaluateplanarPotentials, potential.evaluateplanarRforces, potential.evaluateplanarR2derivs, potential.evaluateplanarphitorques, ]: with pytest.raises(TypeError) as excinfo: func(Pot=potential.toPlanarPotential(MWPotential2014), R=1.0) assert "required positional argument: 'Pot'" in excinfo.value.args[0] return None # Test that Pot is required to be a positional argument for Potential functions def test_potential_Pot_is_positional_linear(): from galpy import potential from galpy.potential import MWPotential2014 for func in [potential.evaluatelinearPotentials, potential.evaluatelinearForces]: with pytest.raises(TypeError) as excinfo: func(Pot=potential.toVerticalPotential(MWPotential2014, 1.0), x=1.0) assert "required positional argument: 'Pot'" in excinfo.value.args[0] return None # Issue #495 def test_diskscf_overflow(): from galpy.actionAngle import estimateDeltaStaeckel from galpy.orbit import Orbit from galpy.potential.mwpotentials import McMillan17 from galpy.util import conversion ro17 = conversion.get_physical(McMillan17)["ro"] vo17 = conversion.get_physical(McMillan17)["vo"] o17 = Orbit([209.3, 26.8, 46.5, -1.16, -0.88, 189.11], radec=True, ro=ro17, vo=vo17) delta = estimateDeltaStaeckel( McMillan17, o17.R(use_physical=False), o17.z(use_physical=False) ) assert not numpy.isnan(delta), ( "estimateDeltaStaeckel returns NaN due to overflow in DiskSCFPotential" ) def test_InterpSnapshotRZPotential_pickling(): # Test that InterpSnapshotRZPotential can be pickled (see #507, #509) if not _PYNBODY_LOADED: pytest.skip() import pickle import pynbody from galpy.potential import InterpSnapshotRZPotential # Set up simple snapshot: 1 star! s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 spi = InterpSnapshotRZPotential(s) test = pickle.dumps(spi) newspi = pickle.loads(test) # Inside the grid assert numpy.fabs(newspi(1.0, 0.0) - spi(1.0, 0.0)) < 1e-10, ( "Unpickled InterpSnapshotRZPotential does not return the same potential as original instance" ) # Outside the grid, needs _origPot assert numpy.fabs(newspi(1.0, 10.0) - spi(1.0, 10.0)) < 1e-10, ( "Unpickled InterpSnapshotRZPotential does not return the same potential as original instance" ) return None # Test that the King potential goes as -GM/r at r > rt def test_king_potential_beyond_tidal(): mass = 3.0 kp = potential.KingPotential(W0=6.0, rt=2.0, M=mass) r = numpy.linspace(2.1, 10.0, 1001) # Accuracy is limited because of the numerical solution of the King ODE and # the fact that interpSphericalPotential doesn't directly use the solved W potential assert numpy.all(numpy.fabs(kp(r, 0.0) / mass * r + 1.0) < 1e-3), ( "King potential does not go as GM/r at r > rt" ) return None def test_dehnen_bar_python_c_consistency(): ## test a quick orbit integration to ensure C and python Dehnen bar potentials are consistent ## (context: there had been a bug where r was used instead of R in the C implementation) diskp = potential.MiyamotoNagaiPotential(amp=10.0, a=0.5, b=0.03, normalize=False) barp = potential.DehnenBarPotential( omegab=0.0, rb=0.5, Af=200.0, barphi=0.0, tform=-10000, tsteady=0 ) pot = [diskp, barp] # initialize orbits to be integrated orb_p = orbit.Orbit(vxvv=[0.2, 2.0, 0.0, 1, 0.0, 2.0], ro=8.0) orb_c = orbit.Orbit(vxvv=[0.2, 2.0, 0.0, 1, 0.0, 2.0], ro=8.0) # integrate one in Python and one in C ts = numpy.linspace(0.0, 1.0, 100) * 0.3 orb_p.integrate(ts, pot=pot, method="dop853") orb_c.integrate(ts, pot=pot, method="dop853_c") # check equal assert numpy.all(numpy.fabs(orb_p.E(ts) / orb_c.E(ts) - 1.0) < 10.0**-10), ( "C orbit integration in a Dehnen bar potential does not work as expected" ) # Test that trying to plot a potential with xy=True and effective=True raises a RuntimeError def test_plotting_xy_effective_error(): # First a single potential kp = potential.KeplerPotential(normalize=1.0) with pytest.raises(RuntimeError) as excinfo: kp.plot(xy=True, effective=True) assert "xy and effective cannot be True at the same time" in excinfo.value.args[0] # Then a list of potentials with pytest.raises(RuntimeError) as excinfo: potential.plotPotentials(potential.MWPotential2014, xy=True, effective=True) assert "xy and effective cannot be True at the same time" in excinfo.value.args[0] return None def test_plotting(): import tempfile # Some tests of the plotting routines, to make sure they don't fail kp = potential.KeplerPotential(normalize=1.0) # Plot the rotation curve kp.plotRotcurve() kp.toPlanar().plotRotcurve() # through planar interface kp.plotRotcurve(Rrange=[0.01, 10.0], grid=101, savefilename=None) potential.plotRotcurve([kp]) potential.plotRotcurve([kp], Rrange=[0.01, 10.0], grid=101, savefilename=None) # Also while saving the result savefile, tmp_savefilename = tempfile.mkstemp() try: os.close(savefile) # Easier this way os.remove(tmp_savefilename) # First save kp.plotRotcurve(Rrange=[0.01, 10.0], grid=101, savefilename=tmp_savefilename) # Then plot using the saved file kp.plotRotcurve(Rrange=[0.01, 10.0], grid=101, savefilename=tmp_savefilename) finally: os.remove(tmp_savefilename) # Plot the escape-velocity curve kp.plotEscapecurve() kp.toPlanar().plotEscapecurve() # Through planar interface kp.plotEscapecurve(Rrange=[0.01, 10.0], grid=101, savefilename=None) potential.plotEscapecurve([kp]) potential.plotEscapecurve([kp], Rrange=[0.01, 10.0], grid=101, savefilename=None) # Also while saving the result savefile, tmp_savefilename = tempfile.mkstemp() try: os.close(savefile) # Easier this way os.remove(tmp_savefilename) # First save kp.plotEscapecurve(Rrange=[0.01, 10.0], grid=101, savefilename=tmp_savefilename) # Then plot using the saved file kp.plotEscapecurve(Rrange=[0.01, 10.0], grid=101, savefilename=tmp_savefilename) finally: os.remove(tmp_savefilename) # Plot the potential itself kp.plot() kp.plot( t=1.0, rmin=0.01, rmax=1.8, nrs=11, zmin=-0.55, zmax=0.55, nzs=11, effective=False, Lz=None, xy=True, xrange=[0.01, 1.8], yrange=[-0.55, 0.55], justcontours=True, ncontours=11, savefilename=None, ) # Also while saving the result savefile, tmp_savefilename = tempfile.mkstemp() try: os.close(savefile) # Easier this way os.remove(tmp_savefilename) # First save kp.plot( t=1.0, rmin=0.01, rmax=1.8, nrs=11, zmin=-0.55, zmax=0.55, nzs=11, effective=False, Lz=None, xrange=[0.01, 1.8], yrange=[-0.55, 0.55], ncontours=11, savefilename=tmp_savefilename, ) # Then plot using the saved file kp.plot( t=1.0, rmin=0.01, rmax=1.8, nrs=11, zmin=-0.55, zmax=0.55, nzs=11, effective=False, Lz=None, xrange=[0.01, 1.8], yrange=[-0.55, 0.55], ncontours=11, savefilename=tmp_savefilename, ) finally: os.remove(tmp_savefilename) potential.plotPotentials([kp]) # Also while saving the result savefile, tmp_savefilename = tempfile.mkstemp() try: os.close(savefile) # Easier this way os.remove(tmp_savefilename) # First save potential.plotPotentials( [kp], rmin=0.01, rmax=1.8, nrs=11, zmin=-0.55, zmax=0.55, nzs=11, justcontours=True, xy=True, ncontours=11, savefilename=tmp_savefilename, ) # Then plot using the saved file potential.plotPotentials( [kp], t=1.0, rmin=0.01, rmax=1.8, nrs=11, zmin=-0.55, zmax=0.55, nzs=11, ncontours=11, savefilename=tmp_savefilename, ) finally: os.remove(tmp_savefilename) # Plot the effective potential kp.plot() kp.plot(effective=True, Lz=1.0) try: kp.plot(effective=True, Lz=None) except RuntimeError: pass else: raise AssertionError( "Potential.plot with effective=True, but Lz=None did not return a RuntimeError" ) potential.plotPotentials([kp], effective=True, Lz=1.0) try: potential.plotPotentials([kp], effective=True, Lz=None) except RuntimeError: pass else: raise AssertionError( "Potential.plot with effective=True, but Lz=None did not return a RuntimeError" ) # Plot the density of a LogarithmicHaloPotential lp = potential.LogarithmicHaloPotential(normalize=1.0) lp.plotDensity() lp.plotDensity( t=1.0, rmin=0.05, rmax=1.8, nrs=11, zmin=-0.55, zmax=0.55, nzs=11, aspect=1.0, log=True, justcontours=True, xy=True, ncontours=11, savefilename=None, ) # Also while saving the result savefile, tmp_savefilename = tempfile.mkstemp() try: os.close(savefile) # Easier this way os.remove(tmp_savefilename) # First save lp.plotDensity(savefilename=tmp_savefilename) # Then plot using the saved file lp.plotDensity(savefilename=tmp_savefilename) finally: os.remove(tmp_savefilename) potential.plotDensities([lp]) potential.plotDensities( [lp], t=1.0, rmin=0.05, rmax=1.8, nrs=11, zmin=-0.55, zmax=0.55, nzs=11, aspect=1.0, log=True, xy=True, justcontours=True, ncontours=11, savefilename=None, ) # Plot the surface density of a LogarithmicHaloPotential lp = potential.LogarithmicHaloPotential(normalize=1.0) lp.plotSurfaceDensity() lp.plotSurfaceDensity( t=1.0, z=2.0, xmin=0.05, xmax=1.8, nxs=11, ymin=-0.55, ymax=0.55, nys=11, aspect=1.0, log=True, justcontours=True, ncontours=11, savefilename=None, ) # Also while saving the result savefile, tmp_savefilename = tempfile.mkstemp() try: os.close(savefile) # Easier this way os.remove(tmp_savefilename) # First save lp.plotSurfaceDensity(savefilename=tmp_savefilename) # Then plot using the saved file lp.plotSurfaceDensity(savefilename=tmp_savefilename) finally: os.remove(tmp_savefilename) potential.plotSurfaceDensities([lp]) potential.plotSurfaceDensities( [lp], t=1.0, z=2.0, xmin=0.05, xmax=1.8, nxs=11, ymin=-0.55, ymax=0.55, nys=11, aspect=1.0, log=True, justcontours=True, ncontours=11, savefilename=None, ) # Plot the potential itself for a 2D potential kp.toPlanar().plot() savefile, tmp_savefilename = tempfile.mkstemp() try: os.close(savefile) # Easier this way os.remove(tmp_savefilename) # First save kp.toPlanar().plot(Rrange=[0.01, 1.8], grid=11, savefilename=tmp_savefilename) # Then plot using the saved file kp.toPlanar().plot(Rrange=[0.01, 1.8], grid=11, savefilename=tmp_savefilename) finally: os.remove(tmp_savefilename) dp = potential.EllipticalDiskPotential() savefile, tmp_savefilename = tempfile.mkstemp() try: os.close(savefile) # Easier this way os.remove(tmp_savefilename) # First save dp.plot( xrange=[0.01, 1.8], yrange=[0.01, 1.8], gridx=11, gridy=11, ncontours=11, savefilename=tmp_savefilename, ) # Then plot using the saved file dp.plot( xrange=[0.01, 1.8], yrange=[0.01, 1.8], gridx=11, gridy=11, ncontours=11, savefilename=tmp_savefilename, ) finally: os.remove(tmp_savefilename) potential.plotplanarPotentials([dp], gridx=11, gridy=11) # Tests of linearPotential plotting lip = potential.RZToverticalPotential( potential.MiyamotoNagaiPotential(normalize=1.0), 1.0 ) lip.plot() savefile, tmp_savefilename = tempfile.mkstemp() try: os.close(savefile) # Easier this way os.remove(tmp_savefilename) # First save lip.plot(t=0.0, min=-15.0, max=15, ns=21, savefilename=tmp_savefilename) # Then plot using the saved file lip.plot(t=0.0, min=-15.0, max=15, ns=21, savefilename=tmp_savefilename) finally: os.remove(tmp_savefilename) savefile, tmp_savefilename = tempfile.mkstemp() try: os.close(savefile) # Easier this way os.remove(tmp_savefilename) # First save potential.plotlinearPotentials( lip, t=0.0, min=-15.0, max=15, ns=21, savefilename=tmp_savefilename ) # Then plot using the saved file potential.plotlinearPotentials( lip, t=0.0, min=-15.0, max=15, ns=21, savefilename=tmp_savefilename ) finally: os.remove(tmp_savefilename) return None # Classes for testing Integer TwoSphericalPotential and for testing special # cases of some other potentials from galpy.potential import ( BurkertPotential, DiskSCFPotential, FerrersPotential, FlattenedPowerPotential, LogarithmicHaloPotential, MiyamotoNagaiPotential, MN3ExponentialDiskPotential, MWPotential, NullPotential, PowerSphericalPotential, SoftenedNeedleBarPotential, SpiralArmsPotential, TriaxialHernquistPotential, TriaxialJaffePotential, TriaxialNFWPotential, TwoPowerSphericalPotential, TwoPowerTriaxialPotential, interpRZPotential, ) class mockSphericalSoftenedNeedleBarPotential(SoftenedNeedleBarPotential): def __init__(self): SoftenedNeedleBarPotential.__init__( self, amp=1.0, a=0.000001, b=0.0, c=10.0, omegab=0.0, pa=0.0 ) self.normalize(1.0) self.isNonAxi = False return None def _evaluate(self, R, z, phi=0.0, t=0.0): if phi is None: phi = 0.0 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.0 / self._a class specialTwoPowerSphericalPotential(TwoPowerSphericalPotential): def __init__(self): TwoPowerSphericalPotential.__init__(self, amp=1.0, a=5.0, alpha=1.5, beta=3.0) return None class DehnenTwoPowerSphericalPotential(TwoPowerSphericalPotential): def __init__(self): TwoPowerSphericalPotential.__init__(self, amp=1.0, a=5.0, alpha=1.5, beta=4.0) return None class DehnenCoreTwoPowerSphericalPotential(TwoPowerSphericalPotential): def __init__(self): TwoPowerSphericalPotential.__init__(self, amp=1.0, a=5.0, alpha=0, beta=4.0) return None class HernquistTwoPowerSphericalPotential(TwoPowerSphericalPotential): def __init__(self): TwoPowerSphericalPotential.__init__(self, amp=1.0, a=5.0, alpha=1.0, beta=4.0) return None class JaffeTwoPowerSphericalPotential(TwoPowerSphericalPotential): def __init__(self): TwoPowerSphericalPotential.__init__(self, amp=1.0, a=5.0, alpha=2.0, beta=4.0) return None class NFWTwoPowerSphericalPotential(TwoPowerSphericalPotential): def __init__(self): TwoPowerSphericalPotential.__init__(self, amp=1.0, a=5.0, alpha=1.0, beta=3.0) return None class specialPowerSphericalPotential(PowerSphericalPotential): def __init__(self): PowerSphericalPotential.__init__(self, amp=1.0, alpha=2.0) return None class specialMiyamotoNagaiPotential(MiyamotoNagaiPotential): def __init__(self): MiyamotoNagaiPotential.__init__(self, amp=1.0, a=0.0, b=0.1) return None class specialFlattenedPowerPotential(FlattenedPowerPotential): def __init__(self): FlattenedPowerPotential.__init__(self, alpha=0.0) return None class specialMN3ExponentialDiskPotentialPD(MN3ExponentialDiskPotential): def __init__(self): MN3ExponentialDiskPotential.__init__(self, normalize=1.0, posdens=True) return None class specialMN3ExponentialDiskPotentialSECH(MN3ExponentialDiskPotential): def __init__(self): MN3ExponentialDiskPotential.__init__(self, normalize=1.0, sech=True) return None class BurkertPotentialNoC(BurkertPotential): def __init__(self): # Just to force not using C BurkertPotential.__init__(self) self.hasC = False self.hasC_dxdv = False return None class oblateHernquistPotential(TriaxialHernquistPotential): def __init__(self): TriaxialHernquistPotential.__init__(self, normalize=1.0, b=1.0, c=0.2) return None class oblateNFWPotential(TriaxialNFWPotential): def __init__(self): TriaxialNFWPotential.__init__(self, normalize=1.0, b=1.0, c=0.2) return None class oblatenoGLNFWPotential(TriaxialNFWPotential): def __init__(self): TriaxialNFWPotential.__init__(self, normalize=1.0, b=1.0, c=0.2, glorder=None) return None class oblateJaffePotential(TriaxialJaffePotential): def __init__(self): TriaxialJaffePotential.__init__(self, normalize=1.0, b=1.0, c=0.2) return None class prolateHernquistPotential(TriaxialHernquistPotential): def __init__(self): TriaxialHernquistPotential.__init__(self, normalize=1.0, b=1.0, c=1.8) return None class prolateNFWPotential(TriaxialNFWPotential): def __init__(self): TriaxialNFWPotential.__init__(self, normalize=1.0, b=1.0, c=1.8) return None class prolateJaffePotential(TriaxialJaffePotential): def __init__(self): TriaxialJaffePotential.__init__(self, normalize=1.0, b=1.0, c=1.8) return None class rotatingSpiralArmsPotential(SpiralArmsPotential): def __init__(self): SpiralArmsPotential.__init__(self, omega=1.1) class specialSpiralArmsPotential(SpiralArmsPotential): def __init__(self): SpiralArmsPotential.__init__( self, omega=1.3, N=4.0, Cs=[8.0 / 3.0 / numpy.pi, 1.0 / 2.0, 8.0 / 15.0 / numpy.pi], ) class triaxialHernquistPotential(TriaxialHernquistPotential): def __init__(self): TriaxialHernquistPotential.__init__(self, normalize=1.0, b=1.4, c=0.6) return None class triaxialNFWPotential(TriaxialNFWPotential): def __init__(self): TriaxialNFWPotential.__init__(self, normalize=1.0, b=0.2, c=1.8) return None class triaxialJaffePotential(TriaxialJaffePotential): def __init__(self): TriaxialJaffePotential.__init__(self, normalize=1.0, b=0.4, c=0.7) return None class zRotatedTriaxialNFWPotential(TriaxialNFWPotential): def __init__(self): TriaxialNFWPotential.__init__( self, normalize=1.0, b=1.5, c=0.2, zvec=[numpy.sin(0.5), 0.0, numpy.cos(0.5)], ) return None class yRotatedTriaxialNFWPotential(TriaxialNFWPotential): def __init__(self): TriaxialNFWPotential.__init__(self, normalize=1.0, b=1.5, c=0.2, pa=0.2) return None class fullyRotatedTriaxialNFWPotential(TriaxialNFWPotential): def __init__(self): TriaxialNFWPotential.__init__( self, normalize=1.0, b=1.5, c=0.2, zvec=[numpy.sin(0.5), 0.0, numpy.cos(0.5)], pa=0.2, ) return None class fullyRotatednoGLTriaxialNFWPotential(TriaxialNFWPotential): def __init__(self): TriaxialNFWPotential.__init__( self, normalize=1.0, b=1.5, c=0.2, zvec=[numpy.sin(0.5), 0.0, numpy.cos(0.5)], pa=0.2, glorder=None, ) return None class triaxialLogarithmicHaloPotential(LogarithmicHaloPotential): def __init__(self): LogarithmicHaloPotential.__init__(self, normalize=1.0, b=0.7, q=0.9, core=0.5) return None def OmegaP(self): return 0.0 # Implementations through TwoPowerTriaxialPotential class HernquistTwoPowerTriaxialPotential(TwoPowerTriaxialPotential): def __init__(self): TwoPowerTriaxialPotential.__init__( self, amp=1.0, a=5.0, alpha=1.0, beta=4.0, b=0.3, c=1.8 ) return None class NFWTwoPowerTriaxialPotential(TwoPowerTriaxialPotential): def __init__(self): TwoPowerTriaxialPotential.__init__( self, amp=1.0, a=2.0, alpha=1.0, beta=3.0, b=1.3, c=0.8 ) self.isNonAxi = True # to test planar-from-full return None class JaffeTwoPowerTriaxialPotential(TwoPowerTriaxialPotential): def __init__(self): TwoPowerTriaxialPotential.__init__( self, amp=1.0, a=5.0, alpha=2.0, beta=4.0, b=1.3, c=1.8 ) return None class testNullPotential(NullPotential): def normalize(self, norm): pass # Other DiskSCFPotentials class sech2DiskSCFPotential(DiskSCFPotential): def __init__(self): DiskSCFPotential.__init__( self, dens=lambda R, z: numpy.exp(-3.0 * R) * 1.0 / numpy.cosh(z / 2.0 * 27.0) ** 2.0 / 4.0 * 27.0, Sigma={"h": 1.0 / 3.0, "type": "exp", "amp": 1.0}, hz={"type": "sech2", "h": 1.0 / 27.0}, a=1.0, N=5, L=5, ) return None class expwholeDiskSCFPotential(DiskSCFPotential): def __init__(self): # Add a Hernquist potential because otherwise the density near the # center is zero from galpy.potential import HernquistPotential hp = HernquistPotential(normalize=0.5) DiskSCFPotential.__init__( self, dens=lambda R, z: 13.5 * numpy.exp(-0.5 / (R + 10.0**-10.0) - 3.0 * R - numpy.fabs(z) * 27.0) + hp.dens(R, z), Sigma={"h": 1.0 / 3.0, "type": "expwhole", "amp": 1.0, "Rhole": 0.5}, hz={"type": "exp", "h": 1.0 / 27.0}, a=1.0, N=5, L=5, ) return None # Same as above, but specify type as 'exp' and give Rhole, to make sure that # case is handled correctly class altExpwholeDiskSCFPotential(DiskSCFPotential): def __init__(self): # Add a Hernquist potential because otherwise the density near the # center is zero from galpy.potential import HernquistPotential hp = HernquistPotential(normalize=0.5) DiskSCFPotential.__init__( self, dens=lambda R, z: 13.5 * numpy.exp(-0.5 / (R + 10.0**-10.0) - 3.0 * R - numpy.fabs(z) * 27.0) + hp.dens(R, z), Sigma={"h": 1.0 / 3.0, "type": "exp", "amp": 1.0, "Rhole": 0.5}, hz={"type": "exp", "h": 1.0 / 27.0}, a=1.0, N=5, L=5, ) return None class nonaxiDiskSCFPotential(DiskSCFPotential): def __init__(self): thp = triaxialHernquistPotential() DiskSCFPotential.__init__( self, dens=lambda R, z, phi: 13.5 * numpy.exp(-3.0 * R) * numpy.exp(-27.0 * numpy.fabs(z)) + thp.dens(R, z, phi=phi), Sigma_amp=[0.5, 0.5], Sigma=[lambda R: numpy.exp(-3.0 * R), lambda R: numpy.exp(-3.0 * R)], dSigmadR=[ lambda R: -3.0 * numpy.exp(-3.0 * R), lambda R: -3.0 * numpy.exp(-3.0 * R), ], d2SigmadR2=[ lambda R: 9.0 * numpy.exp(-3.0 * R), lambda R: 9.0 * numpy.exp(-3.0 * R), ], hz=lambda z: 13.5 * numpy.exp(-27.0 * numpy.fabs(z)), Hz=lambda z: (numpy.exp(-27.0 * numpy.fabs(z)) - 1.0 + 27.0 * numpy.fabs(z)) / 54.0, dHzdz=lambda z: 0.5 * numpy.sign(z) * (1.0 - numpy.exp(-27.0 * numpy.fabs(z))), N=5, L=5, ) return None # An axisymmetric FerrersPotential class mockAxisymmetricFerrersPotential(FerrersPotential): def __init__(self): FerrersPotential.__init__(self, normalize=1.0, b=1.0, c=0.2) return None class mockInterpRZPotential(interpRZPotential): def __init__(self): interpRZPotential.__init__( self, RZPot=MWPotential, rgrid=(0.01, 2.1, 101), zgrid=(0.0, 0.26, 101), logR=True, interpPot=True, interpRforce=True, interpzforce=True, interpDens=True, ) class mockSnapshotRZPotential(potential.SnapshotRZPotential): def __init__(self): # Test w/ equivalent of KeplerPotential: one mass kp = potential.KeplerPotential(amp=1.0) s = pynbody.new(star=1) s["mass"] = 1.0 / numpy.fabs(kp.Rforce(1.0, 0.0)) # forces vc(1,0)=1 s["eps"] = 0.0 potential.SnapshotRZPotential.__init__(self, s) class mockInterpSnapshotRZPotential(potential.InterpSnapshotRZPotential): def __init__(self): # Test w/ equivalent of KeplerPotential: one mass kp = potential.KeplerPotential(amp=1.0) s = pynbody.new(star=1) s["mass"] = 1.0 / numpy.fabs(kp.Rforce(1.0, 0.0)) # forces vc(1,0)=1 s["eps"] = 0.0 potential.InterpSnapshotRZPotential.__init__( self, s, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.3, 101), logR=False, interpPot=True, zsym=True, ) # Some special cases of 2D, non-axisymmetric potentials, to make sure they # are covered; need 3 to capture all of the transient behavior from galpy.potential import ( CosmphiDiskPotential, DehnenBarPotential, EllipticalDiskPotential, HenonHeilesPotential, SteadyLogSpiralPotential, TransientLogSpiralPotential, ) class mockDehnenBarPotentialT1(DehnenBarPotential): def __init__(self): DehnenBarPotential.__init__( self, omegab=1.9, rb=0.4, barphi=25.0 * numpy.pi / 180.0, beta=0.0, tform=0.5, tsteady=0.5, alpha=0.01, Af=0.04, ) class mockDehnenBarPotentialTm1(DehnenBarPotential): def __init__(self): DehnenBarPotential.__init__( self, omegab=1.9, rb=0.6, barphi=25.0 * numpy.pi / 180.0, beta=0.0, tform=-1.0, tsteady=1.01, alpha=0.01, Af=0.04, ) # Also one with omegab=0. to test that that works class mockDehnenBarPotentialTm1Omega0(DehnenBarPotential): def __init__(self): DehnenBarPotential.__init__( self, omegab=0.0, rb=0.6, barphi=25.0 * numpy.pi / 180.0, beta=0.0, tform=-1.0, tsteady=1.01, alpha=0.01, Af=0.04, ) class mockDehnenBarPotentialTm5(DehnenBarPotential): def __init__(self): DehnenBarPotential.__init__( self, omegab=1.9, rb=0.4, barphi=25.0 * numpy.pi / 180.0, beta=0.0, tform=-5.0, tsteady=2.0, alpha=0.01, Af=0.04, ) class mockCosmphiDiskPotentialnegcp(CosmphiDiskPotential): def __init__(self): CosmphiDiskPotential.__init__( self, amp=1.0, phib=25.0 * numpy.pi / 180.0, p=1.0, phio=0.01, m=1.0, rb=0.9, cp=-0.05, sp=0.05, ) class mockCosmphiDiskPotentialnegp(CosmphiDiskPotential): def __init__(self): CosmphiDiskPotential.__init__( self, amp=1.0, phib=25.0 * numpy.pi / 180.0, p=-1.0, phio=0.01, m=1.0, cp=-0.05, sp=0.05, ) class mockEllipticalDiskPotentialT1(EllipticalDiskPotential): def __init__(self): EllipticalDiskPotential.__init__( self, amp=1.0, phib=25.0 * numpy.pi / 180.0, p=1.0, twophio=0.02, tform=0.5, tsteady=1.0, cp=0.05, sp=0.05, ) class mockEllipticalDiskPotentialTm1(EllipticalDiskPotential): def __init__(self): EllipticalDiskPotential.__init__( self, amp=1.0, phib=25.0 * numpy.pi / 180.0, p=1.0, twophio=0.02, tform=-1.0, tsteady=None, cp=-0.05, sp=0.05, ) class mockEllipticalDiskPotentialTm5(EllipticalDiskPotential): def __init__(self): EllipticalDiskPotential.__init__( self, amp=1.0, phib=25.0 * numpy.pi / 180.0, p=1.0, twophio=0.02, tform=-5.0, tsteady=-1.0, cp=-0.05, sp=0.05, ) class mockSteadyLogSpiralPotentialT1(SteadyLogSpiralPotential): def __init__(self): SteadyLogSpiralPotential.__init__( self, amp=1.0, omegas=0.65, A=-0.035, m=2, gamma=numpy.pi / 4.0, p=-0.3, tform=0.5, tsteady=1.0, ) class mockSteadyLogSpiralPotentialTm1(SteadyLogSpiralPotential): def __init__(self): SteadyLogSpiralPotential.__init__( self, amp=1.0, omegas=0.65, A=-0.035, m=2, gamma=numpy.pi / 4.0, p=-0.3, tform=-1.0, tsteady=None, ) # Also one with omegab=0. to test that that works class mockSteadyLogSpiralPotentialTm1Omega0(SteadyLogSpiralPotential): def __init__(self): SteadyLogSpiralPotential.__init__( self, amp=1.0, omegas=0.0, A=-0.035, m=2, gamma=numpy.pi / 4.0, p=-0.3, tform=-1.0, tsteady=None, ) class mockSteadyLogSpiralPotentialTm5(SteadyLogSpiralPotential): def __init__(self): SteadyLogSpiralPotential.__init__( self, amp=1.0, omegas=0.65, A=-0.035, m=2, gamma=numpy.pi / 4.0, p=-0.3, tform=-1.0, tsteady=-5.0, ) class mockTransientLogSpiralPotential(TransientLogSpiralPotential): def __init__(self): TransientLogSpiralPotential.__init__( self, amp=1.0, omegas=0.65, A=-0.035, m=2, gamma=numpy.pi / 4.0, p=-0.3 ) ##Potentials used for mock SCF def rho_Zeeuw(R, z=0.0, phi=0.0, a=1.0): r, theta, phi = coords.cyl_to_spher(R, z, phi) return 3.0 / (4 * numpy.pi) * numpy.power((a + r), -4.0) * a def axi_density1(R, z=0, phi=0.0): r, theta, phi = coords.cyl_to_spher(R, z, phi) h = potential.HernquistPotential() return h.dens(R, z, phi) * (1 + numpy.cos(theta) + numpy.cos(theta) ** 2.0) def axi_density2(R, z=0, phi=0.0): r, theta, phi = coords.cyl_to_spher(R, z, phi) return rho_Zeeuw(R, z, phi) * (1 + numpy.cos(theta) + numpy.cos(theta) ** 2) def scf_density(R, z=0, phi=0.0): eps = 0.1 return axi_density2(R, z, phi) * (1 + eps * (numpy.cos(phi) + numpy.sin(phi))) ##Mock SCF class class mockSCFZeeuwPotential(potential.SCFPotential): def __init__(self): Acos, Asin = potential.scf_compute_coeffs_spherical(rho_Zeeuw, 2) potential.SCFPotential.__init__(self, amp=1.0, Acos=Acos, Asin=Asin) class mockSCFNFWPotential(potential.SCFPotential): def __init__(self): nfw = potential.NFWPotential() Acos, Asin = potential.scf_compute_coeffs_spherical(nfw.dens, 10) potential.SCFPotential.__init__(self, amp=1.0, Acos=Acos, Asin=Asin) class mockSCFAxiDensity1Potential(potential.SCFPotential): def __init__(self): Acos, Asin = potential.scf_compute_coeffs_axi(axi_density1, 10, 2) potential.SCFPotential.__init__(self, amp=1.0, Acos=Acos, Asin=Asin) class mockSCFAxiDensity2Potential(potential.SCFPotential): def __init__(self): Acos, Asin = potential.scf_compute_coeffs_axi(axi_density2, 10, 2) potential.SCFPotential.__init__(self, amp=1.0, Acos=Acos, Asin=Asin) class mockSCFDensityPotential(potential.SCFPotential): def __init__(self): Acos, Asin = potential.scf_compute_coeffs(scf_density, 10, 10, phi_order=30) potential.SCFPotential.__init__(self, amp=1.0, Acos=Acos, Asin=Asin) # Test interpSphericalPotential class mockInterpSphericalPotential(potential.interpSphericalPotential): def __init__(self): hp = potential.HomogeneousSpherePotential(normalize=1.0, R=1.1) potential.interpSphericalPotential.__init__( self, rforce=hp, rgrid=numpy.linspace(0.0, 1.1, 201) ) class mockInterpSphericalPotentialwForce(potential.interpSphericalPotential): def __init__(self): hp = potential.HomogeneousSpherePotential(normalize=1.0, R=1.1) potential.interpSphericalPotential.__init__( self, rforce=lambda r: hp.Rforce(r, 0.0), Phi0=hp(0.0, 0.0), rgrid=numpy.linspace(0.0, 1.1, 201), ) # Class to test potentials given as lists, st we can use their methods as class. from galpy.potential import ( Potential, _isNonAxi, evaluateDensities, evaluatephitorques, evaluatephizderivs, evaluateplanarphitorques, evaluateplanarPotentials, evaluateplanarR2derivs, evaluateplanarRforces, evaluatePotentials, evaluateR2derivs, evaluateRforces, evaluateRzderivs, evaluateSurfaceDensities, evaluatez2derivs, evaluatezforces, planarPotential, ) class testMWPotential(Potential): """Initialize with potential in natural units""" def __init__(self, potlist=MWPotential): self._potlist = potlist Potential.__init__(self, amp=1.0) self.isNonAxi = _isNonAxi(self._potlist) return None def _evaluate(self, R, z, phi=0, t=0, dR=0, dphi=0): return evaluatePotentials(self._potlist, R, z, phi=phi, t=t, dR=dR, dphi=dphi) def _Rforce(self, R, z, phi=0.0, t=0.0): return evaluateRforces(self._potlist, R, z, phi=phi, t=t) def _phitorque(self, R, z, phi=0.0, t=0.0): return evaluatephitorques(self._potlist, R, z, phi=phi, t=t) def _zforce(self, R, z, phi=0.0, t=0.0): return evaluatezforces(self._potlist, R, z, phi=phi, t=t) def _R2deriv(self, R, z, phi=0.0, t=0.0): return evaluateR2derivs(self._potlist, R, z, phi=phi, t=t) def _z2deriv(self, R, z, phi=0.0, t=0.0): return evaluatez2derivs(self._potlist, R, z, phi=phi, t=t) def _Rzderiv(self, R, z, phi=0.0, t=0.0): return evaluateRzderivs(self._potlist, R, z, phi=phi, t=t) def _phi2deriv(self, R, z, phi=0.0, t=0.0): return evaluatePotentials(self._potlist, R, z, phi=phi, t=t, dphi=2) def _Rphideriv(self, R, z, phi=0.0, t=0.0): return evaluatePotentials(self._potlist, R, z, phi=phi, t=t, dR=1, dphi=1) def _phizderiv(self, R, z, phi=0.0, t=0.0): return evaluatephizderivs(self._potlist, R, z, phi=phi, t=t) def _dens(self, R, z, phi=0.0, t=0.0, forcepoisson=False): return evaluateDensities( self._potlist, R, z, phi=phi, t=t, forcepoisson=forcepoisson ) def _surfdens(self, R, z, phi=0.0, t=0.0, forcepoisson=False): return evaluateSurfaceDensities( self._potlist, R, z, phi=phi, t=t, forcepoisson=forcepoisson ) def vcirc(self, R): return potential.vcirc(self._potlist, R) def normalize(self, norm, t=0.0): self._amp = norm def OmegaP(self): return 1.0 # Class to test lists of planarPotentials class testplanarMWPotential(planarPotential): """Initialize with potential in natural units""" def __init__(self, potlist=MWPotential): self._potlist = [p.toPlanar() for p in potlist if isinstance(p, Potential)] self._potlist.extend([p for p in potlist if isinstance(p, planarPotential)]) planarPotential.__init__(self, amp=1.0) self.isNonAxi = _isNonAxi(self._potlist) return None def _evaluate(self, R, phi=0, t=0, dR=0, dphi=0): return evaluateplanarPotentials(self._potlist, R, phi=phi, t=t) def _Rforce(self, R, phi=0.0, t=0.0): return evaluateplanarRforces(self._potlist, R, phi=phi, t=t) def _phitorque(self, R, phi=0.0, t=0.0): return evaluateplanarphitorques(self._potlist, R, phi=phi, t=t) def _R2deriv(self, R, phi=0.0, t=0.0): return evaluateplanarR2derivs(self._potlist, R, phi=phi, t=t) def _phi2deriv(self, R, phi=0.0, t=0.0): return evaluateplanarPotentials(self._potlist, R, phi=phi, t=t, dphi=2) def _Rphideriv(self, R, phi=0.0, t=0.0): return evaluateplanarPotentials(self._potlist, R, phi=phi, t=t, dR=1, dphi=1) def vcirc(self, R): return potential.vcirc(self._potlist, R) def normalize(self, norm, t=0.0): self._amp = norm def OmegaP(self): return 1.0 class mockFlatEllipticalDiskPotential(testplanarMWPotential): def __init__(self): testplanarMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.EllipticalDiskPotential( phib=numpy.pi / 2.0, p=0.0, tform=None, tsteady=None, twophio=14.0 / 220.0, ), ], ) def OmegaP(self): return 0.0 class mockSlowFlatEllipticalDiskPotential(testplanarMWPotential): def __init__(self): testplanarMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.EllipticalDiskPotential( phib=numpy.pi / 2.0, p=0.0, twophio=14.0 / 220.0, tform=1.0, tsteady=250.0, ), ], ) def OmegaP(self): return 0.0 class mockFlatLopsidedDiskPotential(testplanarMWPotential): def __init__(self): testplanarMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.LopsidedDiskPotential( phib=numpy.pi / 2.0, p=0.0, phio=10.0 / 220.0 ), ], ) def OmegaP(self): return 0.0 class mockFlatCosmphiDiskPotential(testplanarMWPotential): def __init__(self): testplanarMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.CosmphiDiskPotential( phib=numpy.pi / 2.0, p=0.0, phio=10.0 / 220.0 ), ], ) def OmegaP(self): return 0.0 class mockFlatCosmphiDiskwBreakPotential(testplanarMWPotential): def __init__(self): testplanarMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.CosmphiDiskPotential( phib=numpy.pi / 2.0, p=0.0, phio=10.0 / 220.0, rb=0.99, m=6 ), ], ) def OmegaP(self): return 0.0 class mockFlatDehnenBarPotential(testMWPotential): def __init__(self): testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.DehnenBarPotential(), ], ) def OmegaP(self): return self._potlist[1].OmegaP() class mockSlowFlatDehnenBarPotential(testMWPotential): def __init__(self): testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.DehnenBarPotential(tform=1.0, tsteady=250.0, rolr=2.5), ], ) def OmegaP(self): return self._potlist[1].OmegaP() class mockFlatSteadyLogSpiralPotential(testplanarMWPotential): def __init__(self): testplanarMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.SteadyLogSpiralPotential(), ], ) def OmegaP(self): return self._potlist[1].OmegaP() class mockSlowFlatSteadyLogSpiralPotential(testplanarMWPotential): def __init__(self): testplanarMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.SteadyLogSpiralPotential(tform=0.1, tsteady=25.0), ], ) def OmegaP(self): return self._potlist[1].OmegaP() class mockFlatTransientLogSpiralPotential(testplanarMWPotential): def __init__(self): testplanarMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.TransientLogSpiralPotential(to=-10.0), ], ) # this way, it's basically a steady spiral def OmegaP(self): return self._potlist[1].OmegaP() class mockFlatSpiralArmsPotential(testMWPotential): def __init__(self): testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.SpiralArmsPotential(), ], ) def OmegaP(self): return self._potlist[1].OmegaP() class mockRotatingFlatSpiralArmsPotential(testMWPotential): def __init__(self): testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.SpiralArmsPotential(omega=1.3), ], ) def OmegaP(self): return self._potlist[1].OmegaP() class mockSpecialRotatingFlatSpiralArmsPotential(testMWPotential): def __init__(self): testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), potential.SpiralArmsPotential( omega=1.3, N=4, Cs=[8 / 3 / numpy.pi, 1 / 2, 8 / 15 / numpy.pi] ), ], ) def OmegaP(self): return self._potlist[1].OmegaP() # Class to test lists of linearPotentials from galpy.potential import ( RZToverticalPotential, evaluatelinearForces, evaluatelinearPotentials, linearPotential, ) class testlinearMWPotential(linearPotential): """Initialize with potential in natural units""" def __init__(self, potlist=MWPotential): self._potlist = RZToverticalPotential(potlist, 1.0) linearPotential.__init__(self, amp=1.0) return None def _evaluate(self, R, phi=0, t=0, dR=0, dphi=0): return evaluatelinearPotentials(self._potlist, R, t=t) def _force(self, R, t=0.0): return evaluatelinearForces(self._potlist, R, t=t) def normalize(self, norm, t=0.0): self._amp = norm class mockCombLinearPotential(testlinearMWPotential): def __init__(self): testlinearMWPotential.__init__( self, potlist=[ potential.MWPotential[0], potential.MWPotential[1].toVertical(1.0), potential.MWPotential[2].toVertical(1.0), ], ) class mockSimpleLinearPotential(testlinearMWPotential): def __init__(self): testlinearMWPotential.__init__( self, potlist=potential.MiyamotoNagaiPotential(normalize=1.0).toVertical(1.0), ) from galpy.potential import PlummerPotential class mockMovingObjectPotential(testMWPotential): def __init__(self, rc=0.75, maxt=1.0, nt=50): from galpy.orbit import Orbit self._rc = rc o1 = Orbit([self._rc, 0.0, 1.0, 0.0, 0.0, 0.0]) o2 = Orbit([self._rc, 0.0, 1.0, 0.0, 0.0, numpy.pi]) lp = potential.LogarithmicHaloPotential(normalize=1.0) times = numpy.linspace(0.0, maxt, nt) o1.integrate(times, lp, method="dopr54_c") o2.integrate(times, lp, method="dopr54_c") self._o1p = potential.MovingObjectPotential(o1) self._o2p = potential.MovingObjectPotential(o2) testMWPotential.__init__(self, [self._o1p, self._o2p]) self.isNonAxi = True return None def phi2deriv(self, R, z, phi=0.0, t=0.0): raise AttributeError def OmegaP(self): return 1.0 / self._rc class mockMovingObjectPotentialExplPlummer(testMWPotential): def __init__(self, rc=0.75, maxt=1.0, nt=50): from galpy.orbit import Orbit self._rc = rc o1 = Orbit([self._rc, 0.0, 1.0, 0.0, 0.0, 0.0]) o2 = Orbit([self._rc, 0.0, 1.0, 0.0, 0.0, numpy.pi]) lp = potential.LogarithmicHaloPotential(normalize=1.0) times = numpy.linspace(0.0, maxt, nt) o1.integrate(times, lp, method="dopr54_c") o2.integrate(times, lp, method="dopr54_c") oplum = potential.PlummerPotential(amp=0.06, b=0.01) self._o1p = potential.MovingObjectPotential(o1, pot=oplum) self._o2p = potential.MovingObjectPotential(o2, pot=oplum) testMWPotential.__init__(self, [self._o1p, self._o2p]) self.isNonAxi = True return None def phi2deriv(self, R, z, phi=0.0, t=0.0): raise AttributeError def OmegaP(self): return 1.0 / self._rc class mockMovingObjectLongIntPotential(mockMovingObjectPotential): def __init__(self, rc=0.75): mockMovingObjectPotential.__init__(self, rc=rc, maxt=15.0, nt=3001) return None # Classes to test wrappers from galpy.potential import ( AdiabaticContractionWrapperPotential, CorotatingRotationWrapperPotential, DehnenSmoothWrapperPotential, GaussianAmplitudeWrapperPotential, KuzminLikeWrapperPotential, RotateAndTiltWrapperPotential, SolidBodyRotationWrapperPotential, TimeDependentAmplitudeWrapperPotential, ) from galpy.potential.WrapperPotential import parentWrapperPotential class DehnenSmoothDehnenBarPotential(DehnenSmoothWrapperPotential): # This wrapped potential should be the same as the default DehnenBar # for t > -99 # # Need to use __new__ because new Wrappers are created using __new__ def __new__(cls, *args, **kwargs): if kwargs.get("_init", False): return parentWrapperPotential.__new__(cls, *args, **kwargs) dpn = DehnenBarPotential(tform=-100.0, tsteady=1.0) # on after t=-99 return DehnenSmoothWrapperPotential.__new__( cls, amp=1.0, pot=dpn, tform=-4.0 * 2.0 * numpy.pi / dpn.OmegaP() ) # Additional DehnenSmooth instances to catch all smoothing cases class mockDehnenSmoothBarPotentialT1(DehnenSmoothWrapperPotential): def __new__(cls, *args, **kwargs): if kwargs.get("_init", False): return parentWrapperPotential.__new__(cls, *args, **kwargs) dpn = DehnenBarPotential( omegab=1.9, rb=0.4, barphi=25.0 * numpy.pi / 180.0, beta=0.0, alpha=0.01, Af=0.04, tform=-99.0, tsteady=1.0, ) return DehnenSmoothWrapperPotential.__new__( cls, amp=1.0, pot=dpn, # tform=-4.*2.*numpy.pi/dpn.OmegaP()) tform=0.5, tsteady=0.5, ) class mockDehnenSmoothBarPotentialTm1(DehnenSmoothWrapperPotential): def __new__(cls, *args, **kwargs): if kwargs.get("_init", False): return parentWrapperPotential.__new__(cls, *args, **kwargs) dpn = DehnenBarPotential( omegab=1.9, rb=0.4, barphi=25.0 * numpy.pi / 180.0, beta=0.0, alpha=0.01, Af=0.04, tform=-99.0, tsteady=1.0, ) return DehnenSmoothWrapperPotential.__new__( cls, amp=1.0, pot=dpn, tform=-1.0, tsteady=1.01 ) class mockDehnenSmoothBarPotentialTm5(DehnenSmoothWrapperPotential): def __new__(cls, *args, **kwargs): if kwargs.get("_init", False): return parentWrapperPotential.__new__(cls, *args, **kwargs) dpn = DehnenBarPotential( omegab=1.9, rb=0.4, barphi=25.0 * numpy.pi / 180.0, beta=0.0, alpha=0.01, Af=0.04, tform=-99.0, tsteady=1.0, ) return DehnenSmoothWrapperPotential.__new__( cls, amp=1.0, pot=dpn, tform=-5.0, tsteady=2.0 ) class mockDehnenSmoothBarPotentialDecay(DehnenSmoothWrapperPotential): def __new__(cls, *args, **kwargs): if kwargs.get("_init", False): return parentWrapperPotential.__new__(cls, *args, **kwargs) dpn = DehnenBarPotential( omegab=1.9, rb=0.4, barphi=25.0 * numpy.pi / 180.0, beta=0.0, alpha=0.01, Af=0.04, tform=-99.0, tsteady=1.0, ) return DehnenSmoothWrapperPotential.__new__( cls, amp=1.0, pot=dpn, # tform=-4.*2.*numpy.pi/dpn.OmegaP()) tform=-0.5, tsteady=1.0, decay=True, ) class mockFlatDehnenSmoothBarPotential(testMWPotential): def __init__(self): dpn = DehnenBarPotential( omegab=1.9, rb=0.4, barphi=25.0 * numpy.pi / 180.0, beta=0.0, alpha=0.01, Af=0.04, tform=-99.0, tsteady=1.0, ) testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), DehnenSmoothWrapperPotential( amp=1.0, pot=dpn, tform=-4.0 * 2.0 * numpy.pi / dpn.OmegaP(), tsteady=2.0 * 2 * numpy.pi / dpn.OmegaP(), ), ], ) def OmegaP(self): return self._potlist[1]._pot.OmegaP() class mockSlowFlatDehnenSmoothBarPotential(testMWPotential): def __init__(self): dpn = DehnenBarPotential( omegab=1.9, rb=0.4, barphi=25.0 * numpy.pi / 180.0, beta=0.0, alpha=0.01, Af=0.04, tform=-99.0, tsteady=1.0, ) testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), DehnenSmoothWrapperPotential( amp=1.0, pot=dpn, tform=0.1, tsteady=500.0 ), ], ) def OmegaP(self): return self._potlist[1]._pot.OmegaP() class mockSlowFlatDecayingDehnenSmoothBarPotential(testMWPotential): def __init__(self): dpn = DehnenBarPotential( omegab=1.9, rb=0.4, barphi=25.0 * numpy.pi / 180.0, beta=0.0, alpha=0.01, Af=0.04, tform=-99.0, tsteady=1.0, ) testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), DehnenSmoothWrapperPotential( amp=1.0, pot=dpn, tform=-250.0, tsteady=500.0, decay=True ), ], ) def OmegaP(self): return self._potlist[1]._pot.OmegaP() # A DehnenSmoothWrappered version of LogarithmicHaloPotential for simple aAtest class mockSmoothedLogarithmicHaloPotential(DehnenSmoothWrapperPotential): def __new__(cls, *args, **kwargs): if kwargs.get("_init", False): return parentWrapperPotential.__new__(cls, *args, **kwargs) return DehnenSmoothWrapperPotential.__new__( cls, amp=1.0, pot=potential.LogarithmicHaloPotential(normalize=1.0), tform=-1.0, tsteady=0.5, ) # SolidBodyWrapperPotential class SolidBodyRotationSpiralArmsPotential(SolidBodyRotationWrapperPotential): def __new__(cls, *args, **kwargs): if kwargs.get("_init", False): return parentWrapperPotential.__new__(cls, *args, **kwargs) spn = potential.SpiralArmsPotential(omega=0.0, phi_ref=0.0) return SolidBodyRotationWrapperPotential.__new__( cls, amp=1.0, pot=spn.toPlanar(), omega=1.1, pa=0.4 ) class mockFlatSolidBodyRotationSpiralArmsPotential(testMWPotential): def __init__(self): testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), SolidBodyRotationWrapperPotential( amp=1.0, pot=potential.SpiralArmsPotential(), omega=1.3 ), ], ) def OmegaP(self): return self._potlist[1].OmegaP() # Special case to test handling of pure planarWrapper, not necessary for new wrappers class mockFlatSolidBodyRotationPlanarSpiralArmsPotential(testplanarMWPotential): def __init__(self): testplanarMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0).toPlanar(), SolidBodyRotationWrapperPotential( amp=1.0, pot=potential.SpiralArmsPotential().toPlanar(), omega=1.3 ), ], ) def OmegaP(self): return self._potlist[1].OmegaP() class testorbitHenonHeilesPotential(testplanarMWPotential): # Need this class, bc orbit tests skip potentials that do not have # .normalize, and HenonHeiles as a non-axi planarPotential instance # does not def __init__(self): testplanarMWPotential.__init__(self, potlist=[HenonHeilesPotential(amp=1.0)]) def OmegaP(self): # Non-axi, so need to set this to zero for Jacobi return 0.0 # CorotatingWrapperPotential class CorotatingRotationSpiralArmsPotential(CorotatingRotationWrapperPotential): def __new__(cls, *args, **kwargs): if kwargs.get("_init", False): return parentWrapperPotential.__new__(cls, *args, **kwargs) spn = potential.SpiralArmsPotential(omega=0.0, phi_ref=0.0) return CorotatingRotationWrapperPotential.__new__( cls, amp=1.0, pot=spn.toPlanar(), vpo=1.1, beta=-0.2, pa=0.4, to=3.0 ) class mockFlatCorotatingRotationSpiralArmsPotential(testMWPotential): # With beta=1 this has a fixed pattern speed --> Jacobi conserved def __init__(self): testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), CorotatingRotationWrapperPotential( amp=1.0, pot=potential.SpiralArmsPotential(), vpo=1.3, beta=1.0, pa=0.3, to=3.0, ), ], ) def OmegaP(self): return 1.3 # beta =/= 1 --> Liouville should still hold! class mockFlatTrulyCorotatingRotationSpiralArmsPotential(testMWPotential): # With beta=1 this has a fixed pattern speed --> Jacobi conserved def __init__(self): testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), CorotatingRotationWrapperPotential( amp=1.0, pot=potential.SpiralArmsPotential(), vpo=1.3, beta=0.1, pa=-0.3, to=-3.0, ), ], ) def OmegaP(self): return 1.3 # GaussianAmplitudeWrapperPotential class GaussianAmplitudeDehnenBarPotential(GaussianAmplitudeWrapperPotential): # Need to use __new__ because new Wrappers are created using __new__ def __new__(cls, *args, **kwargs): if kwargs.get("_init", False): return parentWrapperPotential.__new__(cls, *args, **kwargs) dpn = DehnenBarPotential(tform=-100.0, tsteady=1.0) # on after t=-99 return GaussianAmplitudeWrapperPotential.__new__( cls, amp=1.0, pot=dpn, to=0.0, sigma=1.0 ) # Basically constant class mockFlatGaussianAmplitudeBarPotential(testMWPotential): def __init__(self): dpn = DehnenBarPotential( omegab=1.9, rb=0.4, barphi=25.0 * numpy.pi / 180.0, beta=0.0, alpha=0.01, Af=0.04, tform=-99.0, tsteady=1.0, ) testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), GaussianAmplitudeWrapperPotential( amp=1.0, pot=dpn, to=10, sigma=1000000.0 ), ], ) def OmegaP(self): return self._potlist[1]._pot.OmegaP() # For Liouville class mockFlatTrulyGaussianAmplitudeBarPotential(testMWPotential): def __init__(self): dpn = DehnenBarPotential( omegab=1.9, rb=0.4, barphi=25.0 * numpy.pi / 180.0, beta=0.0, alpha=0.01, Af=0.04, tform=-99.0, tsteady=1.0, ) testMWPotential.__init__( self, potlist=[ potential.LogarithmicHaloPotential(normalize=1.0), GaussianAmplitudeWrapperPotential(amp=1.0, pot=dpn, to=10, sigma=1.0), ], ) def OmegaP(self): return self._potlist[1]._pot.OmegaP() # A GaussianAmplitudeWrappered version of LogarithmicHaloPotential for simple aAtest class mockGaussianAmplitudeSmoothedLogarithmicHaloPotential( GaussianAmplitudeWrapperPotential ): def __new__(cls, *args, **kwargs): if kwargs.get("_init", False): return parentWrapperPotential.__new__(cls, *args, **kwargs) return GaussianAmplitudeWrapperPotential.__new__( cls, amp=1.0, pot=potential.LogarithmicHaloPotential(normalize=1.0), to=0.0, sigma=100000000000000.0, ) class nestedListPotential(testMWPotential): def __init__(self): testMWPotential.__init__( self, potlist=[potential.MWPotential2014, potential.SpiralArmsPotential()] ) def OmegaP(self): return self._potlist[1].OmegaP() class mockAdiabaticContractionMWP14WrapperPotential( AdiabaticContractionWrapperPotential ): def __init__(self): AdiabaticContractionWrapperPotential.__init__( self, pot=potential.MWPotential2014[2], baryonpot=potential.MWPotential2014[:2], f_bar=None, ) class mockAdiabaticContractionMWP14ExplicitfbarWrapperPotential( AdiabaticContractionWrapperPotential ): def __init__(self): AdiabaticContractionWrapperPotential.__init__( self, pot=potential.MWPotential2014[2], baryonpot=potential.MWPotential2014[:2], f_bar=0.1, ) def normalize(self, norm): self._amp *= norm / numpy.fabs(self.Rforce(1.0, 0.0, use_physical=False)) class mockRotatedAndTiltedMWP14WrapperPotential(testMWPotential): def __init__(self): testMWPotential.__init__( self, potlist=[ RotateAndTiltWrapperPotential( pot=potential.MWPotential2014, zvec=[ numpy.sqrt(1 / 3.0), numpy.sqrt(1 / 3.0), numpy.sqrt(1 / 3.0), ], galaxy_pa=0.4, ) ], ) def OmegaP(self): return 0.0 class mockRotatedAndTiltedMWP14WrapperPotentialwInclination(testMWPotential): def __init__(self): testMWPotential.__init__( self, potlist=[ RotateAndTiltWrapperPotential( pot=potential.MWPotential2014, inclination=2.0, galaxy_pa=0.3, sky_pa=None, ) ], ) def OmegaP(self): return 0.0 class mockRotatedAndTiltedTriaxialLogHaloPotentialwInclination(testMWPotential): def __init__(self): testMWPotential.__init__( self, potlist=[ RotateAndTiltWrapperPotential( pot=potential.LogarithmicHaloPotential(normalize=1.0, b=0.7, q=0.5), inclination=2.0, galaxy_pa=0.3, sky_pa=None, ) ], ) def OmegaP(self): return 0.0 class mockRotatedTiltedOffsetMWP14WrapperPotential(testMWPotential): def __init__(self): testMWPotential.__init__( self, potlist=[ RotateAndTiltWrapperPotential( pot=potential.MWPotential2014, zvec=[ numpy.sqrt(1 / 3.0), numpy.sqrt(1 / 3.0), numpy.sqrt(1 / 3.0), ], galaxy_pa=0.4, offset=[1.0, 1.0, 1.0], ), ], ) def OmegaP(self): return 0.0 class mockOffsetMWP14WrapperPotential(testMWPotential): def __init__(self): testMWPotential.__init__( self, potlist=[ RotateAndTiltWrapperPotential( pot=potential.MWPotential2014, zvec=None, galaxy_pa=None, offset=[1.0, 1.0, 1.0], ), ], ) def OmegaP(self): return 0.0 # TimeDependentAmplitudeWrapperPotential class mockTimeDependentAmplitudeWrapperPotential( TimeDependentAmplitudeWrapperPotential ): # Need to use __new__ because new Wrappers are created using __new__ def __new__(cls, *args, **kwargs): if kwargs.get("_init", False): return parentWrapperPotential.__new__(cls, *args, **kwargs) dpn = DehnenBarPotential(tform=-100.0, tsteady=1.0) # on after t=-99 dps = DehnenSmoothWrapperPotential( pot=dpn, tform=-4.0 * 2.0 * numpy.pi / dpn.OmegaP() ) return DehnenSmoothWrapperPotential.__new__( cls, amp=1.0, pot=dpn, A=dps._smooth ) # A TimeDependentAmplitudeWrapperPotential version of LogarithmicHaloPotential for simple aAtest class mockSmoothedLogarithmicHaloPotentialwTimeDependentAmplitudeWrapperPotential( TimeDependentAmplitudeWrapperPotential ): def __new__(cls, *args, **kwargs): if kwargs.get("_init", False): return parentWrapperPotential.__new__(cls, *args, **kwargs) dps = DehnenSmoothWrapperPotential( pot=potential.LogarithmicHaloPotential(normalize=1.0), tform=-1.0, tsteady=0.5, ) return TimeDependentAmplitudeWrapperPotential.__new__( cls, amp=1.0, pot=potential.LogarithmicHaloPotential(normalize=1.0), A=dps._smooth, ) class mockKuzminLikeWrapperPotential(KuzminLikeWrapperPotential): def __init__(self): KuzminLikeWrapperPotential.__init__( self, pot=LogarithmicHaloPotential(normalize=1.0), a=1.0, b=0.1, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_pv2qdf.py0000644000175100001660000004640714761352023016305 0ustar00runnerdocker# Tests of the quasiisothermaldf module import numpy from galpy.actionAngle import actionAngleAdiabatic, actionAngleStaeckel from galpy.df import quasiisothermaldf # fiducial setup uses these from galpy.potential import MWPotential, epifreq, omegac, vcirc, verticalfreq aAA = actionAngleAdiabatic(pot=MWPotential, c=True) aAS = actionAngleStaeckel(pot=MWPotential, c=True, delta=0.5) def test_pvRvT_adiabatic(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) R, z = 0.8, 0.1 vRs = numpy.linspace(-1.0, 1.0, 21) vTs = numpy.linspace(0.0, 1.5, 51) pvRvT = numpy.array([[qdf.pvRvT(vr, vt, R, z) for vt in vTs] for vr in vRs]) tvR = numpy.tile(vRs, (len(vTs), 1)).T tvT = numpy.tile(vTs, (len(vRs), 1)) mvR = numpy.sum(tvR * pvRvT) / numpy.sum(pvRvT) mvT = numpy.sum(tvT * pvRvT) / numpy.sum(pvRvT) svR = numpy.sqrt(numpy.sum(tvR**2.0 * pvRvT) / numpy.sum(pvRvT) - mvR**2.0) svT = numpy.sqrt(numpy.sum(tvT**2.0 * pvRvT) / numpy.sum(pvRvT) - mvT**2.0) svRvT = (numpy.sum(tvR * tvT * pvRvT) / numpy.sum(pvRvT) - mvR * mvT) / svR / svT assert numpy.fabs(mvR) < 0.01, ( "mean vR calculated from pvRvT not equal to zero for adiabatic actions" ) assert numpy.fabs(mvT - qdf.meanvT(R, z)) < 0.01, ( "mean vT calculated from pvRvT not equal to zero for adiabatic actions" ) assert numpy.fabs(numpy.log(svR) - 0.5 * numpy.log(qdf.sigmaR2(R, z))) < 0.01, ( "sigma vR calculated from pvRvT not equal to that from sigmaR2 for adiabatic actions" ) assert numpy.fabs(numpy.log(svT) - 0.5 * numpy.log(qdf.sigmaT2(R, z))) < 0.01, ( "sigma vT calculated from pvRvT not equal to that from sigmaT2 for adiabatic actions" ) assert numpy.fabs(svRvT) < 0.01, ( "correlation between vR and vT calculated from pvRvT not equal to zero for adiabatic actions" ) return None def test_pvRvT_staeckel(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 vRs = numpy.linspace(-1.0, 1.0, 21) vTs = numpy.linspace(0.0, 1.5, 51) pvRvT = numpy.array([[qdf.pvRvT(vr, vt, R, z) for vt in vTs] for vr in vRs]) tvR = numpy.tile(vRs, (len(vTs), 1)).T tvT = numpy.tile(vTs, (len(vRs), 1)) mvR = numpy.sum(tvR * pvRvT) / numpy.sum(pvRvT) mvT = numpy.sum(tvT * pvRvT) / numpy.sum(pvRvT) svR = numpy.sqrt(numpy.sum(tvR**2.0 * pvRvT) / numpy.sum(pvRvT) - mvR**2.0) svT = numpy.sqrt(numpy.sum(tvT**2.0 * pvRvT) / numpy.sum(pvRvT) - mvT**2.0) svRvT = (numpy.sum(tvR * tvT * pvRvT) / numpy.sum(pvRvT) - mvR * mvT) / svR / svT assert numpy.fabs(mvR) < 0.01, ( "mean vR calculated from pvRvT not equal to zero for staeckel actions" ) assert numpy.fabs(mvT - qdf.meanvT(R, z)) < 0.01, ( "mean vT calculated from pvRvT not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svR) - 0.5 * numpy.log(qdf.sigmaR2(R, z))) < 0.01, ( "sigma vR calculated from pvRvT not equal to that from sigmaR2 for staeckel actions" ) assert numpy.fabs(numpy.log(svT) - 0.5 * numpy.log(qdf.sigmaT2(R, z))) < 0.01, ( "sigma vT calculated from pvRvT not equal to that from sigmaT2 for staeckel actions" ) assert numpy.fabs(svRvT) < 0.01, ( "correlation between vR and vT calculated from pvRvT not equal to zero for staeckel actions" ) return None def test_pvRvT_staeckel_diffngl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 vRs = numpy.linspace(-1.0, 1.0, 21) vTs = numpy.linspace(0.0, 1.5, 51) # ngl=10 pvRvT = numpy.array([[qdf.pvRvT(vr, vt, R, z, ngl=10) for vt in vTs] for vr in vRs]) tvR = numpy.tile(vRs, (len(vTs), 1)).T tvT = numpy.tile(vTs, (len(vRs), 1)) mvR = numpy.sum(tvR * pvRvT) / numpy.sum(pvRvT) mvT = numpy.sum(tvT * pvRvT) / numpy.sum(pvRvT) svR = numpy.sqrt(numpy.sum(tvR**2.0 * pvRvT) / numpy.sum(pvRvT) - mvR**2.0) svT = numpy.sqrt(numpy.sum(tvT**2.0 * pvRvT) / numpy.sum(pvRvT) - mvT**2.0) svRvT = (numpy.sum(tvR * tvT * pvRvT) / numpy.sum(pvRvT) - mvR * mvT) / svR / svT assert numpy.fabs(mvR) < 0.01, ( "mean vR calculated from pvRvT not equal to zero for staeckel actions" ) assert numpy.fabs(mvT - qdf.meanvT(R, z)) < 0.01, ( "mean vT calculated from pvRvT not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svR) - 0.5 * numpy.log(qdf.sigmaR2(R, z))) < 0.01, ( "sigma vR calculated from pvRvT not equal to that from sigmaR2 for staeckel actions" ) assert numpy.fabs(numpy.log(svT) - 0.5 * numpy.log(qdf.sigmaT2(R, z))) < 0.01, ( "sigma vT calculated from pvRvT not equal to that from sigmaT2 for staeckel actions" ) assert numpy.fabs(svRvT) < 0.01, ( "correlation between vR and vT calculated from pvRvT not equal to zero for staeckel actions" ) # ngl=24 pvRvT = numpy.array([[qdf.pvRvT(vr, vt, R, z, ngl=40) for vt in vTs] for vr in vRs]) mvR = numpy.sum(tvR * pvRvT) / numpy.sum(pvRvT) mvT = numpy.sum(tvT * pvRvT) / numpy.sum(pvRvT) svR = numpy.sqrt(numpy.sum(tvR**2.0 * pvRvT) / numpy.sum(pvRvT) - mvR**2.0) svT = numpy.sqrt(numpy.sum(tvT**2.0 * pvRvT) / numpy.sum(pvRvT) - mvT**2.0) svRvT = (numpy.sum(tvR * tvT * pvRvT) / numpy.sum(pvRvT) - mvR * mvT) / svR / svT assert numpy.fabs(mvR) < 0.01, ( "mean vR calculated from pvRvT not equal to zero for staeckel actions" ) assert numpy.fabs(mvT - qdf.meanvT(R, z)) < 0.01, ( "mean vT calculated from pvRvT not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svR) - 0.5 * numpy.log(qdf.sigmaR2(R, z))) < 0.01, ( "sigma vR calculated from pvRvT not equal to that from sigmaR2 for staeckel actions" ) assert numpy.fabs(numpy.log(svT) - 0.5 * numpy.log(qdf.sigmaT2(R, z))) < 0.01, ( "sigma vT calculated from pvRvT not equal to that from sigmaT2 for staeckel actions" ) assert numpy.fabs(svRvT) < 0.01, ( "correlation between vR and vT calculated from pvRvT not equal to zero for staeckel actions" ) # ngl=11, shouldn't work try: pvRvT = numpy.array( [[qdf.pvRvT(vr, vt, R, z, ngl=11) for vt in vTs] for vr in vRs] ) except ValueError: pass else: raise AssertionError("pvz w/ ngl=odd did not raise ValueError") return None def test_pvTvz_adiabatic(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) R, z = 0.8, 0.1 vTs = numpy.linspace(0.0, 1.5, 51) vzs = numpy.linspace(-1.0, 1.0, 21) pvTvz = numpy.array([[qdf.pvTvz(vt, vz, R, z) for vt in vTs] for vz in vzs]) tvT = numpy.tile(vTs, (len(vzs), 1)) tvz = numpy.tile(vzs, (len(vTs), 1)).T mvz = numpy.sum(tvz * pvTvz) / numpy.sum(pvTvz) mvT = numpy.sum(tvT * pvTvz) / numpy.sum(pvTvz) svz = numpy.sqrt(numpy.sum(tvz**2.0 * pvTvz) / numpy.sum(pvTvz) - mvz**2.0) svT = numpy.sqrt(numpy.sum(tvT**2.0 * pvTvz) / numpy.sum(pvTvz) - mvT**2.0) svTvz = (numpy.sum(tvz * tvT * pvTvz) / numpy.sum(pvTvz) - mvz * mvT) / svz / svT assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvTvz not equal to zero for adiabatic actions" ) assert numpy.fabs(mvT - qdf.meanvT(R, z)) < 0.01, ( "mean vT calculated from pvTvz not equal to zero for adiabatic actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(qdf.sigmaz2(R, z))) < 0.01, ( "sigma vz calculated from pvTvz not equal to that from sigmaz2 for adiabatic actions" ) assert numpy.fabs(numpy.log(svT) - 0.5 * numpy.log(qdf.sigmaT2(R, z))) < 0.01, ( "sigma vT calculated from pvTvz not equal to that from sigmaT2 for adiabatic actions" ) assert numpy.fabs(svTvz) < 0.01, ( "correlation between vz and vT calculated from pvTvz not equal to zero for adiabatic actions" ) return None def test_pvTvz_staeckel(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 vzs = numpy.linspace(-1.0, 1.0, 21) vTs = numpy.linspace(0.0, 1.5, 51) pvTvz = numpy.array([[qdf.pvTvz(vt, vz, R, z) for vt in vTs] for vz in vzs]) tvz = numpy.tile(vzs, (len(vTs), 1)).T tvT = numpy.tile(vTs, (len(vzs), 1)) mvz = numpy.sum(tvz * pvTvz) / numpy.sum(pvTvz) mvT = numpy.sum(tvT * pvTvz) / numpy.sum(pvTvz) svz = numpy.sqrt(numpy.sum(tvz**2.0 * pvTvz) / numpy.sum(pvTvz) - mvz**2.0) svT = numpy.sqrt(numpy.sum(tvT**2.0 * pvTvz) / numpy.sum(pvTvz) - mvT**2.0) svTvz = (numpy.sum(tvz * tvT * pvTvz) / numpy.sum(pvTvz) - mvz * mvT) / svz / svT assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvTvz not equal to zero for staeckel actions" ) assert numpy.fabs(mvT - qdf.meanvT(R, z)) < 0.01, ( "mean vT calculated from pvTvz not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(qdf.sigmaz2(R, z))) < 0.01, ( "sigma vz calculated from pvTvz not equal to that from sigmaz2 for staeckel actions" ) assert numpy.fabs(numpy.log(svT) - 0.5 * numpy.log(qdf.sigmaT2(R, z))) < 0.01, ( "sigma vT calculated from pvTvz not equal to that from sigmaT2 for staeckel actions" ) assert numpy.fabs(svTvz) < 0.01, ( "correlation between vz and vT calculated from pvTvz not equal to zero for staeckel actions" ) return None def test_pvTvz_staeckel_diffngl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 vzs = numpy.linspace(-1.0, 1.0, 21) vTs = numpy.linspace(0.0, 1.5, 51) # ngl=10 pvTvz = numpy.array([[qdf.pvTvz(vt, vz, R, z, ngl=10) for vt in vTs] for vz in vzs]) tvz = numpy.tile(vzs, (len(vTs), 1)).T tvT = numpy.tile(vTs, (len(vzs), 1)) mvz = numpy.sum(tvz * pvTvz) / numpy.sum(pvTvz) mvT = numpy.sum(tvT * pvTvz) / numpy.sum(pvTvz) svz = numpy.sqrt(numpy.sum(tvz**2.0 * pvTvz) / numpy.sum(pvTvz) - mvz**2.0) svT = numpy.sqrt(numpy.sum(tvT**2.0 * pvTvz) / numpy.sum(pvTvz) - mvT**2.0) svTvz = (numpy.sum(tvz * tvT * pvTvz) / numpy.sum(pvTvz) - mvz * mvT) / svz / svT assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvTvz not equal to zero for staeckel actions" ) assert numpy.fabs(mvT - qdf.meanvT(R, z)) < 0.01, ( "mean vT calculated from pvTvz not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(qdf.sigmaz2(R, z))) < 0.01, ( "sigma vz calculated from pvTvz not equal to that from sigmaz2 for staeckel actions" ) assert numpy.fabs(numpy.log(svT) - 0.5 * numpy.log(qdf.sigmaT2(R, z))) < 0.01, ( "sigma vT calculated from pvTvz not equal to that from sigmaT2 for staeckel actions" ) assert numpy.fabs(svTvz) < 0.01, ( "correlation between vz and vT calculated from pvTvz not equal to zero for staeckel actions" ) # ngl=24 pvTvz = numpy.array([[qdf.pvTvz(vt, vz, R, z, ngl=40) for vt in vTs] for vz in vzs]) mvz = numpy.sum(tvz * pvTvz) / numpy.sum(pvTvz) mvT = numpy.sum(tvT * pvTvz) / numpy.sum(pvTvz) svz = numpy.sqrt(numpy.sum(tvz**2.0 * pvTvz) / numpy.sum(pvTvz) - mvz**2.0) svT = numpy.sqrt(numpy.sum(tvT**2.0 * pvTvz) / numpy.sum(pvTvz) - mvT**2.0) svTvz = (numpy.sum(tvz * tvT * pvTvz) / numpy.sum(pvTvz) - mvz * mvT) / svz / svT assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvTvz not equal to zero for staeckel actions" ) assert numpy.fabs(mvT - qdf.meanvT(R, z)) < 0.01, ( "mean vT calculated from pvTvz not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(qdf.sigmaz2(R, z))) < 0.01, ( "sigma vz calculated from pvTvz not equal to that from sigmaz2 for staeckel actions" ) assert numpy.fabs(numpy.log(svT) - 0.5 * numpy.log(qdf.sigmaT2(R, z))) < 0.01, ( "sigma vT calculated from pvTvz not equal to that from sigmaT2 for staeckel actions" ) assert numpy.fabs(svTvz) < 0.01, ( "correlation between vz and vT calculated from pvTvz not equal to zero for staeckel actions" ) # ngl=11, shouldn't work try: pvTvz = numpy.array( [[qdf.pvTvz(vt, vz, R, z, ngl=11) for vt in vTs] for vz in vzs] ) except ValueError: pass else: raise AssertionError("pvz w/ ngl=odd did not raise ValueError") return None def test_pvRvz_adiabatic(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) R, z = 0.8, 0.1 vRs = numpy.linspace(-1.0, 1.0, 21) vzs = numpy.linspace(-1.0, 1.0, 21) pvRvz = numpy.array([[qdf.pvRvz(vr, vz, R, z) for vz in vzs] for vr in vRs]) tvR = numpy.tile(vRs, (len(vzs), 1)).T tvz = numpy.tile(vzs, (len(vRs), 1)) mvR = numpy.sum(tvR * pvRvz) / numpy.sum(pvRvz) mvz = numpy.sum(tvz * pvRvz) / numpy.sum(pvRvz) svR = numpy.sqrt(numpy.sum(tvR**2.0 * pvRvz) / numpy.sum(pvRvz) - mvR**2.0) svz = numpy.sqrt(numpy.sum(tvz**2.0 * pvRvz) / numpy.sum(pvRvz) - mvz**2.0) svRvz = (numpy.sum(tvR * tvz * pvRvz) / numpy.sum(pvRvz) - mvR * mvz) / svR / svz sR2 = qdf.sigmaR2(R, z) # direct calculation sz2 = qdf.sigmaz2(R, z) assert numpy.fabs(mvR) < 0.01, ( "mean vR calculated from pvRvz not equal to zero for adiabatic actions" ) assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvRvz not equal to zero for adiabatic actions" ) assert numpy.fabs(numpy.log(svR) - 0.5 * numpy.log(sR2)) < 0.01, ( "sigma vR calculated from pvRvz not equal to that from sigmaR2 for adiabatic actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(sz2)) < 0.01, ( "sigma vz calculated from pvRvz not equal to that from sigmaz2 for adiabatic actions" ) assert numpy.fabs(svRvz - qdf.sigmaRz(R, z) / numpy.sqrt(sR2 * sz2)) < 0.01, ( "correlation between vR and vz calculated from pvRvz not equal to zero for adiabatic actions" ) return None def test_pvRvz_staeckel(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 vRs = numpy.linspace(-1.0, 1.0, 21) vzs = numpy.linspace(-1.0, 1.0, 21) pvRvz = numpy.array([[qdf.pvRvz(vr, vz, R, z) for vz in vzs] for vr in vRs]) tvR = numpy.tile(vRs, (len(vzs), 1)).T tvz = numpy.tile(vzs, (len(vRs), 1)) mvR = numpy.sum(tvR * pvRvz) / numpy.sum(pvRvz) mvz = numpy.sum(tvz * pvRvz) / numpy.sum(pvRvz) svR = numpy.sqrt(numpy.sum(tvR**2.0 * pvRvz) / numpy.sum(pvRvz) - mvR**2.0) svz = numpy.sqrt(numpy.sum(tvz**2.0 * pvRvz) / numpy.sum(pvRvz) - mvz**2.0) svRvz = (numpy.sum(tvR * tvz * pvRvz) / numpy.sum(pvRvz) - mvR * mvz) / svR / svz sR2 = qdf.sigmaR2(R, z) # direct calculation sz2 = qdf.sigmaz2(R, z) assert numpy.fabs(mvR) < 0.01, ( "mean vR calculated from pvRvz not equal to zero for staeckel actions" ) assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvRvz not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svR) - 0.5 * numpy.log(sR2)) < 0.01, ( "sigma vR calculated from pvRvz not equal to that from sigmaR2 for staeckel actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(sz2)) < 0.01, ( "sigma vz calculated from pvRvz not equal to that from sigmaz2 for staeckel actions" ) assert numpy.fabs(svRvz - qdf.sigmaRz(R, z) / numpy.sqrt(sR2 * sz2)) < 0.01, ( "correlation between vR and vz calculated from pvRvz not equal to zero for adiabatic actions" ) return None def test_pvRvz_staeckel_diffngl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 vRs = numpy.linspace(-1.0, 1.0, 21) vzs = numpy.linspace(-1.0, 1.0, 21) # ngl=10 pvRvz = numpy.array([[qdf.pvRvz(vr, vz, R, z, ngl=10) for vz in vzs] for vr in vRs]) tvR = numpy.tile(vRs, (len(vzs), 1)).T tvz = numpy.tile(vzs, (len(vRs), 1)) mvR = numpy.sum(tvR * pvRvz) / numpy.sum(pvRvz) mvz = numpy.sum(tvz * pvRvz) / numpy.sum(pvRvz) svR = numpy.sqrt(numpy.sum(tvR**2.0 * pvRvz) / numpy.sum(pvRvz) - mvR**2.0) svz = numpy.sqrt(numpy.sum(tvz**2.0 * pvRvz) / numpy.sum(pvRvz) - mvz**2.0) svRvz = (numpy.sum(tvR * tvz * pvRvz) / numpy.sum(pvRvz) - mvR * mvz) / svR / svz sR2 = qdf.sigmaR2(R, z) # direct calculation sz2 = qdf.sigmaz2(R, z) assert numpy.fabs(mvR) < 0.01, ( "mean vR calculated from pvRvz not equal to zero for staeckel actions" ) assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvRvz not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svR) - 0.5 * numpy.log(sR2)) < 0.01, ( "sigma vR calculated from pvRvz not equal to that from sigmaR2 for staeckel actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(sz2)) < 0.01, ( "sigma vz calculated from pvRvz not equal to that from sigmaz2 for staeckel actions" ) assert numpy.fabs(svRvz - qdf.sigmaRz(R, z) / numpy.sqrt(sR2 * sz2)) < 0.01, ( "correlation between vR and vz calculated from pvRvz not equal to zero for adiabatic actions" ) # ngl=24 pvRvz = numpy.array([[qdf.pvRvz(vr, vz, R, z, ngl=40) for vz in vzs] for vr in vRs]) mvR = numpy.sum(tvR * pvRvz) / numpy.sum(pvRvz) mvz = numpy.sum(tvz * pvRvz) / numpy.sum(pvRvz) svR = numpy.sqrt(numpy.sum(tvR**2.0 * pvRvz) / numpy.sum(pvRvz) - mvR**2.0) svz = numpy.sqrt(numpy.sum(tvz**2.0 * pvRvz) / numpy.sum(pvRvz) - mvz**2.0) svRvz = (numpy.sum(tvR * tvz * pvRvz) / numpy.sum(pvRvz) - mvR * mvz) / svR / svz assert numpy.fabs(mvR) < 0.01, ( "mean vR calculated from pvRvz not equal to zero for staeckel actions" ) assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvRvz not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svR) - 0.5 * numpy.log(sR2)) < 0.01, ( "sigma vR calculated from pvRvz not equal to that from sigmaR2 for staeckel actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(sz2)) < 0.01, ( "sigma vz calculated from pvRvz not equal to that from sigmaz2 for staeckel actions" ) assert numpy.fabs(svRvz - qdf.sigmaRz(R, z) / numpy.sqrt(sR2 * sz2)) < 0.01, ( "correlation between vR and vz calculated from pvRvz not equal to zero for adiabatic actions" ) # ngl=11, shouldn't work try: pvRvz = numpy.array( [[qdf.pvRvz(vr, vz, R, z, ngl=11) for vz in vzs] for vr in vRs] ) except ValueError: pass else: raise AssertionError("pvz w/ ngl=odd did not raise ValueError") return None def test_pvRvz_staeckel_arrayin(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 pvRvz = qdf.pvRvz( 0.1 * numpy.ones(2), 0.05 * numpy.ones(2), R * numpy.ones(2), z * numpy.ones(2) ) assert numpy.all( numpy.fabs(numpy.log(pvRvz) - numpy.log(qdf.pvRvz(0.1, 0.05, R, z))) < 10.0**-10.0 ), ( "pvRvz calculated with R and z array input does not equal to calculated with scalar input" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_qdf.py0000644000175100001660000016251014761352023015647 0ustar00runnerdocker# Tests of the quasiisothermaldf module import numpy from galpy.actionAngle import actionAngleAdiabatic, actionAngleStaeckel from galpy.df import quasiisothermaldf # fiducial setup uses these from galpy.potential import MWPotential, epifreq, omegac, vcirc, verticalfreq aAA = actionAngleAdiabatic(pot=MWPotential, c=True) aAS = actionAngleStaeckel(pot=MWPotential, c=True, delta=0.5) def test_meanvR_adiabatic_gl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) # In the mid-plane assert numpy.fabs(qdf.meanvR(0.9, 0.0, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) # higher up assert numpy.fabs(qdf.meanvR(0.9, 0.2, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) assert numpy.fabs(qdf.meanvR(0.9, -0.25, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) return None def test_meanvR_adiabatic_mc(): numpy.random.seed(1) # test nested list of potentials qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=[MWPotential[0], MWPotential[1:]], aA=aAA, cutcounter=True, ) # In the mid-plane assert numpy.fabs(qdf.meanvR(0.9, 0.0, mc=True)) < 0.01, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) # higher up assert numpy.fabs(qdf.meanvR(0.9, 0.2, mc=True)) < 0.05, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) assert numpy.fabs(qdf.meanvR(0.9, -0.25, mc=True)) < 0.05, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) return None def test_meanvR_adiabatic_gl_center(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) # In the mid-plane assert numpy.fabs(qdf.meanvR(0.001, 0.0, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) return None def test_meanvR_staeckel_gl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # In the mid-plane assert numpy.fabs(qdf.meanvR(0.9, 0.0, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for staeckel approx." ) # higher up assert numpy.fabs(qdf.meanvR(0.9, 0.2, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for staeckel approx." ) assert numpy.fabs(qdf.meanvR(0.9, -0.25, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for staeckel approx." ) return None def test_meanvR_staeckel_mc(): numpy.random.seed(1) qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # In the mid-plane assert numpy.fabs(qdf.meanvR(0.9, 0.0, mc=True)) < 0.01, ( "qdf's meanvr is not equal to zero for staeckel approx." ) # higher up assert numpy.fabs(qdf.meanvR(0.9, 0.2, mc=True)) < 0.05, ( "qdf's meanvr is not equal to zero for staeckel approx." ) assert numpy.fabs(qdf.meanvR(0.9, -0.25, mc=True)) < 0.05, ( "qdf's meanvr is not equal to zero for staeckel approx." ) return None def test_meanvT_adiabatic_gl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) from galpy.df import dehnendf # baseline dfc = dehnendf(profileParams=(1.0 / 4.0, 1.0, 0.2), beta=0.0, correct=False) # In the mid-plane vtp9 = qdf.meanvT(0.9, 0.0, gl=True) assert numpy.fabs(vtp9 - dfc.meanvT(0.9)) < 0.05, ( "qdf's meanvT is not close to that of dehnendf" ) assert vtp9 < vcirc(MWPotential, 0.9), ( "qdf's meanvT is not less than the circular velocity (which we expect)" ) # higher up assert qdf.meanvR(0.9, 0.2, gl=True) < vtp9, ( "qdf's meanvT above the plane is not less than in the plane (which we expect)" ) assert qdf.meanvR(0.9, -0.25, gl=True) < vtp9, ( "qdf's meanvT above the plane is not less than in the plane (which we expect)" ) return None def test_meanvT_adiabatic_mc(): numpy.random.seed(1) qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) from galpy.df import dehnendf # baseline dfc = dehnendf(profileParams=(1.0 / 4.0, 1.0, 0.2), beta=0.0, correct=False) # In the mid-plane vtp9 = qdf.meanvT(0.9, 0.0, mc=True) assert numpy.fabs(vtp9 - dfc.meanvT(0.9)) < 0.05, ( "qdf's meanvT is not close to that of dehnendf" ) assert vtp9 < vcirc(MWPotential, 0.9), ( "qdf's meanvT is not less than the circular velocity (which we expect)" ) # higher up assert qdf.meanvR(0.9, 0.2, mc=True) < vtp9, ( "qdf's meanvT above the plane is not less than in the plane (which we expect)" ) assert qdf.meanvR(0.9, -0.25, mc=True) < vtp9, ( "qdf's meanvT above the plane is not less than in the plane (which we expect)" ) return None def test_meanvT_staeckel_gl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) from galpy.df import dehnendf # baseline dfc = dehnendf(profileParams=(1.0 / 4.0, 1.0, 0.2), beta=0.0, correct=False) # In the mid-plane vtp9 = qdf.meanvT(0.9, 0.0, gl=True) assert numpy.fabs(vtp9 - dfc.meanvT(0.9)) < 0.05, ( "qdf's meanvT is not close to that of dehnendf" ) assert vtp9 < vcirc(MWPotential, 0.9), ( "qdf's meanvT is not less than the circular velocity (which we expect)" ) # higher up assert qdf.meanvR(0.9, 0.2, gl=True) < vtp9, ( "qdf's meanvT above the plane is not less than in the plane (which we expect)" ) assert qdf.meanvR(0.9, -0.25, gl=True) < vtp9, ( "qdf's meanvT above the plane is not less than in the plane (which we expect)" ) return None def test_meanvT_staeckel_mc(): numpy.random.seed(1) qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) from galpy.df import dehnendf # baseline dfc = dehnendf(profileParams=(1.0 / 4.0, 1.0, 0.2), beta=0.0, correct=False) # In the mid-plane vtp9 = qdf.meanvT(0.9, 0.0, mc=True) assert numpy.fabs(vtp9 - dfc.meanvT(0.9)) < 0.05, ( "qdf's meanvT is not close to that of dehnendf" ) assert vtp9 < vcirc(MWPotential, 0.9), ( "qdf's meanvT is not less than the circular velocity (which we expect)" ) # higher up assert qdf.meanvR(0.9, 0.2, mc=True) < vtp9, ( "qdf's meanvT above the plane is not less than in the plane (which we expect)" ) assert qdf.meanvR(0.9, -0.25, mc=True) < vtp9, ( "qdf's meanvT above the plane is not less than in the plane (which we expect)" ) return None def test_meanvz_adiabatic_gl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) # In the mid-plane assert numpy.fabs(qdf.meanvz(0.9, 0.0, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) # higher up assert numpy.fabs(qdf.meanvz(0.9, 0.2, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) assert numpy.fabs(qdf.meanvz(0.9, -0.25, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) return None def test_meanvz_adiabatic_mc(): numpy.random.seed(1) qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) # In the mid-plane assert numpy.fabs(qdf.meanvz(0.9, 0.0, mc=True)) < 0.01, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) # higher up assert numpy.fabs(qdf.meanvz(0.9, 0.2, mc=True)) < 0.05, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) assert numpy.fabs(qdf.meanvz(0.9, -0.25, mc=True)) < 0.05, ( "qdf's meanvr is not equal to zero for adiabatic approx." ) return None def test_meanvz_staeckel_gl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # In the mid-plane assert numpy.fabs(qdf.meanvz(0.9, 0.0, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for staeckel approx." ) # higher up assert numpy.fabs(qdf.meanvz(0.9, 0.2, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for staeckel approx." ) assert numpy.fabs(qdf.meanvz(0.9, -0.25, gl=True)) < 0.01, ( "qdf's meanvr is not equal to zero for staeckel approx." ) return None def test_meanvz_staeckel_mc(): numpy.random.seed(1) qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # In the mid-plane assert numpy.fabs(qdf.meanvz(0.9, 0.0, mc=True)) < 0.01, ( "qdf's meanvr is not equal to zero for staeckel approx." ) # higher up assert numpy.fabs(qdf.meanvz(0.9, 0.2, mc=True)) < 0.05, ( "qdf's meanvr is not equal to zero for staeckel approx." ) assert numpy.fabs(qdf.meanvz(0.9, -0.25, mc=True)) < 0.05, ( "qdf's meanvr is not equal to zero for staeckel approx." ) return None def test_sigmar_staeckel_gl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # In the mid-plane assert ( numpy.fabs( numpy.log(qdf.sigmaR2(0.9, 0.0, gl=True)) - 2.0 * numpy.log(0.2) - 0.2 ) < 0.2 ), "qdf's sigmaR2 deviates more than expected from input for staeckel approx." # higher up, also w/ different ngl assert ( numpy.fabs( numpy.log(qdf.sigmaR2(0.9, 0.2, gl=True, ngl=20)) - 2.0 * numpy.log(0.2) - 0.2 ) < 0.3 ), "qdf's sigmaR2 deviates more than expected from input for staeckel approx." assert ( numpy.fabs( numpy.log(qdf.sigmaR2(0.9, -0.25, gl=True, ngl=24)) - 2.0 * numpy.log(0.2) - 0.2 ) < 0.3 ), "qdf's sigmaR2 deviates more than expected from input for staeckel approx." return None def test_sigmar_staeckel_mc(): numpy.random.seed(1) qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # In the mid-plane assert ( numpy.fabs( numpy.log(qdf.sigmaR2(0.9, 0.0, mc=True)) - 2.0 * numpy.log(0.2) - 0.2 ) < 0.2 ), "qdf's sigmaR2 deviates more than expected from input for staeckel approx." # higher up assert ( numpy.fabs( numpy.log(qdf.sigmaR2(0.9, 0.2, mc=True)) - 2.0 * numpy.log(0.2) - 0.2 ) < 0.4 ), "qdf's sigmaR2 deviates more than expected from input for staeckel approx." assert ( numpy.fabs( numpy.log(qdf.sigmaR2(0.9, -0.25, mc=True)) - 2.0 * numpy.log(0.2) - 0.2 ) < 0.3 ), "qdf's sigmaR2 deviates more than expected from input for staeckel approx." return None def test_sigmat_staeckel_gl(): # colder, st closer to epicycle expectation qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # In the mid-plane gamma = 2.0 * omegac(MWPotential, 0.9) / epifreq(MWPotential, 0.9) assert ( numpy.fabs( numpy.log(qdf.sigmaT2(0.9, 0.0, gl=True) / qdf.sigmaR2(0.9, 0.0, gl=True)) + 2.0 * numpy.log(gamma) ) < 0.3 ), ( "qdf's sigmaT2/sigmaR2 deviates more than expected from input for staeckel approx." ) # higher up assert ( numpy.fabs( numpy.log(qdf.sigmaT2(0.9, 0.2, gl=True) / qdf.sigmaR2(0.9, 0.2, gl=True)) + 2.0 * numpy.log(gamma) ) < 0.3 ), ( "qdf's sigmaT2/sigmaR2 deviates more than expected from input for staeckel approx." ) return None def test_sigmat_staeckel_mc(): numpy.random.seed(2) qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # In the mid-plane gamma = 2.0 * omegac(MWPotential, 0.9) / epifreq(MWPotential, 0.9) assert ( numpy.fabs( numpy.log(qdf.sigmaT2(0.9, 0.0, mc=True) / qdf.sigmaR2(0.9, 0.0, mc=True)) + 2.0 * numpy.log(gamma) ) < 0.3 ), ( "qdf's sigmaT2/sigmaR2 deviates more than expected from input for staeckel approx." ) # higher up assert ( numpy.fabs( numpy.log(qdf.sigmaT2(0.9, 0.2, mc=True) / qdf.sigmaR2(0.9, 0.2, mc=True)) + 2.0 * numpy.log(gamma) ) < 0.3 ), ( "qdf's sigmaT2/sigmaR2 deviates more than expected from input for staeckel approx." ) return None def test_sigmaz_staeckel_gl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # In the mid-plane assert ( numpy.fabs( numpy.log(qdf.sigmaz2(0.9, 0.0, gl=True)) - 2.0 * numpy.log(0.1) - 0.2 ) < 0.5 ), "qdf's sigmaz2 deviates more than expected from input for staeckel approx." # from Bovy & Rix 2013, we know that this has to be smaller assert ( numpy.log(qdf.sigmaz2(0.9, 0.0, gl=True)) < 2.0 * numpy.log(0.1) + 0.2 < 0.5 ), "qdf's sigmaz2 deviates more than expected from input for staeckel approx." # higher up assert ( numpy.fabs( numpy.log(qdf.sigmaz2(0.9, 0.2, gl=True)) - 2.0 * numpy.log(0.1) - 0.2 ) < 0.5 ), "qdf's sigmaz2 deviates more than expected from input for staeckel approx." assert ( numpy.log(qdf.sigmaz2(0.9, 0.2, gl=True)) < 2.0 * numpy.log(0.1) + 0.2 < 0.5 ), "qdf's sigmaz2 deviates more than expected from input for staeckel approx." assert ( numpy.fabs( numpy.log(qdf.sigmaz2(0.9, -0.25, gl=True)) - 2.0 * numpy.log(0.1) - 0.2 ) < 0.5 ), "qdf's sigmaz2 deviates more than expected from input for staeckel approx." assert ( numpy.log(qdf.sigmaz2(0.9, -0.25, gl=True)) < 2.0 * numpy.log(0.1) + 0.2 < 0.5 ), "qdf's sigmaz2 deviates more than expected from input for staeckel approx." return None def test_sigmaz_staeckel_mc(): numpy.random.seed(1) qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # In the mid-plane assert ( numpy.fabs( numpy.log(qdf.sigmaz2(0.9, 0.0, mc=True)) - 2.0 * numpy.log(0.1) - 0.2 ) < 0.5 ), "qdf's sigmaz2 deviates more than expected from input for staeckel approx." # from Bovy & Rix 2013, we know that this has to be smaller assert ( numpy.log(qdf.sigmaz2(0.9, 0.0, mc=True)) < 2.0 * numpy.log(0.1) + 0.2 < 0.5 ), "qdf's sigmaz2 deviates more than expected from input for staeckel approx." # higher up assert ( numpy.fabs( numpy.log(qdf.sigmaz2(0.9, 0.2, mc=True)) - 2.0 * numpy.log(0.1) - 0.2 ) < 0.5 ), "qdf's sigmaz2 deviates more than expected from input for staeckel approx." assert ( numpy.log(qdf.sigmaz2(0.9, 0.2, mc=True)) < 2.0 * numpy.log(0.1) + 0.2 < 0.5 ), "qdf's sigmaz2 deviates more than expected from input for staeckel approx." assert ( numpy.fabs( numpy.log(qdf.sigmaz2(0.9, -0.25, mc=True)) - 2.0 * numpy.log(0.1) - 0.2 ) < 0.5 ), "qdf's sigmaz2 deviates more than expected from input for staeckel approx." assert ( numpy.log(qdf.sigmaz2(0.9, -0.25, mc=True)) < 2.0 * numpy.log(0.1) + 0.2 < 0.5 ), "qdf's sigmaz2 deviates more than expected from input for staeckel approx." return None def test_sigmarz_adiabatic_gl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) # In the mid-plane, should be zero assert numpy.fabs(qdf.sigmaRz(0.9, 0.0, gl=True)) < 0.05, ( "qdf's sigmaRz deviates more than expected from zero in the mid-plane for adiabatic approx." ) return None def test_sigmarz_adiabatic_mc(): numpy.random.seed(1) qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) # In the mid-plane, should be zero assert numpy.fabs(qdf.sigmaRz(0.9, 0.0, mc=True)) < 0.05, ( "qdf's sigmaRz deviates more than expected from zero in the mid-plane for adiabatic approx." ) return None def test_sigmarz_staeckel_gl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # In the mid-plane, should be zero assert numpy.fabs(qdf.sigmaRz(0.9, 0.0, gl=True)) < 0.05, ( "qdf's sigmaRz deviates more than expected from zero in the mid-plane for staeckel approx." ) return None def test_sigmarz_staeckel_mc(): numpy.random.seed(1) qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # In the mid-plane, should be zero assert numpy.fabs(qdf.sigmaRz(0.9, 0.0, mc=True)) < 0.05, ( "qdf's sigmaRz deviates more than expected from zero in the mid-plane for staeckel approx." ) return None def test_tilt_adiabatic_gl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) # should be zero everywhere assert numpy.fabs(qdf.tilt(0.9, 0.0, gl=True)) < 0.05 / 180.0 * numpy.pi, ( "qdf's tilt deviates more than expected from zero for adiabatic approx." ) assert numpy.fabs(qdf.tilt(0.9, 0.2, gl=True)) < 0.05 / 180.0 * numpy.pi, ( "qdf's tilt deviates more than expected from zero for adiabatic approx." ) assert numpy.fabs(qdf.tilt(0.9, -0.25, gl=True)) < 0.05 / 180.0 * numpy.pi, ( "qdf's tilt deviates more than expected from zero for adiabatic approx." ) return None def test_tilt_adiabatic_mc(): numpy.random.seed(1) qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) # should be zero everywhere assert numpy.fabs(qdf.tilt(0.9, 0.0, mc=True)) < 0.05 / 180.0 * numpy.pi, ( "qdf's tilt deviates more than expected from zero for adiabatic approx." ) assert numpy.fabs(qdf.tilt(0.9, 0.2, mc=True)) < 0.05 / 180.0 * numpy.pi, ( "qdf's tilt deviates more than expected from zero for adiabatic approx." ) assert numpy.fabs(qdf.tilt(0.9, -0.25, mc=True)) < 0.05 / 180.0 * numpy.pi, ( "qdf's tilt deviates more than expected from zero for adiabatic approx." ) return None def test_tilt_staeckel_gl(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # should be zero in the mid-plane and roughly toward the GC elsewhere assert numpy.fabs(qdf.tilt(0.9, 0.0, gl=True)) < 0.05 / 180.0 * numpy.pi, ( "qdf's tilt deviates more than expected from zero in the mid-plane for staeckel approx." ) assert ( numpy.fabs(qdf.tilt(0.9, 0.1, gl=True) - numpy.arctan(0.1 / 0.9)) < 2.0 / 180.0 * numpy.pi ), "qdf's tilt deviates more than expected from expected for staeckel approx." assert ( numpy.fabs(qdf.tilt(0.9, -0.15, gl=True) - numpy.arctan(-0.15 / 0.9)) < 2.5 / 180.0 * numpy.pi ), "qdf's tilt deviates more than expected from expected for staeckel approx." assert ( numpy.fabs(qdf.tilt(0.9, -0.25, gl=True) - numpy.arctan(-0.25 / 0.9)) < 4.0 / 180.0 * numpy.pi ), "qdf's tilt deviates more than expected from expected for staeckel approx." return None def test_tilt_staeckel_mc(): numpy.random.seed(1) qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # should be zero in the mid-plane and roughly toward the GC elsewhere assert numpy.fabs(qdf.tilt(0.9, 0.0, mc=True)) < 1.0 / 180.0 * numpy.pi, ( "qdf's tilt deviates more than expected from zero in the mid-plane for staeckel approx." ) # this is tough assert ( numpy.fabs(qdf.tilt(0.9, 0.1, mc=True) - numpy.arctan(0.1 / 0.9)) < 3.0 / 180.0 * numpy.pi ), "qdf's tilt deviates more than expected from expected for staeckel approx." return None def test_estimate_hr(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) assert numpy.fabs((qdf.estimate_hr(0.9, z=0.0) - 0.25) / 0.25) < 0.1, ( "estimated scale length deviates more from input scale length than expected" ) # Another one qdf = quasiisothermaldf( 1.0 / 2.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) assert numpy.fabs((qdf.estimate_hr(0.9, z=None) - 0.5) / 0.5) < 0.15, ( "estimated scale length deviates more from input scale length than expected" ) # Another one qdf = quasiisothermaldf( 1.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) assert ( numpy.fabs((qdf.estimate_hr(0.9, z=None, fixed_quad=False) - 1.0) / 1.0) < 0.3 ), "estimated scale length deviates more from input scale length than expected" return None def test_estimate_hz(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) from scipy import integrate from galpy.potential import evaluateDensities expec_hz = ( 0.1**2.0 / 2.0 / integrate.quad(lambda x: evaluateDensities(MWPotential, 0.9, x), 0.0, 0.125)[ 0 ] / 2.0 / numpy.pi ) assert numpy.fabs((qdf.estimate_hz(0.9, z=0.125) - expec_hz) / expec_hz) < 0.1, ( "estimated scale height not as expected" ) assert qdf.estimate_hz(0.9, z=0.0) > 1.0, ( "estimated scale height at z=0 not very large" ) # Another one qdf = quasiisothermaldf( 1.0 / 4.0, 0.3, 0.2, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) expec_hz = ( 0.2**2.0 / 2.0 / integrate.quad(lambda x: evaluateDensities(MWPotential, 0.9, x), 0.0, 0.125)[ 0 ] / 2.0 / numpy.pi ) assert numpy.fabs((qdf.estimate_hz(0.9, z=0.125) - expec_hz) / expec_hz) < 0.15, ( "estimated scale height not as expected" ) return None def test_estimate_hsr(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) assert numpy.fabs((qdf.estimate_hsr(0.9, z=0.0) - 1.0) / 1.0) < 0.25, ( "estimated radial-dispersion scale length deviates more from input scale length than expected" ) # Another one qdf = quasiisothermaldf( 1.0 / 2.0, 0.2, 0.1, 2.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) assert numpy.fabs((qdf.estimate_hsr(0.9, z=0.05) - 2.0) / 2.0) < 0.25, ( "estimated radial-dispersion scale length deviates more from input scale length than expected" ) return None def test_estimate_hsz(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) assert numpy.fabs((qdf.estimate_hsz(0.9, z=0.0) - 1.0) / 1.0) < 0.25, ( "estimated vertical-dispersion scale length deviates more from input scale length than expected" ) # Another one qdf = quasiisothermaldf( 1.0 / 2.0, 0.2, 0.1, 1.0, 2.0, pot=MWPotential, aA=aAS, cutcounter=True ) assert numpy.fabs((qdf.estimate_hsz(0.9, z=0.05) - 2.0) / 2.0) < 0.25, ( "estimated vertical-dispersion scale length deviates more from input scale length than expected" ) return None def test_meanjr(): # This is a *very* rough test against a rough estimate of the mean qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) assert ( numpy.fabs( numpy.log(qdf.meanjr(0.9, 0.0, mc=True)) - 2.0 * numpy.log(0.2) - 0.2 + numpy.log(epifreq(MWPotential, 0.9)) ) < 0.4 ), "meanjr is not what is expected" assert ( numpy.fabs( numpy.log(qdf.meanjr(0.5, 0.0, mc=True)) - 2.0 * numpy.log(0.2) - 1.0 + numpy.log(epifreq(MWPotential, 0.5)) ) < 0.4 ), "meanjr is not what is expected" return None def test_meanjr_center(): # Just checking that this isn't NaN! qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) assert not numpy.isnan(qdf.meanjr(0.001, 0.0, mc=True)), ( "meanjr at the center is NaN" ) return None def test_meanlz(): # This is a *very* rough test against a rough estimate of the mean qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) from galpy.df import dehnendf # baseline dfc = dehnendf(profileParams=(1.0 / 4.0, 1.0, 0.2), beta=0.0, correct=False) assert ( numpy.fabs( numpy.log(qdf.meanlz(0.9, 0.0, mc=True)) - numpy.log(0.9 * dfc.meanvT(0.9)) ) < 0.1 ), "meanlz is not what is expected" assert ( numpy.fabs( numpy.log(qdf.meanlz(0.5, 0.0, mc=True)) - numpy.log(0.5 * dfc.meanvT(0.5)) ) < 0.2 ), "meanlz is not what is expected" return None def test_meanjz(): # This is a *very* rough test against a rough estimate of the mean qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) ldiff = ( numpy.log(qdf.meanjz(0.9, 0.0, mc=True)) - 2.0 * numpy.log(0.1) - 0.2 + numpy.log(verticalfreq(MWPotential, 0.9)) ) # expect this to be smaller than the rough estimate, but not by more than an order of magnitude assert ldiff > -1.0 and ldiff < 0.0, "meanjz is not what is expected" ldiff = ( numpy.log(qdf.meanjz(0.5, 0.0, mc=True)) - 2.0 * numpy.log(0.1) - 1.0 + numpy.log(verticalfreq(MWPotential, 0.5)) ) assert ldiff > -1.0 and ldiff < 0.0, "meanjz is not what is expected" return None def test_sampleV(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) numpy.random.seed(1) samples = qdf.sampleV(0.8, 0.1, n=1000) # test vR assert numpy.fabs(numpy.mean(samples[:, 0])) < 0.02, "sampleV vR mean is not zero" assert ( numpy.fabs( numpy.log(numpy.std(samples[:, 0])) - 0.5 * numpy.log(qdf.sigmaR2(0.8, 0.1)) ) < 0.05 ), "sampleV vR stddev is not equal to sigmaR" # test vT assert numpy.fabs(numpy.mean(samples[:, 1] - qdf.meanvT(0.8, 0.1))) < 0.015, ( "sampleV vT mean is not equal to meanvT" ) assert ( numpy.fabs( numpy.log(numpy.std(samples[:, 1])) - 0.5 * numpy.log(qdf.sigmaT2(0.8, 0.1)) ) < 0.05 ), "sampleV vT stddev is not equal to sigmaT" # test vz assert numpy.fabs(numpy.mean(samples[:, 2])) < 0.01, "sampleV vz mean is not zero" assert ( numpy.fabs( numpy.log(numpy.std(samples[:, 2])) - 0.5 * numpy.log(qdf.sigmaz2(0.8, 0.1)) ) < 0.05 ), "sampleV vz stddev is not equal to sigmaz" return None def test_sampleV_physical(): # Test physical output of sampleV qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) numpy.random.seed(1) vo = 225.0 samples = qdf.sampleV(0.8, 0.1, n=1000, vo=vo) # test vR assert numpy.fabs(numpy.mean(samples[:, 0])) < 0.02 * vo, ( "sampleV vR mean is not zero" ) assert ( numpy.fabs( numpy.log(numpy.std(samples[:, 0])) - 0.5 * numpy.log(qdf.sigmaR2(0.8, 0.1, vo=vo)) ) < 0.05 ), "sampleV vR stddev is not equal to sigmaR" # test vT assert ( numpy.fabs(numpy.mean(samples[:, 1] - qdf.meanvT(0.8, 0.1, vo=vo))) < 0.015 * vo ), "sampleV vT mean is not equal to meanvT" assert ( numpy.fabs( numpy.log(numpy.std(samples[:, 1])) - 0.5 * numpy.log(qdf.sigmaT2(0.8, 0.1, vo=vo)) ) < 0.05 ), "sampleV vT stddev is not equal to sigmaT" # test vz assert numpy.fabs(numpy.mean(samples[:, 2])) < 0.01 * vo, ( "sampleV vz mean is not zero" ) assert ( numpy.fabs( numpy.log(numpy.std(samples[:, 2])) - 0.5 * numpy.log(qdf.sigmaz2(0.8, 0.1, vo=vo)) ) < 0.05 ), "sampleV vz stddev is not equal to sigmaz" return None def test_sampleV_interpolate(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) vo = 225.0 numpy.random.seed(3) def Rz_array(R_array, z_array, num_std=3, R_min=None, R_max=None, z_max=None): R = numpy.hstack([i * numpy.ones(1000) for i in R_array]) z = numpy.hstack([i * numpy.ones(1000) for i in z_array]) # add outlier R = numpy.append(R, 8.0) z = numpy.append(z, 5.0) # apply sample V interpolate samples = qdf.sampleV_interpolate( R=R, z=z, R_pixel=0.07, z_pixel=0.07, num_std=num_std, R_min=R_min, R_max=R_max, z_max=z_max, ) samples = samples[1000:2000, :] # test vR assert numpy.fabs(numpy.mean(samples[:, 0])) < 0.02, ( "sampleV interpolate vR mean is not zero" ) assert ( numpy.fabs( numpy.log(numpy.std(samples[:, 0])) - 0.5 * numpy.log(qdf.sigmaR2(0.8, 0.1)) ) < 0.05 ), "sampleV interpolate vR stddev is not equal to sigmaR" # test vT assert numpy.fabs(numpy.mean(samples[:, 1] - qdf.meanvT(0.8, 0.1))) < 0.015, ( "sampleV interpolate vT mean is not equal to meanvT" ) assert ( numpy.fabs( numpy.log(numpy.std(samples[:, 1])) - 0.5 * numpy.log(qdf.sigmaT2(0.8, 0.1)) ) < 0.05 ), "sampleV interpolate vT stddev is not equal to sigmaT" # test vz assert numpy.fabs(numpy.mean(samples[:, 2])) < 0.01, ( "sampleV interpolate vz mean is not zero" ) assert ( numpy.fabs( numpy.log(numpy.std(samples[:, 2])) - 0.5 * numpy.log(qdf.sigmaz2(0.8, 0.1)) ) < 0.05 ), "sampleV vz interpolate stddev is not equal to sigmaz" return None # test the sampling at (0.8,0.1) with different order of interpolation # which is determined by R_number and z_number Rz_array([0.7, 0.8, 0.9], [0.0, 0.1, 0.2]) # R_number=2, z_number=2 Rz_array([0.7, 0.8, 0.9, 1.0], [0.0, 0.1, 0.2, 0.3]) # R_number=4, z_number=4 Rz_array([0.8, 0.8, 0.9, 1.0], [0.0, 0.1, 0.2, 0.3]) # R_number=2, z_number=4 Rz_array([0.7, 0.8, 0.9, 1.0], [0.0, 0.1, 0.2, 0.2]) # R_number=4, z_number=2 # test saved hash and interpolation object Rz_array([0.7, 0.8, 0.9, 1.0], [-0.3, 0.1, 0.2, 0.3]) hash1 = qdf._maxVT_hash ip1 = qdf._maxVT_ip Rz_array([0.7, 0.8, 0.9, 1.0], [-0.3, 0.1, 0.2, 0.3]) hash2 = qdf._maxVT_hash ip2 = qdf._maxVT_ip Rz_array([0.6, 0.8, 0.9, 1.0], [-0.3, 0.1, 0.2, 0.3]) hash3 = qdf._maxVT_hash ip3 = qdf._maxVT_ip assert hash1 == hash2, "sampleV interpolate hash is changed unexpectedly" assert ip1 == ip2, ( "sampleV interpolate interpolation object is changed unexpectedly" ) assert hash3 != hash2, "sampleV interpolate hash did not changed as expected" assert ip3 != ip2, ( "sampleV interpolate interpolation object did not changed as expected" ) # test user-specified grid edges # since num_std is set so high, the extra outlier of (8,5) is not covered # by it. So in order for this function to run in a reasonable time, it must # be that the user-specified grid edges are doing their job Rz_array( [0.7, 0.8, 0.9, 1.0], [0.0, 0.1, 0.2, 0.3], num_std=10, R_min=0.7, R_max=1.0, z_max=0.3, ) # test absolute value, also test non-astropy unit-support numpy.random.seed(1) pos = qdf.sampleV_interpolate( numpy.array([0.7, 0.8, 0.9, 1.0]), numpy.array([0.1, 0.2, 0.3, 0.4]), R_pixel=0.07, z_pixel=0.07, vo=vo, ) numpy.random.seed(1) neg = qdf.sampleV_interpolate( numpy.array([0.7, 0.8, 0.9, 1.0]), numpy.array([-0.1, -0.2, 0.3, -0.4]), R_pixel=0.07, z_pixel=0.07, vo=vo, ) assert numpy.all(numpy.fabs(pos - neg) / vo < 10.0**-8.0), ( "sampleV interpolate absolute value of z is incorrect" ) return None def test_pvR_adiabatic(): # Test pvR by calculating its mean and stddev by Riemann sum qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) R, z = 0.8, 0.1 vRs = numpy.linspace(-1.0, 1.0, 51) pvR = numpy.array([qdf.pvR(vr, R, z) for vr in vRs]) mvR = numpy.sum(vRs * pvR) / numpy.sum(pvR) svR = numpy.sqrt(numpy.sum(vRs**2.0 * pvR) / numpy.sum(pvR) - mvR**2.0) assert numpy.fabs(mvR) < 0.01, ( "mean vR calculated from pvR not equal to zero for adiabatic actions" ) assert numpy.fabs(numpy.log(svR) - 0.5 * numpy.log(qdf.sigmaR2(R, z))) < 0.01, ( "sigma vR calculated from pvR not equal to that from sigmaR2 for adiabatic actions" ) return None def test_pvR_staeckel(): # Test pvR by calculating its mean and stddev by Riemann sum qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 vRs = numpy.linspace(-1.0, 1.0, 51) pvR = numpy.array([qdf.pvR(vr, R, z) for vr in vRs]) mvR = numpy.sum(vRs * pvR) / numpy.sum(pvR) svR = numpy.sqrt(numpy.sum(vRs**2.0 * pvR) / numpy.sum(pvR) - mvR**2.0) assert numpy.fabs(mvR) < 0.01, ( "mean vR calculated from pvR not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svR) - 0.5 * numpy.log(qdf.sigmaR2(R, z))) < 0.01, ( "sigma vR calculated from pvR not equal to that from sigmaR2 for staeckel actions" ) return None def test_pvR_staeckel_diffngl(): # Test pvR by calculating its mean and stddev by Riemann sum qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 vRs = numpy.linspace(-1.0, 1.0, 51) # ngl=10 pvR = numpy.array([qdf.pvR(vr, R, z, ngl=10) for vr in vRs]) mvR = numpy.sum(vRs * pvR) / numpy.sum(pvR) svR = numpy.sqrt(numpy.sum(vRs**2.0 * pvR) / numpy.sum(pvR) - mvR**2.0) assert numpy.fabs(mvR) < 0.01, ( "mean vR calculated from pvR not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svR) - 0.5 * numpy.log(qdf.sigmaR2(R, z))) < 0.01, ( "sigma vR calculated from pvR not equal to that from sigmaR2 for staeckel actions" ) # ngl=40 pvR = numpy.array([qdf.pvR(vr, R, z, ngl=40) for vr in vRs]) mvR = numpy.sum(vRs * pvR) / numpy.sum(pvR) svR = numpy.sqrt(numpy.sum(vRs**2.0 * pvR) / numpy.sum(pvR) - mvR**2.0) assert numpy.fabs(mvR) < 0.01, ( "mean vR calculated from pvR not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svR) - 0.5 * numpy.log(qdf.sigmaR2(R, z))) < 0.01, ( "sigma vR calculated from pvR not equal to that from sigmaR2 for staeckel actions" ) # ngl=11, shouldn't work try: pvR = numpy.array([qdf.pvR(vr, R, z, ngl=11) for vr in vRs]) except ValueError: pass else: raise AssertionError("pvR w/ ngl=odd did not raise ValueError") return None def test_pvT_adiabatic(): # Test pvT by calculating its mean and stddev by Riemann sum qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) R, z = 0.8, 0.1 vTs = numpy.linspace(0.0, 1.5, 101) pvT = numpy.array([qdf.pvT(vt, R, z) for vt in vTs]) mvT = numpy.sum(vTs * pvT) / numpy.sum(pvT) svT = numpy.sqrt(numpy.sum(vTs**2.0 * pvT) / numpy.sum(pvT) - mvT**2.0) assert numpy.fabs(mvT - qdf.meanvT(R, z)) < 0.01, ( "mean vT calculated from pvT not equal to zero for adiabatic actions" ) assert numpy.fabs(numpy.log(svT) - 0.5 * numpy.log(qdf.sigmaT2(R, z))) < 0.01, ( "sigma vT calculated from pvT not equal to that from sigmaT2 for adiabatic actions" ) return None def test_pvT_staeckel(): # Test pvT by calculating its mean and stddev by Riemann sum qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 vTs = numpy.linspace(0.0, 1.5, 101) pvT = numpy.array([qdf.pvT(vt, R, z) for vt in vTs]) mvT = numpy.sum(vTs * pvT) / numpy.sum(pvT) svT = numpy.sqrt(numpy.sum(vTs**2.0 * pvT) / numpy.sum(pvT) - mvT**2.0) assert numpy.fabs(mvT - qdf.meanvT(R, z)) < 0.01, ( "mean vT calculated from pvT not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svT) - 0.5 * numpy.log(qdf.sigmaT2(R, z))) < 0.01, ( "sigma vT calculated from pvT not equal to that from sigmaT2 for staeckel actions" ) return None def test_pvT_staeckel_diffngl(): # Test pvT by calculating its mean and stddev by Riemann sum qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 vTs = numpy.linspace(0.0, 1.5, 101) # ngl=10 pvT = numpy.array([qdf.pvT(vt, R, z, ngl=10) for vt in vTs]) mvT = numpy.sum(vTs * pvT) / numpy.sum(pvT) svT = numpy.sqrt(numpy.sum(vTs**2.0 * pvT) / numpy.sum(pvT) - mvT**2.0) assert numpy.fabs(mvT - qdf.meanvT(R, z)) < 0.01, ( "mean vT calculated from pvT not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svT) - 0.5 * numpy.log(qdf.sigmaT2(R, z))) < 0.01, ( "sigma vT calculated from pvT not equal to that from sigmaT2 for staeckel actions" ) # ngl=40 pvT = numpy.array([qdf.pvT(vt, R, z, ngl=40) for vt in vTs]) mvT = numpy.sum(vTs * pvT) / numpy.sum(pvT) svT = numpy.sqrt(numpy.sum(vTs**2.0 * pvT) / numpy.sum(pvT) - mvT**2.0) assert numpy.fabs(mvT - qdf.meanvT(R, z)) < 0.01, ( "mean vT calculated from pvT not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svT) - 0.5 * numpy.log(qdf.sigmaT2(R, z))) < 0.01, ( "sigma vT calculated from pvT not equal to that from sigmaT2 for staeckel actions" ) # ngl=11, shouldn't work try: pvT = numpy.array([qdf.pvT(vt, R, z, ngl=11) for vt in vTs]) except ValueError: pass else: raise AssertionError("pvT w/ ngl=odd did not raise ValueError") return None def test_pvz_adiabatic(): # Test pvz by calculating its mean and stddev by Riemann sum qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAA, cutcounter=True ) R, z = 0.8, 0.1 vzs = numpy.linspace(-1.0, 1.0, 51) pvz = numpy.array([qdf.pvz(vz, R, z) for vz in vzs]) mvz = numpy.sum(vzs * pvz) / numpy.sum(pvz) svz = numpy.sqrt(numpy.sum(vzs**2.0 * pvz) / numpy.sum(pvz) - mvz**2.0) assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvz not equal to zero for adiabatic actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(qdf.sigmaz2(R, z))) < 0.01, ( "sigma vz calculated from pvz not equal to that from sigmaz2 for adiabatic actions" ) return None def test_pvz_staeckel(): # Test pvz by calculating its mean and stddev by Riemann sum qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 vzs = numpy.linspace(-1.0, 1.0, 51) pvz = numpy.array([qdf.pvz(vz, R, z) for vz in vzs]) mvz = numpy.sum(vzs * pvz) / numpy.sum(pvz) svz = numpy.sqrt(numpy.sum(vzs**2.0 * pvz) / numpy.sum(pvz) - mvz**2.0) assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvz not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(qdf.sigmaz2(R, z))) < 0.01, ( "sigma vz calculated from pvz not equal to that from sigmaz2 for staeckel actions" ) # same w/ explicit sigmaR input pvz = numpy.array( [qdf.pvz(vz, R, z, _sigmaR1=0.95 * numpy.sqrt(qdf.sigmaR2(R, z))) for vz in vzs] ) mvz = numpy.sum(vzs * pvz) / numpy.sum(pvz) svz = numpy.sqrt(numpy.sum(vzs**2.0 * pvz) / numpy.sum(pvz) - mvz**2.0) assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvz not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(qdf.sigmaz2(R, z))) < 0.01, ( "sigma vz calculated from pvz not equal to that from sigmaz2 for staeckel actions" ) return None def test_pvz_staeckel_diffngl(): # Test pvz by calculating its mean and stddev by Riemann sum qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 vzs = numpy.linspace(-1.0, 1.0, 51) # ngl=10 pvz = numpy.array([qdf.pvz(vz, R, z, ngl=10) for vz in vzs]) mvz = numpy.sum(vzs * pvz) / numpy.sum(pvz) svz = numpy.sqrt(numpy.sum(vzs**2.0 * pvz) / numpy.sum(pvz) - mvz**2.0) assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvz not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(qdf.sigmaz2(R, z))) < 0.01, ( "sigma vz calculated from pvz not equal to that from sigmaz2 for staeckel actions" ) # ngl=40 pvz = numpy.array([qdf.pvz(vz, R, z, ngl=40) for vz in vzs]) mvz = numpy.sum(vzs * pvz) / numpy.sum(pvz) svz = numpy.sqrt(numpy.sum(vzs**2.0 * pvz) / numpy.sum(pvz) - mvz**2.0) assert numpy.fabs(mvz) < 0.01, ( "mean vz calculated from pvz not equal to zero for staeckel actions" ) assert numpy.fabs(numpy.log(svz) - 0.5 * numpy.log(qdf.sigmaz2(R, z))) < 0.01, ( "sigma vz calculated from pvz not equal to that from sigmaz2 for staeckel actions" ) # ngl=11, shouldn't work try: pvz = numpy.array([qdf.pvz(vz, R, z, ngl=11) for vz in vzs]) except ValueError: pass else: raise AssertionError("pvz w/ ngl=odd did not raise ValueError") return None def test_pvz_staeckel_arrayin(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 pvz = qdf.pvz(0.05 * numpy.ones(2), R * numpy.ones(2), z * numpy.ones(2)) assert numpy.all( numpy.fabs(numpy.log(pvz) - numpy.log(qdf.pvz(0.05, R, z))) < 10.0**-10.0 ), ( "pvz calculated with R and z array input does not equal to calculated with scalar input" ) return None def test_setup_diffsetups(): # Test the different ways to setup a qdf object # Test errors try: qdf = quasiisothermaldf(1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, aA=aAS, cutcounter=True) except OSError: pass else: raise AssertionError("qdf setup w/o pot set did not raise exception") try: qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, cutcounter=True ) except OSError: pass else: raise AssertionError("qdf setup w/o aA set did not raise exception") from galpy.potential import LogarithmicHaloPotential try: qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=LogarithmicHaloPotential(), aA=aAS, cutcounter=True, ) except OSError: pass else: raise AssertionError( "qdf setup w/ aA potential different from pot= did not raise exception" ) # qdf setup with an actionAngleIsochrone instance (issue #190) from galpy.actionAngle import actionAngleIsochrone from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=2.0) try: qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=ip, aA=actionAngleIsochrone(ip=ip), cutcounter=True, ) except: raise raise AssertionError( "quasi-isothermaldf setup w/ an actionAngleIsochrone instance failed" ) # qdf setup with an actionAngleIsochrone instance should raise error if potentials are not the same ip = IsochronePotential(normalize=1.0, b=2.0) try: qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=ip, aA=actionAngleIsochrone(ip=IsochronePotential(normalize=1.0, b=2.5)), cutcounter=True, ) except OSError: pass else: raise AssertionError( "qdf setup w/ aA potential different from pot= did not raise exception" ) # precompute qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True, _precomputerg=True, ) qdfnpc = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True, _precomputerg=False, ) assert numpy.fabs(qdf._rg(1.1) - qdfnpc._rg(1.1)) < 10.0**-5.0, ( "rg calculated from qdf instance w/ precomputerg set is not the same as that computed from an instance w/o it set" ) def test_call_diffinoutputs(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # when specifying rg etc., first get these from a previous output val, trg, tkappa, tnu, tOmega = qdf((0.03, 0.9, 0.02), _return_freqs=True) # First check that just supplying these again works assert ( numpy.fabs( val - qdf((0.03, 0.9, 0.02), rg=trg, kappa=tkappa, nu=tnu, Omega=tOmega) ) < 10.0**-8.0 ), "qdf calls w/ rg, and frequencies specified and w/ not specified do not agrees" # Also calculate the frequencies assert ( numpy.fabs( val - qdf( (0.03, 0.9, 0.02), rg=trg, kappa=epifreq(MWPotential, trg), nu=verticalfreq(MWPotential, trg), Omega=omegac(MWPotential, trg), ) ) < 10.0**-8.0 ), "qdf calls w/ rg, and frequencies specified and w/ not specified do not agrees" # Also test _return_actions val, jr, lz, jz = qdf(0.9, 0.1, 0.95, 0.1, 0.08, _return_actions=True) assert numpy.fabs(val - qdf((jr, lz, jz))) < 10.0**-8.0, ( "qdf call w/ R,vR,... and actions specified do not agree" ) acs = aAS(0.9, 0.1, 0.95, 0.1, 0.08) assert numpy.fabs(acs[0] - jr) < 10.0**-8.0, ( "direct calculation of jr and that returned from qdf.__call__ does not agree" ) assert numpy.fabs(acs[1] - lz) < 10.0**-8.0, ( "direct calculation of lz and that returned from qdf.__call__ does not agree" ) assert numpy.fabs(acs[2] - jz) < 10.0**-8.0, ( "direct calculation of jz and that returned from qdf.__call__ does not agree" ) # Test unbound orbits # Find unbound orbit, new qdf s.t. we can get UnboundError (only with taAS = actionAngleStaeckel(pot=MWPotential, c=False, delta=0.5) qdfnc = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=taAS, cutcounter=True ) from galpy.actionAngle import UnboundError try: acs = taAS(0.9, 10.0, -20.0, 0.1, 10.0) except UnboundError: pass else: print(acs) raise AssertionError("Test orbit in qdf that is supposed to be unbound is not") assert qdfnc(0.9, 10.0, -20.0, 0.1, 10.0) < 10.0**-10.0, ( "unbound orbit does not return qdf equal to zero" ) assert qdfnc(0.9, 10.0, -20.0, 0.1, 10.0, log=True) < -10.0 * numpy.log(10.0), ( "unbound orbit does not return qdf equal to zero" ) # Test negative lz assert qdf((0.03, -0.1, 0.02)) < 10.0**-8.0, ( "qdf w/ cutcounter=True and negative lz does not return 0" ) assert ( qdf((0.03, -0.1, 0.02), log=True) <= numpy.finfo(numpy.dtype(numpy.float64)).min + 1.0 ), "qdf w/ cutcounter=True and negative lz does not return 0" # Test func val = qdf((0.03, 0.9, 0.02)) fval = qdf( (0.03, 0.9, 0.02), func=lambda x, y, z: numpy.sin(x) * numpy.cos(y) * numpy.exp(z), ) assert ( numpy.fabs(val * numpy.sin(0.03) * numpy.cos(0.9) * numpy.exp(0.02) - fval) < 10.0**-8 ), "qdf __call__ w/ func does not work as expected" lfval = qdf( (0.03, 0.9, 0.02), func=lambda x, y, z: numpy.sin(x) * numpy.cos(y) * numpy.exp(z), log=True, ) assert ( numpy.fabs( numpy.log(val) + numpy.log(numpy.sin(0.03) * numpy.cos(0.9) * numpy.exp(0.02)) - lfval ) < 10.0**-8 ), "qdf __call__ w/ func does not work as expected" return None def test_vmomentdensity_diffinoutputs(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # Test that we can input use different ngl R, z = 0.8, 0.1 sigmar2 = qdf.sigmaR2(R, z, gl=True) assert ( numpy.fabs( numpy.log( qdf.sigmaR2( R, z, gl=True, _sigmaR1=0.95 * numpy.sqrt(qdf.sigmaR2(R, z)), _sigmaz1=0.95 * numpy.sqrt(qdf.sigmaz2(R, z)), ) ) - numpy.log(sigmar2) ) < 0.01 ), "sigmaR2 calculated w/ explicit sigmaR1 and sigmaz1 do not agree" # Test ngl inputs further try: qdf.vmomentdensity(R, z, 0, 0, 0, gl=True, ngl=11) except ValueError: pass else: raise AssertionError( "qdf.vmomentdensity w/ ngl == odd does not raise ValueError" ) surfmass, glqeval = qdf.vmomentdensity(R, z, 0.0, 0.0, 0.0, gl=True, _returngl=True) # This shouldn't reuse gleval, but should work nonetheless assert ( numpy.fabs( numpy.log(surfmass) - numpy.log( qdf.vmomentdensity( R, z, 0.0, 0.0, 0.0, gl=True, _glqeval=glqeval, ngl=30 ) ) ) < 0.05 ), "vmomentsurfmass w/ wrong glqeval input does not work" # Test that we can reuse jr, etc. surfmass, jr, lz, jz = qdf.vmomentdensity( R, z, 0.0, 0.0, 0.0, gl=True, _return_actions=True ) assert ( numpy.fabs( numpy.log(surfmass) - numpy.log( qdf.vmomentdensity(R, z, 0.0, 0.0, 0.0, gl=True, _jr=jr, _lz=lz, _jz=jz) ) ) < 0.01 ), "surfacemass calculated from reused actions does not agree with that before" surfmass, jr, lz, jz, rg, kappa, nu, Omega = qdf.vmomentdensity( R, z, 0.0, 0.0, 0.0, gl=True, _return_actions=True, _return_freqs=True ) assert ( numpy.fabs( numpy.log(surfmass) - numpy.log( qdf.vmomentdensity( R, z, 0.0, 0.0, 0.0, gl=True, _jr=jr, _lz=lz, _jz=jz, _rg=rg, _kappa=kappa, _nu=nu, _Omega=Omega, ) ) ) < 0.01 ), "surfacemass calculated from reused actions does not agree with that before" # Some tests of mc=True surfmass, vrs, vts, vzs = qdf.vmomentdensity( R, z, 0.0, 0.0, 0.0, mc=True, gl=False, _rawgausssamples=True, _returnmc=True ) assert ( numpy.fabs( numpy.log(surfmass) - numpy.log( qdf.vmomentdensity( R, z, 0.0, 0.0, 0.0, mc=True, gl=False, _rawgausssamples=True, _vrs=vrs, _vts=vts, _vzs=vzs, ) ) ) < 0.0001 ), ( "qdf.vmomentdensity w/ rawgausssamples and mc=True does not agree with that w/o rawgausssamples" ) return None def test_vmomentdensity_physical(): # Test physical output of vmomentdensity qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) R, z = 0.8, 0.1 ro, vo = 7.0, 230.0 assert ( numpy.fabs( qdf.vmomentdensity(R, z, 0, 0, 0, gl=True, ngl=12, ro=ro, vo=vo) - qdf.vmomentdensity(R, z, 0, 0, 0, gl=True, ngl=12) / ro**3 ) < 10.0**-8.0 ), ( "vmomentdensity with use_physical does not correspond to vmomentdensity without physical" ) assert ( numpy.fabs( qdf.vmomentdensity(R, z, 0, 1, 0, gl=True, ngl=12, ro=ro, vo=vo) - qdf.vmomentdensity(R, z, 0, 1, 0, gl=True, ngl=12) * vo / ro**3 ) < 10.0**-8.0 ), ( "vmomentdensity with use_physical does not correspond to vmomentdensity without physical" ) return None def test_jmomentdensity_diffinoutputs(): qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # Some tests of mc=True R, z = 0.8, 0.1 jr2surfmass, vrs, vts, vzs = qdf.jmomentdensity( R, z, 2.0, 0.0, 0.0, mc=True, _returnmc=True ) assert ( numpy.fabs( numpy.log(jr2surfmass) - numpy.log( qdf.jmomentdensity( R, z, 2.0, 0.0, 0.0, mc=True, _vrs=vrs, _vts=vts, _vzs=vzs ) ) ) < 0.0001 ), ( "qdf.jmomentdensity w/ rawgausssamples and mc=True does not agree with that w/o rawgausssamples" ) return None def test_jmomentdensity_physical(): # Test physical output of jmomentdensity qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) ro, vo = 7.0, 230.0 assert ( numpy.fabs( qdf.jmomentdensity(1.1, 0.1, 0, 0, 0, nmc=100000, ro=ro, vo=vo) - qdf.jmomentdensity(1.1, 0.1, 0, 0, 0, nmc=100000) / ro**3 * (ro * vo) ** 0 ) < 10.0**-4.0 ), "quasiisothermaldf method jmomentdensity does not return correct Quantity" assert ( numpy.fabs( qdf.jmomentdensity( 1.1, 0.1, 1, 0, 0, nmc=100000, ro=ro, vo=vo, use_physical=True ) - qdf.jmomentdensity(1.1, 0.1, 1, 0, 0, nmc=100000) / ro**3 * (ro * vo) ** 1 ) < 10.0**-2.0 ), "quasiisothermaldf method jmomentdensity does not return correct Quantity" return None def test_pvz_diffinoutput(): # pvz, similarly to vmomentdensity, can output certain intermediate results qdf = quasiisothermaldf( 1.0 / 4.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aAS, cutcounter=True ) # test reusing the actions R, z = 0.8, 0.1 tpvz, jr, lz, jz = qdf.pvz(0.1, R, z, _return_actions=True) assert ( numpy.fabs( numpy.log(qdf.pvz(0.1, R, z, _jr=jr, _lz=lz, _jz=jz)) - numpy.log(tpvz) ) < 0.001 ), "qdf.pvz does not return the same result when reusing the actions" # test reusing the frequencies tpvz, rg, kappa, nu, Omega = qdf.pvz(0.1, R, z, _return_freqs=True) assert ( numpy.fabs( numpy.log(qdf.pvz(0.1, R, z, _rg=rg, _kappa=kappa, _nu=nu, _Omega=Omega)) - numpy.log(tpvz) ) < 0.001 ), "qdf.pvz does not return the same result when reusing the frequencies" # test reusing the actions and the frequencies tpvz, jr, lz, jz, rg, kappa, nu, Omega = qdf.pvz( 0.1, R, z, _return_actions=True, _return_freqs=True ) assert ( numpy.fabs( numpy.log( qdf.pvz( 0.1, R, z, _jr=jr, _lz=lz, _jz=jz, _rg=rg, _kappa=kappa, _nu=nu, _Omega=Omega, ) ) - numpy.log(tpvz) ) < 0.001 ), ( "qdf.pvz does not return the same result when reusing the actions and the frequencies" ) return None def test_meanjz_noaac_issue300(): # Test of issue 300 reported by Ruth Angus: failure of qdf.meanjz when not using C integration for action-angle calculations taA = actionAngleAdiabatic(pot=MWPotential, c=False) hr = 1 / 3.0 sr = 0.2 sz = 0.1 hsr = 1.0 hsz = 1.0 qdf = quasiisothermaldf( hr, sr, sz, hsr, hsz, pot=MWPotential, aA=taA, cutcounter=True ) assert numpy.fabs(qdf.meanjz(1.0, 0.125, nmc=100) - 0.0157468008111) < 0.01, ( "Mean Jz computed using MC with Python actionAngleAdiabatic integration fails" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_quantity.py0000644000175100001660000247115314761352023016763 0ustar00runnerdocker# Make sure to set configuration, needs to be before any galpy imports import pytest from packaging.version import parse as parse_version from galpy.util import config config.__config__.set("astropy", "astropy-units", "True") import numpy _NUMPY_VERSION = parse_version(numpy.__version__) _NUMPY_1_22 = (_NUMPY_VERSION > parse_version("1.21")) * ( _NUMPY_VERSION < parse_version("1.23") ) + (_NUMPY_VERSION > parse_version("1.23")) * ( _NUMPY_VERSION < parse_version("1.25") ) # For testing 1.22/1.24 precision issues from astropy import constants, units sdf_sanders15 = None # so we can set this up and then use in other tests sdf_sanders15_nou = None # so we can set this up and then use in other tests def test_parsers(): from galpy.util import conversion # Unitless assert numpy.fabs(conversion.parse_length(2.0) - 2.0) < 1e-10, ( "parse_length does not parse unitless position correctly" ) assert numpy.fabs(conversion.parse_energy(3.0) - 3.0) < 1e-10, ( "parse_energy does not parse unitless energy correctly" ) assert numpy.fabs(conversion.parse_angmom(-1.5) + 1.5) < 1e-10, ( "parse_angmom does not parse unitless angular momentum correctly" ) # Quantity input ro, vo = 7.0, 230.0 assert ( numpy.fabs( conversion.parse_length(2.0 * units.parsec, ro=ro, vo=vo) - (0.002 / ro) ) < 1e-10 ), "parse_length does parse Quantity position correctly" assert ( numpy.fabs( conversion.parse_energy(-30.0 * units.km**2 / units.s**2, ro=ro, vo=vo) - (-30.0 / vo**2) ) < 1e-10 ), "parse_energy does parse Quantity energy correctly" assert ( numpy.fabs( conversion.parse_angmom( 2200.0 * units.kpc * units.km / units.s, ro=ro, vo=vo ) - (2200.0 / ro / vo) ) < 1e-10 ), "parse_angmom does parse Quantity angular momentum correctly" return None def test_parsers_with_unrecognized_inputs(): # Test related to $542: test that an error is raised when parsing an object # that is not a float/... or an astropy Quantity (e.g., a different unit system) from galpy.util import conversion # Just some object class other_quantity_object: def __init__(self): return None obj = other_quantity_object() ro, vo = 7.0, 230.0 with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_length(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_length_kpc(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_velocity(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_velocity_kms(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_angle(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_time(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_mass(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_energy(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_angmom(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_frequency(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_force(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_dens(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_surfdens(obj, ro=ro, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_numdens(obj, ro=ro, vo=vo) return None def test_parsers_rovo_input(): # Test that providing ro in kpc and vo in km/s to the parsers works from galpy.util import conversion ro, vo = 7.0, 230.0 assert ( numpy.fabs( conversion.parse_length(2.0 * units.parsec, ro=ro, vo=vo) - conversion.parse_length( 2.0 * units.parsec, ro=ro * units.kpc, vo=vo * units.km / units.s ) ) < 1e-10 ), ( "parse_length does parse Quantity position correctly when specifying ro and vo as Quantities" ) assert ( numpy.fabs( conversion.parse_energy(-30.0 * units.km**2 / units.s**2, ro=ro, vo=vo) - conversion.parse_energy( -30.0 * units.km**2 / units.s**2, ro=(ro * units.kpc).to(units.m), vo=(vo * units.km / units.s).to(units.pc / units.Myr), ) ) < 1e-10 ), ( "parse_energy does parse Quantity energy correctly when specifying ro and vo as Quantities" ) return None def test_parsers_rovo_wronginputtype(): # Test that giving ro and vo that can't be understood gives an error from galpy.util import conversion # Just some object class other_quantity_object: def __init__(self): return None obj = other_quantity_object() ro, vo = 7.0, 230.0 with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_length(8.0 * units.kpc, ro=obj, vo=vo) with pytest.raises( RuntimeError, match="should either be a number or an astropy Quantity" ): assert conversion.parse_length(8.0 * units.kpc, ro=ro, vo=obj) return None def test_warn_internal_when_use_physical(): import warnings from galpy import potential from galpy.util import galpyWarning with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) potential.evaluateRforces( potential.MWPotential2014, 1.0, 0.0, use_physical=True ) raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "Returning output(s) in internal units even though use_physical=True, because ro and/or vo not set" ) if raisedWarning: break assert raisedWarning, ( "No warning raised when returning internal-units with use_physical=True" ) return None def test_orbit_setup_radec_basic(): from galpy.orbit import Orbit o = Orbit( [ 10.0 * units.deg, -20.0 * units.deg, 3.0 * units.kpc, -3.0 * units.mas / units.yr, 2.0 * units.mas / units.yr, 130.0 * units.km / units.s, ], radec=True, ) assert numpy.fabs(o.ra(quantity=False) - 10.0) < 10.0**-8.0, ( "Orbit initialization with RA as Quantity does not work as expected" ) assert numpy.fabs(o.dec(quantity=False) + 20.0) < 10.0**-8.0, ( "Orbit initialization with Dec as Quantity does not work as expected" ) assert numpy.fabs(o.dist(quantity=False) - 3.0) < 10.0**-8.0, ( "Orbit initialization with distance as Quantity does not work as expected" ) assert numpy.fabs(o.pmra(quantity=False) + 3.0) < 10.0**-8.0, ( "Orbit initialization with pmra as Quantity does not work as expected" ) assert numpy.fabs(o.pmdec(quantity=False) - 2.0) < 10.0**-8.0, ( "Orbit initialization with pmdec as Quantity does not work as expected" ) assert numpy.fabs(o.vlos(quantity=False) - 130.0) < 10.0**-8.0, ( "Orbit initialization with vlos as Quantity does not work as expected" ) return None def test_orbit_setup_radec_oddunits(): from galpy.orbit import Orbit o = Orbit( [ 1.0 * units.rad, -0.25 * units.rad, 3000.0 * units.lyr, -3.0 * units.mas / units.s, 2.0 * units.mas / units.kyr, 130.0 * units.pc / units.Myr, ], radec=True, ) assert numpy.fabs(o.ra(quantity=False) - 1.0 / numpy.pi * 180.0) < 10.0**-8.0, ( "Orbit initialization with RA as Quantity does not work as expected" ) assert numpy.fabs(o.dec(quantity=False) + 0.25 / numpy.pi * 180.0) < 10.0**-8.0, ( "Orbit initialization with Dec as Quantity does not work as expected" ) assert numpy.fabs(o.dist(quantity=False) - 3.0 / 3.26156) < 10.0**-5.0, ( "Orbit initialization with distance as Quantity does not work as expected" ) assert ( numpy.fabs( (o.pmra(quantity=False) + 3.0 * units.yr.to(units.s)) / o.pmra(quantity=False) ) < 10.0**-8.0 ), "Orbit initialization with pmra as Quantity does not work as expected" assert ( numpy.fabs( (o.pmdec(quantity=False) - 2.0 / 10.0**3.0) / o.pmdec(quantity=False) ) < 10.0**-4.0 ), "Orbit initialization with pmdec as Quantity does not work as expected" assert ( numpy.fabs(o.vlos(quantity=False) - 130.0 / 1.0227121655399913) < 10.0**-5.0 ), "Orbit initialization with vlos as Quantity does not work as expected" return None def test_orbit_setup_radec_uvw(): from galpy.orbit import Orbit o = Orbit( [ 1.0 * units.rad, -0.25 * units.rad, 3000.0 * units.pc, -30.0 * units.km / units.s, 20.0 * units.km / units.s, 130.0 * units.km / units.s, ], radec=True, uvw=True, ) assert numpy.fabs(o.ra(quantity=False) - 1.0 / numpy.pi * 180.0) < 10.0**-8.0, ( "Orbit initialization with RA as Quantity does not work as expected" ) assert numpy.fabs(o.dec(quantity=False) + 0.25 / numpy.pi * 180.0) < 10.0**-8.0, ( "Orbit initialization with Dec as Quantity does not work as expected" ) assert numpy.fabs(o.dist(quantity=False) - 3.0) < 10.0**-8.0, ( "Orbit initialization with distance as Quantity does not work as expected" ) assert numpy.fabs(o.U(quantity=False) + 30.0) < 10.0**-8.0, ( "Orbit initialization with U as Quantity does not work as expected" ) assert numpy.fabs(o.V(quantity=False) - 20.0) < 10.0**-8.0, ( "Orbit initialization with V as Quantity does not work as expected" ) assert numpy.fabs(o.W(quantity=False) - 130.0) < 10.0**-8.0, ( "Orbit initialization with W as Quantity does not work as expected" ) return None def test_orbit_setup_radec_uvw_oddunits(): from galpy.orbit import Orbit o = Orbit( [ 1.0 * units.rad, -0.25 * units.rad, 3000.0 * units.pc, -30.0 * units.pc / units.Myr, 20.0 * units.pc / units.Myr, 130.0 * units.pc / units.Myr, ], radec=True, uvw=True, ) assert numpy.fabs(o.ra(quantity=False) - 1.0 / numpy.pi * 180.0) < 10.0**-8.0, ( "Orbit initialization with RA as Quantity does not work as expected" ) assert numpy.fabs(o.dec(quantity=False) + 0.25 / numpy.pi * 180.0) < 10.0**-8.0, ( "Orbit initialization with Dec as Quantity does not work as expected" ) assert numpy.fabs(o.dist(quantity=False) - 3.0) < 10.0**-8.0, ( "Orbit initialization with distance as Quantity does not work as expected" ) assert numpy.fabs(o.U(quantity=False) + 30.0 / 1.0227121655399913) < 10.0**-5.0, ( "Orbit initialization with U as Quantity does not work as expected" ) assert numpy.fabs(o.V(quantity=False) - 20.0 / 1.0227121655399913) < 10.0**-5.0, ( "Orbit initialization with V as Quantity does not work as expected" ) assert numpy.fabs(o.W(quantity=False) - 130.0 / 1.0227121655399913) < 10.0**-5.0, ( "Orbit initialization with W as Quantity does not work as expected" ) return None def test_orbit_setup_lb_basic(): from galpy.orbit import Orbit o = Orbit( [ 10.0 * units.deg, -20.0 * units.deg, 3.0 * units.kpc, -3.0 * units.mas / units.yr, 2.0 * units.mas / units.yr, 130.0 * units.km / units.s, ], lb=True, ) assert numpy.fabs(o.ll(quantity=False) - 10.0) < 10.0**-8.0, ( "Orbit initialization with ll as Quantity does not work as expected" ) assert numpy.fabs(o.bb(quantity=False) + 20.0) < 10.0**-8.0, ( "Orbit initialization with bb as Quantity does not work as expected" ) assert numpy.fabs(o.dist(quantity=False) - 3.0) < 10.0**-8.0, ( "Orbit initialization with distance as Quantity does not work as expected" ) assert numpy.fabs(o.pmll(quantity=False) + 3.0) < 10.0**-8.0, ( "Orbit initialization with pmra as Quantity does not work as expected" ) assert numpy.fabs(o.pmbb(quantity=False) - 2.0) < 10.0**-8.0, ( "Orbit initialization with pmdec as Quantity does not work as expected" ) assert numpy.fabs(o.vlos(quantity=False) - 130.0) < 10.0**-8.0, ( "Orbit initialization with vlos as Quantity does not work as expected" ) return None def test_orbit_setup_lb_oddunits(): from galpy.orbit import Orbit o = Orbit( [ 1.0 * units.rad, -0.25 * units.rad, 3000.0 * units.lyr, -3.0 * units.mas / units.s, 2.0 * units.mas / units.kyr, 130.0 * units.pc / units.Myr, ], lb=True, ) assert numpy.fabs(o.ll(quantity=False) - 1.0 / numpy.pi * 180.0) < 10.0**-8.0, ( "Orbit initialization with ll as Quantity does not work as expected" ) assert numpy.fabs(o.bb(quantity=False) + 0.25 / numpy.pi * 180.0) < 10.0**-8.0, ( "Orbit initialization with bb as Quantity does not work as expected" ) assert numpy.fabs(o.dist(quantity=False) - 3.0 / 3.26156) < 10.0**-5.0, ( "Orbit initialization with distance as Quantity does not work as expected" ) assert ( numpy.fabs( (o.pmll(quantity=False) + 3.0 * units.yr.to(units.s)) / o.pmll(quantity=False) ) < 10.0**-8.0 ), "Orbit initialization with pmll as Quantity does not work as expected" assert ( numpy.fabs((o.pmbb(quantity=False) - 2.0 / 10.0**3.0) / o.pmbb(quantity=False)) < 10.0**-4.0 ), "Orbit initialization with pmbb as Quantity does not work as expected" assert ( numpy.fabs(o.vlos(quantity=False) - 130.0 / 1.0227121655399913) < 10.0**-5.0 ), "Orbit initialization with vlos as Quantity does not work as expected" return None def test_orbit_setup_lb_uvw(): from galpy.orbit import Orbit o = Orbit( [ 1.0 * units.rad, -0.25 * units.rad, 3000.0 * units.pc, -30.0 * units.km / units.s, 20.0 * units.km / units.s, 130.0 * units.km / units.s, ], lb=True, uvw=True, ) assert numpy.fabs(o.ll(quantity=False) - 1.0 / numpy.pi * 180.0) < 10.0**-8.0, ( "Orbit initialization with ll as Quantity does not work as expected" ) assert numpy.fabs(o.bb(quantity=False) + 0.25 / numpy.pi * 180.0) < 10.0**-8.0, ( "Orbit initialization with bb as Quantity does not work as expected" ) assert numpy.fabs(o.dist(quantity=False) - 3.0) < 10.0**-8.0, ( "Orbit initialization with distance as Quantity does not work as expected" ) assert numpy.fabs(o.U(quantity=False) + 30.0) < 10.0**-8.0, ( "Orbit initialization with pmll as Quantity does not work as expected" ) assert numpy.fabs(o.V(quantity=False) - 20.0) < 10.0**-8.0, ( "Orbit initialization with pmbb as Quantity does not work as expected" ) assert numpy.fabs(o.W(quantity=False) - 130.0) < 10.0**-8.0, ( "Orbit initialization with W as Quantity does not work as expected" ) return None def test_orbit_setup_lb_uvw_oddunits(): from galpy.orbit import Orbit o = Orbit( [ 1.0 * units.rad, -0.25 * units.rad, 3000.0 * units.pc, -30.0 * units.pc / units.Myr, 20.0 * units.pc / units.Myr, 130.0 * units.pc / units.Myr, ], lb=True, uvw=True, ) assert numpy.fabs(o.ll(quantity=False) - 1.0 / numpy.pi * 180.0) < 10.0**-8.0, ( "Orbit initialization with ll as Quantity does not work as expected" ) assert numpy.fabs(o.bb(quantity=False) + 0.25 / numpy.pi * 180.0) < 10.0**-8.0, ( "Orbit initialization with bb as Quantity does not work as expected" ) assert numpy.fabs(o.dist(quantity=False) - 3.0) < 10.0**-8.0, ( "Orbit initialization with distance as Quantity does not work as expected" ) assert numpy.fabs(o.U(quantity=False) + 30.0 / 1.0227121655399913) < 10.0**-5.0, ( "Orbit initialization with U as Quantity does not work as expected" ) assert numpy.fabs(o.V(quantity=False) - 20.0 / 1.0227121655399913) < 10.0**-5.0, ( "Orbit initialization with V as Quantity does not work as expected" ) assert numpy.fabs(o.W(quantity=False) - 130.0 / 1.0227121655399913) < 10.0**-5.0, ( "Orbit initialization with W as Quantity does not work as expected" ) return None def test_orbit_setup_vxvv_fullorbit(): from galpy.orbit import Orbit o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ] ) assert numpy.fabs(o.R(use_physical=False) * o._ro - 10.0) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for FullOrbit" ) assert numpy.fabs(o.vR(use_physical=False) * o._vo + 20.0) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for FullOrbit" ) assert numpy.fabs(o.vT(use_physical=False) * o._vo - 210.0) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for FullOrbit" ) assert numpy.fabs(o.z(use_physical=False) * o._ro - 0.5) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for FullOrbit" ) assert numpy.fabs(o.vz(use_physical=False) * o._vo + 12) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for FullOrbit" ) assert ( numpy.fabs(o.phi(use_physical=False) - 45.0 / 180.0 * numpy.pi) < 10.0**-8.0 ), ( "Orbit initialization with vxvv as Quantity does not work as expected for FullOrbit" ) return None def test_orbit_setup_vxvv_rzorbit(): from galpy.orbit import Orbit o = Orbit( [ 10000.0 * units.lyr, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.pc / units.Myr, ] ) assert numpy.fabs(o.R(use_physical=False) * o._ro - 10.0 / 3.26156) < 10.0**-5.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) assert numpy.fabs(o.vR(use_physical=False) * o._vo + 20.0) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) assert numpy.fabs(o.vT(use_physical=False) * o._vo - 210.0) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) assert numpy.fabs(o.z(use_physical=False) * o._ro - 0.5) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) assert ( numpy.fabs(o.vz(use_physical=False) * o._vo + 12.0 / 1.0227121655399913) < 10.0**-5.0 ), ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) return None def test_orbit_setup_vxvv_planarorbit(): from galpy.orbit import Orbit o = Orbit( [ 10000.0 * units.lyr, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 3.0 * units.rad, ] ) assert numpy.fabs(o.R(use_physical=False) * o._ro - 10.0 / 3.26156) < 10.0**-5.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) assert numpy.fabs(o.vR(use_physical=False) * o._vo + 20.0) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) assert numpy.fabs(o.vT(use_physical=False) * o._vo - 210.0) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) assert numpy.fabs(o.phi(use_physical=False) - 3.0) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for FullOrbit" ) return None def test_orbit_setup_vxvv_planarrorbit(): from galpy.orbit import Orbit o = Orbit( [7.0 * units.kpc, -2.0 * units.km / units.s, 210.0 * units.km / units.s], ro=10.0, vo=150.0, ) assert numpy.fabs(o.R(use_physical=False) * o._ro - 7.0) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) assert numpy.fabs(o.vR(use_physical=False) * o._vo + 2.0) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) assert numpy.fabs(o.vT(use_physical=False) * o._vo - 210.0) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) return None def test_orbit_setup_vxvv_linearorbit(): from galpy.orbit import Orbit o = Orbit([7.0 * units.kpc, -21.0 * units.pc / units.Myr]) assert numpy.fabs(o.x(use_physical=False) * o._ro - 7.0) < 10.0**-8.0, ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) assert ( numpy.fabs(o.vx(use_physical=False) * o._vo + 21.0 / 1.0227121655399913) < 10.0**-5.0 ), ( "Orbit initialization with vxvv as Quantity does not work as expected for RZOrbit" ) return None def test_orbit_setup_solarmotion(): from galpy.orbit import Orbit o = Orbit( [1.0, 0.1, 1.1, 0.2, 0.1, 0.0], solarmotion=units.Quantity([13.0, 25.0, 8.0], unit=units.km / units.s), ) assert numpy.fabs(o._solarmotion[0] - 13.0) < 10.0**-8.0, ( "solarmotion in Orbit setup as Quantity does not work as expected" ) assert numpy.fabs(o._solarmotion[1] - 25.0) < 10.0**-8.0, ( "solarmotion in Orbit setup as Quantity does not work as expected" ) assert numpy.fabs(o._solarmotion[2] - 8.0) < 10.0**-8.0, ( "solarmotion in Orbit setup as Quantity does not work as expected" ) return None def test_orbit_setup_solarmotion_oddunits(): from galpy.orbit import Orbit o = Orbit( [1.0, 0.1, 1.1, 0.2, 0.1, 0.0], solarmotion=units.Quantity([13.0, 25.0, 8.0], unit=units.kpc / units.Gyr), ) assert numpy.fabs(o._solarmotion[0] - 13.0 / 1.0227121655399913) < 10.0**-5.0, ( "solarmotion in Orbit setup as Quantity does not work as expected" ) assert numpy.fabs(o._solarmotion[1] - 25.0 / 1.0227121655399913) < 10.0**-5.0, ( "solarmotion in Orbit setup as Quantity does not work as expected" ) assert numpy.fabs(o._solarmotion[2] - 8.0 / 1.0227121655399913) < 10.0**-5.0, ( "solarmotion in Orbit setup as Quantity does not work as expected" ) return None def test_orbit_setup_roAsQuantity(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.2, 0.1, 0.0], ro=11 * units.kpc) assert numpy.fabs(o._ro - 11.0) < 10.0**-10.0, ( "ro in Orbit setup as Quantity does not work as expected" ) assert numpy.fabs(o._ro - 11.0) < 10.0**-10.0, ( "ro in Orbit setup as Quantity does not work as expected" ) return None def test_orbit_setup_roAsQuantity_oddunits(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.2, 0.1, 0.0], ro=11 * units.lyr) assert numpy.fabs(o._ro - 11.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in Orbit setup as Quantity does not work as expected" ) assert numpy.fabs(o._ro - 11.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in Orbit setup as Quantity does not work as expected" ) return None def test_orbit_setup_voAsQuantity(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.2, 0.1, 0.0], vo=210 * units.km / units.s) assert numpy.fabs(o._vo - 210.0) < 10.0**-10.0, ( "vo in Orbit setup as Quantity does not work as expected" ) assert numpy.fabs(o._vo - 210.0) < 10.0**-10.0, ( "vo in Orbit setup as Quantity does not work as expected" ) return None def test_orbit_setup_voAsQuantity_oddunits(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.2, 0.1, 0.0], vo=210 * units.pc / units.Myr) assert ( numpy.fabs(o._vo - 210.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "vo in Orbit setup as Quantity does not work as expected" assert ( numpy.fabs(o._vo - 210.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "vo in Orbit setup as Quantity does not work as expected" return None def test_orbit_setup_zoAsQuantity(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.2, 0.1, 0.0], zo=12 * units.pc) assert numpy.fabs(o._zo - 0.012) < 10.0**-10.0, ( "zo in Orbit setup as Quantity does not work as expected" ) return None def test_orbit_setup_zoAsQuantity_oddunits(): from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.2, 0.1, 0.0], zo=13 * units.lyr) assert numpy.fabs(o._zo - 13.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "zo in Orbit setup as Quantity does not work as expected" ) return None def test_orbit_method_returntype_scalar(): from galpy.orbit import Orbit o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ] ) from galpy.potential import MWPotential2014 assert isinstance(o.E(pot=MWPotential2014), units.Quantity), ( "Orbit method E does not return Quantity when it should" ) assert isinstance(o.ER(pot=MWPotential2014), units.Quantity), ( "Orbit method ER does not return Quantity when it should" ) assert isinstance(o.Ez(pot=MWPotential2014), units.Quantity), ( "Orbit method Ez does not return Quantity when it should" ) assert isinstance(o.Jacobi(pot=MWPotential2014), units.Quantity), ( "Orbit method Jacobi does not return Quantity when it should" ) assert isinstance(o.L(), units.Quantity), ( "Orbit method L does not return Quantity when it should" ) assert isinstance(o.Lz(), units.Quantity), ( "Orbit method Lz does not return Quantity when it should" ) assert isinstance(o.rap(pot=MWPotential2014, analytic=True), units.Quantity), ( "Orbit method rap does not return Quantity when it should" ) assert isinstance(o.rperi(pot=MWPotential2014, analytic=True), units.Quantity), ( "Orbit method rperi does not return Quantity when it should" ) assert isinstance(o.rguiding(pot=MWPotential2014), units.Quantity), ( "Orbit method rguiding does not return Quantity when it should" ) assert isinstance(o.rE(pot=MWPotential2014), units.Quantity), ( "Orbit method rE does not return Quantity when it should" ) assert isinstance(o.LcE(pot=MWPotential2014), units.Quantity), ( "Orbit method LcE does not return Quantity when it should" ) assert isinstance(o.zmax(pot=MWPotential2014, analytic=True), units.Quantity), ( "Orbit method zmax does not return Quantity when it should" ) assert isinstance( o.jr(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method jr does not return Quantity when it should" assert isinstance( o.jp(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method jp does not return Quantity when it should" assert isinstance( o.jz(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method jz does not return Quantity when it should" assert isinstance( o.wr(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method wr does not return Quantity when it should" assert isinstance( o.wp(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method wp does not return Quantity when it should" assert isinstance( o.wz(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method wz does not return Quantity when it should" assert isinstance( o.Tr(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method Tr does not return Quantity when it should" assert isinstance( o.Tp(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method Tp does not return Quantity when it should" assert isinstance( o.Tz(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method Tz does not return Quantity when it should" assert isinstance( o.Or(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method Or does not return Quantity when it should" assert isinstance( o.Op(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method Op does not return Quantity when it should" assert isinstance( o.Oz(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method Oz does not return Quantity when it should" assert isinstance(o.time(), units.Quantity), ( "Orbit method time does not return Quantity when it should" ) assert isinstance(o.R(), units.Quantity), ( "Orbit method R does not return Quantity when it should" ) assert isinstance(o.r(), units.Quantity), ( "Orbit method r does not return Quantity when it should" ) assert isinstance(o.vR(), units.Quantity), ( "Orbit method vR does not return Quantity when it should" ) assert isinstance(o.vT(), units.Quantity), ( "Orbit method vT does not return Quantity when it should" ) assert isinstance(o.z(), units.Quantity), ( "Orbit method z does not return Quantity when it should" ) assert isinstance(o.vz(), units.Quantity), ( "Orbit method vz does not return Quantity when it should" ) assert isinstance(o.phi(), units.Quantity), ( "Orbit method phi does not return Quantity when it should" ) assert isinstance(o.vphi(), units.Quantity), ( "Orbit method vphi does not return Quantity when it should" ) assert isinstance(o.x(), units.Quantity), ( "Orbit method x does not return Quantity when it should" ) assert isinstance(o.y(), units.Quantity), ( "Orbit method y does not return Quantity when it should" ) assert isinstance(o.vx(), units.Quantity), ( "Orbit method vx does not return Quantity when it should" ) assert isinstance(o.vy(), units.Quantity), ( "Orbit method vy does not return Quantity when it should" ) assert isinstance(o.ra(), units.Quantity), ( "Orbit method ra does not return Quantity when it should" ) assert isinstance(o.dec(), units.Quantity), ( "Orbit method dec does not return Quantity when it should" ) assert isinstance(o.ll(), units.Quantity), ( "Orbit method ll does not return Quantity when it should" ) assert isinstance(o.bb(), units.Quantity), ( "Orbit method bb does not return Quantity when it should" ) assert isinstance(o.dist(), units.Quantity), ( "Orbit method dist does not return Quantity when it should" ) assert isinstance(o.pmra(), units.Quantity), ( "Orbit method pmra does not return Quantity when it should" ) assert isinstance(o.pmdec(), units.Quantity), ( "Orbit method pmdec does not return Quantity when it should" ) assert isinstance(o.pmll(), units.Quantity), ( "Orbit method pmll does not return Quantity when it should" ) assert isinstance(o.pmbb(), units.Quantity), ( "Orbit method pmbb does not return Quantity when it should" ) assert isinstance(o.vlos(), units.Quantity), ( "Orbit method vlos does not return Quantity when it should" ) assert isinstance(o.vra(), units.Quantity), ( "Orbit method vra does not return Quantity when it should" ) assert isinstance(o.vdec(), units.Quantity), ( "Orbit method vdec does not return Quantity when it should" ) assert isinstance(o.vll(), units.Quantity), ( "Orbit method vll does not return Quantity when it should" ) assert isinstance(o.vbb(), units.Quantity), ( "Orbit method vbb does not return Quantity when it should" ) assert isinstance(o.helioX(), units.Quantity), ( "Orbit method helioX does not return Quantity when it should" ) assert isinstance(o.helioY(), units.Quantity), ( "Orbit method helioY does not return Quantity when it should" ) assert isinstance(o.helioZ(), units.Quantity), ( "Orbit method helioZ does not return Quantity when it should" ) assert isinstance(o.U(), units.Quantity), ( "Orbit method U does not return Quantity when it should" ) assert isinstance(o.V(), units.Quantity), ( "Orbit method V does not return Quantity when it should" ) assert isinstance(o.W(), units.Quantity), ( "Orbit method W does not return Quantity when it should" ) return None def test_orbit_method_returntype(): from galpy.orbit import Orbit o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ] ) from galpy.potential import MWPotential2014 ts = numpy.linspace(0.0, 6.0, 1001) o.integrate(ts, MWPotential2014) assert isinstance(o.E(ts), units.Quantity), ( "Orbit method E does not return Quantity when it should" ) assert isinstance(o.ER(ts), units.Quantity), ( "Orbit method ER does not return Quantity when it should" ) assert isinstance(o.Ez(ts), units.Quantity), ( "Orbit method Ez does not return Quantity when it should" ) assert isinstance(o.Jacobi(ts), units.Quantity), ( "Orbit method Jacobi does not return Quantity when it should" ) assert isinstance(o.L(ts), units.Quantity), ( "Orbit method L does not return Quantity when it should" ) assert isinstance(o.Lz(ts), units.Quantity), ( "Orbit method L does not return Quantity when it should" ) assert isinstance(o.time(ts), units.Quantity), ( "Orbit method time does not return Quantity when it should" ) assert isinstance(o.R(ts), units.Quantity), ( "Orbit method R does not return Quantity when it should" ) assert isinstance(o.r(ts), units.Quantity), ( "Orbit method r does not return Quantity when it should" ) assert isinstance(o.vR(ts), units.Quantity), ( "Orbit method vR does not return Quantity when it should" ) assert isinstance(o.vT(ts), units.Quantity), ( "Orbit method vT does not return Quantity when it should" ) assert isinstance(o.z(ts), units.Quantity), ( "Orbit method z does not return Quantity when it should" ) assert isinstance(o.vz(ts), units.Quantity), ( "Orbit method vz does not return Quantity when it should" ) assert isinstance(o.phi(ts), units.Quantity), ( "Orbit method phi does not return Quantity when it should" ) assert isinstance(o.vphi(ts), units.Quantity), ( "Orbit method vphi does not return Quantity when it should" ) assert isinstance(o.x(ts), units.Quantity), ( "Orbit method x does not return Quantity when it should" ) assert isinstance(o.y(ts), units.Quantity), ( "Orbit method y does not return Quantity when it should" ) assert isinstance(o.vx(ts), units.Quantity), ( "Orbit method vx does not return Quantity when it should" ) assert isinstance(o.vy(ts), units.Quantity), ( "Orbit method vy does not return Quantity when it should" ) assert isinstance(o.ra(ts), units.Quantity), ( "Orbit method ra does not return Quantity when it should" ) assert isinstance(o.dec(ts), units.Quantity), ( "Orbit method dec does not return Quantity when it should" ) assert isinstance(o.ll(ts), units.Quantity), ( "Orbit method ll does not return Quantity when it should" ) assert isinstance(o.bb(ts), units.Quantity), ( "Orbit method bb does not return Quantity when it should" ) assert isinstance(o.dist(ts), units.Quantity), ( "Orbit method dist does not return Quantity when it should" ) assert isinstance(o.pmra(ts), units.Quantity), ( "Orbit method pmra does not return Quantity when it should" ) assert isinstance(o.pmdec(ts), units.Quantity), ( "Orbit method pmdec does not return Quantity when it should" ) assert isinstance(o.pmll(ts), units.Quantity), ( "Orbit method pmll does not return Quantity when it should" ) assert isinstance(o.pmbb(ts), units.Quantity), ( "Orbit method pmbb does not return Quantity when it should" ) assert isinstance(o.vlos(ts), units.Quantity), ( "Orbit method vlos does not return Quantity when it should" ) assert isinstance(o.vra(ts), units.Quantity), ( "Orbit method vra does not return Quantity when it should" ) assert isinstance(o.vdec(ts), units.Quantity), ( "Orbit method vdec does not return Quantity when it should" ) assert isinstance(o.vll(ts), units.Quantity), ( "Orbit method vll does not return Quantity when it should" ) assert isinstance(o.vbb(ts), units.Quantity), ( "Orbit method vbb does not return Quantity when it should" ) assert isinstance(o.helioX(ts), units.Quantity), ( "Orbit method helioX does not return Quantity when it should" ) assert isinstance(o.helioY(ts), units.Quantity), ( "Orbit method helioY does not return Quantity when it should" ) assert isinstance(o.helioZ(ts), units.Quantity), ( "Orbit method helioZ does not return Quantity when it should" ) assert isinstance(o.U(ts), units.Quantity), ( "Orbit method U does not return Quantity when it should" ) assert isinstance(o.V(ts), units.Quantity), ( "Orbit method V does not return Quantity when it should" ) assert isinstance(o.W(ts), units.Quantity), ( "Orbit method W does not return Quantity when it should" ) return None def test_orbit_method_returnunit(): from galpy.orbit import Orbit o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ] ) from galpy.potential import MWPotential2014 try: o.E(pot=MWPotential2014).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Orbit method E does not return Quantity with the right units" ) try: o.ER(pot=MWPotential2014).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Orbit method ER does not return Quantity with the right units" ) try: o.Ez(pot=MWPotential2014).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Orbit method Ez does not return Quantity with the right units" ) try: o.Jacobi(pot=MWPotential2014).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Orbit method Jacobi does not return Quantity with the right units" ) try: o.L().to(units.km**2 / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method L does not return Quantity with the right units" ) try: o.Lz().to(units.km**2 / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method Lz does not return Quantity with the right units" ) try: o.rap(pot=MWPotential2014, analytic=True).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method rap does not return Quantity with the right units" ) try: o.rperi(pot=MWPotential2014, analytic=True).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method rperi does not return Quantity with the right units" ) try: o.rguiding(pot=MWPotential2014).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method rguiding does not return Quantity with the right units" ) try: o.rE(pot=MWPotential2014).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method rE does not return Quantity with the right units" ) try: o.LcE(pot=MWPotential2014).to(units.kpc * units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method LcE does not return Quantity with the right units" ) try: o.zmax(pot=MWPotential2014, analytic=True).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method zmax does not return Quantity with the right units" ) try: o.jr(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.km**2 / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method jr does not return Quantity with the right units" ) try: o.jp(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.km**2 / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method jp does not return Quantity with the right units" ) try: o.jz(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.km**2 / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method jz does not return Quantity with the right units" ) try: o.wr(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method wr does not return Quantity with the right units" ) try: o.wp(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method wp does not return Quantity with the right units" ) try: o.wz(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method wz does not return Quantity with the right units" ) try: o.Tr(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method Tr does not return Quantity with the right units" ) try: o.Tp(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method Tp does not return Quantity with the right units" ) try: o.Tz(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method Tz does not return Quantity with the right units" ) try: o.Or(pot=MWPotential2014, type="staeckel", delta=0.5).to(1 / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method Or does not return Quantity with the right units" ) try: o.Op(pot=MWPotential2014, type="staeckel", delta=0.5).to(1 / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method Op does not return Quantity with the right units" ) try: o.Oz(pot=MWPotential2014, type="staeckel", delta=0.5).to(1 / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method Oz does not return Quantity with the right units" ) try: o.time().to(units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method time does not return Quantity with the right units" ) try: o.R().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method R does not return Quantity with the right units" ) try: o.r().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method r does not return Quantity with the right units" ) try: o.vR().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vR does not return Quantity with the right units" ) try: o.vT().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vT does not return Quantity with the right units" ) try: o.z().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method z does not return Quantity with the right units" ) try: o.vz().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vz does not return Quantity with the right units" ) try: o.phi().to(units.deg) except units.UnitConversionError: raise AssertionError( "Orbit method phi does not return Quantity with the right units" ) try: o.vphi().to(units.km / units.s / units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method vphi does not return Quantity with the right units" ) try: o.x().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method x does not return Quantity with the right units" ) try: o.y().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method y does not return Quantity with the right units" ) try: o.vx().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vx does not return Quantity with the right units" ) try: o.vy().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vy does not return Quantity with the right units" ) try: o.ra().to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method ra does not return Quantity with the right units" ) try: o.dec().to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method dec does not return Quantity with the right units" ) try: o.ll().to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method ll does not return Quantity with the right units" ) try: o.bb().to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method bb does not return Quantity with the right units" ) try: o.dist().to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method dist does not return Quantity with the right units" ) try: o.pmra().to(units.mas / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method pmra does not return Quantity with the right units" ) try: o.pmdec().to(units.mas / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method pmdec does not return Quantity with the right units" ) try: o.pmll().to(units.mas / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method pmll does not return Quantity with the right units" ) try: o.pmbb().to(units.mas / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method pmbb does not return Quantity with the right units" ) try: o.vlos().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vlos does not return Quantity with the right units" ) try: o.vra().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vra does not return Quantity with the right units" ) try: o.vdec().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vdec does not return Quantity with the right units" ) try: o.vll().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vll does not return Quantity with the right units" ) try: o.vbb().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vbb does not return Quantity with the right units" ) try: o.helioX().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method helioX does not return Quantity with the right units" ) try: o.helioY().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method helioY does not return Quantity with the right units" ) try: o.helioZ().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method helioZ does not return Quantity with the right units" ) try: o.U().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method U does not return Quantity with the right units" ) try: o.V().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method V does not return Quantity with the right units" ) try: o.W().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method W does not return Quantity with the right units" ) return None def test_orbit_method_value(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 from galpy.util import conversion o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ] ) oc = o() oc.turn_physical_off() assert ( numpy.fabs( o.E(pot=MWPotential2014).to(units.km**2 / units.s**2).value - oc.E(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method E does not return the correct value as Quantity" assert ( numpy.fabs( o.ER(pot=MWPotential2014).to(units.km**2 / units.s**2).value - oc.ER(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method ER does not return the correct value as Quantity" assert ( numpy.fabs( o.Ez(pot=MWPotential2014).to(units.km**2 / units.s**2).value - oc.Ez(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method Ez does not return the correct value as Quantity" assert ( numpy.fabs( o.Jacobi(pot=MWPotential2014).to(units.km**2 / units.s**2).value - oc.Jacobi(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method Jacobi does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.L(pot=MWPotential2014).to(units.km / units.s * units.kpc).value - oc.L(pot=MWPotential2014) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method L does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.Lz(pot=MWPotential2014).to(units.km / units.s * units.kpc).value - oc.Lz(pot=MWPotential2014) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method L does not return the correct value as Quantity" assert ( numpy.fabs( o.rap(pot=MWPotential2014, analytic=True).to(units.kpc).value - oc.rap(pot=MWPotential2014, analytic=True) * o._ro ) < 10.0**-8.0 ), "Orbit method rap does not return the correct value as Quantity" assert ( numpy.fabs( o.rperi(pot=MWPotential2014, analytic=True).to(units.kpc).value - oc.rperi(pot=MWPotential2014, analytic=True) * o._ro ) < 10.0**-8.0 ), "Orbit method rperi does not return the correct value as Quantity" assert ( numpy.fabs( o.rguiding(pot=MWPotential2014).to(units.kpc).value - oc.rguiding(pot=MWPotential2014) * o._ro ) < 10.0**-8.0 ), "Orbit method rguiding does not return the correct value as Quantity" assert ( numpy.fabs( o.rE(pot=MWPotential2014).to(units.kpc).value - oc.rE(pot=MWPotential2014) * o._ro ) < 10.0**-8.0 ), "Orbit method rE does not return the correct value as Quantity" assert ( numpy.fabs( o.LcE(pot=MWPotential2014).to(units.kpc * units.km / units.s).value - oc.LcE(pot=MWPotential2014) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method LcE does not return the correct value as Quantity" assert ( numpy.fabs( o.zmax(pot=MWPotential2014, analytic=True).to(units.kpc).value - oc.zmax(pot=MWPotential2014, analytic=True) * o._ro ) < 10.0**-8.0 ), "Orbit method zmax does not return the correct value as Quantity" assert ( numpy.fabs( o.jr(pot=MWPotential2014, type="staeckel", delta=0.5) .to(units.km / units.s * units.kpc) .value - oc.jr(pot=MWPotential2014, type="staeckel", delta=0.5) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method jr does not return the correct value as Quantity" assert ( numpy.fabs( o.jp(pot=MWPotential2014, type="staeckel", delta=4.0 * units.kpc) .to(units.km / units.s * units.kpc) .value - oc.jp(pot=MWPotential2014, type="staeckel", delta=0.5) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method jp does not return the correct value as Quantity" assert ( numpy.fabs( o.jz(pot=MWPotential2014, type="isochroneapprox", b=0.8 * 8.0 * units.kpc) .to(units.km / units.s * units.kpc) .value - oc.jz(pot=MWPotential2014, type="isochroneapprox", b=0.8) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method jz does not return the correct value as Quantity" assert ( numpy.fabs( o.wr(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.rad).value - oc.wr(pot=MWPotential2014, type="staeckel", delta=0.5) ) < 10.0**-8.0 ), "Orbit method wr does not return the correct value as Quantity" assert ( numpy.fabs( o.wp(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.rad).value - oc.wp(pot=MWPotential2014, type="staeckel", delta=0.5) ) < 10.0**-8.0 ), "Orbit method wp does not return the correct value as Quantity" assert ( numpy.fabs( o.wz(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.rad).value - oc.wz(pot=MWPotential2014, type="staeckel", delta=0.5) ) < 10.0**-8.0 ), "Orbit method wz does not return the correct value as Quantity" assert ( numpy.fabs( o.Tr(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.Gyr).value - oc.Tr(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Tr does not return the correct value as Quantity" assert ( numpy.fabs( o.Tp(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.Gyr).value - oc.Tp(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Tp does not return the correct value as Quantity" assert ( numpy.fabs( o.Tz(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.Gyr).value - oc.Tz(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Tz does not return the correct value as Quantity" assert ( numpy.fabs( o.Or(pot=MWPotential2014, type="staeckel", delta=0.5) .to(1 / units.Gyr) .value - oc.Or(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.freq_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Or does not return the correct value as Quantity" assert ( numpy.fabs( o.Op(pot=MWPotential2014, type="staeckel", delta=0.5) .to(1 / units.Gyr) .value - oc.Op(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.freq_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Opbit method Or does not return the correct value as Quantity" assert ( numpy.fabs( o.Oz(pot=MWPotential2014, type="staeckel", delta=0.5) .to(1 / units.Gyr) .value - oc.Oz(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.freq_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Ozbit method Or does not return the correct value as Quantity" assert ( numpy.fabs( o.time().to(units.Gyr).value - oc.time() * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method time does not return the correct value as Quantity" assert numpy.fabs(o.R().to(units.kpc).value - oc.R() * o._ro) < 10.0**-8.0, ( "Orbit method R does not return the correct value as Quantity" ) assert numpy.fabs(o.r().to(units.kpc).value - oc.r() * o._ro) < 10.0**-8.0, ( "Orbit method r does not return the correct value as Quantity" ) assert ( numpy.fabs(o.vR().to(units.km / units.s).value - oc.vR() * o._vo) < 10.0**-8.0 ), "Orbit method vR does not return the correct value as Quantity" assert ( numpy.fabs(o.vT().to(units.km / units.s).value - oc.vT() * o._vo) < 10.0**-8.0 ), "Orbit method vT does not return the correct value as Quantity" assert numpy.fabs(o.z().to(units.kpc).value - oc.z() * o._ro) < 10.0**-8.0, ( "Orbit method z does not return the correct value as Quantity" ) assert ( numpy.fabs(o.vz().to(units.km / units.s).value - oc.vz() * o._vo) < 10.0**-8.0 ), "Orbit method vz does not return the correct value as Quantity" assert numpy.fabs(o.phi().to(units.rad).value - oc.phi()) < 10.0**-8.0, ( "Orbit method phi does not return the correct value as Quantity" ) assert ( numpy.fabs( o.vphi().to(units.km / units.s / units.kpc).value - oc.vphi() * o._vo / o._ro ) < 10.0**-8.0 ), "Orbit method vphi does not return the correct value as Quantity" assert numpy.fabs(o.x().to(units.kpc).value - oc.x() * o._ro) < 10.0**-8.0, ( "Orbit method x does not return the correct value as Quantity" ) assert numpy.fabs(o.y().to(units.kpc).value - oc.y() * o._ro) < 10.0**-8.0, ( "Orbit method y does not return the correct value as Quantity" ) assert ( numpy.fabs(o.vx().to(units.km / units.s).value - oc.vx() * o._vo) < 10.0**-8.0 ), "Orbit method vx does not return the correct value as Quantity" assert ( numpy.fabs(o.vy().to(units.km / units.s).value - oc.vy() * o._vo) < 10.0**-8.0 ), "Orbit method vy does not return the correct value as Quantity" assert ( numpy.fabs(o.ra().to(units.deg).value - oc.ra(quantity=False)) < 10.0**-8.0 ), "Orbit method ra does not return the correct value as Quantity" assert ( numpy.fabs(o.dec().to(units.deg).value - oc.dec(quantity=False)) < 10.0**-8.0 ), "Orbit method dec does not return the correct value as Quantity" assert ( numpy.fabs(o.ll().to(units.deg).value - oc.ll(quantity=False)) < 10.0**-8.0 ), "Orbit method ll does not return the correct value as Quantity" assert ( numpy.fabs(o.bb().to(units.deg).value - oc.bb(quantity=False)) < 10.0**-8.0 ), "Orbit method bb does not return the correct value as Quantity" assert ( numpy.fabs(o.dist().to(units.kpc).value - oc.dist(quantity=False)) < 10.0**-8.0 ), "Orbit method dist does not return the correct value as Quantity" assert ( numpy.fabs(o.pmra().to(units.mas / units.yr).value - oc.pmra(quantity=False)) < 10.0**-8.0 ), "Orbit method pmra does not return the correct value as Quantity" assert ( numpy.fabs(o.pmdec().to(units.mas / units.yr).value - oc.pmdec(quantity=False)) < 10.0**-8.0 ), "Orbit method pmdec does not return the correct value as Quantity" assert ( numpy.fabs(o.pmll().to(units.mas / units.yr).value - oc.pmll(quantity=False)) < 10.0**-8.0 ), "Orbit method pmll does not return the correct value as Quantity" assert ( numpy.fabs(o.pmbb().to(units.mas / units.yr).value - oc.pmbb(quantity=False)) < 10.0**-8.0 ), "Orbit method pmbb does not return the correct value as Quantity" assert ( numpy.fabs(o.vlos().to(units.km / units.s).value - oc.vlos(quantity=False)) < 10.0**-8.0 ), "Orbit method vlos does not return the correct value as Quantity" assert ( numpy.fabs(o.vra().to(units.km / units.s).value - oc.vra(quantity=False)) < 10.0**-8.0 ), "Orbit method vra does not return the correct value as Quantity" assert ( numpy.fabs(o.vdec().to(units.km / units.s).value - oc.vdec(quantity=False)) < 10.0**-8.0 ), "Orbit method vdec does not return the correct value as Quantity" assert ( numpy.fabs(o.vll().to(units.km / units.s).value - oc.vll(quantity=False)) < 10.0**-8.0 ), "Orbit method vll does not return the correct value as Quantity" assert ( numpy.fabs(o.vbb().to(units.km / units.s).value - oc.vbb(quantity=False)) < 10.0**-8.0 ), "Orbit method vbb does not return the correct value as Quantity" assert ( numpy.fabs(o.helioX().to(units.kpc).value - oc.helioX(quantity=False)) < 10.0**-8.0 ), "Orbit method helioX does not return the correct value as Quantity" assert ( numpy.fabs(o.helioY().to(units.kpc).value - oc.helioY(quantity=False)) < 10.0**-8.0 ), "Orbit method helioY does not return the correct value as Quantity" assert ( numpy.fabs(o.helioZ().to(units.kpc).value - oc.helioZ(quantity=False)) < 10.0**-8.0 ), "Orbit method helioZ does not return the correct value as Quantity" assert ( numpy.fabs(o.U().to(units.km / units.s).value - oc.U(quantity=False)) < 10.0**-8.0 ), "Orbit method U does not return the correct value as Quantity" assert ( numpy.fabs(o.V().to(units.km / units.s).value - oc.V(quantity=False)) < 10.0**-8.0 ), "Orbit method V does not return the correct value as Quantity" assert ( numpy.fabs(o.W().to(units.km / units.s).value - oc.W(quantity=False)) < 10.0**-8.0 ), "Orbit method W does not return the correct value as Quantity" return None def test_orbit_method_value_turnquantityoff(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 from galpy.util import conversion o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ] ) oc = o() oc.turn_physical_off() assert ( numpy.fabs( o.E(pot=MWPotential2014, quantity=False) - oc.E(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method E does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.ER(pot=MWPotential2014, quantity=False) - oc.ER(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method ER does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.Ez(pot=MWPotential2014, quantity=False) - oc.Ez(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method Ez does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.Jacobi(pot=MWPotential2014, quantity=False) - oc.Jacobi(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method Jacobi does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.L(pot=MWPotential2014, quantity=False) - oc.L(pot=MWPotential2014) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method L does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.Lz(pot=MWPotential2014, quantity=False) - oc.Lz(pot=MWPotential2014) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method L does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.rap(pot=MWPotential2014, analytic=True, quantity=False) - oc.rap(pot=MWPotential2014, analytic=True) * o._ro ) < 10.0**-8.0 ), "Orbit method rap does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.rperi(pot=MWPotential2014, analytic=True, quantity=False) - oc.rperi(pot=MWPotential2014, analytic=True) * o._ro ) < 10.0**-8.0 ), "Orbit method rperi does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.rguiding(pot=MWPotential2014, quantity=False) - oc.rguiding(pot=MWPotential2014) * o._ro ) < 10.0**-8.0 ), ( "Orbit method rguiding does not return the correct value when Quantity turned off" ) assert ( numpy.fabs( o.rE(pot=MWPotential2014, quantity=False) - oc.rE(pot=MWPotential2014) * o._ro ) < 10.0**-8.0 ), "Orbit method rE does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.LcE(pot=MWPotential2014, quantity=False) - oc.LcE(pot=MWPotential2014) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method LcE does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.zmax(pot=MWPotential2014, analytic=True, quantity=False) - oc.zmax(pot=MWPotential2014, analytic=True) * o._ro ) < 10.0**-8.0 ), "Orbit method zmax does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.jr(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.jr(pot=MWPotential2014, type="staeckel", delta=0.5) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method jr does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.jp( pot=MWPotential2014, type="staeckel", delta=4.0 * units.kpc, quantity=False, ) - oc.jp(pot=MWPotential2014, type="staeckel", delta=0.5) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method jp does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.jz( pot=MWPotential2014, type="isochroneapprox", b=0.8 * 8.0 * units.kpc, quantity=False, ) - oc.jz(pot=MWPotential2014, type="isochroneapprox", b=0.8) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method jz does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.wr(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.wr(pot=MWPotential2014, type="staeckel", delta=0.5) ) < 10.0**-8.0 ), "Orbit method wr does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.wp(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.wp(pot=MWPotential2014, type="staeckel", delta=0.5) ) < 10.0**-8.0 ), "Orbit method wp does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.wz(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.wz(pot=MWPotential2014, type="staeckel", delta=0.5) ) < 10.0**-8.0 ), "Orbit method wz does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.Tr(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.Tr(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Tr does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.Tp(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.Tp(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Tp does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.Tz(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.Tz(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Tz does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.Or(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.Or(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.freq_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Or does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.Op(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.Op(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.freq_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Opbit method Or does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.Oz(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.Oz(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.freq_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Ozbit method Or does not return the correct value when Quantity turned off" assert ( numpy.fabs( o.time(quantity=False) - oc.time() * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method time does not return the correct value when Quantity turned off" assert numpy.fabs(o.R(quantity=False) - oc.R() * o._ro) < 10.0**-8.0, ( "Orbit method R does not return the correct value when Quantity turned off" ) assert numpy.fabs(o.r(quantity=False) - oc.r() * o._ro) < 10.0**-8.0, ( "Orbit method r does not return the correct value when Quantity turned off" ) assert numpy.fabs(o.vR(quantity=False) - oc.vR() * o._vo) < 10.0**-8.0, ( "Orbit method vR does not return the correct value when Quantity turned off" ) assert numpy.fabs(o.vT(quantity=False) - oc.vT() * o._vo) < 10.0**-8.0, ( "Orbit method vT does not return the correct value when Quantity turned off" ) assert numpy.fabs(o.z(quantity=False) - oc.z() * o._ro) < 10.0**-8.0, ( "Orbit method z does not return the correct value when Quantity turned off" ) assert numpy.fabs(o.vz(quantity=False) - oc.vz() * o._vo) < 10.0**-8.0, ( "Orbit method vz does not return the correct value when Quantity turned off" ) assert numpy.fabs(o.phi(quantity=False) - oc.phi()) < 10.0**-8.0, ( "Orbit method phi does not return the correct value when Quantity turned off" ) assert ( numpy.fabs(o.vphi(quantity=False) - oc.vphi() * o._vo / o._ro) < 10.0**-8.0 ), "Orbit method vphi does not return the correct value when Quantity turned off" assert numpy.fabs(o.x(quantity=False) - oc.x() * o._ro) < 10.0**-8.0, ( "Orbit method x does not return the correct value when Quantity turned off" ) assert numpy.fabs(o.y(quantity=False) - oc.y() * o._ro) < 10.0**-8.0, ( "Orbit method y does not return the correct value when Quantity turned off" ) assert numpy.fabs(o.vx(quantity=False) - oc.vx() * o._vo) < 10.0**-8.0, ( "Orbit method vx does not return the correct value when Quantity turned off" ) assert numpy.fabs(o.vy(quantity=False) - oc.vy() * o._vo) < 10.0**-8.0, ( "Orbit method vy does not return the correct value when Quantity turned off" ) return None def test_integrate_timeAsQuantity(): import copy from galpy.orbit import Orbit from galpy.potential import MWPotential from galpy.util import conversion ro, vo = 8.0, 200.0 o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ) oc = o() ts_nounits = numpy.linspace(0.0, 1.0, 1001) ts = units.Quantity(copy.copy(ts_nounits), unit=units.Gyr) ts_nounits /= conversion.time_in_Gyr(vo, ro) # Integrate both with Quantity time and with unitless time o.integrate(ts, MWPotential) oc.integrate(ts_nounits, MWPotential) assert numpy.all(numpy.fabs(o.x(ts) - oc.x(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.y(ts) - oc.y(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.z(ts) - oc.z(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.vx(ts) - oc.vx(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.vy(ts) - oc.vy(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.vz(ts) - oc.vz(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) return None def test_integrate_timeAsQuantity_Myr(): import copy from galpy.orbit import Orbit from galpy.potential import MWPotential from galpy.util import conversion ro, vo = 8.0, 200.0 o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ) oc = o() ts_nounits = numpy.linspace(0.0, 1000.0, 1001) ts = units.Quantity(copy.copy(ts_nounits), unit=units.Myr) ts_nounits /= conversion.time_in_Gyr(vo, ro) * 1000.0 # Integrate both with Quantity time and with unitless time o.integrate(ts, MWPotential) oc.integrate(ts_nounits, MWPotential) assert numpy.all(numpy.fabs(o.x(ts) - oc.x(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.y(ts) - oc.y(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.z(ts) - oc.z(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.vx(ts) - oc.vx(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.vy(ts) - oc.vy(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.vz(ts) - oc.vz(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) return None def test_integrate_dtimeAsQuantity(): import copy from galpy.orbit import Orbit from galpy.potential import MWPotential from galpy.util import conversion ro, vo = 8.0, 200.0 o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ) oc = o() ts_nounits = numpy.linspace(0.0, 1.0, 1001) dt_nounits = (ts_nounits[1] - ts_nounits[0]) / 10.0 ts = units.Quantity(copy.copy(ts_nounits), unit=units.Gyr) dt = dt_nounits * units.Gyr ts_nounits /= conversion.time_in_Gyr(vo, ro) dt_nounits /= conversion.time_in_Gyr(vo, ro) # Integrate both with Quantity time and with unitless time o.integrate(ts, MWPotential, dt=dt) oc.integrate(ts_nounits, MWPotential, dt=dt_nounits) assert numpy.all(numpy.fabs(o.x(ts) - oc.x(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.y(ts) - oc.y(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.z(ts) - oc.z(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.vx(ts) - oc.vx(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.vy(ts) - oc.vy(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.vz(ts) - oc.vz(ts_nounits)).value < 10.0**-8.0), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) return None def test_integrate_dxdv_timeAsQuantity(): import copy from galpy.orbit import Orbit from galpy.potential import MWPotential from galpy.util import conversion ro, vo = 8.0, 200.0 o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ) oc = o() ts_nounits = numpy.linspace(0.0, 1.0, 1001) ts = units.Quantity(copy.copy(ts_nounits), unit=units.Gyr) ts_nounits /= conversion.time_in_Gyr(vo, ro) # Integrate both with Quantity time and with unitless time o.integrate_dxdv([1.0, 0.3, 0.4, 0.2], ts, MWPotential, rectIn=True, rectOut=True) oc.integrate_dxdv( [1.0, 0.3, 0.4, 0.2], ts_nounits, MWPotential, rectIn=True, rectOut=True ) dx = o.getOrbit_dxdv() dxc = oc.getOrbit_dxdv() assert numpy.all(numpy.fabs(dx - dxc) < 10.0**-8.0), ( "Orbit integrated_dxdv with times specified as Quantity does not agree with Orbit integrated_dxdv with time specified as array" ) return None def test_integrate_dxdv_timeAsQuantity_Myr(): import copy from galpy.orbit import Orbit from galpy.potential import MWPotential from galpy.util import conversion ro, vo = 8.0, 200.0 o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ) oc = o() ts_nounits = numpy.linspace(0.0, 1.0, 1001) ts = units.Quantity(copy.copy(ts_nounits), unit=units.Myr) ts_nounits /= conversion.time_in_Gyr(vo, ro) * 1000.0 # Integrate both with Quantity time and with unitless time o.integrate_dxdv([1.0, 0.3, 0.4, 0.2], ts, MWPotential, rectIn=True, rectOut=True) oc.integrate_dxdv( [1.0, 0.3, 0.4, 0.2], ts_nounits, MWPotential, rectIn=True, rectOut=True ) dx = o.getOrbit_dxdv() dxc = oc.getOrbit_dxdv() assert numpy.all(numpy.fabs(dx - dxc) < 10.0**-8.0), ( "Orbit integrated_dxdv with times specified as Quantity does not agree with Orbit integrated_dxdv with time specified as array" ) return None def test_integrate_SOS_psiQuantity(): import copy from galpy.orbit import Orbit from galpy.potential import MWPotential from galpy.util import conversion ro, vo = 8.0, 200.0 o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ) oc = o() psis_nounits = numpy.linspace(0.0, 400.0, 1001) psis = units.Quantity(copy.copy(psis_nounits), unit=units.deg) psis_nounits /= 180.0 / numpy.pi t0_nounits = 1.0 t0 = units.Quantity(copy.copy(t0_nounits), unit=units.Gyr) t0_nounits /= conversion.time_in_Gyr(vo, ro) # Integrate both with Quantity time and with unitless time o.integrate_SOS(psis, MWPotential, t0=t0) oc.integrate_SOS(psis_nounits, MWPotential, t0=t0_nounits) assert numpy.all(numpy.fabs(o.x(o.t) - oc.x(oc.t)).value < 10.0**-8.0), ( "Orbit SOS integrated with psis specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.y(o.t) - oc.y(oc.t)).value < 10.0**-8.0), ( "Orbit SOS integrated with psis specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.z(o.t) - oc.z(oc.t)).value < 10.0**-8.0), ( "Orbit SOS integrated with psis specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.vx(o.t) - oc.vx(oc.t)).value < 10.0**-8.0), ( "Orbit SOS integrated with psis specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.vy(o.t) - oc.vy(oc.t)).value < 10.0**-8.0), ( "Orbit SOS integrated with psis specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all(numpy.fabs(o.vz(o.t) - oc.vz(oc.t)).value < 10.0**-8.0), ( "Orbit SOS integrated with psis specified as Quantity does not agree with Orbit integrated with time specified as array" ) return None def test_orbit_inconsistentPotentialUnits_error(): from galpy.orbit import Orbit from galpy.potential import IsochronePotential ro, vo = 9.0, 220.0 o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ) ts = numpy.linspace(0.0, 10.0, 1001) * units.Gyr # single, ro wrong pot = IsochronePotential(normalize=1.0, ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pot) # list, ro wrong pot = IsochronePotential(normalize=1.0, ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, [pot]) # single, vo wrong pot = IsochronePotential(normalize=1.0, ro=9.0, vo=250.0) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pot) # list, vo wrong pot = IsochronePotential(normalize=1.0, ro=9.0, vo=250.0) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, [pot]) return None def test_orbits_setup_roAsQuantity(): from galpy.orbit import Orbit ro = 7.0 * units.kpc # Initialize Orbits from list of Orbit instances orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], ro=ro), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], ro=ro), ] orbits = Orbit(orbits_list, ro=ro) assert numpy.fabs(orbits._ro - 7.0) < 10.0**-10.0, ( "ro in Orbit setup as Quantity does not work as expected" ) return None def test_orbits_setup_voAsQuantity(): from galpy.orbit import Orbit vo = 230.0 * units.km / units.s # Initialize Orbits from list of Orbit instances orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], vo=vo), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], vo=vo), ] orbits = Orbit(orbits_list, vo=vo) assert numpy.fabs(orbits._vo - 230.0) < 10.0**-10.0, ( "vo in Orbit setup as Quantity does not work as expected" ) return None def test_orbits_setup_zoAsQuantity(): from galpy.orbit import Orbit zo = 23.0 * units.pc # Initialize Orbits from list of Orbit instances orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], zo=zo), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], zo=zo), ] orbits = Orbit(orbits_list, zo=zo) assert numpy.fabs(orbits._zo - 0.023) < 10.0**-10.0, ( "zo in Orbit setup as Quantity does not work as expected" ) return None def test_orbits_setup_solarmotionAsQuantity(): from galpy.orbit import Orbit solarmotion = numpy.array([-10.0, 20.0, 30.0]) * units.kpc / units.Gyr # Initialize Orbits from list of Orbit instances orbits_list = [ Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -3.0], solarmotion=solarmotion), Orbit([1.0, 0.1, 1.0, 0.1, 0.2, -4.0], solarmotion=solarmotion), ] orbits = Orbit(orbits_list, solarmotion=solarmotion) assert numpy.all( numpy.fabs(orbits._solarmotion - solarmotion.to(units.km / units.s).value) < 10.0**-10.0 ), "solarmotion in Orbit setup as Quantity does not work as expected" return None def test_orbits_method_returntype_scalar(): from galpy.orbit import Orbit o = Orbit( [ [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], [ -20.0 * units.kpc, 10.0 * units.km / units.s, 230.0 * units.km / units.s, -300.0 * units.pc, 12.0 * units.km / units.s, 125.0 * units.deg, ], ] ) from galpy.potential import MWPotential2014 assert isinstance(o.E(pot=MWPotential2014), units.Quantity), ( "Orbit method E does not return Quantity when it should" ) assert isinstance(o.ER(pot=MWPotential2014), units.Quantity), ( "Orbit method ER does not return Quantity when it should" ) assert isinstance(o.Ez(pot=MWPotential2014), units.Quantity), ( "Orbit method Ez does not return Quantity when it should" ) assert isinstance(o.Jacobi(pot=MWPotential2014), units.Quantity), ( "Orbit method Jacobi does not return Quantity when it should" ) assert isinstance(o.L(), units.Quantity), ( "Orbit method L does not return Quantity when it should" ) assert isinstance(o.Lz(), units.Quantity), ( "Orbit method Lz does not return Quantity when it should" ) assert isinstance(o.rap(pot=MWPotential2014, analytic=True), units.Quantity), ( "Orbit method rap does not return Quantity when it should" ) assert isinstance(o.rperi(pot=MWPotential2014, analytic=True), units.Quantity), ( "Orbit method rperi does not return Quantity when it should" ) assert isinstance(o.rguiding(pot=MWPotential2014), units.Quantity), ( "Orbit method rguiding does not return Quantity when it should" ) assert isinstance(o.rE(pot=MWPotential2014), units.Quantity), ( "Orbit method rE does not return Quantity when it should" ) assert isinstance(o.LcE(pot=MWPotential2014), units.Quantity), ( "Orbit method LcE does not return Quantity when it should" ) assert isinstance(o.zmax(pot=MWPotential2014, analytic=True), units.Quantity), ( "Orbit method zmax does not return Quantity when it should" ) assert isinstance( o.jr(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method jr does not return Quantity when it should" assert isinstance( o.jp(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method jp does not return Quantity when it should" assert isinstance( o.jz(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method jz does not return Quantity when it should" assert isinstance( o.wr(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method wr does not return Quantity when it should" assert isinstance( o.wp(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method wp does not return Quantity when it should" assert isinstance( o.wz(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method wz does not return Quantity when it should" assert isinstance( o.Tr(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method Tr does not return Quantity when it should" assert isinstance( o.Tp(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method Tp does not return Quantity when it should" assert isinstance( o.Tz(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method Tz does not return Quantity when it should" assert isinstance( o.Or(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method Or does not return Quantity when it should" assert isinstance( o.Op(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method Op does not return Quantity when it should" assert isinstance( o.Oz(pot=MWPotential2014, type="staeckel", delta=0.5), units.Quantity ), "Orbit method Oz does not return Quantity when it should" assert isinstance(o.time(), units.Quantity), ( "Orbit method time does not return Quantity when it should" ) assert isinstance(o.R(), units.Quantity), ( "Orbit method R does not return Quantity when it should" ) assert isinstance(o.r(), units.Quantity), ( "Orbit method r does not return Quantity when it should" ) assert isinstance(o.vR(), units.Quantity), ( "Orbit method vR does not return Quantity when it should" ) assert isinstance(o.vT(), units.Quantity), ( "Orbit method vT does not return Quantity when it should" ) assert isinstance(o.z(), units.Quantity), ( "Orbit method z does not return Quantity when it should" ) assert isinstance(o.vz(), units.Quantity), ( "Orbit method vz does not return Quantity when it should" ) assert isinstance(o.phi(), units.Quantity), ( "Orbit method phi does not return Quantity when it should" ) assert isinstance(o.vphi(), units.Quantity), ( "Orbit method vphi does not return Quantity when it should" ) assert isinstance(o.x(), units.Quantity), ( "Orbit method x does not return Quantity when it should" ) assert isinstance(o.y(), units.Quantity), ( "Orbit method y does not return Quantity when it should" ) assert isinstance(o.vx(), units.Quantity), ( "Orbit method vx does not return Quantity when it should" ) assert isinstance(o.vy(), units.Quantity), ( "Orbit method vy does not return Quantity when it should" ) assert isinstance(o.ra(), units.Quantity), ( "Orbit method ra does not return Quantity when it should" ) assert isinstance(o.dec(), units.Quantity), ( "Orbit method dec does not return Quantity when it should" ) assert isinstance(o.ll(), units.Quantity), ( "Orbit method ll does not return Quantity when it should" ) assert isinstance(o.bb(), units.Quantity), ( "Orbit method bb does not return Quantity when it should" ) assert isinstance(o.dist(), units.Quantity), ( "Orbit method dist does not return Quantity when it should" ) assert isinstance(o.pmra(), units.Quantity), ( "Orbit method pmra does not return Quantity when it should" ) assert isinstance(o.pmdec(), units.Quantity), ( "Orbit method pmdec does not return Quantity when it should" ) assert isinstance(o.pmll(), units.Quantity), ( "Orbit method pmll does not return Quantity when it should" ) assert isinstance(o.pmbb(), units.Quantity), ( "Orbit method pmbb does not return Quantity when it should" ) assert isinstance(o.vlos(), units.Quantity), ( "Orbit method vlos does not return Quantity when it should" ) assert isinstance(o.vra(), units.Quantity), ( "Orbit method vra does not return Quantity when it should" ) assert isinstance(o.vdec(), units.Quantity), ( "Orbit method vdec does not return Quantity when it should" ) assert isinstance(o.vll(), units.Quantity), ( "Orbit method vll does not return Quantity when it should" ) assert isinstance(o.vbb(), units.Quantity), ( "Orbit method vbb does not return Quantity when it should" ) assert isinstance(o.helioX(), units.Quantity), ( "Orbit method helioX does not return Quantity when it should" ) assert isinstance(o.helioY(), units.Quantity), ( "Orbit method helioY does not return Quantity when it should" ) assert isinstance(o.helioZ(), units.Quantity), ( "Orbit method helioZ does not return Quantity when it should" ) assert isinstance(o.U(), units.Quantity), ( "Orbit method U does not return Quantity when it should" ) assert isinstance(o.V(), units.Quantity), ( "Orbit method V does not return Quantity when it should" ) assert isinstance(o.W(), units.Quantity), ( "Orbit method W does not return Quantity when it should" ) return None def test_orbits_method_returntype(): from galpy.orbit import Orbit o = Orbit( [ [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], [ -20.0 * units.kpc, 10.0 * units.km / units.s, 230.0 * units.km / units.s, -300.0 * units.pc, 12.0 * units.km / units.s, 125.0 * units.deg, ], ] ) from galpy.potential import MWPotential2014 ts = numpy.linspace(0.0, 6.0, 1001) o.integrate(ts, MWPotential2014) assert isinstance(o.E(ts), units.Quantity), ( "Orbit method E does not return Quantity when it should" ) assert isinstance(o.ER(ts), units.Quantity), ( "Orbit method ER does not return Quantity when it should" ) assert isinstance(o.Ez(ts), units.Quantity), ( "Orbit method Ez does not return Quantity when it should" ) assert isinstance(o.Jacobi(ts), units.Quantity), ( "Orbit method Jacobi does not return Quantity when it should" ) assert isinstance(o.L(ts), units.Quantity), ( "Orbit method L does not return Quantity when it should" ) assert isinstance(o.Lz(ts), units.Quantity), ( "Orbit method L does not return Quantity when it should" ) assert isinstance(o.time(ts), units.Quantity), ( "Orbit method time does not return Quantity when it should" ) assert isinstance(o.R(ts), units.Quantity), ( "Orbit method R does not return Quantity when it should" ) assert isinstance(o.r(ts), units.Quantity), ( "Orbit method r does not return Quantity when it should" ) assert isinstance(o.vR(ts), units.Quantity), ( "Orbit method vR does not return Quantity when it should" ) assert isinstance(o.vT(ts), units.Quantity), ( "Orbit method vT does not return Quantity when it should" ) assert isinstance(o.z(ts), units.Quantity), ( "Orbit method z does not return Quantity when it should" ) assert isinstance(o.vz(ts), units.Quantity), ( "Orbit method vz does not return Quantity when it should" ) assert isinstance(o.phi(ts), units.Quantity), ( "Orbit method phi does not return Quantity when it should" ) assert isinstance(o.vphi(ts), units.Quantity), ( "Orbit method vphi does not return Quantity when it should" ) assert isinstance(o.x(ts), units.Quantity), ( "Orbit method x does not return Quantity when it should" ) assert isinstance(o.y(ts), units.Quantity), ( "Orbit method y does not return Quantity when it should" ) assert isinstance(o.vx(ts), units.Quantity), ( "Orbit method vx does not return Quantity when it should" ) assert isinstance(o.vy(ts), units.Quantity), ( "Orbit method vy does not return Quantity when it should" ) assert isinstance(o.ra(ts), units.Quantity), ( "Orbit method ra does not return Quantity when it should" ) assert isinstance(o.dec(ts), units.Quantity), ( "Orbit method dec does not return Quantity when it should" ) assert isinstance(o.ll(ts), units.Quantity), ( "Orbit method ll does not return Quantity when it should" ) assert isinstance(o.bb(ts), units.Quantity), ( "Orbit method bb does not return Quantity when it should" ) assert isinstance(o.dist(ts), units.Quantity), ( "Orbit method dist does not return Quantity when it should" ) assert isinstance(o.pmra(ts), units.Quantity), ( "Orbit method pmra does not return Quantity when it should" ) assert isinstance(o.pmdec(ts), units.Quantity), ( "Orbit method pmdec does not return Quantity when it should" ) assert isinstance(o.pmll(ts), units.Quantity), ( "Orbit method pmll does not return Quantity when it should" ) assert isinstance(o.pmbb(ts), units.Quantity), ( "Orbit method pmbb does not return Quantity when it should" ) assert isinstance(o.vlos(ts), units.Quantity), ( "Orbit method vlos does not return Quantity when it should" ) assert isinstance(o.vra(ts), units.Quantity), ( "Orbit method vra does not return Quantity when it should" ) assert isinstance(o.vdec(ts), units.Quantity), ( "Orbit method vdec does not return Quantity when it should" ) assert isinstance(o.vll(ts), units.Quantity), ( "Orbit method vll does not return Quantity when it should" ) assert isinstance(o.vbb(ts), units.Quantity), ( "Orbit method vbb does not return Quantity when it should" ) assert isinstance(o.helioX(ts), units.Quantity), ( "Orbit method helioX does not return Quantity when it should" ) assert isinstance(o.helioY(ts), units.Quantity), ( "Orbit method helioY does not return Quantity when it should" ) assert isinstance(o.helioZ(ts), units.Quantity), ( "Orbit method helioZ does not return Quantity when it should" ) assert isinstance(o.U(ts), units.Quantity), ( "Orbit method U does not return Quantity when it should" ) assert isinstance(o.V(ts), units.Quantity), ( "Orbit method V does not return Quantity when it should" ) assert isinstance(o.W(ts), units.Quantity), ( "Orbit method W does not return Quantity when it should" ) return None def test_orbits_method_returnunit(): from galpy.orbit import Orbit o = Orbit( [ [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], [ -20.0 * units.kpc, 10.0 * units.km / units.s, 230.0 * units.km / units.s, -300.0 * units.pc, 12.0 * units.km / units.s, 125.0 * units.deg, ], ] ) from galpy.potential import MWPotential2014 try: o.E(pot=MWPotential2014).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Orbit method E does not return Quantity with the right units" ) try: o.ER(pot=MWPotential2014).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Orbit method ER does not return Quantity with the right units" ) try: o.Ez(pot=MWPotential2014).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Orbit method Ez does not return Quantity with the right units" ) try: o.Jacobi(pot=MWPotential2014).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Orbit method Jacobi does not return Quantity with the right units" ) try: o.L().to(units.km**2 / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method L does not return Quantity with the right units" ) try: o.Lz().to(units.km**2 / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method Lz does not return Quantity with the right units" ) try: o.rap(pot=MWPotential2014, analytic=True).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method rap does not return Quantity with the right units" ) try: o.rperi(pot=MWPotential2014, analytic=True).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method rperi does not return Quantity with the right units" ) try: o.rguiding(pot=MWPotential2014).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method rguiding does not return Quantity with the right units" ) try: o.rE(pot=MWPotential2014).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method rE does not return Quantity with the right units" ) try: o.LcE(pot=MWPotential2014).to(units.kpc * units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method LcE does not return Quantity with the right units" ) try: o.zmax(pot=MWPotential2014, analytic=True).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method zmax does not return Quantity with the right units" ) try: o.jr(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.km**2 / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method jr does not return Quantity with the right units" ) try: o.jp(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.km**2 / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method jp does not return Quantity with the right units" ) try: o.jz(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.km**2 / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method jz does not return Quantity with the right units" ) try: o.wr(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method wr does not return Quantity with the right units" ) try: o.wp(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method wp does not return Quantity with the right units" ) try: o.wz(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method wz does not return Quantity with the right units" ) try: o.Tr(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method Tr does not return Quantity with the right units" ) try: o.Tp(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method Tp does not return Quantity with the right units" ) try: o.Tz(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method Tz does not return Quantity with the right units" ) try: o.Or(pot=MWPotential2014, type="staeckel", delta=0.5).to(1 / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method Or does not return Quantity with the right units" ) try: o.Op(pot=MWPotential2014, type="staeckel", delta=0.5).to(1 / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method Op does not return Quantity with the right units" ) try: o.Oz(pot=MWPotential2014, type="staeckel", delta=0.5).to(1 / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method Oz does not return Quantity with the right units" ) try: o.time().to(units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method time does not return Quantity with the right units" ) try: o.R().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method R does not return Quantity with the right units" ) try: o.r().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method r does not return Quantity with the right units" ) try: o.vR().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vR does not return Quantity with the right units" ) try: o.vT().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vT does not return Quantity with the right units" ) try: o.z().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method z does not return Quantity with the right units" ) try: o.vz().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vz does not return Quantity with the right units" ) try: o.phi().to(units.deg) except units.UnitConversionError: raise AssertionError( "Orbit method phi does not return Quantity with the right units" ) try: o.vphi().to(units.km / units.s / units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method vphi does not return Quantity with the right units" ) try: o.x().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method x does not return Quantity with the right units" ) try: o.y().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method y does not return Quantity with the right units" ) try: o.vx().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vx does not return Quantity with the right units" ) try: o.vy().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vy does not return Quantity with the right units" ) try: o.ra().to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method ra does not return Quantity with the right units" ) try: o.dec().to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method dec does not return Quantity with the right units" ) try: o.ll().to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method ll does not return Quantity with the right units" ) try: o.bb().to(units.rad) except units.UnitConversionError: raise AssertionError( "Orbit method bb does not return Quantity with the right units" ) try: o.dist().to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method dist does not return Quantity with the right units" ) try: o.pmra().to(units.mas / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method pmra does not return Quantity with the right units" ) try: o.pmdec().to(units.mas / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method pmdec does not return Quantity with the right units" ) try: o.pmll().to(units.mas / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method pmll does not return Quantity with the right units" ) try: o.pmbb().to(units.mas / units.yr) except units.UnitConversionError: raise AssertionError( "Orbit method pmbb does not return Quantity with the right units" ) try: o.vlos().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vlos does not return Quantity with the right units" ) try: o.vra().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vra does not return Quantity with the right units" ) try: o.vdec().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vdec does not return Quantity with the right units" ) try: o.vll().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vll does not return Quantity with the right units" ) try: o.vbb().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method vbb does not return Quantity with the right units" ) try: o.helioX().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method helioX does not return Quantity with the right units" ) try: o.helioY().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method helioY does not return Quantity with the right units" ) try: o.helioZ().to(units.pc) except units.UnitConversionError: raise AssertionError( "Orbit method helioZ does not return Quantity with the right units" ) try: o.U().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method U does not return Quantity with the right units" ) try: o.V().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method V does not return Quantity with the right units" ) try: o.W().to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Orbit method W does not return Quantity with the right units" ) return None def test_orbits_method_value(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 from galpy.util import conversion o = Orbit( [ [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], [ -20.0 * units.kpc, 10.0 * units.km / units.s, 230.0 * units.km / units.s, -300.0 * units.pc, 12.0 * units.km / units.s, 125.0 * units.deg, ], ] ) oc = o() oc.turn_physical_off() assert numpy.all( numpy.fabs( o.E(pot=MWPotential2014).to(units.km**2 / units.s**2).value - oc.E(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method E does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.ER(pot=MWPotential2014).to(units.km**2 / units.s**2).value - oc.ER(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method ER does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.Ez(pot=MWPotential2014).to(units.km**2 / units.s**2).value - oc.Ez(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method Ez does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.Jacobi(pot=MWPotential2014).to(units.km**2 / units.s**2).value - oc.Jacobi(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method Jacobi does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.L(pot=MWPotential2014).to(units.km / units.s * units.kpc).value - oc.L(pot=MWPotential2014) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method L does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.Lz(pot=MWPotential2014).to(units.km / units.s * units.kpc).value - oc.Lz(pot=MWPotential2014) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method L does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.rap(pot=MWPotential2014, analytic=True).to(units.kpc).value - oc.rap(pot=MWPotential2014, analytic=True) * o._ro ) < 10.0**-8.0 ), "Orbit method rap does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.rperi(pot=MWPotential2014, analytic=True).to(units.kpc).value - oc.rperi(pot=MWPotential2014, analytic=True) * o._ro ) < 10.0**-8.0 ), "Orbit method rperi does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.rguiding(pot=MWPotential2014).to(units.kpc).value - oc.rguiding(pot=MWPotential2014) * o._ro ) < 10.0**-8.0 ), "Orbit method rguiding does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.rE(pot=MWPotential2014).to(units.kpc).value - oc.rE(pot=MWPotential2014) * o._ro ) < 10.0**-8.0 ), "Orbit method rE does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.LcE(pot=MWPotential2014).to(units.kpc * units.km / units.s).value - oc.LcE(pot=MWPotential2014) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method LcE does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.zmax(pot=MWPotential2014, analytic=True).to(units.kpc).value - oc.zmax(pot=MWPotential2014, analytic=True) * o._ro ) < 10.0**-8.0 ), "Orbit method zmax does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.jr(pot=MWPotential2014, type="staeckel", delta=0.5) .to(units.km / units.s * units.kpc) .value - oc.jr(pot=MWPotential2014, type="staeckel", delta=0.5) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method jr does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.jp(pot=MWPotential2014, type="staeckel", delta=4.0 * units.kpc) .to(units.km / units.s * units.kpc) .value - oc.jp(pot=MWPotential2014, type="staeckel", delta=0.5) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method jp does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.jz(pot=MWPotential2014, type="isochroneapprox", b=0.8 * 8.0 * units.kpc) .to(units.km / units.s * units.kpc) .value - oc.jz(pot=MWPotential2014, type="isochroneapprox", b=0.8) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method jz does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.wr(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.rad).value - oc.wr(pot=MWPotential2014, type="staeckel", delta=0.5) ) < 10.0**-8.0 ), "Orbit method wr does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.wp(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.rad).value - oc.wp(pot=MWPotential2014, type="staeckel", delta=0.5) ) < 10.0**-8.0 ), "Orbit method wp does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.wz(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.rad).value - oc.wz(pot=MWPotential2014, type="staeckel", delta=0.5) ) < 10.0**-8.0 ), "Orbit method wz does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.Tr(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.Gyr).value - oc.Tr(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Tr does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.Tp(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.Gyr).value - oc.Tp(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Tp does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.Tz(pot=MWPotential2014, type="staeckel", delta=0.5).to(units.Gyr).value - oc.Tz(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Tz does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.Or(pot=MWPotential2014, type="staeckel", delta=0.5) .to(1 / units.Gyr) .value - oc.Or(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.freq_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Or does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.Op(pot=MWPotential2014, type="staeckel", delta=0.5) .to(1 / units.Gyr) .value - oc.Op(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.freq_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Opbit method Or does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.Oz(pot=MWPotential2014, type="staeckel", delta=0.5) .to(1 / units.Gyr) .value - oc.Oz(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.freq_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Ozbit method Or does not return the correct value as Quantity" assert numpy.all( numpy.fabs( o.time().to(units.Gyr).value - oc.time() * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method time does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.R().to(units.kpc).value - oc.R() * o._ro) < 10.0**-8.0 ), "Orbit method R does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.r().to(units.kpc).value - oc.r() * o._ro) < 10.0**-8.0 ), "Orbit method r does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.vR().to(units.km / units.s).value - oc.vR() * o._vo) < 10.0**-8.0 ), "Orbit method vR does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.vT().to(units.km / units.s).value - oc.vT() * o._vo) < 10.0**-8.0 ), "Orbit method vT does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.z().to(units.kpc).value - oc.z() * o._ro) < 10.0**-8.0 ), "Orbit method z does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.vz().to(units.km / units.s).value - oc.vz() * o._vo) < 10.0**-8.0 ), "Orbit method vz does not return the correct value as Quantity" assert numpy.all(numpy.fabs(o.phi().to(units.rad).value - oc.phi()) < 10.0**-8.0), ( "Orbit method phi does not return the correct value as Quantity" ) assert numpy.all( numpy.fabs( o.vphi().to(units.km / units.s / units.kpc).value - oc.vphi() * o._vo / o._ro ) < 10.0**-8.0 ), "Orbit method vphi does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.x().to(units.kpc).value - oc.x() * o._ro) < 10.0**-8.0 ), "Orbit method x does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.y().to(units.kpc).value - oc.y() * o._ro) < 10.0**-8.0 ), "Orbit method y does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.vx().to(units.km / units.s).value - oc.vx() * o._vo) < 10.0**-8.0 ), "Orbit method vx does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.vy().to(units.km / units.s).value - oc.vy() * o._vo) < 10.0**-8.0 ), "Orbit method vy does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.ra().to(units.deg).value - oc.ra(quantity=False)) < 10.0**-8.0 ), "Orbit method ra does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.dec().to(units.deg).value - oc.dec(quantity=False)) < 10.0**-8.0 ), "Orbit method dec does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.ll().to(units.deg).value - oc.ll(quantity=False)) < 10.0**-8.0 ), "Orbit method ll does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.bb().to(units.deg).value - oc.bb(quantity=False)) < 10.0**-8.0 ), "Orbit method bb does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.dist().to(units.kpc).value - oc.dist(quantity=False)) < 10.0**-8.0 ), "Orbit method dist does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.pmra().to(units.mas / units.yr).value - oc.pmra(quantity=False)) < 10.0**-8.0 ), "Orbit method pmra does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.pmdec().to(units.mas / units.yr).value - oc.pmdec(quantity=False)) < 10.0**-8.0 ), "Orbit method pmdec does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.pmll().to(units.mas / units.yr).value - oc.pmll(quantity=False)) < 10.0**-8.0 ), "Orbit method pmll does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.pmbb().to(units.mas / units.yr).value - oc.pmbb(quantity=False)) < 10.0**-8.0 ), "Orbit method pmbb does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.vlos().to(units.km / units.s).value - oc.vlos(quantity=False)) < 10.0**-8.0 ), "Orbit method vlos does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.vra().to(units.km / units.s).value - oc.vra(quantity=False)) < 10.0**-8.0 ), "Orbit method vra does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.vdec().to(units.km / units.s).value - oc.vdec(quantity=False)) < 10.0**-8.0 ), "Orbit method vdec does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.vll().to(units.km / units.s).value - oc.vll(quantity=False)) < 10.0**-8.0 ), "Orbit method vll does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.vbb().to(units.km / units.s).value - oc.vbb(quantity=False)) < 10.0**-8.0 ), "Orbit method vbb does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.helioX().to(units.kpc).value - oc.helioX(quantity=False)) < 10.0**-8.0 ), "Orbit method helioX does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.helioY().to(units.kpc).value - oc.helioY(quantity=False)) < 10.0**-8.0 ), "Orbit method helioY does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.helioZ().to(units.kpc).value - oc.helioZ(quantity=False)) < 10.0**-8.0 ), "Orbit method helioZ does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.U().to(units.km / units.s).value - oc.U(quantity=False)) < 10.0**-8.0 ), "Orbit method U does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.V().to(units.km / units.s).value - oc.V(quantity=False)) < 10.0**-8.0 ), "Orbit method V does not return the correct value as Quantity" assert numpy.all( numpy.fabs(o.W().to(units.km / units.s).value - oc.W(quantity=False)) < 10.0**-8.0 ), "Orbit method W does not return the correct value as Quantity" return None def test_orbits_method_value_turnquantityoff(): from galpy.orbit import Orbit from galpy.potential import MWPotential2014 from galpy.util import conversion o = Orbit( [ [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], [ -20.0 * units.kpc, 10.0 * units.km / units.s, 230.0 * units.km / units.s, -300.0 * units.pc, 12.0 * units.km / units.s, 125.0 * units.deg, ], ] ) oc = o() oc.turn_physical_off() assert numpy.all( numpy.fabs( o.E(pot=MWPotential2014, quantity=False) - oc.E(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method E does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.ER(pot=MWPotential2014, quantity=False) - oc.ER(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method ER does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.Ez(pot=MWPotential2014, quantity=False) - oc.Ez(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method Ez does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.Jacobi(pot=MWPotential2014, quantity=False) - oc.Jacobi(pot=MWPotential2014) * o._vo**2.0 ) < 10.0**-8.0 ), "Orbit method Jacobi does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.L(pot=MWPotential2014, quantity=False) - oc.L(pot=MWPotential2014) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method L does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.Lz(pot=MWPotential2014, quantity=False) - oc.Lz(pot=MWPotential2014) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method L does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.rap(pot=MWPotential2014, analytic=True, quantity=False) - oc.rap(pot=MWPotential2014, analytic=True) * o._ro ) < 10.0**-8.0 ), "Orbit method rap does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.rperi(pot=MWPotential2014, analytic=True, quantity=False) - oc.rperi(pot=MWPotential2014, analytic=True) * o._ro ) < 10.0**-8.0 ), "Orbit method rperi does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.rguiding(pot=MWPotential2014, quantity=False) - oc.rguiding(pot=MWPotential2014) * o._ro ) < 10.0**-8.0 ), ( "Orbit method rguiding does not return the correct value when Quantity turned off" ) assert numpy.all( numpy.fabs( o.rE(pot=MWPotential2014, quantity=False) - oc.rE(pot=MWPotential2014) * o._ro ) < 10.0**-8.0 ), "Orbit method rE does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.LcE(pot=MWPotential2014, quantity=False) - oc.LcE(pot=MWPotential2014) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method LcE does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.zmax(pot=MWPotential2014, analytic=True, quantity=False) - oc.zmax(pot=MWPotential2014, analytic=True) * o._ro ) < 10.0**-8.0 ), "Orbit method zmax does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.jr(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.jr(pot=MWPotential2014, type="staeckel", delta=0.5) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method jr does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.jp( pot=MWPotential2014, type="staeckel", delta=4.0 * units.kpc, quantity=False, ) - oc.jp(pot=MWPotential2014, type="staeckel", delta=0.5) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method jp does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.jz( pot=MWPotential2014, type="isochroneapprox", b=0.8 * 8.0 * units.kpc, quantity=False, ) - oc.jz(pot=MWPotential2014, type="isochroneapprox", b=0.8) * o._ro * o._vo ) < 10.0**-8.0 ), "Orbit method jz does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.wr(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.wr(pot=MWPotential2014, type="staeckel", delta=0.5) ) < 10.0**-8.0 ), "Orbit method wr does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.wp(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.wp(pot=MWPotential2014, type="staeckel", delta=0.5) ) < 10.0**-8.0 ), "Orbit method wp does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.wz(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.wz(pot=MWPotential2014, type="staeckel", delta=0.5) ) < 10.0**-8.0 ), "Orbit method wz does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.Tr(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.Tr(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Tr does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.Tp(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.Tp(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Tp does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.Tz(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.Tz(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Tz does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.Or(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.Or(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.freq_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method Or does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.Op(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.Op(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.freq_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Opbit method Or does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.Oz(pot=MWPotential2014, type="staeckel", delta=0.5, quantity=False) - oc.Oz(pot=MWPotential2014, type="staeckel", delta=0.5) * conversion.freq_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Ozbit method Or does not return the correct value when Quantity turned off" assert numpy.all( numpy.fabs( o.time(quantity=False) - oc.time() * conversion.time_in_Gyr(o._vo, o._ro) ) < 10.0**-8.0 ), "Orbit method time does not return the correct value when Quantity turned off" assert numpy.all(numpy.fabs(o.R(quantity=False) - oc.R() * o._ro) < 10.0**-8.0), ( "Orbit method R does not return the correct value when Quantity turned off" ) assert numpy.all(numpy.fabs(o.r(quantity=False) - oc.r() * o._ro) < 10.0**-8.0), ( "Orbit method r does not return the correct value when Quantity turned off" ) assert numpy.all(numpy.fabs(o.vR(quantity=False) - oc.vR() * o._vo) < 10.0**-8.0), ( "Orbit method vR does not return the correct value when Quantity turned off" ) assert numpy.all(numpy.fabs(o.vT(quantity=False) - oc.vT() * o._vo) < 10.0**-8.0), ( "Orbit method vT does not return the correct value when Quantity turned off" ) assert numpy.all(numpy.fabs(o.z(quantity=False) - oc.z() * o._ro) < 10.0**-8.0), ( "Orbit method z does not return the correct value when Quantity turned off" ) assert numpy.all(numpy.fabs(o.vz(quantity=False) - oc.vz() * o._vo) < 10.0**-8.0), ( "Orbit method vz does not return the correct value when Quantity turned off" ) assert numpy.all(numpy.fabs(o.phi(quantity=False) - oc.phi()) < 10.0**-8.0), ( "Orbit method phi does not return the correct value when Quantity turned off" ) assert numpy.all( numpy.fabs(o.vphi(quantity=False) - oc.vphi() * o._vo / o._ro) < 10.0**-8.0 ), "Orbit method vphi does not return the correct value when Quantity turned off" assert numpy.all(numpy.fabs(o.x(quantity=False) - oc.x() * o._ro) < 10.0**-8.0), ( "Orbit method x does not return the correct value when Quantity turned off" ) assert numpy.all(numpy.fabs(o.y(quantity=False) - oc.y() * o._ro) < 10.0**-8.0), ( "Orbit method y does not return the correct value when Quantity turned off" ) assert numpy.all(numpy.fabs(o.vx(quantity=False) - oc.vx() * o._vo) < 10.0**-8.0), ( "Orbit method vx does not return the correct value when Quantity turned off" ) assert numpy.all(numpy.fabs(o.vy(quantity=False) - oc.vy() * o._vo) < 10.0**-8.0), ( "Orbit method vy does not return the correct value when Quantity turned off" ) return None def test_integrate_orbits_timeAsQuantity(): import copy from galpy.orbit import Orbit from galpy.potential import MWPotential from galpy.util import conversion ro, vo = 8.0, 200.0 o = Orbit( [ Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), ] ) oc = Orbit( [ Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), ] ) ts_nounits = numpy.linspace(0.0, 1.0, 1001) ts = units.Quantity(copy.copy(ts_nounits), unit=units.Gyr) ts_nounits /= conversion.time_in_Gyr(vo, ro) # Integrate both with Quantity time and with unitless time o.integrate(ts, MWPotential) oc.integrate(ts_nounits, MWPotential) # Turn physical units off for ease o.turn_physical_off() oc.turn_physical_off() assert numpy.all( numpy.fabs(numpy.array(o.x(ts)) - numpy.array(oc.x(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.y(ts)) - numpy.array(oc.y(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.z(ts)) - numpy.array(oc.z(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.vx(ts)) - numpy.array(oc.vx(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.vy(ts)) - numpy.array(oc.vy(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.vz(ts)) - numpy.array(oc.vz(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) return None def test_orbits_integrate_timeAsQuantity_Myr(): import copy from galpy.orbit import Orbit from galpy.potential import MWPotential from galpy.util import conversion ro, vo = 8.0, 200.0 o = Orbit( [ Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), ] ) oc = Orbit( [ Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), ] ) ts_nounits = numpy.linspace(0.0, 1000.0, 1001) ts = units.Quantity(copy.copy(ts_nounits), unit=units.Myr) ts_nounits /= conversion.time_in_Gyr(vo, ro) * 1000.0 # Integrate both with Quantity time and with unitless time o.integrate(ts, MWPotential) oc.integrate(ts_nounits, MWPotential) # Turn physical units off for ease o.turn_physical_off() oc.turn_physical_off() assert numpy.all( numpy.fabs(numpy.array(o.x(ts)) - numpy.array(oc.x(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.y(ts)) - numpy.array(oc.y(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.z(ts)) - numpy.array(oc.z(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.vx(ts)) - numpy.array(oc.vx(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.vy(ts)) - numpy.array(oc.vy(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.vz(ts)) - numpy.array(oc.vz(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) return None def test_orbits_integrate_dtimeAsQuantity(): import copy from galpy.orbit import Orbit from galpy.potential import MWPotential from galpy.util import conversion ro, vo = 8.0, 200.0 o = Orbit( [ Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), ] ) oc = Orbit( [ Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), ] ) ts_nounits = numpy.linspace(0.0, 1.0, 1001) dt_nounits = (ts_nounits[1] - ts_nounits[0]) / 10.0 ts = units.Quantity(copy.copy(ts_nounits), unit=units.Gyr) dt = dt_nounits * units.Gyr ts_nounits /= conversion.time_in_Gyr(vo, ro) dt_nounits /= conversion.time_in_Gyr(vo, ro) # Integrate both with Quantity time and with unitless time o.integrate(ts, MWPotential, dt=dt) oc.integrate(ts_nounits, MWPotential, dt=dt_nounits) # Turn physical units off for ease o.turn_physical_off() oc.turn_physical_off() assert numpy.all( numpy.fabs(numpy.array(o.x(ts)) - numpy.array(oc.x(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.y(ts)) - numpy.array(oc.y(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.z(ts)) - numpy.array(oc.z(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.vx(ts)) - numpy.array(oc.vx(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.vy(ts)) - numpy.array(oc.vy(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) assert numpy.all( numpy.fabs(numpy.array(o.vz(ts)) - numpy.array(oc.vz(ts_nounits))) < 10.0**-8.0 ), ( "Orbit integrated with times specified as Quantity does not agree with Orbit integrated with time specified as array" ) return None def test_orbits_inconsistentPotentialUnits_error(): from galpy.orbit import Orbit from galpy.potential import IsochronePotential ro, vo = 9.0, 220.0 o = Orbit( [ Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ), ] ) ts = numpy.linspace(0.0, 10.0, 1001) * units.Gyr # single, ro wrong pot = IsochronePotential(normalize=1.0, ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pot) # list, ro wrong pot = IsochronePotential(normalize=1.0, ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, [pot]) # single, vo wrong pot = IsochronePotential(normalize=1.0, ro=9.0, vo=250.0) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, pot) # list, vo wrong pot = IsochronePotential(normalize=1.0, ro=9.0, vo=250.0) with pytest.raises(AssertionError) as excinfo: o.integrate(ts, [pot]) return None def test_orbit_method_inputAsQuantity(): from galpy import potential from galpy.orbit import Orbit ro, vo = 7.0, 210.0 o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 500.0 * units.pc, -12.0 * units.km / units.s, 45.0 * units.deg, ], ro=ro, vo=vo, ) assert ( numpy.fabs( o.Jacobi( pot=potential.MWPotential, OmegaP=41 * units.km / units.s / units.kpc, use_physical=False, ) - o.Jacobi( pot=potential.MWPotential, OmegaP=41.0 * ro / vo, use_physical=False ) ) < 10.0**-8.0 ), ( "Orbit method Jacobi does not return the correct value when input OmegaP is Quantity" ) return None def test_change_ro_config(): from galpy.orbit import Orbit from galpy.util import config o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 45.0 * units.deg, ] ) assert numpy.fabs(o._ro - 8.0) < 10.0**-10.0, "Default ro value not as expected" # Change value newro = 9.0 config.set_ro(newro) o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 45.0 * units.deg, ] ) assert numpy.fabs(o._ro - newro) < 10.0**-10.0, "Default ro value not as expected" # Change value as Quantity newro = 9.0 * units.kpc config.set_ro(newro) o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 45.0 * units.deg, ] ) assert numpy.fabs(o._ro - newro.value) < 10.0**-10.0, ( "Default ro value not as expected" ) # Back to default config.set_ro(8.0) return None def test_change_vo_config(): from galpy.orbit import Orbit from galpy.util import config o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 45.0 * units.deg, ] ) assert numpy.fabs(o._vo - 220.0) < 10.0**-10.0, "Default ro value not as expected" # Change value newvo = 250.0 config.set_vo(newvo) o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 45.0 * units.deg, ] ) assert numpy.fabs(o._vo - newvo) < 10.0**-10.0, "Default ro value not as expected" # Change value as Quantity newvo = 250.0 * units.km / units.s config.set_vo(newvo) o = Orbit( [ 10.0 * units.kpc, -20.0 * units.km / units.s, 210.0 * units.km / units.s, 45.0 * units.deg, ] ) assert numpy.fabs(o._vo - newvo.value) < 10.0**-10.0, ( "Default ro value not as expected" ) # Back to default config.set_vo(220.0) return None def test_potential_method_returntype(): from galpy.potential import PlummerPotential pot = PlummerPotential(normalize=True, ro=8.0, vo=220.0) assert isinstance(pot(1.1, 0.1), units.Quantity), ( "Potential method __call__ does not return Quantity when it should" ) assert isinstance(pot.Rforce(1.1, 0.1), units.Quantity), ( "Potential method Rforce does not return Quantity when it should" ) assert isinstance(pot.rforce(1.1, 0.1), units.Quantity), ( "Potential method rforce does not return Quantity when it should" ) assert isinstance(pot.zforce(1.1, 0.1), units.Quantity), ( "Potential method zforce does not return Quantity when it should" ) assert isinstance(pot.phitorque(1.1, 0.1), units.Quantity), ( "Potential method phitorque does not return Quantity when it should" ) assert isinstance(pot.dens(1.1, 0.1), units.Quantity), ( "Potential method dens does not return Quantity when it should" ) assert isinstance(pot.surfdens(1.1, 0.1), units.Quantity), ( "Potential method surfdens does not return Quantity when it should" ) assert isinstance(pot.mass(1.1, 0.1), units.Quantity), ( "Potential method mass does not return Quantity when it should" ) assert isinstance(pot.R2deriv(1.1, 0.1), units.Quantity), ( "Potential method R2deriv does not return Quantity when it should" ) assert isinstance(pot.z2deriv(1.1, 0.1), units.Quantity), ( "Potential method z2deriv does not return Quantity when it should" ) assert isinstance(pot.Rzderiv(1.1, 0.1), units.Quantity), ( "Potential method Rzderiv does not return Quantity when it should" ) assert isinstance(pot.Rphideriv(1.1, 0.1), units.Quantity), ( "Potential method Rphideriv does not return Quantity when it should" ) assert isinstance(pot.phi2deriv(1.1, 0.1), units.Quantity), ( "Potential method phi2deriv does not return Quantity when it should" ) assert isinstance(pot.phizderiv(1.1, 0.1), units.Quantity), ( "Potential method phizderiv does not return Quantity when it should" ) assert isinstance(pot.flattening(1.1, 0.1), units.Quantity), ( "Potential method flattening does not return Quantity when it should" ) assert isinstance(pot.vcirc(1.1), units.Quantity), ( "Potential method vcirc does not return Quantity when it should" ) assert isinstance(pot.dvcircdR(1.1), units.Quantity), ( "Potential method dvcircdR does not return Quantity when it should" ) assert isinstance(pot.omegac(1.1), units.Quantity), ( "Potential method omegac does not return Quantity when it should" ) assert isinstance(pot.epifreq(1.1), units.Quantity), ( "Potential method epifreq does not return Quantity when it should" ) assert isinstance(pot.verticalfreq(1.1), units.Quantity), ( "Potential method verticalfreq does not return Quantity when it should" ) assert pot.lindbladR(0.9) is None, ( "Potential method lindbladR does not return None, even when it should return a Quantity, when it should" ) assert isinstance(pot.lindbladR(0.9, m="corot"), units.Quantity), ( "Potential method lindbladR does not return Quantity when it should" ) assert isinstance(pot.vesc(1.3), units.Quantity), ( "Potential method vesc does not return Quantity when it should" ) assert isinstance(pot.rl(1.3), units.Quantity), ( "Potential method rl does not return Quantity when it should" ) assert isinstance(pot.rE(-1.14), units.Quantity), ( "Potential method rE does not return Quantity when it should" ) assert isinstance(pot.LcE(-1.14), units.Quantity), ( "Potential method LcE does not return Quantity when it should" ) assert isinstance(pot.vterm(45.0), units.Quantity), ( "Potential method vterm does not return Quantity when it should" ) assert isinstance(pot.rtide(1.0, 0.0, M=1.0), units.Quantity), ( "Potential method rtide does not return Quantity when it should" ) assert isinstance(pot.ttensor(1.0, 0.0), units.Quantity), ( "Potential method ttensor does not return Quantity when it should" ) assert isinstance(pot.ttensor(1.0, 0.0, eigenval=True), units.Quantity), ( "Potential method ttensor does not return Quantity when it should" ) assert isinstance(pot.zvc_range(-1.9, 0.2), units.Quantity), ( "Potential method zvc_range does not return Quantity when it should" ) assert isinstance(pot.zvc(0.4, -1.9, 0.2), units.Quantity), ( "Potential method zvc does not return Quantity when it should" ) assert isinstance(pot.rhalf(), units.Quantity), ( "Potential method rhalf does not return Quantity when it should" ) assert isinstance(pot.tdyn(1.1), units.Quantity), ( "Potential method tdyn does not return Quantity when it should" ) return None def test_dissipativeforce_method_returntype(): from galpy.potential import ChandrasekharDynamicalFrictionForce pot = ChandrasekharDynamicalFrictionForce(GMs=0.1, rhm=1.2 / 8.0, ro=8.0, vo=220.0) assert isinstance( pot.phitorque(1.1, 0.1, phi=2.0, v=[0.1, 1.2, 0.3]), units.Quantity ), "Potential method phitorque does not return Quantity when it should" assert isinstance( pot.Rforce(1.1, 0.1, phi=2.0, v=[0.1, 1.2, 0.3]), units.Quantity ), "Potential method Rforce does not return Quantity when it should" assert isinstance( pot.zforce(1.1, 0.1, phi=2.0, v=[0.1, 1.2, 0.3]), units.Quantity ), "Potential method zforce does not return Quantity when it should" return None def test_planarPotential_method_returntype(): from galpy.potential import PlummerPotential pot = PlummerPotential(normalize=True, ro=8.0, vo=220.0).toPlanar() assert isinstance(pot(1.1), units.Quantity), ( "Potential method __call__ does not return Quantity when it should" ) assert isinstance(pot.Rforce(1.1), units.Quantity), ( "Potential method Rforce does not return Quantity when it should" ) assert isinstance(pot.phitorque(1.1), units.Quantity), ( "Potential method phitorque does not return Quantity when it should" ) assert isinstance(pot.R2deriv(1.1), units.Quantity), ( "Potential method R2deriv does not return Quantity when it should" ) assert isinstance(pot.Rphideriv(1.1), units.Quantity), ( "Potential method Rphideriv does not return Quantity when it should" ) assert isinstance(pot.phi2deriv(1.1), units.Quantity), ( "Potential method phi2deriv does not return Quantity when it should" ) assert isinstance(pot.vcirc(1.1), units.Quantity), ( "Potential method vcirc does not return Quantity when it should" ) assert isinstance(pot.omegac(1.1), units.Quantity), ( "Potential method omegac does not return Quantity when it should" ) assert isinstance(pot.epifreq(1.1), units.Quantity), ( "Potential method epifreq does not return Quantity when it should" ) assert pot.lindbladR(0.9) is None, ( "Potential method lindbladR does not return None, even when it should return a Quantity, when it should" ) assert isinstance(pot.lindbladR(0.9, m="corot"), units.Quantity), ( "Potential method lindbladR does not return Quantity when it should" ) assert isinstance(pot.vesc(1.3), units.Quantity), ( "Potential method vesc does not return Quantity when it should" ) return None def test_linearPotential_method_returntype(): from galpy.potential import PlummerPotential pot = PlummerPotential(normalize=True, ro=8.0, vo=220.0).toVertical(1.1) assert isinstance(pot(1.1), units.Quantity), ( "Potential method __call__ does not return Quantity when it should" ) assert isinstance(pot.force(1.1), units.Quantity), ( "Potential method Rforce does not return Quantity when it should" ) return None def test_potential_method_returnunit(): from galpy.potential import PlummerPotential pot = PlummerPotential(normalize=True, ro=8.0, vo=220.0) try: pot(1.1, 0.1).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method __call__ does not return Quantity with the right units" ) try: pot.Rforce(1.1, 0.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method Rforce does not return Quantity with the right units" ) try: pot.rforce(1.1, 0.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method rforce does not return Quantity with the right units" ) try: pot.zforce(1.1, 0.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method zforce does not return Quantity with the right units" ) try: pot.phitorque(1.1, 0.1).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method phitorque does not return Quantity with the right units" ) try: pot.dens(1.1, 0.1).to(units.kg / units.m**3) except units.UnitConversionError: raise AssertionError( "Potential method dens does not return Quantity with the right units" ) try: pot.surfdens(1.1, 0.1).to(units.kg / units.m**2) except units.UnitConversionError: raise AssertionError( "Potential method surfdens does not return Quantity with the right units" ) try: pot.mass(1.1, 0.1).to(units.kg) except units.UnitConversionError: raise AssertionError( "Potential method mass does not return Quantity with the right units" ) try: pot.R2deriv(1.1, 0.1).to(1 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method R2deriv does not return Quantity with the right units" ) try: pot.z2deriv(1.1, 0.1).to(1 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method z2deriv does not return Quantity with the right units" ) try: pot.Rzderiv(1.1, 0.1).to(1 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method Rzderiv does not return Quantity with the right units" ) try: pot.phi2deriv(1.1, 0.1).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method phi2deriv does not return Quantity with the right units" ) try: pot.Rphideriv(1.1, 0.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method Rphideriv does not return Quantity with the right units" ) try: pot.phizderiv(1.1, 0.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method phizderiv does not return Quantity with the right units" ) try: pot.flattening(1.1, 0.1).to(units.dimensionless_unscaled) except units.UnitConversionError: raise AssertionError( "Potential method flattening does not return Quantity with the right units" ) try: pot.vcirc(1.1).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Potential method vcirc does not return Quantity with the right units" ) try: pot.dvcircdR(1.1).to(1.0 / units.s) except units.UnitConversionError: raise AssertionError( "Potential method dvcircdR does not return Quantity with the right units" ) try: pot.omegac(1.1).to(1.0 / units.s) except units.UnitConversionError: raise AssertionError( "Potential method omegac does not return Quantity with the right units" ) try: pot.epifreq(1.1).to(1.0 / units.s) except units.UnitConversionError: raise AssertionError( "Potential method epifreq does not return Quantity with the right units" ) try: pot.verticalfreq(1.1).to(1.0 / units.s) except units.UnitConversionError: raise AssertionError( "Potential method verticalfreq does not return Quantity with the right units" ) try: pot.lindbladR(0.9, m="corot").to(units.km) except units.UnitConversionError: raise AssertionError( "Potential method lindbladR does not return Quantity with the right units" ) try: pot.vesc(1.3).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Potential method vesc does not return Quantity with the right units" ) try: pot.rl(1.3).to(units.km) except units.UnitConversionError: raise AssertionError( "Potential method rl does not return Quantity with the right units" ) try: pot.rE(-1.14).to(units.km) except units.UnitConversionError: raise AssertionError( "Potential method rE does not return Quantity with the right units" ) try: pot.LcE(-1.14).to(units.km / units.s * units.kpc) except units.UnitConversionError: raise AssertionError( "Potential method LcE does not return Quantity with the right units" ) try: pot.vterm(45.0).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Potential method vterm does not return Quantity with the right units" ) try: pot.rtide(1.0, 0.0, M=1.0).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Potential method rtide does not return Quantity with the right units" ) try: pot.ttensor(1.0, 0.0).to(1 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method ttensor does not return Quantity with the right units" ) try: pot.ttensor(1.0, 0.0, eigenval=True).to(1 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method ttensor does not return Quantity with the right units" ) try: pot.zvc_range(-1.9, 0.2).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Potential method zvc_range does not return Quantity with the right units" ) try: pot.zvc(0.4, -1.9, 0.2).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Potential method zvc does not return Quantity with the right units" ) try: pot.rhalf().to(units.kpc) except units.UnitConversionError: raise AssertionError( "Potential method rhalf does not return Quantity with the right units" ) try: pot.tdyn(1.4).to(units.Gyr) except units.UnitConversionError: raise AssertionError( "Potential method tdyn does not return Quantity with the right units" ) return None def test_planarPotential_method_returnunit(): from galpy.potential import PlummerPotential pot = PlummerPotential(normalize=True, ro=8.0, vo=220.0).toPlanar() try: pot(1.1).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method __call__ does not return Quantity with the right units" ) try: pot.Rforce(1.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method Rforce does not return Quantity with the right units" ) try: pot.phitorque(1.1).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method phitorque does not return Quantity with the right units" ) try: pot.R2deriv(1.1).to(1 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method R2deriv does not return Quantity with the right units" ) try: pot.phi2deriv(1.1).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method phi2deriv does not return Quantity with the right units" ) try: pot.Rphideriv(1.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method Rphideriv does not return Quantity with the right units" ) try: pot.vcirc(1.1).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Potential method vcirc does not return Quantity with the right units" ) try: pot.omegac(1.1).to(1.0 / units.s) except units.UnitConversionError: raise AssertionError( "Potential method omegac does not return Quantity with the right units" ) try: pot.epifreq(1.1).to(1.0 / units.s) except units.UnitConversionError: raise AssertionError( "Potential method epifreq does not return Quantity with the right units" ) try: pot.lindbladR(0.9, m="corot").to(units.km) except units.UnitConversionError: raise AssertionError( "Potential method lindbladR does not return Quantity with the right units" ) try: pot.vesc(1.3).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Potential method vesc does not return Quantity with the right units" ) return None def test_linearPotential_method_returnunit(): from galpy.potential import PlummerPotential pot = PlummerPotential(normalize=True, ro=8.0, vo=220.0).toVertical(1.1) try: pot(1.1).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method __call__ does not return Quantity with the right units" ) try: pot.force(1.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential method force does not return Quantity with the right units" ) return None def test_potential_method_value(): from galpy.potential import PlummerPotential from galpy.util import conversion ro, vo = 8.0, 220.0 pot = PlummerPotential(normalize=True, ro=ro, vo=vo) potu = PlummerPotential(normalize=True) assert ( numpy.fabs( pot(1.1, 0.1).to(units.km**2 / units.s**2).value - potu(1.1, 0.1) * vo**2.0 ) < 10.0**-8.0 ), "Potential method __call__ does not return the correct value as Quantity" assert ( numpy.fabs( pot.Rforce(1.1, 0.1).to(units.km / units.s**2).value * 10.0**13.0 - potu.Rforce(1.1, 0.1) * conversion.force_in_10m13kms2(vo, ro) ) < 10.0**-4.0 ), "Potential method Rforce does not return the correct value as Quantity" assert ( numpy.fabs( pot.rforce(1.1, 0.1).to(units.km / units.s**2).value * 10.0**13.0 - potu.rforce(1.1, 0.1) * conversion.force_in_10m13kms2(vo, ro) ) < 10.0**-4.0 ), "Potential method rforce does not return the correct value as Quantity" assert ( numpy.fabs( pot.zforce(1.1, 0.1).to(units.km / units.s**2).value * 10.0**13.0 - potu.zforce(1.1, 0.1) * conversion.force_in_10m13kms2(vo, ro) ) < 10.0**-4.0 ), "Potential method zforce does not return the correct value as Quantity" assert ( numpy.fabs( pot.phitorque(1.1, 0.1).to(units.km**2 / units.s**2).value - potu.phitorque(1.1, 0.1) * vo**2 ) < 10.0**-4.0 ), "Potential method phitorque does not return the correct value as Quantity" assert ( numpy.fabs( pot.dens(1.1, 0.1).to(units.Msun / units.pc**3).value - potu.dens(1.1, 0.1) * conversion.dens_in_msolpc3(vo, ro) ) < 10.0**-8.0 ), "Potential method dens does not return the correct value as Quantity" assert ( numpy.fabs( pot.surfdens(1.1, 0.1).to(units.Msun / units.pc**2).value - potu.surfdens(1.1, 0.1) * conversion.surfdens_in_msolpc2(vo, ro) ) < 10.0**-8.0 ), "Potential method surfdens does not return the correct value as Quantity" assert ( numpy.fabs( pot.mass(1.1, 0.1).to(units.Msun).value / 10.0**10.0 - potu.mass(1.1, 0.1) * conversion.mass_in_1010msol(vo, ro) ) < 10.0**-8.0 ), "Potential method mass does not return the correct value as Quantity" assert ( numpy.fabs( pot.R2deriv(1.1, 0.1).to(units.km**2 / units.s**2.0 / units.kpc**2).value - potu.R2deriv(1.1, 0.1) * vo**2.0 / ro**2.0 ) < 10.0**-8.0 ), "Potential method R2deriv does not return the correct value as Quantity" assert ( numpy.fabs( pot.z2deriv(1.1, 0.1).to(units.km**2 / units.s**2.0 / units.kpc**2).value - potu.z2deriv(1.1, 0.1) * vo**2.0 / ro**2.0 ) < 10.0**-8.0 ), "Potential method z2deriv does not return the correct value as Quantity" assert ( numpy.fabs( pot.Rzderiv(1.1, 0.1).to(units.km**2 / units.s**2.0 / units.kpc**2).value - potu.Rzderiv(1.1, 0.1) * vo**2.0 / ro**2.0 ) < 10.0**-8.0 ), "Potential method Rzderiv does not return the correct value as Quantity" assert ( numpy.fabs( pot.Rphideriv(1.1, 0.1).to(units.km**2 / units.s**2.0 / units.kpc).value - potu.Rphideriv(1.1, 0.1) * vo**2.0 / ro ) < 10.0**-8.0 ), "Potential method Rphideriv does not return the correct value as Quantity" assert ( numpy.fabs( pot.phi2deriv(1.1, 0.1).to(units.km**2 / units.s**2.0).value - potu.phi2deriv(1.1, 0.1) * vo**2.0 ) < 10.0**-8.0 ), "Potential method phi2deriv does not return the correct value as Quantity" assert ( numpy.fabs( pot.phizderiv(1.1, 0.1).to(units.km**2 / units.s**2.0 / units.kpc).value - potu.phizderiv(1.1, 0.1) * vo**2.0 / ro ) < 10.0**-8.0 ), "Potential method phizderiv does not return the correct value as Quantity" assert ( numpy.fabs(pot.flattening(1.1, 0.1).value - potu.flattening(1.1, 0.1)) < 10.0**-8.0 ), "Potential method flattening does not return the correct value as Quantity" assert ( numpy.fabs(pot.vcirc(1.1).to(units.km / units.s).value - potu.vcirc(1.1) * vo) < 10.0**-8.0 ), "Potential method vcirc does not return the correct value as Quantity" assert ( numpy.fabs( pot.dvcircdR(1.1).to(units.km / units.s / units.kpc).value - potu.dvcircdR(1.1) * vo / ro ) < 10.0**-8.0 ), "Potential method dvcircdR does not return the correct value as Quantity" assert ( numpy.fabs( pot.omegac(1.1).to(units.km / units.s / units.kpc).value - potu.omegac(1.1) * vo / ro ) < 10.0**-8.0 ), "Potential method omegac does not return the correct value as Quantity" assert ( numpy.fabs( pot.epifreq(1.1).to(units.km / units.s / units.kpc).value - potu.epifreq(1.1) * vo / ro ) < 10.0**-8.0 ), "Potential method epifreq does not return the correct value as Quantity" assert ( numpy.fabs( pot.verticalfreq(1.1).to(units.km / units.s / units.kpc).value - potu.verticalfreq(1.1) * vo / ro ) < 10.0**-8.0 ), "Potential method verticalfreq does not return the correct value as Quantity" assert ( numpy.fabs( pot.lindbladR(0.9, m="corot").to(units.kpc).value - potu.lindbladR(0.9, m="corot") * ro ) < 10.0**-8.0 ), "Potential method lindbladR does not return the correct value as Quantity" assert ( numpy.fabs(pot.vesc(1.1).to(units.km / units.s).value - potu.vesc(1.1) * vo) < 10.0**-8.0 ), "Potential method vesc does not return the correct value as Quantity" assert ( numpy.fabs(pot.rl(1.1).to(units.kpc).value - potu.rl(1.1) * ro) < 10.0**-8.0 ), "Potential method rl does not return the correct value as Quantity" assert ( numpy.fabs(pot.rE(-1.14).to(units.kpc).value - potu.rE(-1.14) * ro) < 10.0**-8.0 ), "Potential method rE does not return the correct value as Quantity" assert ( numpy.fabs( pot.LcE(-1.14).to(units.kpc * units.km / units.s).value - potu.LcE(-1.14) * ro * vo ) < 10.0**-8.0 ), "Potential method LcE does not return the correct value as Quantity" assert ( numpy.fabs(pot.vterm(45.0).to(units.km / units.s).value - potu.vterm(45.0) * vo) < 10.0**-8.0 ), "Potential method vterm does not return the correct value as Quantity" assert ( numpy.fabs( pot.rtide(1.0, 0.0, M=1.0).to(units.kpc).value - potu.rtide(1.0, 0.0, M=1.0) * ro ) < 10.0**-8.0 ), "Potential method rtide does not return the correct value as Quantity" assert numpy.all( numpy.fabs( pot.ttensor(1.0, 0.0).to(units.km**2 / units.s**2.0 / units.kpc**2).value - potu.ttensor(1.0, 0.0) * vo**2.0 / ro**2.0 ) < 10.0**-8.0 ), "Potential method ttensor does not return the correct value as Quantity" assert numpy.all( numpy.fabs( pot.ttensor(1.0, 0.0, eigenval=True) .to(units.km**2 / units.s**2.0 / units.kpc**2) .value - potu.ttensor(1.0, 0.0, eigenval=True) * vo**2.0 / ro**2.0 ) < 10.0**-8.0 ), "Potential method ttensor does not return the correct value as Quantity" assert numpy.all( numpy.fabs( pot.zvc_range(-1.9, 0.2).to(units.kpc).value - potu.zvc_range(-1.9, 0.2) * ro ) < 10.0**-8.0 ), "Potential method zvc_range does not return the correct value as Quantity" assert numpy.all( numpy.fabs( pot.zvc(0.4, -1.9, 0.2).to(units.kpc).value - potu.zvc(0.4, -1.9, 0.2) * ro ) < 10.0**-8.0 ), "Potential method zvc_range does not return the correct value as Quantity" assert ( numpy.fabs(pot.rhalf().to(units.kpc).value - potu.rhalf() * ro) < 10.0**-8.0 ), "Potential method rhalf does not return the correct value as Quantity" assert ( numpy.fabs( pot.tdyn(1.4).to(units.Gyr).value - potu.tdyn(1.4) * conversion.time_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "Potential method tdyn does not return the correct value as Quantity" return None def test_planarPotential_method_value(): from galpy.potential import PlummerPotential from galpy.util import conversion ro, vo = 8.0, 220.0 pot = PlummerPotential(normalize=True, ro=ro, vo=vo).toPlanar() potu = PlummerPotential(normalize=True).toPlanar() assert ( numpy.fabs(pot(1.1).to(units.km**2 / units.s**2).value - potu(1.1) * vo**2.0) < 10.0**-8.0 ), "Potential method __call__ does not return the correct value as Quantity" assert ( numpy.fabs( pot.Rforce(1.1).to(units.km / units.s**2).value * 10.0**13.0 - potu.Rforce(1.1) * conversion.force_in_10m13kms2(vo, ro) ) < 10.0**-4.0 ), "Potential method Rforce does not return the correct value as Quantity" assert ( numpy.fabs( pot.phitorque(1.1).to(units.km**2 / units.s**2).value - potu.phitorque(1.1) * vo**2 ) < 10.0**-4.0 ), "Potential method phitorque does not return the correct value as Quantity" assert ( numpy.fabs( pot.R2deriv(1.1).to(units.km**2 / units.s**2.0 / units.kpc**2).value - potu.R2deriv(1.1) * vo**2.0 / ro**2.0 ) < 10.0**-8.0 ), "Potential method R2deriv does not return the correct value as Quantity" assert ( numpy.fabs( pot.Rphideriv(1.1).to(units.km**2 / units.s**2.0 / units.kpc).value - potu.Rphideriv(1.1) * vo**2.0 / ro ) < 10.0**-8.0 ), "Potential method Rphideriv does not return the correct value as Quantity" assert ( numpy.fabs( pot.phi2deriv(1.1).to(units.km**2 / units.s**2.0).value - potu.phi2deriv(1.1) * vo**2.0 ) < 10.0**-8.0 ), "Potential method phi2deriv does not return the correct value as Quantity" assert ( numpy.fabs(pot.vcirc(1.1).to(units.km / units.s).value - potu.vcirc(1.1) * vo) < 10.0**-8.0 ), "Potential method vcirc does not return the correct value as Quantity" assert ( numpy.fabs( pot.omegac(1.1).to(units.km / units.s / units.kpc).value - potu.omegac(1.1) * vo / ro ) < 10.0**-8.0 ), "Potential method omegac does not return the correct value as Quantity" assert ( numpy.fabs( pot.epifreq(1.1).to(units.km / units.s / units.kpc).value - potu.epifreq(1.1) * vo / ro ) < 10.0**-8.0 ), "Potential method epifreq does not return the correct value as Quantity" assert ( numpy.fabs(pot.vesc(1.1).to(units.km / units.s).value - potu.vesc(1.1) * vo) < 10.0**-8.0 ), "Potential method vesc does not return the correct value as Quantity" return None def test_linearPotential_method_value(): from galpy.potential import PlummerPotential from galpy.util import conversion ro, vo = 8.0, 220.0 pot = PlummerPotential(normalize=True, ro=ro, vo=vo).toVertical(1.1) potu = PlummerPotential(normalize=True).toVertical(1.1) assert ( numpy.fabs(pot(1.1).to(units.km**2 / units.s**2).value - potu(1.1) * vo**2.0) < 10.0**-8.0 ), "Potential method __call__ does not return the correct value as Quantity" assert ( numpy.fabs( pot.force(1.1).to(units.km / units.s**2).value * 10.0**13.0 - potu.force(1.1) * conversion.force_in_10m13kms2(vo, ro) ) < 10.0**-4.0 ), "Potential method force does not return the correct value as Quantity" return None def test_potential_function_returntype(): from galpy import potential from galpy.potential import PlummerPotential pot = [PlummerPotential(normalize=True, ro=8.0, vo=220.0)] assert isinstance(potential.evaluatePotentials(pot, 1.1, 0.1), units.Quantity), ( "Potential function __call__ does not return Quantity when it should" ) assert isinstance(potential.evaluateRforces(pot, 1.1, 0.1), units.Quantity), ( "Potential function Rforce does not return Quantity when it should" ) assert isinstance(potential.evaluaterforces(pot, 1.1, 0.1), units.Quantity), ( "Potential function rforce does not return Quantity when it should" ) assert isinstance(potential.evaluatezforces(pot, 1.1, 0.1), units.Quantity), ( "Potential function zforce does not return Quantity when it should" ) assert isinstance(potential.evaluatephitorques(pot, 1.1, 0.1), units.Quantity), ( "Potential function phitorque does not return Quantity when it should" ) assert isinstance(potential.evaluateDensities(pot, 1.1, 0.1), units.Quantity), ( "Potential function dens does not return Quantity when it should" ) assert isinstance( potential.evaluateSurfaceDensities(pot, 1.1, 0.1), units.Quantity ), "Potential function surfdens does not return Quantity when it should" assert isinstance(potential.evaluateR2derivs(pot, 1.1, 0.1), units.Quantity), ( "Potential function R2deriv does not return Quantity when it should" ) assert isinstance(potential.evaluatez2derivs(pot, 1.1, 0.1), units.Quantity), ( "Potential function z2deriv does not return Quantity when it should" ) assert isinstance(potential.evaluateRzderivs(pot, 1.1, 0.1), units.Quantity), ( "Potential function Rzderiv does not return Quantity when it should" ) assert isinstance(potential.flattening(pot, 1.1, 0.1), units.Quantity), ( "Potential function flattening does not return Quantity when it should" ) assert isinstance(potential.vcirc(pot, 1.1), units.Quantity), ( "Potential function vcirc does not return Quantity when it should" ) assert isinstance(potential.dvcircdR(pot, 1.1), units.Quantity), ( "Potential function dvcircdR does not return Quantity when it should" ) assert isinstance(potential.omegac(pot, 1.1), units.Quantity), ( "Potential function omegac does not return Quantity when it should" ) assert isinstance(potential.epifreq(pot, 1.1), units.Quantity), ( "Potential function epifreq does not return Quantity when it should" ) assert isinstance(potential.verticalfreq(pot, 1.1), units.Quantity), ( "Potential function verticalfreq does not return Quantity when it should" ) assert potential.lindbladR(pot, 0.9) is None, ( "Potential function lindbladR does not return None, even when it should return a Quantity, when it should" ) assert isinstance(potential.lindbladR(pot, 0.9, m="corot"), units.Quantity), ( "Potential function lindbladR does not return Quantity when it should" ) assert isinstance(potential.vesc(pot, 1.3), units.Quantity), ( "Potential function vesc does not return Quantity when it should" ) assert isinstance(potential.rl(pot, 1.3), units.Quantity), ( "Potential function rl does not return Quantity when it should" ) assert isinstance(potential.rE(pot, -1.14), units.Quantity), ( "Potential function rE does not return Quantity when it should" ) assert isinstance(potential.LcE(pot, -1.14), units.Quantity), ( "Potential function LcE does not return Quantity when it should" ) assert isinstance(potential.vterm(pot, 45.0), units.Quantity), ( "Potential function vterm does not return Quantity when it should" ) assert isinstance(potential.rtide(pot, 1.0, 0.0, M=1.0), units.Quantity), ( "Potential function rtide does not return Quantity when it should" ) assert isinstance(potential.ttensor(pot, 1.0, 0.0), units.Quantity), ( "Potential function ttensor does not return Quantity when it should" ) assert isinstance( potential.ttensor(pot, 1.0, 0.0, eigenval=True), units.Quantity ), "Potential function ttensor does not return Quantity when it should" assert isinstance(potential.zvc_range(pot, -1.9, 0.2), units.Quantity), ( "Potential function zvc_range does not return Quantity when it should" ) assert isinstance(potential.zvc(pot, 0.4, -1.9, 0.2), units.Quantity), ( "Potential function zvc does not return Quantity when it should" ) assert isinstance(potential.rhalf(pot), units.Quantity), ( "Potential function rhalf does not return Quantity when it should" ) assert isinstance(potential.tdyn(pot, 1.4), units.Quantity), ( "Potential function tdyn does not return Quantity when it should" ) return None def test_planarPotential_function_returntype(): from galpy import potential from galpy.potential import PlummerPotential pot = [PlummerPotential(normalize=True, ro=8.0, vo=220.0).toPlanar()] assert isinstance(potential.evaluateplanarPotentials(pot, 1.1), units.Quantity), ( "Potential function __call__ does not return Quantity when it should" ) assert isinstance(potential.evaluateplanarRforces(pot, 1.1), units.Quantity), ( "Potential function Rforce does not return Quantity when it should" ) assert isinstance(potential.evaluateplanarphitorques(pot, 1.1), units.Quantity), ( "Potential function phitorque does not return Quantity when it should" ) assert isinstance(potential.evaluateplanarR2derivs(pot, 1.1), units.Quantity), ( "Potential function R2deriv does not return Quantity when it should" ) assert isinstance(potential.vcirc(pot, 1.1), units.Quantity), ( "Potential function vcirc does not return Quantity when it should" ) assert isinstance(potential.omegac(pot, 1.1), units.Quantity), ( "Potential function omegac does not return Quantity when it should" ) assert isinstance(potential.epifreq(pot, 1.1), units.Quantity), ( "Potential function epifreq does not return Quantity when it should" ) assert potential.lindbladR(pot, 0.9) is None, ( "Potential function lindbladR does not return None, even when it should return a Quantity, when it should" ) assert isinstance(potential.lindbladR(pot, 0.9, m="corot"), units.Quantity), ( "Potential function lindbladR does not return Quantity when it should" ) assert isinstance(potential.vesc(pot, 1.3), units.Quantity), ( "Potential function vesc does not return Quantity when it should" ) return None def test_linearPotential_function_returntype(): from galpy import potential from galpy.potential import PlummerPotential pot = [PlummerPotential(normalize=True, ro=8.0, vo=220.0).toVertical(1.1)] assert isinstance(potential.evaluatelinearPotentials(pot, 1.1), units.Quantity), ( "Potential function __call__ does not return Quantity when it should" ) assert isinstance(potential.evaluatelinearForces(pot, 1.1), units.Quantity), ( "Potential function Rforce does not return Quantity when it should" ) return None def test_potential_function_returnunit(): from galpy import potential from galpy.potential import PlummerPotential pot = [PlummerPotential(normalize=True, ro=8.0, vo=220.0)] try: potential.evaluatePotentials(pot, 1.1, 0.1).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function __call__ does not return Quantity with the right units" ) try: potential.evaluateRforces(pot, 1.1, 0.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function Rforce does not return Quantity with the right units" ) try: potential.evaluaterforces(pot, 1.1, 0.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function rforce does not return Quantity with the right units" ) try: potential.evaluatezforces(pot, 1.1, 0.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function zforce does not return Quantity with the right units" ) try: potential.evaluatephitorques(pot, 1.1, 0.1).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function phitorque does not return Quantity with the right units" ) try: potential.evaluateDensities(pot, 1.1, 0.1).to(units.kg / units.m**3) except units.UnitConversionError: raise AssertionError( "Potential function dens does not return Quantity with the right units" ) try: potential.evaluateSurfaceDensities(pot, 1.1, 0.1).to(units.kg / units.m**2) except units.UnitConversionError: raise AssertionError( "Potential function surfdens does not return Quantity with the right units" ) try: potential.evaluateR2derivs(pot, 1.1, 0.1).to(1 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function R2deriv does not return Quantity with the right units" ) try: potential.evaluatez2derivs(pot, 1.1, 0.1).to(1 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function z2deriv does not return Quantity with the right units" ) try: potential.evaluateRzderivs(pot, 1.1, 0.1).to(1 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function Rzderiv does not return Quantity with the right units" ) try: potential.flattening(pot, 1.1, 0.1).to(units.dimensionless_unscaled) except units.UnitConversionError: raise AssertionError( "Potential function flattening does not return Quantity with the right units" ) try: potential.vcirc(pot, 1.1).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Potential function vcirc does not return Quantity with the right units" ) try: potential.dvcircdR(pot, 1.1).to(1.0 / units.s) except units.UnitConversionError: raise AssertionError( "Potential function dvcircdR does not return Quantity with the right units" ) try: potential.omegac(pot, 1.1).to(1.0 / units.s) except units.UnitConversionError: raise AssertionError( "Potential function omegac does not return Quantity with the right units" ) try: potential.epifreq(pot, 1.1).to(1.0 / units.s) except units.UnitConversionError: raise AssertionError( "Potential function epifreq does not return Quantity with the right units" ) try: potential.verticalfreq(pot, 1.1).to(1.0 / units.s) except units.UnitConversionError: raise AssertionError( "Potential function verticalfreq does not return Quantity with the right units" ) try: potential.lindbladR(pot, 0.9, m="corot").to(units.km) except units.UnitConversionError: raise AssertionError( "Potential function lindbladR does not return Quantity with the right units" ) try: potential.vesc(pot, 1.3).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Potential function vesc does not return Quantity with the right units" ) try: potential.rl(pot, 1.3).to(units.km) except units.UnitConversionError: raise AssertionError( "Potential function rl does not return Quantity with the right units" ) try: potential.rE(pot, -1.14).to(units.km) except units.UnitConversionError: raise AssertionError( "Potential function rE does not return Quantity with the right units" ) try: potential.LcE(pot, -1.14).to(units.km / units.s * units.kpc) except units.UnitConversionError: raise AssertionError( "Potential function LcE does not return Quantity with the right units" ) try: potential.vterm(pot, 45.0).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Potential function vterm does not return Quantity with the right units" ) try: potential.rtide(pot, 1.0, 0.0, M=1.0).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Potential function rtide does not return Quantity with the right units" ) try: potential.ttensor(pot, 1.0, 0.0).to(1 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function ttensor does not return Quantity with the right units" ) try: potential.ttensor(pot, 1.0, 0.0, eigenval=True).to(1 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function ttensor does not return Quantity with the right units" ) try: potential.zvc_range(pot, -1.9, 0.2).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Potential function zvc_range does not return Quantity with the right units" ) try: potential.zvc(pot, 0.4, -1.9, 0.2).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Potential function zvc does not return Quantity with the right units" ) try: potential.rhalf(pot).to(units.kpc) except units.UnitConversionError: raise AssertionError( "Potential function rhalf does not return Quantity with the right units" ) try: potential.tdyn(pot, 1.4).to(units.Gyr) except units.UnitConversionError: raise AssertionError( "Potential function tdyn does not return Quantity with the right units" ) return None def test_planarPotential_function_returnunit(): from galpy import potential from galpy.potential import LopsidedDiskPotential, PlummerPotential pot = [ PlummerPotential(normalize=True, ro=8.0, vo=220.0).toPlanar(), LopsidedDiskPotential(ro=8.0 * units.kpc, vo=220.0 * units.km / units.s), ] try: potential.evaluateplanarPotentials(pot, 1.1, phi=0.1).to( units.km**2 / units.s**2 ) except units.UnitConversionError: raise AssertionError( "Potential function __call__ does not return Quantity with the right units" ) try: potential.evaluateplanarRforces(pot, 1.1, phi=0.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function Rforce does not return Quantity with the right units" ) try: potential.evaluateplanarphitorques(pot, 1.1, phi=0.1).to( units.km**2 / units.s**2 ) except units.UnitConversionError: raise AssertionError( "Potential function phitorque does not return Quantity with the right units" ) try: potential.evaluateplanarR2derivs(pot, 1.1, phi=0.1).to(1 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function R2deriv does not return Quantity with the right units" ) pot.pop() try: potential.vcirc(pot, 1.1).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Potential function vcirc does not return Quantity with the right units" ) try: potential.omegac(pot, 1.1).to(1.0 / units.s) except units.UnitConversionError: raise AssertionError( "Potential function omegac does not return Quantity with the right units" ) try: potential.epifreq(pot, 1.1).to(1.0 / units.s) except units.UnitConversionError: raise AssertionError( "Potential function epifreq does not return Quantity with the right units" ) try: potential.lindbladR(pot, 0.9, m="corot").to(units.km) except units.UnitConversionError: raise AssertionError( "Potential function lindbladR does not return Quantity with the right units" ) try: potential.vesc(pot, 1.3).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "Potential function vesc does not return Quantity with the right units" ) return None def test_linearPotential_function_returnunit(): from galpy import potential from galpy.potential import KGPotential pot = [KGPotential(ro=8.0 * units.kpc, vo=220.0 * units.km / units.s)] try: potential.evaluatelinearPotentials(pot, 1.1).to(units.km**2 / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function __call__ does not return Quantity with the right units" ) try: potential.evaluatelinearForces(pot, 1.1).to(units.km / units.s**2) except units.UnitConversionError: raise AssertionError( "Potential function force does not return Quantity with the right units" ) return None def test_potential_function_value(): from galpy import potential from galpy.potential import PlummerPotential from galpy.util import conversion ro, vo = 8.0, 220.0 pot = [PlummerPotential(normalize=True, ro=ro, vo=vo)] potu = [PlummerPotential(normalize=True)] assert ( numpy.fabs( potential.evaluatePotentials(pot, 1.1, 0.1) .to(units.km**2 / units.s**2) .value - potential.evaluatePotentials(potu, 1.1, 0.1) * vo**2.0 ) < 10.0**-8.0 ), "Potential function __call__ does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateRforces(pot, 1.1, 0.1).to(units.km / units.s**2).value * 10.0**13.0 - potential.evaluateRforces(potu, 1.1, 0.1) * conversion.force_in_10m13kms2(vo, ro) ) < 10.0**-4.0 ), "Potential function Rforce does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluaterforces(pot, 1.1, 0.1).to(units.km / units.s**2).value * 10.0**13.0 - potential.evaluaterforces(potu, 1.1, 0.1) * conversion.force_in_10m13kms2(vo, ro) ) < 10.0**-4.0 ), "Potential function rforce does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluatezforces(pot, 1.1, 0.1).to(units.km / units.s**2).value * 10.0**13.0 - potential.evaluatezforces(potu, 1.1, 0.1) * conversion.force_in_10m13kms2(vo, ro) ) < 10.0**-4.0 ), "Potential function zforce does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluatephitorques(pot, 1.1, 0.1) .to(units.km**2 / units.s**2) .value - potential.evaluatephitorques(potu, 1.1, 0.1) * vo**2 ) < 10.0**-4.0 ), "Potential function phitorque does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateDensities(pot, 1.1, 0.1) .to(units.Msun / units.pc**3) .value - potential.evaluateDensities(potu, 1.1, 0.1) * conversion.dens_in_msolpc3(vo, ro) ) < 10.0**-8.0 ), "Potential function dens does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateSurfaceDensities(pot, 1.1, 0.1) .to(units.Msun / units.pc**2) .value - potential.evaluateSurfaceDensities(potu, 1.1, 0.1) * conversion.surfdens_in_msolpc2(vo, ro) ) < 10.0**-8.0 ), "Potential function surfdens does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateR2derivs(pot, 1.1, 0.1) .to(units.km**2 / units.s**2.0 / units.kpc**2) .value - potential.evaluateR2derivs(potu, 1.1, 0.1) * vo**2.0 / ro**2.0 ) < 10.0**-8.0 ), "Potential function R2deriv does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluatez2derivs(pot, 1.1, 0.1) .to(units.km**2 / units.s**2.0 / units.kpc**2) .value - potential.evaluatez2derivs(potu, 1.1, 0.1) * vo**2.0 / ro**2.0 ) < 10.0**-8.0 ), "Potential function z2deriv does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateRzderivs(pot, 1.1, 0.1) .to(units.km**2 / units.s**2.0 / units.kpc**2) .value - potential.evaluateRzderivs(potu, 1.1, 0.1) * vo**2.0 / ro**2.0 ) < 10.0**-8.0 ), "Potential function Rzderiv does not return the correct value as Quantity" assert ( numpy.fabs( potential.flattening(pot, 1.1, 0.1).value - potential.flattening(potu, 1.1, 0.1) ) < 10.0**-8.0 ), "Potential function flattening does not return the correct value as Quantity" assert ( numpy.fabs( potential.vcirc(pot, 1.1).to(units.km / units.s).value - potential.vcirc(potu, 1.1) * vo ) < 10.0**-8.0 ), "Potential function vcirc does not return the correct value as Quantity" assert ( numpy.fabs( potential.dvcircdR(pot, 1.1).to(units.km / units.s / units.kpc).value - potential.dvcircdR(potu, 1.1) * vo / ro ) < 10.0**-8.0 ), "Potential function dvcircdR does not return the correct value as Quantity" assert ( numpy.fabs( potential.omegac(pot, 1.1).to(units.km / units.s / units.kpc).value - potential.omegac(potu, 1.1) * vo / ro ) < 10.0**-8.0 ), "Potential function omegac does not return the correct value as Quantity" assert ( numpy.fabs( potential.epifreq(pot, 1.1).to(units.km / units.s / units.kpc).value - potential.epifreq(potu, 1.1) * vo / ro ) < 10.0**-8.0 ), "Potential function epifreq does not return the correct value as Quantity" assert ( numpy.fabs( potential.verticalfreq(pot, 1.1).to(units.km / units.s / units.kpc).value - potential.verticalfreq(potu, 1.1) * vo / ro ) < 10.0**-8.0 ), "Potential function verticalfreq does not return the correct value as Quantity" assert ( numpy.fabs( potential.lindbladR(pot, 0.9, m="corot").to(units.kpc).value - potential.lindbladR(potu, 0.9, m="corot") * ro ) < 10.0**-8.0 ), "Potential function lindbladR does not return the correct value as Quantity" assert ( numpy.fabs( potential.vesc(pot, 1.1).to(units.km / units.s).value - potential.vesc(potu, 1.1) * vo ) < 10.0**-8.0 ), "Potential function vesc does not return the correct value as Quantity" assert ( numpy.fabs( potential.rl(pot, 1.1).to(units.kpc).value - potential.rl(potu, 1.1) * ro ) < 10.0**-8.0 ), "Potential function rl does not return the correct value as Quantity" assert ( numpy.fabs( potential.rE(pot, -1.14).to(units.kpc).value - potential.rE(potu, -1.14) * ro ) < 10.0**-8.0 ), "Potential function rE does not return the correct value as Quantity" assert ( numpy.fabs( potential.LcE(pot, -1.14).to(units.kpc * units.km / units.s).value - potential.LcE(potu, -1.14) * ro * vo ) < 10.0**-8.0 ), "Potential function LcE does not return the correct value as Quantity" assert ( numpy.fabs( potential.vterm(pot, 45.0).to(units.km / units.s).value - potential.vterm(potu, 45.0) * vo ) < 10.0**-8.0 ), "Potential function vterm does not return the correct value as Quantity" assert ( numpy.fabs( potential.rtide(pot, 1.0, 0.0, M=1.0).to(units.kpc).value - potential.rtide(potu, 1.0, 0.0, M=1.0) * ro ) < 10.0**-8.0 ), "Potential function rtide does not return the correct value as Quantity" assert numpy.all( numpy.fabs( potential.ttensor(pot, 1.0, 0.0) .to(units.km**2 / units.s**2 / units.kpc**2) .value - potential.ttensor(potu, 1.0, 0.0) * vo**2 / ro**2 ) < 10.0**-8.0 ), "Potential function ttensor does not return the correct value as Quantity" assert numpy.all( numpy.fabs( potential.ttensor(pot, 1.0, 0.0, eigenval=True) .to(units.km**2 / units.s**2 / units.kpc**2) .value - potential.ttensor(potu, 1.0, 0.0, eigenval=True) * vo**2 / ro**2 ) < 10.0**-8.0 ), "Potential function ttensor does not return the correct value as Quantity" assert numpy.all( numpy.fabs( potential.zvc_range(pot, -1.9, 0.2).to(units.kpc).value - potential.zvc_range(potu, -1.9, 0.2) * ro ) < 10.0**-8.0 ), "Potential function zvc_range does not return the correct value as Quantity" assert numpy.all( numpy.fabs( potential.zvc(pot, 0.4, -1.9, 0.2).to(units.kpc).value - potential.zvc(potu, 0.4, -1.9, 0.2) * ro ) < 10.0**-8.0 ), "Potential function zvc_range does not return the correct value as Quantity" assert ( numpy.fabs( potential.rhalf(pot).to(units.kpc).value - potential.rhalf(potu) * ro ) < 10.0**-8.0 ), "Potential function rhalf does not return the correct value as Quantity" assert ( numpy.fabs( potential.tdyn(pot, 1.4).to(units.Gyr).value - potential.tdyn(potu, 1.4) * conversion.time_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "Potential function tdyn does not return the correct value as Quantity" return None def test_planarPotential_function_value(): from galpy import potential from galpy.potential import PlummerPotential from galpy.util import conversion ro, vo = 8.0, 220.0 pot = [PlummerPotential(normalize=True, ro=ro, vo=vo).toPlanar()] potu = [PlummerPotential(normalize=True).toPlanar()] assert ( numpy.fabs( potential.evaluateplanarPotentials(pot, 1.1) .to(units.km**2 / units.s**2) .value - potential.evaluateplanarPotentials(potu, 1.1) * vo**2.0 ) < 10.0**-8.0 ), "Potential function __call__ does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateplanarRforces(pot, 1.1).to(units.km / units.s**2).value * 10.0**13.0 - potential.evaluateplanarRforces(potu, 1.1) * conversion.force_in_10m13kms2(vo, ro) ) < 10.0**-4.0 ), "Potential function Rforce does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateplanarphitorques(pot, 1.1) .to(units.km**2 / units.s**2) .value - potential.evaluateplanarphitorques(potu, 1.1) * vo**2 ) < 10.0**-4.0 ), "Potential function phitorque does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateplanarR2derivs(pot, 1.1) .to(units.km**2 / units.s**2.0 / units.kpc**2) .value - potential.evaluateplanarR2derivs(potu, 1.1) * vo**2.0 / ro**2.0 ) < 10.0**-8.0 ), "Potential function R2deriv does not return the correct value as Quantity" assert ( numpy.fabs( potential.vcirc(pot, 1.1).to(units.km / units.s).value - potential.vcirc(potu, 1.1) * vo ) < 10.0**-8.0 ), "Potential function vcirc does not return the correct value as Quantity" assert ( numpy.fabs( potential.omegac(pot, 1.1).to(units.km / units.s / units.kpc).value - potential.omegac(potu, 1.1) * vo / ro ) < 10.0**-8.0 ), "Potential function omegac does not return the correct value as Quantity" assert ( numpy.fabs( potential.epifreq(pot, 1.1).to(units.km / units.s / units.kpc).value - potential.epifreq(potu, 1.1) * vo / ro ) < 10.0**-8.0 ), "Potential function epifreq does not return the correct value as Quantity" assert ( numpy.fabs( potential.vesc(pot, 1.1).to(units.km / units.s).value - potential.vesc(potu, 1.1) * vo ) < 10.0**-8.0 ), "Potential function vesc does not return the correct value as Quantity" return None def test_linearPotential_function_value(): from galpy import potential from galpy.potential import PlummerPotential from galpy.util import conversion ro, vo = 8.0, 220.0 pot = [PlummerPotential(normalize=True, ro=ro, vo=vo).toVertical(1.1)] potu = [PlummerPotential(normalize=True).toVertical(1.1)] assert ( numpy.fabs( potential.evaluatelinearPotentials(pot, 1.1) .to(units.km**2 / units.s**2) .value - potential.evaluatelinearPotentials(potu, 1.1) * vo**2.0 ) < 10.0**-8.0 ), "Potential function __call__ does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluatelinearForces(pot, 1.1).to(units.km / units.s**2).value * 10.0**13.0 - potential.evaluatelinearForces(potu, 1.1) * conversion.force_in_10m13kms2(vo, ro) ) < 10.0**-4.0 ), "Potential function force does not return the correct value as Quantity" return None def test_potential_method_inputAsQuantity(): from galpy.potential import PlummerPotential from galpy.util import conversion ro, vo = 8.0 * units.kpc, 220.0 pot = PlummerPotential(normalize=True, ro=ro, vo=vo) potu = PlummerPotential(normalize=True) assert ( numpy.fabs( pot( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method __call__ does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.Rforce( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.Rforce(1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential method Rforce does not return the correct value when input is Quantity" ) # Few more cases for Rforce assert ( numpy.fabs( pot.Rforce( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, ro=9.0, use_physical=False, ) - potu.Rforce(1.1 * 8.0 / 9.0, 0.1 * 8.0 / 9.0) ) < 10.0**-4.0 ), ( "Potential method Rforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.Rforce( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, vo=230.0, use_physical=False, ) - potu.Rforce(1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential method Rforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.rforce( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.rforce(1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential method rforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.zforce( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.zforce(1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential method zforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.phitorque( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.phitorque(1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential method phitorque does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.dens( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.dens(1.1, 0.1) ) < 10.0**-8.0 ), "Potential method dens does not return the correct value when input is Quantity" assert ( numpy.fabs( pot.surfdens( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.surfdens(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method surfdens does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.mass(1.1 * ro, 0.1 * ro, use_physical=False) - potu.mass(1.1, 0.1) ) < 10.0**-8.0 ), "Potential method mass does not return the correct value when input is Quantity" assert ( numpy.fabs( pot.R2deriv( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.R2deriv(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method R2deriv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.z2deriv( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.z2deriv(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method z2deriv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.Rzderiv( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.Rzderiv(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method Rzderiv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.Rphideriv( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.Rphideriv(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method Rphideriv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.phi2deriv( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.phi2deriv(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method phi2deriv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.phizderiv( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.phizderiv(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method phizderiv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.flattening(1.1 * ro, 0.1 * ro, use_physical=False) - potu.flattening(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method flattening does not return the correct value when input is Quantity" ) assert ( numpy.fabs(pot.vcirc(1.1 * ro, use_physical=False) - potu.vcirc(1.1)) < 10.0**-8.0 ), "Potential method vcirc does not return the correct value when input is Quantity" assert ( numpy.fabs(pot.dvcircdR(1.1 * ro, use_physical=False) - potu.dvcircdR(1.1)) < 10.0**-8.0 ), ( "Potential method dvcircdR does not return the correct value when input is Quantity" ) assert ( numpy.fabs(pot.omegac(1.1 * ro, use_physical=False) - potu.omegac(1.1)) < 10.0**-8.0 ), ( "Potential method omegac does not return the correct value when input is Quantity" ) assert ( numpy.fabs(pot.epifreq(1.1 * ro, use_physical=False) - potu.epifreq(1.1)) < 10.0**-8.0 ), ( "Potential method epifreq does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.verticalfreq(1.1 * ro, use_physical=False) - potu.verticalfreq(1.1) ) < 10.0**-8.0 ), ( "Potential method verticalfreq does not return the correct value when input is Quantity" ) assert ( numpy.fabs(pot.vesc(1.1 * ro, use_physical=False) - potu.vesc(1.1)) < 10.0**-8.0 ), "Potential method vesc does not return the correct value when input is Quantity" assert ( numpy.fabs( pot.lindbladR( 0.9 * conversion.freq_in_Gyr(vo, ro.value) / units.Gyr, m="corot", use_physical=False, ) - potu.lindbladR(0.9, m="corot") ) < 10.0**-8.0 ), ( "Potential method lindbladR does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.rl(1.1 * vo * ro * units.km / units.s, use_physical=False) - potu.rl(1.1) ) < 10.0**-8.0 ), "Potential method rl does not return the correct value when input is Quantity" assert ( numpy.fabs( pot.rE(-1.14 * vo**2 * units.km**2 / units.s**2, use_physical=False) - potu.rE(-1.14) ) < 10.0**-8.0 ), "Potential method rE does not return the correct value when input is Quantity" assert ( numpy.fabs( pot.LcE(-1.14 * vo**2 * units.km**2 / units.s**2, use_physical=False) - potu.LcE(-1.14) ) < 10.0**-8.0 ), "Potential method LcE does not return the correct value when input is Quantity" assert ( numpy.fabs(pot.vterm(45.0 * units.deg, use_physical=False) - potu.vterm(45.0)) < 10.0**-8.0 ), "Potential method vterm does not return the correct value when input is Quantity" assert ( numpy.fabs( pot.rtide(1.1 * ro, 0.1 * ro, M=10.0**9.0 * units.Msun, use_physical=False) - potu.rtide(1.1, 0.1, M=10.0**9.0 / conversion.mass_in_msol(vo, ro.value)) ) < 10.0**-8.0 ), "Potential method rtide does not return the correct value when input is Quantity" assert numpy.all( numpy.fabs( pot.ttensor(1.1 * ro, 0.1 * ro, use_physical=False) - potu.ttensor(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method ttensor does not return the correct value when input is Quantity" ) assert numpy.all( numpy.fabs( pot.ttensor(1.1 * ro, 0.1 * ro, eigenval=True, use_physical=False) - potu.ttensor(1.1, 0.1, eigenval=True) ) < 10.0**-8.0 ), ( "Potential method ttensor does not return the correct value when input is Quantity" ) assert numpy.all( numpy.fabs( pot.zvc_range( -92000 * units.km**2 / units.s**2, 45.0 * units.kpc * units.km / units.s, use_physical=False, ) - potu.zvc_range(-92000 / vo**2, 45.0 / ro.to_value(units.kpc) / vo) ) < 10.0**-8.0 ), ( "Potential method zvc_range does not return the correct value when input is Quantity" ) assert numpy.all( numpy.fabs( pot.zvc( 0.4 * ro, -92000 * units.km**2 / units.s**2, 45.0 * units.kpc * units.km / units.s, use_physical=False, ) - potu.zvc(0.4, -92000 / vo**2, 45.0 / ro.to_value(units.kpc) / vo) ) < 10.0**-8.0 ), "Potential method zvc does not return the correct value when input is Quantity" assert ( numpy.fabs(pot.tdyn(1.1 * ro, use_physical=False) - potu.tdyn(1.1)) < 10.0**-8.0 ), "Potential method tdyn does not return the correct value when input is Quantity" return None def test_potential_method_inputAsQuantity_Rzaskwargs(): from galpy.potential import PlummerPotential from galpy.util import conversion ro, vo = 8.0 * units.kpc, 220.0 pot = PlummerPotential(normalize=True, ro=ro, vo=vo) potu = PlummerPotential(normalize=True) assert ( numpy.fabs( pot( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method __call__ does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.Rforce( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.Rforce(1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential method Rforce does not return the correct value when input is Quantity" ) # Few more cases for Rforce assert ( numpy.fabs( pot.Rforce( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, ro=9.0, use_physical=False, ) - potu.Rforce(1.1 * 8.0 / 9.0, 0.1 * 8.0 / 9.0) ) < 10.0**-4.0 ), ( "Potential method Rforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.Rforce( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, vo=230.0, use_physical=False, ) - potu.Rforce(1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential method Rforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.rforce( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.rforce(1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential method rforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.zforce( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.zforce(1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential method zforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.phitorque( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.phitorque(1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential method phitorque does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.dens( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.dens(1.1, 0.1) ) < 10.0**-8.0 ), "Potential method dens does not return the correct value when input is Quantity" assert ( numpy.fabs( pot.surfdens( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.surfdens(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method surfdens does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.mass(R=1.1 * ro, z=0.1 * ro, use_physical=False) - potu.mass(1.1, 0.1) ) < 10.0**-8.0 ), "Potential method mass does not return the correct value when input is Quantity" assert ( numpy.fabs( pot.R2deriv( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.R2deriv(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method R2deriv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.z2deriv( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.z2deriv(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method z2deriv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.Rzderiv( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.Rzderiv(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method Rzderiv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.Rphideriv( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.Rphideriv(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method Rphideriv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.phi2deriv( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.phi2deriv(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method phi2deriv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.phizderiv( R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potu.phizderiv(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method phizderiv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.flattening(R=1.1 * ro, z=0.1 * ro, use_physical=False) - potu.flattening(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method flattening does not return the correct value when input is Quantity" ) assert ( numpy.fabs(pot.vcirc(R=1.1 * ro, use_physical=False) - potu.vcirc(1.1)) < 10.0**-8.0 ), "Potential method vcirc does not return the correct value when input is Quantity" assert ( numpy.fabs(pot.dvcircdR(R=1.1 * ro, use_physical=False) - potu.dvcircdR(1.1)) < 10.0**-8.0 ), ( "Potential method dvcircdR does not return the correct value when input is Quantity" ) assert ( numpy.fabs(pot.omegac(R=1.1 * ro, use_physical=False) - potu.omegac(1.1)) < 10.0**-8.0 ), ( "Potential method omegac does not return the correct value when input is Quantity" ) assert ( numpy.fabs(pot.epifreq(R=1.1 * ro, use_physical=False) - potu.epifreq(1.1)) < 10.0**-8.0 ), ( "Potential method epifreq does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.verticalfreq(R=1.1 * ro, use_physical=False) - potu.verticalfreq(1.1) ) < 10.0**-8.0 ), ( "Potential method verticalfreq does not return the correct value when input is Quantity" ) assert ( numpy.fabs(pot.vesc(R=1.1 * ro, use_physical=False) - potu.vesc(1.1)) < 10.0**-8.0 ), "Potential method vesc does not return the correct value when input is Quantity" assert ( numpy.fabs( pot.rtide( R=1.1 * ro, z=0.1 * ro, M=10.0**9.0 * units.Msun, use_physical=False ) - potu.rtide(1.1, 0.1, M=10.0**9.0 / conversion.mass_in_msol(vo, ro.value)) ) < 10.0**-8.0 ), "Potential method rtide does not return the correct value when input is Quantity" assert numpy.all( numpy.fabs( pot.ttensor(R=1.1 * ro, z=0.1 * ro, use_physical=False) - potu.ttensor(1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential method ttensor does not return the correct value when input is Quantity" ) assert numpy.all( numpy.fabs( pot.ttensor(R=1.1 * ro, z=0.1 * ro, eigenval=True, use_physical=False) - potu.ttensor(1.1, 0.1, eigenval=True) ) < 10.0**-8.0 ), ( "Potential method ttensor does not return the correct value when input is Quantity" ) assert ( numpy.fabs(pot.tdyn(R=1.1 * ro, use_physical=False) - potu.tdyn(1.1)) < 10.0**-8.0 ), "Potential method tdyn does not return the correct value when input is Quantity" return None def test_planarPotential_method_inputAsQuantity(): from galpy.potential import PlummerPotential from galpy.util import conversion ro, vo = 8.0 * units.kpc, 220.0 pot = PlummerPotential(normalize=True, ro=ro, vo=vo) # Force planarPotential setup with default pot._ro = None pot._roSet = False pot._vo = None pot._voSet = False pot = pot.toPlanar() potu = PlummerPotential(normalize=True).toPlanar() assert numpy.fabs(pot(1.1 * ro, use_physical=False) - potu(1.1)) < 10.0**-8.0, ( "Potential method __call__ does not return the correct value as Quantity" ) assert ( numpy.fabs(pot.Rforce(1.1 * ro, use_physical=False) - potu.Rforce(1.1)) < 10.0**-4.0 ), "Potential method Rforce does not return the correct value as Quantity" assert ( numpy.fabs(pot.phitorque(1.1 * ro, use_physical=False) - potu.phitorque(1.1)) < 10.0**-4.0 ), "Potential method phitorque does not return the correct value as Quantity" assert ( numpy.fabs(pot.R2deriv(1.1 * ro, use_physical=False) - potu.R2deriv(1.1)) < 10.0**-8.0 ), "Potential method R2deriv does not return the correct value as Quantity" assert ( numpy.fabs(pot.Rphideriv(1.1 * ro, use_physical=False) - potu.Rphideriv(1.1)) < 10.0**-8.0 ), "Potential method Rphideriv does not return the correct value as Quantity" assert ( numpy.fabs(pot.phi2deriv(1.1 * ro, use_physical=False) - potu.phi2deriv(1.1)) < 10.0**-8.0 ), "Potential method phi2deriv does not return the correct value as Quantity" assert ( numpy.fabs(pot.vcirc(1.1 * ro, use_physical=False) - potu.vcirc(1.1)) < 10.0**-8.0 ), "Potential method vcirc does not return the correct value as Quantity" assert ( numpy.fabs(pot.omegac(1.1 * ro, use_physical=False) - potu.omegac(1.1)) < 10.0**-8.0 ), "Potential method omegac does not return the correct value as Quantity" assert ( numpy.fabs(pot.epifreq(1.1 * ro, use_physical=False) - potu.epifreq(1.1)) < 10.0**-8.0 ), "Potential method epifreq does not return the correct value as Quantity" assert ( numpy.fabs(pot.vesc(1.1 * ro, use_physical=False) - potu.vesc(1.1)) < 10.0**-8.0 ), "Potential method vesc does not return the correct value as Quantity" assert ( numpy.fabs( pot.lindbladR( 0.9 * conversion.freq_in_Gyr(vo, ro.value) / units.Gyr, m="corot", use_physical=False, ) - potu.lindbladR(0.9, m="corot") ) < 10.0**-8.0 ), ( "Potential method lindbladR does not return the correct value when input is Quantity" ) return None def test_planarPotential_method_inputAsQuantity_Raskwarg(): from galpy.potential import PlummerPotential from galpy.util import conversion ro, vo = 8.0 * units.kpc, 220.0 pot = PlummerPotential(normalize=True, ro=ro, vo=vo) # Force planarPotential setup with default pot._ro = None pot._roSet = False pot._vo = None pot._voSet = False pot = pot.toPlanar() potu = PlummerPotential(normalize=True).toPlanar() assert numpy.fabs(pot(R=1.1 * ro, use_physical=False) - potu(1.1)) < 10.0**-8.0, ( "Potential method __call__ does not return the correct value as Quantity" ) assert ( numpy.fabs(pot.Rforce(R=1.1 * ro, use_physical=False) - potu.Rforce(1.1)) < 10.0**-4.0 ), "Potential method Rforce does not return the correct value as Quantity" assert ( numpy.fabs(pot.phitorque(R=1.1 * ro, use_physical=False) - potu.phitorque(1.1)) < 10.0**-4.0 ), "Potential method phitorque does not return the correct value as Quantity" assert ( numpy.fabs(pot.R2deriv(R=1.1 * ro, use_physical=False) - potu.R2deriv(1.1)) < 10.0**-8.0 ), "Potential method R2deriv does not return the correct value as Quantity" assert ( numpy.fabs(pot.Rphideriv(R=1.1 * ro, use_physical=False) - potu.Rphideriv(1.1)) < 10.0**-8.0 ), "Potential method Rphideriv does not return the correct value as Quantity" assert ( numpy.fabs(pot.phi2deriv(R=1.1 * ro, use_physical=False) - potu.phi2deriv(1.1)) < 10.0**-8.0 ), "Potential method phi2deriv does not return the correct value as Quantity" assert ( numpy.fabs(pot.vcirc(R=1.1 * ro, use_physical=False) - potu.vcirc(1.1)) < 10.0**-8.0 ), "Potential method vcirc does not return the correct value as Quantity" assert ( numpy.fabs(pot.omegac(R=1.1 * ro, use_physical=False) - potu.omegac(1.1)) < 10.0**-8.0 ), "Potential method omegac does not return the correct value as Quantity" assert ( numpy.fabs(pot.epifreq(R=1.1 * ro, use_physical=False) - potu.epifreq(1.1)) < 10.0**-8.0 ), "Potential method epifreq does not return the correct value as Quantity" assert ( numpy.fabs(pot.vesc(R=1.1 * ro, use_physical=False) - potu.vesc(1.1)) < 10.0**-8.0 ), "Potential method vesc does not return the correct value as Quantity" return None def test_linearPotential_method_inputAsQuantity(): from galpy import potential from galpy.potential import PlummerPotential, SpiralArmsPotential from galpy.util import conversion ro, vo = 8.0 * units.kpc, 220.0 * units.km / units.s pot = PlummerPotential(normalize=True, ro=ro, vo=vo) # Force linearPotential setup with default pot._ro = None pot._roSet = False pot._vo = None pot._voSet = False pot = pot.toVertical(1.1) potu = potential.RZToverticalPotential(PlummerPotential(normalize=True), 1.1 * ro) assert numpy.fabs(pot(1.1 * ro, use_physical=False) - potu(1.1)) < 10.0**-8.0, ( "Potential method __call__ does not return the correct value as Quantity" ) assert ( numpy.fabs(pot.force(1.1 * ro, use_physical=False) - potu.force(1.1)) < 10.0**-4.0 ), "Potential method force does not return the correct value as Quantity" # also toVerticalPotential w/ non-axi pot = SpiralArmsPotential(ro=ro, vo=vo) # Force linearPotential setup with default pot._ro = None pot._roSet = False pot._vo = None pot._voSet = False pot = pot.toVertical( 1.1, 10.0 / 180.0 * numpy.pi, t0=1.0 / conversion.time_in_Gyr( vo.to(units.km / units.s).value, ro.to(units.kpc).value ), ) potu = potential.toVerticalPotential( SpiralArmsPotential(), 1.1 * ro, phi=10 * units.deg, t0=1.0 * units.Gyr ) assert numpy.fabs(pot(1.1 * ro, use_physical=False) - potu(1.1)) < 10.0**-8.0, ( "Potential method __call__ does not return the correct value as Quantity" ) assert ( numpy.fabs(pot.force(1.1 * ro, use_physical=False) - potu.force(1.1)) < 10.0**-4.0 ), "Potential method force does not return the correct value as Quantity" return None def test_linearPotential_method_inputAsQuantity_xaskwarg(): from galpy import potential from galpy.potential import PlummerPotential, SpiralArmsPotential from galpy.util import conversion ro, vo = 8.0 * units.kpc, 220.0 * units.km / units.s pot = PlummerPotential(normalize=True, ro=ro, vo=vo) # Force linearPotential setup with default pot._ro = None pot._roSet = False pot._vo = None pot._voSet = False pot = pot.toVertical(1.1) potu = potential.RZToverticalPotential(PlummerPotential(normalize=True), 1.1 * ro) assert numpy.fabs(pot(x=1.1 * ro, use_physical=False) - potu(1.1)) < 10.0**-8.0, ( "Potential method __call__ does not return the correct value as Quantity" ) assert ( numpy.fabs(pot.force(x=1.1 * ro, use_physical=False) - potu.force(1.1)) < 10.0**-4.0 ), "Potential method force does not return the correct value as Quantity" # also toVerticalPotential w/ non-axi pot = SpiralArmsPotential(ro=ro, vo=vo) # Force linearPotential setup with default pot._ro = None pot._roSet = False pot._vo = None pot._voSet = False pot = pot.toVertical( 1.1, 10.0 / 180.0 * numpy.pi, t0=1.0 / conversion.time_in_Gyr( vo.to(units.km / units.s).value, ro.to(units.kpc).value ), ) potu = potential.toVerticalPotential( SpiralArmsPotential(), 1.1 * ro, phi=10 * units.deg, t0=1.0 * units.Gyr ) assert numpy.fabs(pot(x=1.1 * ro, use_physical=False) - potu(1.1)) < 10.0**-8.0, ( "Potential method __call__ does not return the correct value as Quantity" ) assert ( numpy.fabs(pot.force(x=1.1 * ro, use_physical=False) - potu.force(1.1)) < 10.0**-4.0 ), "Potential method force does not return the correct value as Quantity" return None def test_dissipativeforce_method_inputAsQuantity(): from galpy.potential import ChandrasekharDynamicalFrictionForce from galpy.util import conversion ro, vo = 8.0 * units.kpc, 220.0 pot = ChandrasekharDynamicalFrictionForce(GMs=0.1, rhm=1.2 / 8.0, ro=ro, vo=vo) potu = ChandrasekharDynamicalFrictionForce(GMs=0.1, rhm=1.2 / 8.0) assert ( numpy.fabs( pot.Rforce( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, v=numpy.array([10.0, 200.0, -20.0]) * units.km / units.s, use_physical=False, ) - potu.Rforce( 1.1, 0.1, phi=10.0 / 180.0 * numpy.pi, v=numpy.array([10.0, 200.0, -20.0]) / vo, ) ) < 10.0**-4.0 ), ( "Potential method Rforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.zforce( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, v=numpy.array([10.0, 200.0, -20.0]) * units.km / units.s, use_physical=False, ) - potu.zforce( 1.1, 0.1, phi=10.0 / 180.0 * numpy.pi, v=numpy.array([10.0, 200.0, -20.0]) / vo, ) ) < 10.0**-4.0 ), ( "Potential method zforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( pot.phitorque( 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, v=numpy.array([10.0, 200.0, -20.0]) * units.km / units.s, use_physical=False, ) - potu.phitorque( 1.1, 0.1, phi=10.0 / 180.0 * numpy.pi, v=numpy.array([10.0, 200.0, -20.0]) / vo, ) ) < 10.0**-4.0 ), ( "Potential method phitorque does not return the correct value when input is Quantity" ) return None def test_potential_function_inputAsQuantity(): from galpy import potential from galpy.potential import PlummerPotential from galpy.util import conversion ro, vo = 8.0 * units.kpc, 220.0 pot = [PlummerPotential(normalize=True, ro=ro, vo=vo)] potu = [PlummerPotential(normalize=True)] assert ( numpy.fabs( potential.evaluatePotentials( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluatePotentials(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function __call__ does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluateRforces( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, ro=8.0 * units.kpc, vo=220.0 * units.km / units.s, use_physical=False, ) - potential.evaluateRforces(potu, 1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential function Rforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluaterforces( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, ro=8.0 * units.kpc, vo=220.0 * units.km / units.s, use_physical=False, ) - potential.evaluaterforces(potu, 1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential function rforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluatezforces( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluatezforces(potu, 1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential function zforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluatephitorques( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluatephitorques(potu, 1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential function phitorque does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluateDensities( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluateDensities(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function dens does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluateSurfaceDensities( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluateSurfaceDensities(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function surfdens does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluateR2derivs( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluateR2derivs(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function R2deriv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluatez2derivs( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluatez2derivs(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function z2deriv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluateRzderivs( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluateRzderivs(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function Rzderiv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.flattening(pot, 1.1 * ro, 0.1 * ro, use_physical=False) - potential.flattening(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function flattening does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.vcirc(pot, 1.1 * ro, use_physical=False) - potential.vcirc(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function vcirc does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.dvcircdR(pot, 1.1 * ro, use_physical=False) - potential.dvcircdR(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function dvcircdR does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.omegac(pot, 1.1 * ro, use_physical=False) - potential.omegac(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function omegac does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.epifreq(pot, 1.1 * ro, use_physical=False) - potential.epifreq(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function epifreq does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.verticalfreq(pot, 1.1 * ro, use_physical=False) - potential.verticalfreq(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function verticalfreq does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.vesc(pot, 1.1 * ro, use_physical=False) - potential.vesc(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function vesc does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.lindbladR( pot, 0.9 * conversion.freq_in_Gyr(vo, ro.value) / units.Gyr, m="corot", use_physical=False, ) - potential.lindbladR(potu, 0.9, m="corot") ) < 10.0**-8.0 ), ( "Potential method lindbladR does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.lindbladR( pot[0], 0.9 * conversion.freq_in_Gyr(vo, ro.value) / units.Gyr, m="corot", use_physical=False, ) - potential.lindbladR(potu, 0.9, m="corot") ) < 10.0**-8.0 ), ( "Potential method lindbladR does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.rl(pot, 1.1 * vo * ro * units.km / units.s, use_physical=False) - potential.rl(potu, 1.1) ) < 10.0**-8.0 ), "Potential function rl does not return the correct value when input is Quantity" assert ( numpy.fabs( potential.rl(pot[0], 1.1 * vo * ro * units.km / units.s, use_physical=False) - potential.rl(potu, 1.1) ) < 10.0**-8.0 ), "Potential function rl does not return the correct value when input is Quantity" assert ( numpy.fabs( potential.rE( pot, -1.14 * vo**2 * units.km**2 / units.s**2, use_physical=False ) - potential.rE(potu, -1.14) ) < 10.0**-8.0 ), "Potential function rE does not return the correct value when input is Quantity" assert ( numpy.fabs( potential.rE( pot[0], -1.14 * vo**2 * units.km**2 / units.s**2, use_physical=False, ) - potential.rE(potu, -1.14) ) < 10.0**-8.0 ), "Potential function rE does not return the correct value when input is Quantity" assert ( numpy.fabs( potential.LcE( pot, -1.14 * vo**2 * units.km**2 / units.s**2, use_physical=False ) - potential.LcE(potu, -1.14) ) < 10.0**-8.0 ), "Potential function LcE does not return the correct value when input is Quantity" assert ( numpy.fabs( potential.LcE( pot[0], -1.14 * vo**2 * units.km**2 / units.s**2, use_physical=False, ) - potential.LcE(potu, -1.14) ) < 10.0**-8.0 ), "Potential function LcE does not return the correct value when input is Quantity" assert ( numpy.fabs( potential.vterm(pot, 45.0 * units.deg, use_physical=False) - potential.vterm(potu, 45.0) ) < 10.0**-8.0 ), ( "Potential function vterm does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.rtide( pot, 1.1 * ro, 0.1 * ro, M=10.0**9.0 * units.Msun, use_physical=False ) - potential.rtide( potu, 1.1, 0.1, M=10.0**9.0 / conversion.mass_in_msol(vo, ro.value) ) ) < 10.0**-8.0 ), ( "Potential function rtide does not return the correct value when input is Quantity" ) # Test non-list for M as well, bc units done in rtide special, and do GM assert ( numpy.fabs( potential.rtide( pot[0], 1.1 * ro, 0.1 * ro, M=constants.G * 10.0**9.0 * units.Msun, use_physical=False, ) - potential.rtide( potu, 1.1, 0.1, M=10.0**9.0 / conversion.mass_in_msol(vo, ro.value) ) ) < 10.0**-8.0 ), ( "Potential function rtide does not return the correct value when input is Quantity" ) assert numpy.all( numpy.fabs( potential.ttensor(pot, 1.1 * ro, 0.1 * ro, use_physical=False) - potential.ttensor(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function ttensor does not return the correct value when input is Quantity" ) assert numpy.all( numpy.fabs( potential.ttensor( pot, 1.1 * ro, 0.1 * ro, eigenval=True, use_physical=False ) - potential.ttensor(potu, 1.1, 0.1, eigenval=True) ) < 10.0**-8.0 ), ( "Potential function ttensor does not return the correct value when input is Quantity" ) assert numpy.all( numpy.fabs( potential.zvc_range( pot, -92000 * units.km**2 / units.s**2, 45.0 * units.kpc * units.km / units.s, use_physical=False, ) - potential.zvc_range( potu, -92000 / vo**2, 45.0 / ro.to_value(units.kpc) / vo ) ) < 10.0**-8.0 ), ( "Potential function zvc_range does not return the correct value when input is Quantity" ) assert numpy.all( numpy.fabs( potential.zvc( pot, 0.4 * ro, -92000 * units.km**2 / units.s**2, 45.0 * units.kpc * units.km / units.s, use_physical=False, ) - potential.zvc( potu, 0.4, -92000 / vo**2, 45.0 / ro.to_value(units.kpc) / vo ) ) < 10.0**-8.0 ), "Potential function zvc does not return the correct value when input is Quantity" assert ( numpy.fabs( potential.tdyn(pot, 1.1 * ro, use_physical=False) - potential.tdyn(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function tdyn does not return the correct value when input is Quantity" ) return None def test_potential_function_inputAsQuantity_Rzaskwargs(): from galpy import potential from galpy.potential import PlummerPotential from galpy.util import conversion ro, vo = 8.0 * units.kpc, 220.0 pot = [PlummerPotential(normalize=True, ro=ro, vo=vo)] potu = [PlummerPotential(normalize=True)] assert ( numpy.fabs( potential.evaluatePotentials( pot, R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluatePotentials(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function __call__ does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluateRforces( pot, R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, ro=8.0 * units.kpc, vo=220.0 * units.km / units.s, use_physical=False, ) - potential.evaluateRforces(potu, 1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential function Rforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluaterforces( pot, R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, ro=8.0 * units.kpc, vo=220.0 * units.km / units.s, use_physical=False, ) - potential.evaluaterforces(potu, 1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential function rforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluatezforces( pot, R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluatezforces(potu, 1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential function zforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluatephitorques( pot, R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluatephitorques(potu, 1.1, 0.1) ) < 10.0**-4.0 ), ( "Potential function phitorque does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluateDensities( pot, R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluateDensities(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function dens does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluateSurfaceDensities( pot, R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluateSurfaceDensities(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function surfdens does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluateR2derivs( pot, R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluateR2derivs(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function R2deriv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluatez2derivs( pot, R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluatez2derivs(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function z2deriv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluateRzderivs( pot, R=1.1 * ro, z=0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, use_physical=False, ) - potential.evaluateRzderivs(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function Rzderiv does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.flattening(pot, R=1.1 * ro, z=0.1 * ro, use_physical=False) - potential.flattening(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function flattening does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.vcirc(pot, R=1.1 * ro, use_physical=False) - potential.vcirc(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function vcirc does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.dvcircdR(pot, R=1.1 * ro, use_physical=False) - potential.dvcircdR(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function dvcircdR does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.omegac(pot, R=1.1 * ro, use_physical=False) - potential.omegac(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function omegac does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.epifreq(pot, R=1.1 * ro, use_physical=False) - potential.epifreq(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function epifreq does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.verticalfreq(pot, R=1.1 * ro, use_physical=False) - potential.verticalfreq(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function verticalfreq does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.vesc(pot, R=1.1 * ro, use_physical=False) - potential.vesc(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function vesc does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.rtide( pot, R=1.1 * ro, z=0.1 * ro, M=10.0**9.0 * units.Msun, use_physical=False, ) - potential.rtide( potu, 1.1, 0.1, M=10.0**9.0 / conversion.mass_in_msol(vo, ro.value) ) ) < 10.0**-8.0 ), ( "Potential function rtide does not return the correct value when input is Quantity" ) # Test non-list for M as well, bc units done in rtide special, and do GM assert ( numpy.fabs( potential.rtide( pot[0], R=1.1 * ro, z=0.1 * ro, M=constants.G * 10.0**9.0 * units.Msun, use_physical=False, ) - potential.rtide( potu, 1.1, 0.1, M=10.0**9.0 / conversion.mass_in_msol(vo, ro.value) ) ) < 10.0**-8.0 ), ( "Potential function rtide does not return the correct value when input is Quantity" ) assert numpy.all( numpy.fabs( potential.ttensor(pot, R=1.1 * ro, z=0.1 * ro, use_physical=False) - potential.ttensor(potu, 1.1, 0.1) ) < 10.0**-8.0 ), ( "Potential function ttensor does not return the correct value when input is Quantity" ) assert numpy.all( numpy.fabs( potential.ttensor( pot, R=1.1 * ro, z=0.1 * ro, eigenval=True, use_physical=False ) - potential.ttensor(potu, 1.1, 0.1, eigenval=True) ) < 10.0**-8.0 ), ( "Potential function ttensor does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.tdyn(pot, R=1.1 * ro, use_physical=False) - potential.tdyn(potu, 1.1) ) < 10.0**-8.0 ), ( "Potential function tdyn does not return the correct value when input is Quantity" ) return None def test_dissipativeforce_function_inputAsQuantity(): from galpy import potential from galpy.potential import ChandrasekharDynamicalFrictionForce from galpy.util import conversion ro, vo = 8.0 * units.kpc, 220.0 pot = ChandrasekharDynamicalFrictionForce(GMs=0.1, rhm=1.2 / 8.0, ro=ro, vo=vo) potu = ChandrasekharDynamicalFrictionForce(GMs=0.1, rhm=1.2 / 8.0) assert ( numpy.fabs( potential.evaluatezforces( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, ro=8.0 * units.kpc, vo=220.0 * units.km / units.s, v=numpy.array([10.0, 200.0, -20.0]) * units.km / units.s, use_physical=False, ) - potential.evaluatezforces( potu, 1.1, 0.1, phi=10.0 / 180.0 * numpy.pi, v=numpy.array([10.0, 200.0, -20.0]) / vo, ) ) < 10.0**-4.0 ), ( "Potential function zforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluateRforces( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, ro=8.0 * units.kpc, vo=220.0 * units.km / units.s, v=numpy.array([10.0, 200.0, -20.0]) * units.km / units.s, use_physical=False, ) - potential.evaluateRforces( potu, 1.1, 0.1, phi=10.0 / 180.0 * numpy.pi, v=numpy.array([10.0, 200.0, -20.0]) / vo, ) ) < 10.0**-4.0 ), ( "Potential function Rforce does not return the correct value when input is Quantity" ) assert ( numpy.fabs( potential.evaluatephitorques( pot, 1.1 * ro, 0.1 * ro, phi=10.0 * units.deg, t=10.0 * units.Gyr, ro=8.0 * units.kpc, vo=220.0 * units.km / units.s, v=numpy.array([10.0, 200.0, -20.0]) * units.km / units.s, use_physical=False, ) - potential.evaluatephitorques( potu, 1.1, 0.1, phi=10.0 / 180.0 * numpy.pi, v=numpy.array([10.0, 200.0, -20.0]) / vo, ) ) < 10.0**-4.0 ), ( "Potential function phitorque does not return the correct value when input is Quantity" ) return None def test_planarPotential_function_inputAsQuantity(): from galpy import potential from galpy.potential import PlummerPotential ro, vo = 8.0 * units.kpc, 220.0 pot = [PlummerPotential(normalize=True, ro=ro, vo=vo).toPlanar()] potu = [PlummerPotential(normalize=True).toPlanar()] assert ( numpy.fabs( potential.evaluateplanarPotentials(pot, 1.1 * ro, use_physical=False) - potential.evaluateplanarPotentials(potu, 1.1) ) < 10.0**-8.0 ), "Potential function __call__ does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateplanarRforces(pot, 1.1 * ro, use_physical=False) - potential.evaluateplanarRforces(potu, 1.1) ) < 10.0**-4.0 ), "Potential function Rforce does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateplanarphitorques(pot, 1.1 * ro, use_physical=False) - potential.evaluateplanarphitorques(potu, 1.1) ) < 10.0**-4.0 ), "Potential function phitorque does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateplanarR2derivs(pot, 1.1 * ro, use_physical=False) - potential.evaluateplanarR2derivs(potu, 1.1) ) < 10.0**-8.0 ), "Potential function R2deriv does not return the correct value as Quantity" assert ( numpy.fabs( potential.vcirc(pot, 1.1 * ro, use_physical=False) - potential.vcirc(potu, 1.1) ) < 10.0**-8.0 ), "Potential function vcirc does not return the correct value as Quantity" assert ( numpy.fabs( potential.omegac(pot, 1.1 * ro, use_physical=False) - potential.omegac(potu, 1.1) ) < 10.0**-8.0 ), "Potential function omegac does not return the correct value as Quantity" assert ( numpy.fabs( potential.epifreq(pot, 1.1 * ro, use_physical=False) - potential.epifreq(potu, 1.1) ) < 10.0**-8.0 ), "Potential function epifreq does not return the correct value as Quantity" assert ( numpy.fabs( potential.vesc(pot, 1.1 * ro, use_physical=False) - potential.vesc(potu, 1.1) ) < 10.0**-8.0 ), "Potential function vesc does not return the correct value as Quantity" return None def test_planarPotential_function_inputAsQuantity_Raskwarg(): from galpy import potential from galpy.potential import PlummerPotential ro, vo = 8.0 * units.kpc, 220.0 pot = [PlummerPotential(normalize=True, ro=ro, vo=vo).toPlanar()] potu = [PlummerPotential(normalize=True).toPlanar()] assert ( numpy.fabs( potential.evaluateplanarPotentials(pot, R=1.1 * ro, use_physical=False) - potential.evaluateplanarPotentials(potu, 1.1) ) < 10.0**-8.0 ), "Potential function __call__ does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateplanarRforces(pot, R=1.1 * ro, use_physical=False) - potential.evaluateplanarRforces(potu, 1.1) ) < 10.0**-4.0 ), "Potential function Rforce does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateplanarphitorques(pot, R=1.1 * ro, use_physical=False) - potential.evaluateplanarphitorques(potu, 1.1) ) < 10.0**-4.0 ), "Potential function phitorque does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluateplanarR2derivs(pot, R=1.1 * ro, use_physical=False) - potential.evaluateplanarR2derivs(potu, 1.1) ) < 10.0**-8.0 ), "Potential function R2deriv does not return the correct value as Quantity" assert ( numpy.fabs( potential.vcirc(pot, R=1.1 * ro, use_physical=False) - potential.vcirc(potu, 1.1) ) < 10.0**-8.0 ), "Potential function vcirc does not return the correct value as Quantity" assert ( numpy.fabs( potential.omegac(pot, R=1.1 * ro, use_physical=False) - potential.omegac(potu, 1.1) ) < 10.0**-8.0 ), "Potential function omegac does not return the correct value as Quantity" assert ( numpy.fabs( potential.epifreq(pot, R=1.1 * ro, use_physical=False) - potential.epifreq(potu, 1.1) ) < 10.0**-8.0 ), "Potential function epifreq does not return the correct value as Quantity" assert ( numpy.fabs( potential.vesc(pot, R=1.1 * ro, use_physical=False) - potential.vesc(potu, 1.1) ) < 10.0**-8.0 ), "Potential function vesc does not return the correct value as Quantity" return None def test_linearPotential_function_inputAsQuantity(): from galpy import potential from galpy.potential import PlummerPotential, SpiralArmsPotential ro, vo = 8.0 * units.kpc, 220.0 pot = [PlummerPotential(normalize=True, ro=ro, vo=vo).toVertical(1.1 * ro)] potu = potential.RZToverticalPotential([PlummerPotential(normalize=True)], 1.1 * ro) assert ( numpy.fabs( potential.evaluatelinearPotentials(pot, 1.1 * ro, use_physical=False) - potential.evaluatelinearPotentials(potu, 1.1) ) < 10.0**-8.0 ), "Potential function __call__ does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluatelinearForces(pot, 1.1 * ro, use_physical=False) - potential.evaluatelinearForces(potu, 1.1) ) < 10.0**-4.0 ), "Potential function force does not return the correct value as Quantity" # Also toVerticalPotential, with non-axi pot = [ SpiralArmsPotential(ro=ro, vo=vo).toVertical( (1.1 * ro).to(units.kpc).value / 8.0, phi=20.0 * units.deg, t0=1.0 * units.Gyr, ) ] potu = potential.toVerticalPotential( [SpiralArmsPotential()], 1.1 * ro, phi=20.0 * units.deg, t0=1.0 * units.Gyr ) assert ( numpy.fabs( potential.evaluatelinearPotentials(pot, 1.1 * ro, use_physical=False) - potential.evaluatelinearPotentials(potu, 1.1) ) < 10.0**-8.0 ), "Potential function __call__ does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluatelinearForces(pot, 1.1 * ro, use_physical=False) - potential.evaluatelinearForces(potu, 1.1) ) < 10.0**-4.0 ), "Potential function force does not return the correct value as Quantity" return None def test_linearPotential_function_inputAsQuantity_xaskwarg(): from galpy import potential from galpy.potential import PlummerPotential, SpiralArmsPotential ro, vo = 8.0 * units.kpc, 220.0 pot = [PlummerPotential(normalize=True, ro=ro, vo=vo).toVertical(1.1 * ro)] potu = potential.RZToverticalPotential([PlummerPotential(normalize=True)], 1.1 * ro) assert ( numpy.fabs( potential.evaluatelinearPotentials(pot, x=1.1 * ro, use_physical=False) - potential.evaluatelinearPotentials(potu, 1.1) ) < 10.0**-8.0 ), "Potential function __call__ does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluatelinearForces(pot, x=1.1 * ro, use_physical=False) - potential.evaluatelinearForces(potu, 1.1) ) < 10.0**-4.0 ), "Potential function force does not return the correct value as Quantity" # Also toVerticalPotential, with non-axi pot = [ SpiralArmsPotential(ro=ro, vo=vo).toVertical( (1.1 * ro).to(units.kpc).value / 8.0, phi=20.0 * units.deg, t0=1.0 * units.Gyr, ) ] potu = potential.toVerticalPotential( [SpiralArmsPotential()], 1.1 * ro, phi=20.0 * units.deg, t0=1.0 * units.Gyr ) assert ( numpy.fabs( potential.evaluatelinearPotentials(pot, x=1.1 * ro, use_physical=False) - potential.evaluatelinearPotentials(potu, 1.1) ) < 10.0**-8.0 ), "Potential function __call__ does not return the correct value as Quantity" assert ( numpy.fabs( potential.evaluatelinearForces(pot, x=1.1 * ro, use_physical=False) - potential.evaluatelinearForces(potu, 1.1) ) < 10.0**-4.0 ), "Potential function force does not return the correct value as Quantity" return None def test_plotting_inputAsQuantity(): from galpy import potential from galpy.potential import PlummerPotential ro, vo = 8.0 * units.kpc, 220.0 pot = PlummerPotential(normalize=True, ro=ro, vo=vo) pot.plot( rmin=1.0 * units.kpc, rmax=4.0 * units.kpc, zmin=-4.0 * units.kpc, zmax=4.0 * units.kpc, ) pot.plotDensity( rmin=1.0 * units.kpc, rmax=4.0 * units.kpc, zmin=-4.0 * units.kpc, zmax=4.0 * units.kpc, ) pot.plotSurfaceDensity( xmin=1.0 * units.kpc, xmax=4.0 * units.kpc, ymin=-4.0 * units.kpc, ymax=4.0 * units.kpc, ) potential.plotPotentials( pot, rmin=1.0 * units.kpc, rmax=4.0 * units.kpc, zmin=-4.0 * units.kpc, zmax=4.0 * units.kpc, ) potential.plotPotentials( [pot], rmin=1.0 * units.kpc, rmax=4.0 * units.kpc, zmin=-4.0 * units.kpc, zmax=4.0 * units.kpc, ) potential.plotDensities( pot, rmin=1.0 * units.kpc, rmax=4.0 * units.kpc, zmin=-4.0 * units.kpc, zmax=4.0 * units.kpc, ) potential.plotDensities( [pot], rmin=1.0 * units.kpc, rmax=4.0 * units.kpc, zmin=-4.0 * units.kpc, zmax=4.0 * units.kpc, ) potential.plotSurfaceDensities( pot, xmin=1.0 * units.kpc, xmax=4.0 * units.kpc, ymin=-4.0 * units.kpc, ymax=4.0 * units.kpc, ) potential.plotSurfaceDensities( [pot], xmin=1.0 * units.kpc, xmax=4.0 * units.kpc, ymin=-4.0 * units.kpc, ymax=4.0 * units.kpc, ) # Planar plpot = pot.toPlanar() plpot.plot( Rrange=[1.0 * units.kpc, 8.0 * units.kpc], xrange=[-4.0 * units.kpc, 4.0 * units.kpc], yrange=[-6.0 * units.kpc, 7.0 * units.kpc], ) potential.plotplanarPotentials( plpot, Rrange=[1.0 * units.kpc, 8.0 * units.kpc], xrange=[-4.0 * units.kpc, 4.0 * units.kpc], yrange=[-6.0 * units.kpc, 7.0 * units.kpc], ) potential.plotplanarPotentials( [plpot], Rrange=[1.0 * units.kpc, 8.0 * units.kpc], xrange=[-4.0 * units.kpc, 4.0 * units.kpc], yrange=[-6.0 * units.kpc, 7.0 * units.kpc], ) # Rotcurve pot.plotRotcurve(Rrange=[1.0 * units.kpc, 8.0 * units.kpc], ro=10.0, vo=250.0) plpot.plotRotcurve( Rrange=[1.0 * units.kpc, 8.0 * units.kpc], ro=10.0 * units.kpc, vo=250.0 * units.km / units.s, ) potential.plotRotcurve(pot, Rrange=[1.0 * units.kpc, 8.0 * units.kpc]) potential.plotRotcurve([pot], Rrange=[1.0 * units.kpc, 8.0 * units.kpc]) # Escapecurve pot.plotEscapecurve(Rrange=[1.0 * units.kpc, 8.0 * units.kpc], ro=10.0, vo=250.0) plpot.plotEscapecurve( Rrange=[1.0 * units.kpc, 8.0 * units.kpc], ro=10.0 * units.kpc, vo=250.0 * units.km / units.s, ) potential.plotEscapecurve(pot, Rrange=[1.0 * units.kpc, 8.0 * units.kpc]) potential.plotEscapecurve([pot], Rrange=[1.0 * units.kpc, 8.0 * units.kpc]) return None def test_potential_ampunits(): # Test that input units for potential amplitudes behave as expected from galpy import potential from galpy.util import conversion ro, vo = 9.0, 210.0 # Burkert pot = potential.BurkertPotential( amp=0.1 * units.Msun / units.pc**3.0, a=2.0, ro=ro, vo=vo ) # density at r=a should be amp/4 assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 / 4.0 ) < 10.0**-8.0 ), "BurkertPotential w/ amp w/ units does not behave as expected" # DoubleExponentialDiskPotential pot = potential.DoubleExponentialDiskPotential( amp=0.1 * units.Msun / units.pc**3.0, hr=2.0, hz=0.2, ro=ro, vo=vo ) # density at zero should be amp assert ( numpy.fabs( pot.dens(0.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 ) < 10.0**-8.0 ), "DoubleExponentialDiskPotential w/ amp w/ units does not behave as expected" # TwoPowerSphericalPotential pot = potential.TwoPowerSphericalPotential( amp=20.0 * units.Msun, a=2.0, alpha=1.5, beta=3.5, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "TwoPowerSphericalPotential w/ amp w/ units does not behave as expected" # TwoPowerSphericalPotential with integer powers pot = potential.TwoPowerSphericalPotential( amp=20.0 * units.Msun, a=2.0, alpha=2.0, beta=5.0, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "TwoPowerSphericalPotential w/ amp w/ units does not behave as expected" # JaffePotential pot = potential.JaffePotential(amp=20.0 * units.Msun, a=2.0, ro=ro, vo=vo) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "JaffePotential w/ amp w/ units does not behave as expected" # HernquistPotential pot = potential.HernquistPotential(amp=20.0 * units.Msun, a=2.0, ro=ro, vo=vo) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "HernquistPotential w/ amp w/ units does not behave as expected" # NFWPotential pot = potential.NFWPotential(amp=20.0 * units.Msun, a=2.0, ro=ro, vo=vo) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "NFWPotential w/ amp w/ units does not behave as expected" # TwoPowerTriaxialPotential pot = potential.TwoPowerTriaxialPotential( amp=20.0 * units.Msun, a=2.0, b=0.3, c=1.4, alpha=1.5, beta=3.5, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "TwoPowerTriaxialPotential w/ amp w/ units does not behave as expected" # TwoPowerTriaxialPotential with integer powers pot = potential.TwoPowerTriaxialPotential( amp=20.0 * units.Msun, a=2.0, b=0.3, c=1.4, alpha=2.0, beta=5.0, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "TwoPowerTriaxialPotential w/ amp w/ units does not behave as expected" # TriaxialJaffePotential pot = potential.TriaxialJaffePotential( amp=20.0 * units.Msun, a=2.0, ro=ro, vo=vo, b=0.3, c=1.4 ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "TriaxialJaffePotential w/ amp w/ units does not behave as expected" # TriaxialHernquistPotential pot = potential.TriaxialHernquistPotential( amp=20.0 * units.Msun, a=2.0, b=0.4, c=1.4, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "TriaxialHernquistPotential w/ amp w/ units does not behave as expected" # TriaxialNFWPotential pot = potential.TriaxialNFWPotential( amp=20.0 * units.Msun, a=2.0, ro=ro, vo=vo, b=1.3, c=0.4 ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "TriaxialNFWPotential w/ amp w/ units does not behave as expected" # SCFPotential, default = spherical Hernquist pot = potential.SCFPotential(amp=20.0 * units.Msun, a=2.0, ro=ro, vo=vo) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "SCFPotential w/ amp w/ units does not behave as expected" # FlattenedPowerPotential pot = potential.FlattenedPowerPotential( amp=40000.0 * units.km**2 / units.s**2, r1=1.0, q=0.9, alpha=0.5, core=0.0, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(2.0, 1.0, use_physical=False) * vo**2.0 + 40000.0 / 0.5 / (2.0**2.0 + (1.0 / 0.9) ** 2.0) ** 0.25 ) < 10.0**-8.0 ), "FlattenedPowerPotential w/ amp w/ units does not behave as expected" # IsochronePotential pot = potential.IsochronePotential(amp=20.0 * units.Msun, b=2.0, ro=ro, vo=vo) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) * vo**2.0 + (20.0 * units.Msun * constants.G) .to(units.pc * units.km**2 / units.s**2) .value / (2.0 + numpy.sqrt(4.0 + 16.0)) / ro / 1000.0 ) < 10.0**-8.0 ), "IsochronePotential w/ amp w/ units does not behave as expected" # KeplerPotential pot = potential.KeplerPotential(amp=20.0 * units.Msun, ro=ro, vo=vo) # Check mass assert ( numpy.fabs( pot.mass(100.0, use_physical=False) * conversion.mass_in_msol(vo, ro) - 20.0 ) < 10.0**-8.0 ), "KeplerPotential w/ amp w/ units does not behave as expected" # KuzminKutuzovStaeckelPotential pot = potential.KuzminKutuzovStaeckelPotential( amp=20.0 * units.Msun, Delta=2.0, ro=ro, vo=vo ) pot_nounits = potential.KuzminKutuzovStaeckelPotential( amp=(20.0 * units.Msun * constants.G) .to(units.kpc * units.km**2 / units.s**2) .value / ro / vo**2, Delta=2.0, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) - pot_nounits(4.0, 0.0, use_physical=False) ) < 10.0**-8.0 ), "KuzminKutuzovStaeckelPotential w/ amp w/ units does not behave as expected" # LogarithmicHaloPotential pot = potential.LogarithmicHaloPotential( amp=40000 * units.km**2 / units.s**2, core=0.0, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) * vo**2.0 - 20000 * numpy.log(16.0) ) < 10.0**-8.0 ), "LogarithmicHaloPotential w/ amp w/ units does not behave as expected" # MiyamotoNagaiPotential pot = potential.MiyamotoNagaiPotential( amp=20 * units.Msun, a=2.0, b=0.5, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 1.0, use_physical=False) * vo**2.0 + (20.0 * units.Msun * constants.G) .to(units.pc * units.km**2 / units.s**2) .value / numpy.sqrt(16.0 + (2.0 + numpy.sqrt(1.0 + 0.25)) ** 2.0) / ro / 1000.0 ) < 10.0**-8.0 ), "MiyamotoNagaiPotential( w/ amp w/ units does not behave as expected" # KuzminDiskPotential pot = potential.KuzminDiskPotential(amp=20 * units.Msun, a=2.0, ro=ro, vo=vo) # Check potential assert ( numpy.fabs( pot(4.0, 1.0, use_physical=False) * vo**2.0 + (20.0 * units.Msun * constants.G) .to(units.pc * units.km**2 / units.s**2) .value / numpy.sqrt(16.0 + (2.0 + 1.0) ** 2.0) / ro / 1000.0 ) < 10.0**-8.0 ), "KuzminDiskPotential( w/ amp w/ units does not behave as expected" # MN3ExponentialDiskPotential pot = potential.MN3ExponentialDiskPotential( amp=0.1 * units.Msun / units.pc**3.0, hr=2.0, hz=0.2, ro=ro, vo=vo ) # density at hr should be assert ( numpy.fabs( pot.dens(2.0, 0.2, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 * numpy.exp(-2.0) ) < 10.0**-3.0 ), "MN3ExponentialDiskPotential w/ amp w/ units does not behave as expected" # PlummerPotential pot = potential.PlummerPotential(amp=20 * units.Msun, b=0.5, ro=ro, vo=vo) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) * vo**2.0 + (20.0 * units.Msun * constants.G) .to(units.pc * units.km**2 / units.s**2) .value / numpy.sqrt(16.0 + 0.25) / ro / 1000.0 ) < 10.0**-8.0 ), "PlummerPotential w/ amp w/ units does not behave as expected" # PowerSphericalPotential pot = potential.PowerSphericalPotential( amp=10.0**10.0 * units.Msun, r1=1.0, alpha=2.0, ro=ro, vo=vo ) # density at r1 assert ( numpy.fabs( pot.dens(1.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 10.0 / ro**3.0 ) < 10.0**-8.0 ), "PowerSphericalPotential w/ amp w/ units does not behave as expected" # PowerSphericalPotentialwCutoff pot = potential.PowerSphericalPotentialwCutoff( amp=0.1 * units.Msun / units.pc**3, r1=1.0, alpha=2.0, rc=2.0, ro=ro, vo=vo ) # density at r1 assert ( numpy.fabs( pot.dens(1.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 * numpy.exp(-0.25) ) < 10.0**-8.0 ), "PowerSphericalPotentialwCutoff w/ amp w/ units does not behave as expected" # PseudoIsothermalPotential pot = potential.PseudoIsothermalPotential( amp=10.0**10.0 * units.Msun, a=2.0, ro=ro, vo=vo ) # density at a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 10.0 / 4.0 / numpy.pi / 8.0 / 2.0 / ro**3.0 ) < 10.0**-8.0 ), "PseudoIsothermalPotential w/ amp w/ units does not behave as expected" # RazorThinExponentialDiskPotential pot = potential.RazorThinExponentialDiskPotential( amp=40.0 * units.Msun / units.pc**2, hr=2.0, ro=ro, vo=vo ) pot_nounits = potential.RazorThinExponentialDiskPotential( amp=(40.0 * units.Msun / units.pc**2 * constants.G) .to(1 / units.kpc * units.km**2 / units.s**2) .value * ro / vo**2, hr=2.0, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) - pot_nounits(4.0, 0.0, use_physical=False) ) < 10.0**-8.0 ), "RazorThinExponentialDiskPotential w/ amp w/ units does not behave as expected" # SoftenedNeedleBarPotential pot = potential.SoftenedNeedleBarPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=1.0, b=2.0, c=3.0, pa=0.0, omegab=0.0, ro=ro, vo=vo, ) pot_nounits = potential.SoftenedNeedleBarPotential( amp=4.0 / conversion.mass_in_1010msol(vo, ro), a=1.0, b=2.0, c=3.0, pa=0.0, omegab=0.0, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "SoftenedNeedleBarPotential w/ amp w/ units does not behave as expected" # FerrersPotential pot = potential.FerrersPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=1.0, b=2.0, c=3.0, pa=0.0, omegab=0.0, ro=ro, vo=vo, ) pot_nounits = potential.FerrersPotential( amp=4.0 / conversion.mass_in_1010msol(vo, ro), a=1.0, b=2.0, c=3.0, pa=0.0, omegab=0.0, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "FerrersPotential w/ amp w/ units does not behave as expected" # # SpiralArmsPotential # pot= potential.SpiralArmsPotential(amp=0.3*units.Msun / units.pc**3) # assert numpy.fabs(pot(1.,0.,phi=1.,use_physical=False)*) < 10.**-8., "SpiralArmsPotential w/ amp w/ units does not behave as expected" # SphericalShellPotential pot = potential.SphericalShellPotential( amp=4.0 * 10.0**10.0 * units.Msun, ro=ro, vo=vo ) pot_nounits = potential.SphericalShellPotential( amp=4.0 / conversion.mass_in_1010msol(vo, ro), ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "SphericalShellPotential w/ amp w/ units does not behave as expected" # RingPotential pot = potential.RingPotential(amp=4.0 * 10.0**10.0 * units.Msun, ro=ro, vo=vo) pot_nounits = potential.RingPotential( amp=4.0 / conversion.mass_in_1010msol(vo, ro), ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "RingPotential w/ amp w/ units does not behave as expected" # PerfectEllipsoidPotential pot = potential.PerfectEllipsoidPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=2.0, ro=ro, vo=vo, b=1.3, c=0.4 ) pot_nounits = potential.PerfectEllipsoidPotential( amp=4.0 / conversion.mass_in_1010msol(vo, ro), a=2.0, ro=ro, vo=vo, b=1.3, c=0.4 ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "PerfectEllipsoidPotential w/ amp w/ units does not behave as expected" # HomogeneousSpherePotential pot = potential.HomogeneousSpherePotential( amp=0.1 * units.Msun / units.pc**3.0, R=2.0, ro=ro, vo=vo ) pot_nounits = potential.HomogeneousSpherePotential( amp=0.1 / conversion.dens_in_msolpc3(vo, ro), R=2.0, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(1.1, 0.2, phi=1.0, use_physical=False) - pot_nounits(1.1, 0.2, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "HomogeneousSpherePotential w/ amp w/ units does not behave as expected" # TriaxialGaussianPotential pot = potential.TriaxialGaussianPotential( amp=4.0 * 10.0**10.0 * units.Msun, sigma=2.0, ro=ro, vo=vo, b=1.3, c=0.4 ) pot_nounits = potential.TriaxialGaussianPotential( amp=4.0 / conversion.mass_in_1010msol(vo, ro), sigma=2.0, ro=ro, vo=vo, b=1.3, c=0.4, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "TriaxialGaussianPotential w/ amp w/ units does not behave as expected" # NullPotential pot = potential.NullPotential(amp=(200.0 * units.km / units.s) ** 2, ro=ro, vo=vo) pot_nounits = potential.NullPotential(amp=(200 / vo) ** 2.0, ro=ro, vo=vo) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "NullPotential w/ amp w/ units does not behave as expected" return None def test_potential_ampunits_altunits(): # Test that input units for potential amplitudes behave as expected, alternative where G*M is given from galpy import potential from galpy.util import conversion ro, vo = 9.0, 210.0 # Burkert pot = potential.BurkertPotential( amp=0.1 * units.Msun / units.pc**3.0 * constants.G, a=2.0, ro=ro, vo=vo ) # density at r=a should be amp/4 assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 / 4.0 ) < 10.0**-8.0 ), "BurkertPotential w/ amp w/ units does not behave as expected" # DoubleExponentialDiskPotential pot = potential.DoubleExponentialDiskPotential( amp=0.1 * units.Msun / units.pc**3.0 * constants.G, hr=2.0, hz=0.2, ro=ro, vo=vo, ) # density at zero should be amp assert ( numpy.fabs( pot.dens(0.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 ) < 10.0**-8.0 ), "DoubleExponentialDiskPotential w/ amp w/ units does not behave as expected" # TwoPowerSphericalPotential pot = potential.TwoPowerSphericalPotential( amp=20.0 * units.Msun * constants.G, a=2.0, alpha=1.5, beta=3.5, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "TwoPowerSphericalPotential w/ amp w/ units does not behave as expected" # TwoPowerSphericalPotential with integer powers pot = potential.TwoPowerSphericalPotential( amp=20.0 * units.Msun * constants.G, a=2.0, alpha=2.0, beta=5.0, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "TwoPowerSphericalPotential w/ amp w/ units does not behave as expected" # JaffePotential pot = potential.JaffePotential( amp=20.0 * units.Msun * constants.G, a=2.0, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "JaffePotential w/ amp w/ units does not behave as expected" # HernquistPotential pot = potential.HernquistPotential( amp=20.0 * units.Msun * constants.G, a=2.0, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "HernquistPotential w/ amp w/ units does not behave as expected" # NFWPotential pot = potential.NFWPotential( amp=20.0 * units.Msun * constants.G, a=2.0, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "NFWPotential w/ amp w/ units does not behave as expected" # SCFPotential, default = Hernquist pot = potential.SCFPotential( amp=20.0 * units.Msun * constants.G, a=2.0, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "SCFPotential w/ amp w/ units does not behave as expected" # TwoPowerTriaxialPotential pot = potential.TwoPowerTriaxialPotential( amp=20.0 * units.Msun * constants.G, a=2.0, b=0.3, c=1.4, alpha=1.5, beta=3.5, ro=ro, vo=vo, ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "TwoPowerTriaxialPotential w/ amp w/ units does not behave as expected" # TwoPowerTriaxialPotential with integer powers pot = potential.TwoPowerTriaxialPotential( amp=20.0 * units.Msun * constants.G, a=2.0, b=0.5, c=0.3, alpha=2.0, beta=5.0, ro=ro, vo=vo, ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "TwoPowerTriaxialPotential w/ amp w/ units does not behave as expected" # TriaxialJaffePotential pot = potential.TriaxialJaffePotential( amp=20.0 * units.Msun * constants.G, a=2.0, b=0.4, c=0.9, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "TriaxialJaffePotential w/ amp w/ units does not behave as expected" # TriaxialHernquistPotential pot = potential.TriaxialHernquistPotential( amp=20.0 * units.Msun * constants.G, a=2.0, b=1.3, c=0.3, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "TriaxialHernquistPotential w/ amp w/ units does not behave as expected" # TriaxialNFWPotential pot = potential.TriaxialNFWPotential( amp=20.0 * units.Msun * constants.G, a=2.0, b=1.2, c=0.6, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "TriaxialNFWPotential w/ amp w/ units does not behave as expected" # IsochronePotential pot = potential.IsochronePotential( amp=20.0 * units.Msun * constants.G, b=2.0, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) * vo**2.0 + (20.0 * units.Msun * constants.G) .to(units.pc * units.km**2 / units.s**2) .value / (2.0 + numpy.sqrt(4.0 + 16.0)) / ro / 1000.0 ) < 10.0**-8.0 ), "IsochronePotential w/ amp w/ units does not behave as expected" # KeplerPotential pot = potential.KeplerPotential(amp=20.0 * units.Msun * constants.G, ro=ro, vo=vo) # Check mass assert ( numpy.fabs( pot.mass(100.0, use_physical=False) * conversion.mass_in_msol(vo, ro) - 20.0 ) < 10.0**-8.0 ), "KeplerPotential w/ amp w/ units does not behave as expected" # KuzminKutuzovStaeckelPotential pot = potential.KuzminKutuzovStaeckelPotential( amp=20.0 * units.Msun * constants.G, Delta=2.0, ro=ro, vo=vo ) pot_nounits = potential.KuzminKutuzovStaeckelPotential( amp=(20.0 * units.Msun * constants.G) .to(units.kpc * units.km**2 / units.s**2) .value / ro / vo**2, Delta=2.0, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) - pot_nounits(4.0, 0.0, use_physical=False) ) < 10.0**-8.0 ), "KuzminKutuzovStaeckelPotential w/ amp w/ units does not behave as expected" # MiyamotoNagaiPotential pot = potential.MiyamotoNagaiPotential( amp=20 * units.Msun * constants.G, a=2.0, b=0.5, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 1.0, use_physical=False) * vo**2.0 + (20.0 * units.Msun * constants.G) .to(units.pc * units.km**2 / units.s**2) .value / numpy.sqrt(16.0 + (2.0 + numpy.sqrt(1.0 + 0.25)) ** 2.0) / ro / 1000.0 ) < 10.0**-8.0 ), "MiyamotoNagaiPotential( w/ amp w/ units does not behave as expected" # KuzminDiskPotential pot = potential.KuzminDiskPotential( amp=20 * units.Msun * constants.G, a=2.0, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 1.0, use_physical=False) * vo**2.0 + (20.0 * units.Msun * constants.G) .to(units.pc * units.km**2 / units.s**2) .value / numpy.sqrt(16.0 + (2.0 + 1.0) ** 2.0) / ro / 1000.0 ) < 10.0**-8.0 ), "KuzminDiskPotential( w/ amp w/ units does not behave as expected" # MN3ExponentialDiskPotential pot = potential.MN3ExponentialDiskPotential( amp=0.1 * units.Msun * constants.G / units.pc**3.0, hr=2.0, hz=0.2, ro=ro, vo=vo, ) # density at hr should be assert ( numpy.fabs( pot.dens(2.0, 0.2, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 * numpy.exp(-2.0) ) < 10.0**-3.0 ), "MN3ExponentialDiskPotential w/ amp w/ units does not behave as expected" # PlummerPotential pot = potential.PlummerPotential( amp=20 * units.Msun * constants.G, b=0.5, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) * vo**2.0 + (20.0 * units.Msun * constants.G) .to(units.pc * units.km**2 / units.s**2) .value / numpy.sqrt(16.0 + 0.25) / ro / 1000.0 ) < 10.0**-8.0 ), "PlummerPotential w/ amp w/ units does not behave as expected" # PowerSphericalPotential pot = potential.PowerSphericalPotential( amp=10.0**10.0 * units.Msun * constants.G, r1=1.0, alpha=2.0, ro=ro, vo=vo ) # density at r1 assert ( numpy.fabs( pot.dens(1.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 10.0 / ro**3.0 ) < 10.0**-8.0 ), "PowerSphericalPotential w/ amp w/ units does not behave as expected" # PowerSphericalPotentialwCutoff pot = potential.PowerSphericalPotentialwCutoff( amp=0.1 * units.Msun * constants.G / units.pc**3, r1=1.0, alpha=2.0, rc=2.0, ro=ro, vo=vo, ) # density at r1 assert ( numpy.fabs( pot.dens(1.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 * numpy.exp(-0.25) ) < 10.0**-8.0 ), "PowerSphericalPotentialwCutoff w/ amp w/ units does not behave as expected" # PseudoIsothermalPotential pot = potential.PseudoIsothermalPotential( amp=10.0**10.0 * units.Msun * constants.G, a=2.0, ro=ro, vo=vo ) # density at a assert ( numpy.fabs( pot.dens(2.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 10.0 / 4.0 / numpy.pi / 8.0 / 2.0 / ro**3.0 ) < 10.0**-8.0 ), "PseudoIsothermalPotential w/ amp w/ units does not behave as expected" # RazorThinExponentialDiskPotential pot = potential.RazorThinExponentialDiskPotential( amp=40.0 * units.Msun * constants.G / units.pc**2, hr=2.0, ro=ro, vo=vo ) pot_nounits = potential.RazorThinExponentialDiskPotential( amp=(40.0 * units.Msun / units.pc**2 * constants.G) .to(1 / units.kpc * units.km**2 / units.s**2) .value * ro / vo**2, hr=2.0, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) - pot_nounits(4.0, 0.0, use_physical=False) ) < 10.0**-8.0 ), "RazorThinExponentialDiskPotential w/ amp w/ units does not behave as expected" # SoftenedNeedleBarPotential pot = potential.SoftenedNeedleBarPotential( amp=4.0 * 10.0**10.0 * units.Msun * constants.G, a=1.0, b=2.0, c=3.0, pa=0.0, omegab=0.0, ro=ro, vo=vo, ) pot_nounits = potential.SoftenedNeedleBarPotential( amp=4.0 / conversion.mass_in_1010msol(vo, ro), a=1.0, b=2.0, c=3.0, pa=0.0, omegab=0.0, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "SoftenedNeedleBarPotential w/ amp w/ units does not behave as expected" # FerrersPotential pot = potential.FerrersPotential( amp=4.0 * 10.0**10.0 * units.Msun * constants.G, a=1.0, b=2.0, c=3.0, pa=0.0, omegab=0.0, ro=ro, vo=vo, ) pot_nounits = potential.FerrersPotential( amp=4.0 / conversion.mass_in_1010msol(vo, ro), a=1.0, b=2.0, c=3.0, pa=0.0, omegab=0.0, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "FerrersPotential w/ amp w/ units does not behave as expected" # SphericalShellPotential pot = potential.SphericalShellPotential( amp=4.0 * 10.0**10.0 * units.Msun * constants.G, ro=ro, vo=vo ) pot_nounits = potential.SphericalShellPotential( amp=4.0 / conversion.mass_in_1010msol(vo, ro), ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "SphericalShellPotential w/ amp w/ units does not behave as expected" # RingPotential pot = potential.RingPotential( amp=4.0 * 10.0**10.0 * units.Msun * constants.G, ro=ro, vo=vo ) pot_nounits = potential.RingPotential( amp=4.0 / conversion.mass_in_1010msol(vo, ro), ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "RingPotential w/ amp w/ units does not behave as expected" # PerfectEllipsoidPotential pot = potential.PerfectEllipsoidPotential( amp=4.0 * 10.0**10.0 * units.Msun * constants.G, a=2.0, ro=ro, vo=vo, b=1.3, c=0.4, ) pot_nounits = potential.PerfectEllipsoidPotential( amp=4.0 / conversion.mass_in_1010msol(vo, ro), a=2.0, ro=ro, vo=vo, b=1.3, c=0.4 ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "PerfectEllipsoidPotential w/ amp w/ units does not behave as expected" # HomogeneousSpherePotential pot = potential.HomogeneousSpherePotential( amp=0.1 * units.Msun / units.pc**3.0 * constants.G, R=2.0, ro=ro, vo=vo ) pot_nounits = potential.HomogeneousSpherePotential( amp=0.1 / conversion.dens_in_msolpc3(vo, ro), R=2.0, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(1.1, 0.2, phi=1.0, use_physical=False) - pot_nounits(1.1, 0.2, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "HomogeneousSpherePotential w/ amp w/ units does not behave as expected" # TriaxialGaussianPotential pot = potential.TriaxialGaussianPotential( amp=4.0 * 10.0**10.0 * units.Msun * constants.G, sigma=2.0, ro=ro, vo=vo, b=1.3, c=0.4, ) pot_nounits = potential.TriaxialGaussianPotential( amp=4.0 / conversion.mass_in_1010msol(vo, ro), sigma=2.0, ro=ro, vo=vo, b=1.3, c=0.4, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "TriaxialGaussianPotential w/ amp w/ units does not behave as expected" return None def test_potential_ampunits_wrongunits(): # Test that input units for potential amplitudes behave as expected from galpy import potential ro, vo = 9.0, 210.0 # Burkert with pytest.raises(units.UnitConversionError) as excinfo: potential.BurkertPotential( amp=0.1 * units.Msun / units.pc**2.0, a=2.0, ro=ro, vo=vo ) # DoubleExponentialDiskPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.DoubleExponentialDiskPotential( amp=0.1 * units.Msun / units.pc**2.0 * constants.G, hr=2.0, hz=0.2, ro=ro, vo=vo, ) # TwoPowerSphericalPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.TwoPowerSphericalPotential( amp=20.0 * units.Msun / units.pc**3, a=2.0, alpha=1.5, beta=3.5, ro=ro, vo=vo, ) # TwoPowerSphericalPotential with integer powers with pytest.raises(units.UnitConversionError) as excinfo: potential.TwoPowerSphericalPotential( amp=20.0 * units.Msun / units.pc**3 * constants.G, a=2.0, alpha=2.0, beta=5.0, ro=ro, vo=vo, ) # JaffePotential with pytest.raises(units.UnitConversionError) as excinfo: potential.JaffePotential(amp=20.0 * units.kpc, a=2.0, ro=ro, vo=vo) # HernquistPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.HernquistPotential( amp=20.0 * units.Msun / units.pc**3, a=2.0, ro=ro, vo=vo ) # NFWPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.NFWPotential(amp=20.0 * units.km**2 / units.s**2, a=2.0, ro=ro, vo=vo) # SCFPotential, default = Hernquist with pytest.raises(units.UnitConversionError) as excinfo: potential.SCFPotential(amp=20.0 * units.Msun / units.pc**3, a=2.0, ro=ro, vo=vo) # TwoPowerTriaxialPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.TwoPowerTriaxialPotential( amp=20.0 * units.Msun / units.pc**3, a=2.0, alpha=1.5, beta=3.5, ro=ro, vo=vo, ) # TriaxialJaffePotential with pytest.raises(units.UnitConversionError) as excinfo: potential.TriaxialJaffePotential(amp=20.0 * units.kpc, a=2.0, ro=ro, vo=vo) # TriaxialHernquistPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.TriaxialHernquistPotential( amp=20.0 * units.Msun / units.pc**3, a=2.0, ro=ro, vo=vo ) # TriaxialNFWPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.TriaxialNFWPotential( amp=20.0 * units.km**2 / units.s**2, a=2.0, ro=ro, vo=vo ) # FlattenedPowerPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.FlattenedPowerPotential( amp=40000.0 * units.km**2 / units.s, r1=1.0, q=0.9, alpha=0.5, core=0.0, ro=ro, vo=vo, ) # IsochronePotential with pytest.raises(units.UnitConversionError) as excinfo: potential.IsochronePotential( amp=20.0 * units.km**2 / units.s**2, b=2.0, ro=ro, vo=vo ) # KeplerPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.KeplerPotential(amp=20.0 * units.Msun / units.pc**3, ro=ro, vo=vo) # KuzminKutuzovStaeckelPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.KuzminKutuzovStaeckelPotential( amp=20.0 * units.Msun / units.pc**2, Delta=2.0, ro=ro, vo=vo ) # LogarithmicHaloPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.LogarithmicHaloPotential(amp=40 * units.Msun, core=0.0, ro=ro, vo=vo) # MiyamotoNagaiPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.MiyamotoNagaiPotential( amp=20 * units.km**2 / units.s**2, a=2.0, b=0.5, ro=ro, vo=vo ) # KuzminDiskPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.KuzminDiskPotential( amp=20 * units.km**2 / units.s**2, a=2.0, ro=ro, vo=vo ) # MN3ExponentialDiskPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.MN3ExponentialDiskPotential( amp=0.1 * units.Msun * constants.G, hr=2.0, hz=0.2, ro=ro, vo=vo ) # PlummerPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.PlummerPotential( amp=20 * units.km**2 / units.s**2, b=0.5, ro=ro, vo=vo ) # PowerSphericalPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.PowerSphericalPotential( amp=10.0**10.0 * units.Msun / units.pc**3, r1=1.0, alpha=2.0, ro=ro, vo=vo, ) # PowerSphericalPotentialwCutoff with pytest.raises(units.UnitConversionError) as excinfo: potential.PowerSphericalPotentialwCutoff( amp=0.1 * units.Msun / units.pc**2, r1=1.0, alpha=2.0, rc=2.0, ro=ro, vo=vo, ) # PseudoIsothermalPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.PseudoIsothermalPotential( amp=10.0**10.0 * units.Msun / units.pc**3, a=2.0, ro=ro, vo=vo ) # RazorThinExponentialDiskPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.RazorThinExponentialDiskPotential( amp=40.0 * units.Msun / units.pc**3, hr=2.0, ro=ro, vo=vo ) # SoftenedNeedleBarPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.SoftenedNeedleBarPotential( amp=40.0 * units.Msun / units.pc**2, a=2.0, ro=ro, vo=vo ) # FerrersPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.FerrersPotential( amp=40.0 * units.Msun / units.pc**2, a=2.0, ro=ro, vo=vo ) # DiskSCFPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.DiskSCFPotential(amp=40.0 * units.Msun / units.pc**2) # SpiralArmsPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.SpiralArmsPotential(amp=10**10 * units.Msun) # SphericalShellPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.SphericalShellPotential( amp=40.0 * units.Msun / units.pc**2, a=2.0, ro=ro, vo=vo ) # RingPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.RingPotential( amp=40.0 * units.Msun / units.pc**2, a=2.0, ro=ro, vo=vo ) # PerfectEllipsoidPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.PerfectEllipsoidPotential( amp=40.0 * units.Msun / units.pc**2, a=2.0, ro=ro, vo=vo, b=1.3, c=0.4 ) # HomogeneousSpherePotential with pytest.raises(units.UnitConversionError) as excinfo: potential.HomogeneousSpherePotential(amp=40.0 * units.Msun, R=2.0, ro=ro, vo=vo) # TriaxialGaussianPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.TriaxialGaussianPotential( amp=40.0 * units.Msun / units.pc**2, sigma=2.0, ro=ro, vo=vo, b=1.3, c=0.4 ) # NullPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.NullPotential(amp=40.0 * units.Msun, ro=ro, vo=vo) return None def test_potential_paramunits(): # Test that input units for potential parameters other than the amplitude # behave as expected from galpy import potential from galpy.util import conversion ro, vo = 7.0, 230.0 # Burkert pot = potential.BurkertPotential( amp=0.1 * units.Msun / units.pc**3.0, a=2.0 * units.kpc, ro=ro, vo=vo ) # density at r=a should be amp/4 assert ( numpy.fabs( pot.dens(2.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 / 4.0 ) < 10.0**-8.0 ), "BurkertPotential w/ parameters w/ units does not behave as expected" # DoubleExponentialDiskPotential pot = potential.DoubleExponentialDiskPotential( amp=0.1 * units.Msun / units.pc**3.0, hr=4.0 * units.kpc, hz=200.0 * units.pc, ro=ro, vo=vo, ) # density at zero should be amp assert ( numpy.fabs( pot.dens(0.0, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 ) < 10.0**-8.0 ), ( "DoubleExponentialDiskPotential w/ parameters w/ units does not behave as expected" ) # density at 1. is... assert ( numpy.fabs( pot.dens(1.0, 0.1, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 * numpy.exp(-1.0 / 4.0 * ro - 0.1 / 0.2 * ro) ) < 10.0**-8.0 ), ( "DoubleExponentialDiskPotential w/ parameters w/ units does not behave as expected" ) # TwoPowerSphericalPotential pot = potential.TwoPowerSphericalPotential( amp=20.0 * units.Msun, a=10.0 * units.kpc, alpha=1.5, beta=3.5, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(10.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "TwoPowerSphericalPotential w/ parameters w/ units does not behave as expected" # TwoPowerSphericalPotential with integer powers pot = potential.TwoPowerSphericalPotential( amp=20.0 * units.Msun, a=12000.0 * units.lyr, alpha=2.0, beta=5.0, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens( (12000.0 * units.lyr).to(units.kpc).value / ro, 0.0, use_physical=False ) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "TwoPowerSphericalPotential w/ parameters w/ units does not behave as expected" # JaffePotential pot = potential.JaffePotential( amp=20.0 * units.Msun, a=0.02 * units.Mpc, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(20.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "JaffePotential w/ parameters w/ units does not behave as expected" # HernquistPotential pot = potential.HernquistPotential( amp=20.0 * units.Msun, a=10.0 * units.kpc, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(10.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "HernquistPotential w/ parameters w/ units does not behave as expected" # NFWPotential pot = potential.NFWPotential( amp=20.0 * units.Msun, a=15.0 * units.kpc, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(15.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "NFWPotential w/ parameters w/ units does not behave as expected" # NFWPotential, rmax,vmax pot = potential.NFWPotential( rmax=10.0 * units.kpc, vmax=175.0 * units.km / units.s, ro=ro, vo=vo ) # Check velocity at r=rmax assert ( numpy.fabs(pot.vcirc(10.0 / ro, use_physical=False) * vo - 175.0) < 10.0**-8.0 ), "NFWPotential w/ parameters w/ units does not behave as expected" # SCFPotential, default = Hernquist pot = potential.SCFPotential( amp=20.0 * units.Msun, a=10.0 * units.kpc, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(10.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "SCFPotential w/ parameters w/ units does not behave as expected" # TwoPowerTriaxialPotential pot = potential.TwoPowerTriaxialPotential( amp=20.0 * units.Msun, a=10.0 * units.kpc, alpha=1.5, beta=3.5, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(10.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "TwoPowerTriaxialPotential w/ parameters w/ units does not behave as expected" # TriaxialJaffePotential pot = potential.TriaxialJaffePotential( amp=20.0 * units.Msun, a=0.02 * units.Mpc, b=0.2, c=0.8, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(20.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "TriaxialJaffePotential w/ parameters w/ units does not behave as expected" # TriaxialHernquistPotential pot = potential.TriaxialHernquistPotential( amp=20.0 * units.Msun, a=10.0 * units.kpc, b=0.7, c=0.9, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(10.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 8.0 ) < 10.0**-8.0 ), "TriaxialHernquistPotential w/ parameters w/ units does not behave as expected" # TriaxialNFWPotential pot = potential.TriaxialNFWPotential( amp=20.0 * units.Msun, a=15.0 * units.kpc, b=1.3, c=0.2, ro=ro, vo=vo ) # Check density at r=a assert ( numpy.fabs( pot.dens(15.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 20.0 / 4.0 / numpy.pi / 8.0 / ro**3.0 / 10.0**9.0 / 4.0 ) < 10.0**-8.0 ), "TriaxialNFWPotential w/ parameters w/ units does not behave as expected" # Also do pa pot = potential.TriaxialNFWPotential( amp=20.0 * units.Msun, a=15.0 * units.kpc, pa=30.0 * units.deg, b=1.3, c=0.2, ro=ro, vo=vo, ) assert ( numpy.fabs(numpy.arccos(pot._rot[0, 0]) - 30.0 / 180.0 * numpy.pi) < 10.0**-8.0 ), "TriaxialNFWPotential w/ parameters w/ units does not behave as expected" # FlattenedPowerPotential pot = potential.FlattenedPowerPotential( amp=40000.0 * units.km**2 / units.s**2, r1=10.0 * units.kpc, q=0.9, alpha=0.5, core=1.0 * units.kpc, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(2.0, 1.0, use_physical=False) * vo**2.0 + 40000.0 * (10.0 / ro) ** 0.5 / 0.5 / (2.0**2.0 + (1.0 / 0.9) ** 2.0 + (1.0 / ro) ** 2.0) ** 0.25 ) < 10.0**-8.0 ), "FlattenedPowerPotential w/ parameters w/ units does not behave as expected" # IsochronePotential pot = potential.IsochronePotential( amp=20.0 * units.Msun, b=10.0 * units.kpc, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) * vo**2.0 + (20.0 * units.Msun * constants.G) .to(units.pc * units.km**2 / units.s**2) .value / (10.0 / ro + numpy.sqrt((10.0 / ro) ** 2.0 + 16.0)) / ro / 1000.0 ) < 10.0**-8.0 ), "IsochronePotential w/ parameters w/ units does not behave as expected" # KuzminKutuzovStaeckelPotential pot = potential.KuzminKutuzovStaeckelPotential( amp=20.0 * units.Msun, Delta=10.0 * units.kpc, ro=ro, vo=vo ) pot_nounits = potential.KuzminKutuzovStaeckelPotential( amp=(20.0 * units.Msun * constants.G) .to(units.kpc * units.km**2 / units.s**2) .value / ro / vo**2, Delta=10.0 / ro, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) - pot_nounits(4.0, 0.0, use_physical=False) ) < 10.0**-8.0 ), ( "KuzminKutuzovStaeckelPotential w/ parameters w/ units does not behave as expected" ) # LogarithmicHaloPotential pot = potential.LogarithmicHaloPotential( amp=40000 * units.km**2 / units.s**2, core=1.0 * units.kpc, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) * vo**2.0 - 20000 * numpy.log(16.0 + (1.0 / ro) ** 2.0) ) < 10.0**-8.0 ), "LogarithmicHaloPotential w/ parameters w/ units does not behave as expected" # DehnenBarPotential pot = potential.DehnenBarPotential( amp=1.0, omegab=50.0 * units.km / units.s / units.kpc, rb=4.0 * units.kpc, Af=1290.0 * units.km**2 / units.s**2, barphi=20.0 * units.deg, ro=ro, vo=vo, ) pot_nounits = potential.DehnenBarPotential( amp=1.0, omegab=50.0 * ro / vo, rb=4.0 / ro, Af=1290.0 / vo**2.0, barphi=20.0 / 180.0 * numpy.pi, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(1.5, 0.3, phi=0.1, use_physical=False) - pot_nounits(1.5, 0.3, phi=0.1, use_physical=False) ) < 10.0**-8.0 ), "DehnenBarPotential w/ parameters w/ units does not behave as expected" # DehnenBarPotential, alternative setup pot = potential.DehnenBarPotential( amp=1.0, rolr=8.0 * units.kpc, chi=0.8, alpha=0.02, beta=0.2, barphi=20.0 * units.deg, ro=ro, vo=vo, ) pot_nounits = potential.DehnenBarPotential( amp=1.0, rolr=8.0 / ro, chi=0.8, alpha=0.02, beta=0.2, barphi=20.0 / 180.0 * numpy.pi, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(1.5, 0.3, phi=0.1, use_physical=False) - pot_nounits(1.5, 0.3, phi=0.1, use_physical=False) ) < 10.0**-8.0 ), "DehnenBarPotential w/ parameters w/ units does not behave as expected" # MiyamotoNagaiPotential pot = potential.MiyamotoNagaiPotential( amp=20 * units.Msun, a=5.0 * units.kpc, b=300.0 * units.pc, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 1.0, use_physical=False) * vo**2.0 + (20.0 * units.Msun * constants.G) .to(units.pc * units.km**2 / units.s**2) .value / numpy.sqrt(16.0 + (5.0 / ro + numpy.sqrt(1.0 + (0.3 / ro) ** 2.0)) ** 2.0) / ro / 1000.0 ) < 10.0**-8.0 ), "MiyamotoNagaiPotential( w/ parameters w/ units does not behave as expected" # KuzminDiskPotential pot = potential.KuzminDiskPotential( amp=20 * units.Msun, a=5.0 * units.kpc, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 1.0, use_physical=False) * vo**2.0 + (20.0 * units.Msun * constants.G) .to(units.pc * units.km**2 / units.s**2) .value / numpy.sqrt(16.0 + (5.0 / ro + 1.0) ** 2.0) / ro / 1000.0 ) < 10.0**-8.0 ), "KuzminDiskPotential( w/ parameters w/ units does not behave as expected" # MN3ExponentialDiskPotential pot = potential.MN3ExponentialDiskPotential( amp=0.1 * units.Msun / units.pc**3.0, hr=6.0 * units.kpc, hz=300.0 * units.pc, ro=ro, vo=vo, ) # density at hr should be assert ( numpy.fabs( pot.dens(6.0 / ro, 0.3 / ro, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 * numpy.exp(-2.0) ) < 10.0**-3.0 ), "MN3ExponentialDiskPotential w/ parameters w/ units does not behave as expected" # PlummerPotential pot = potential.PlummerPotential( amp=20 * units.Msun, b=5.0 * units.kpc, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) * vo**2.0 + (20.0 * units.Msun * constants.G) .to(units.pc * units.km**2 / units.s**2) .value / numpy.sqrt(16.0 + (5.0 / ro) ** 2.0) / ro / 1000.0 ) < 10.0**-8.0 ), "PlummerPotential w/ parameters w/ units does not behave as expected" # PowerSphericalPotential pot = potential.PowerSphericalPotential( amp=10.0**10.0 * units.Msun, r1=10.0 * units.kpc, alpha=2.0, ro=ro, vo=vo ) # density at r1 assert ( numpy.fabs( pot.dens(10.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 10.0 / ro**3.0 / (10.0 / ro) ** 3.0 ) < 10.0**-8.0 ), "PowerSphericalPotential w/ parameters w/ units does not behave as expected" # PowerSphericalPotentialwCutoff pot = potential.PowerSphericalPotentialwCutoff( amp=0.1 * units.Msun / units.pc**3, r1=10.0 * units.kpc, alpha=2.0, rc=12.0 * units.kpc, ro=ro, vo=vo, ) # density at r1 assert ( numpy.fabs( pot.dens(10.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 0.1 * numpy.exp(-((10.0 / 12.0) ** 2.0)) ) < 10.0**-8.0 ), ( "PowerSphericalPotentialwCutoff w/ parameters w/ units does not behave as expected" ) # PseudoIsothermalPotential pot = potential.PseudoIsothermalPotential( amp=10.0**10.0 * units.Msun, a=20.0 * units.kpc, ro=ro, vo=vo ) # density at a assert ( numpy.fabs( pot.dens(20.0 / ro, 0.0, use_physical=False) * conversion.dens_in_msolpc3(vo, ro) - 10.0 / 4.0 / numpy.pi / (20.0 / ro) ** 3.0 / 2.0 / ro**3.0 ) < 10.0**-8.0 ), "PseudoIsothermalPotential w/ parameters w/ units does not behave as expected" # RazorThinExponentialDiskPotential pot = potential.RazorThinExponentialDiskPotential( amp=40.0 * units.Msun / units.pc**2, hr=10.0 * units.kpc, ro=ro, vo=vo ) pot_nounits = potential.RazorThinExponentialDiskPotential( amp=(40.0 * units.Msun / units.pc**2 * constants.G) .to(1 / units.kpc * units.km**2 / units.s**2) .value * ro / vo**2, hr=10.0 / ro, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, use_physical=False) - pot_nounits(4.0, 0.0, use_physical=False) ) < 10.0**-8.0 ), ( "RazorThinExponentialDiskPotential w/ parameters w/ units does not behave as expected" ) # SoftenedNeedleBarPotential pot = potential.SoftenedNeedleBarPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=10.0 * units.kpc, b=2.0 * units.kpc, c=3.0 * units.kpc, pa=10.0 * units.deg, omegab=20.0 * units.km / units.s / units.kpc, ro=ro, vo=vo, ) pot_nounits = potential.SoftenedNeedleBarPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=10.0 / ro, b=2.0 / ro, c=3.0 / ro, pa=10.0 / 180.0 * numpy.pi, omegab=20.0 / conversion.freq_in_kmskpc(vo, ro), ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "SoftenedNeedleBarPotential w/ amp w/ units does not behave as expected" # FerrersPotential pot = potential.FerrersPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=10.0 * units.kpc, b=2.0, c=3.0, pa=10.0 * units.deg, omegab=20.0 * units.km / units.s / units.kpc, ro=ro, vo=vo, ) pot_nounits = potential.FerrersPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=10.0 / ro, b=2.0, c=3.0, pa=10.0 / 180.0 * numpy.pi, omegab=20.0 / conversion.freq_in_kmskpc(vo, ro), ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "FerrersPotential w/ amp w/ units does not behave as expected" # DiskSCFPotential pot = potential.DiskSCFPotential( dens=lambda R, z: 1.0, # doesn't matter Sigma=[ {"type": "exp", "h": 1.0 / 3.0, "amp": 1.0}, {"type": "expwhole", "h": 1.0 / 3.0, "amp": 1.0, "Rhole": 0.5}, ], hz=[{"type": "exp", "h": 1.0 / 27.0}, {"type": "sech2", "h": 1.0 / 27.0}], a=8.0 * units.kpc, N=2, L=2, ro=ro, vo=vo, ) pot_nounits = potential.DiskSCFPotential( dens=lambda R, z: 1.0, # doesn't matter Sigma=[ {"type": "exp", "h": 1.0 / 3.0, "amp": 1.0}, {"type": "expwhole", "h": 1.0 / 3.0, "amp": 1.0, "Rhole": 0.5}, ], hz=[{"type": "exp", "h": 1.0 / 27.0}, {"type": "sech2", "h": 1.0 / 27.0}], a=8.0 / ro, N=2, L=2, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "DiskSCFPotential w/ a w/ units does not behave as expected" # SpiralArmsPotential pot = potential.SpiralArmsPotential( amp=1, ro=ro, vo=vo, N=2, alpha=13 * units.deg, r_ref=0.8 * units.kpc, phi_ref=90.0 * units.deg, Rs=8 * units.kpc, H=0.1 * units.kpc, omega=20.0 * units.km / units.s / units.kpc, Cs=[1], ) pot_nounits = potential.SpiralArmsPotential( amp=1, ro=ro, vo=vo, N=2, alpha=13 * numpy.pi / 180.0, r_ref=0.8 / ro, phi_ref=numpy.pi / 2, Rs=8.0 / ro, H=0.1 / ro, omega=20.0 / conversion.freq_in_kmskpc(vo, ro), Cs=[1], ) # Check potential assert ( numpy.fabs( pot(1.5, 0.3, phi=0.1, use_physical=False) - pot_nounits(1.5, 0.3, phi=0.1, use_physical=False) ) < 10.0**-8.0 ), "SpiralArmsPotential w/ parameters w/ units does not behave as expected" # DehnenSmoothWrapperPotential dpn = potential.DehnenBarPotential(tform=-100.0, tsteady=1.0) pot = potential.DehnenSmoothWrapperPotential( pot=dpn, tform=-1.0 * units.Gyr, tsteady=3.0 * units.Gyr, ro=ro, vo=vo ) pot_nounits = potential.DehnenSmoothWrapperPotential( pot=dpn, tform=-1.0 / conversion.time_in_Gyr(vo, ro), tsteady=3.0 / conversion.time_in_Gyr(vo, ro), ) # Check potential assert ( numpy.fabs( pot(1.5, 0.3, phi=0.1, use_physical=False) - pot_nounits(1.5, 0.3, phi=0.1, use_physical=False) ) < 10.0**-8.0 ), "DehnenSmoothWrapperPotential w/ parameters w/ units does not behave as expected" # SolidBodyRotationWrapperPotential spn = potential.SpiralArmsPotential(omega=0.0, phi_ref=0.0) pot = potential.SolidBodyRotationWrapperPotential( pot=spn, omega=20.0 * units.km / units.s / units.kpc, pa=30.0 * units.deg, ro=ro, vo=vo, ) pot_nounits = potential.SolidBodyRotationWrapperPotential( pot=spn, omega=20.0 / conversion.freq_in_kmskpc(vo, ro), pa=30.0 / 180.0 * numpy.pi, ) # Check potential assert ( numpy.fabs( pot(1.5, 0.3, phi=0.1, use_physical=False) - pot_nounits(1.5, 0.3, phi=0.1, use_physical=False) ) < 10.0**-8.0 ), ( "SolidBodyRotationWrapperPotential w/ parameters w/ units does not behave as expected" ) # CorotatingRotationWrapperPotential spn = potential.SpiralArmsPotential(omega=0.0, phi_ref=0.0) pot = potential.CorotatingRotationWrapperPotential( pot=spn, vpo=200.0 * units.km / units.s, to=1.0 * units.Gyr, pa=30.0 * units.deg, ro=ro, vo=vo, ) pot_nounits = potential.CorotatingRotationWrapperPotential( pot=spn, vpo=200.0 / vo, to=1.0 / conversion.time_in_Gyr(vo, ro), pa=30.0 / 180.0 * numpy.pi, ) # Check potential assert ( numpy.fabs( pot(1.5, 0.3, phi=0.1, use_physical=False) - pot_nounits(1.5, 0.3, phi=0.1, use_physical=False) ) < 10.0**-8.0 ), ( "CorotatingRotationWrapperPotential w/ parameters w/ units does not behave as expected" ) # GaussianAmplitudeWrapperPotential dpn = potential.DehnenBarPotential(tform=-100.0, tsteady=1.0) pot = potential.GaussianAmplitudeWrapperPotential( pot=dpn, to=-1.0 * units.Gyr, sigma=10.0 * units.Gyr, ro=ro, vo=vo ) pot_nounits = potential.GaussianAmplitudeWrapperPotential( pot=dpn, to=-1.0 / conversion.time_in_Gyr(vo, ro), sigma=10.0 / conversion.time_in_Gyr(vo, ro), ) # Check potential assert ( numpy.fabs( pot(1.5, 0.3, phi=0.1, use_physical=False) - pot_nounits(1.5, 0.3, phi=0.1, use_physical=False) ) < 10.0**-8.0 ), ( "GaussianAmplitudeWrapperPotential w/ parameters w/ units does not behave as expected" ) # ChandrasekharDynamicalFrictionForce pot = potential.ChandrasekharDynamicalFrictionForce( GMs=10.0**9.0 * units.Msun, rhm=1.2 * units.kpc, minr=1.0 * units.pc, maxr=100.0 * units.kpc, ro=ro, vo=vo, ) pot_nounits = potential.ChandrasekharDynamicalFrictionForce( GMs=10.0**9.0 / conversion.mass_in_msol(vo, ro), rhm=1.2 / ro, minr=1.0 / ro / 1000.0, maxr=100.0 / ro, ) # Check potential assert ( numpy.fabs( pot.Rforce(1.5, 0.3, phi=0.1, v=[1.0, 0.0, 0.0], use_physical=False) - pot_nounits.Rforce( 1.5, 0.3, phi=0.1, v=[1.0, 0.0, 0.0], use_physical=False ) ) < 10.0**-8.0 ), ( "ChandrasekharDynamicalFrictionForce w/ parameters w/ units does not behave as expected" ) # Also check that this works after changing GMs and rhm on the fly (specific to ChandrasekharDynamicalFrictionForce old_force = pot.Rforce(1.5, 0.3, phi=0.1, v=[1.0, 0.0, 0.0], use_physical=False) pot.GMs = 10.0**8.0 * units.Msun pot_nounits.GMs = 10.0**8.0 / conversion.mass_in_msol(vo, ro) # units should still work assert ( numpy.fabs( pot.Rforce(1.5, 0.3, phi=0.1, v=[1.0, 0.0, 0.0], use_physical=False) - pot_nounits.Rforce( 1.5, 0.3, phi=0.1, v=[1.0, 0.0, 0.0], use_physical=False ) ) < 10.0**-8.0 ), ( "ChandrasekharDynamicalFrictionForce w/ parameters w/ units does not behave as expected" ) # and now for GMs pot.GMs = 10.0**8.0 * units.Msun * constants.G pot_nounits.GMs = 10.0**8.0 / conversion.mass_in_msol(vo, ro) # units should still work assert ( numpy.fabs( pot.Rforce(1.5, 0.3, phi=0.1, v=[1.0, 0.0, 0.0], use_physical=False) - pot_nounits.Rforce( 1.5, 0.3, phi=0.1, v=[1.0, 0.0, 0.0], use_physical=False ) ) < 10.0**-8.0 ), ( "ChandrasekharDynamicalFrictionForce w/ parameters w/ units does not behave as expected" ) # Quick test that other units don't work with pytest.raises(units.UnitConversionError) as excinfo: pot.GMs = 10.0**8.0 * units.Msun / units.pc**2 # and force should be /10 of previous (because linear in mass assert ( numpy.fabs( pot.Rforce(1.5, 0.3, phi=0.1, v=[1.0, 0.0, 0.0], use_physical=False) - old_force / 10.0 ) < 10.0**-8.0 ), ( "ChandrasekharDynamicalFrictionForce w/ parameters w/ units does not behave as expected" ) # Now do rhm pot.rhm = 12 * units.kpc pot_nounits.rhm = 12 / ro assert ( numpy.fabs( pot.Rforce(1.5, 0.3, phi=0.1, v=[1.0, 0.0, 0.0], use_physical=False) - pot_nounits.Rforce( 1.5, 0.3, phi=0.1, v=[1.0, 0.0, 0.0], use_physical=False ) ) < 10.0**-8.0 ), ( "ChandrasekharDynamicalFrictionForce w/ parameters w/ units does not behave as expected" ) # Compare changed rhm to one that has rhm directly set to this value # to make sure that the change is okay pot_nounits_direct = potential.ChandrasekharDynamicalFrictionForce( GMs=10.0**8.0 / conversion.mass_in_msol(vo, ro), rhm=12 / ro, minr=1.0 / ro / 1000.0, maxr=100.0 / ro, ) assert ( numpy.fabs( pot_nounits.Rforce(1.5, 0.3, phi=0.1, v=[1.0, 0.0, 0.0], use_physical=False) - pot_nounits_direct.Rforce( 1.5, 0.3, phi=0.1, v=[1.0, 0.0, 0.0], use_physical=False ) ) < 10.0**-8.0 ), ( "ChandrasekharDynamicalFrictionForce w/ parameters w/ units does not behave as expected" ) # SphericalShellPotential pot = potential.SphericalShellPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=5.0 * units.kpc, ro=ro, vo=vo ) pot_nounits = potential.SphericalShellPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=5.0 / ro, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "SphericalShellPotential w/ amp w/ units does not behave as expected" # RingPotential pot = potential.RingPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=5.0 * units.kpc, ro=ro, vo=vo ) pot_nounits = potential.RingPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=5.0 / ro, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "RingPotential w/ amp w/ units does not behave as expected" # If you add one here, don't base it on ChandrasekharDynamicalFrictionForce!! # PerfectEllipsoidPotential pot = potential.PerfectEllipsoidPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=5.0 * units.kpc, ro=ro, vo=vo, b=1.3, c=0.4, ) pot_nounits = potential.PerfectEllipsoidPotential( amp=4.0 * 10.0**10.0 * units.Msun, a=5.0 / ro, ro=ro, vo=vo, b=1.3, c=0.4 ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "PerfectEllipsoidPotential w/ amp w/ units does not behave as expected" # If you add one here, don't base it on ChandrasekharDynamicalFrictionForce!! # HomogeneousSpherePotential pot = potential.HomogeneousSpherePotential( amp=0.1 * units.Msun / units.pc**3, R=10.0 * units.kpc, ro=ro, vo=vo ) pot_nounits = potential.HomogeneousSpherePotential( amp=0.1 * units.Msun / units.pc**3, R=10.0 / ro, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(1.1, 0.2, phi=1.0, use_physical=False) - pot_nounits(1.1, 0.2, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "HomogeneousSpherePotential w/ amp w/ units does not behave as expected" # TriaxialGaussianPotential pot = potential.TriaxialGaussianPotential( amp=4.0 * 10.0**10.0 * units.Msun, sigma=5.0 * units.kpc, ro=ro, vo=vo, b=1.3, c=0.4, ) pot_nounits = potential.TriaxialGaussianPotential( amp=4.0 * 10.0**10.0 * units.Msun, sigma=5.0 / ro, ro=ro, vo=vo, b=1.3, c=0.4 ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "TriaxialGaussianPotential w/ amp w/ units does not behave as expected" # If you add one here, don't base it on ChandrasekharDynamicalFrictionForce!! # KingPotential pot = potential.KingPotential( W0=3.0, M=4.0 * 10.0**6.0 * units.Msun, rt=10.0 * units.pc, ro=ro, vo=vo ) pot_nounits = potential.KingPotential( W0=3.0, M=4.0 * 10.0**6.0 / conversion.mass_in_msol(vo, ro), rt=10.0 / 1000 / ro, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "KingPotential w/ amp w/ units does not behave as expected" # AnyAxisymmetricRazorThinDiskPotential pot = potential.AnyAxisymmetricRazorThinDiskPotential( surfdens=lambda R: 1.5 * conversion.surfdens_in_msolpc2(vo, ro) * units.Msun / units.pc**2 * numpy.exp(-R), ro=ro, vo=vo, ) pot_nounits = potential.AnyAxisymmetricRazorThinDiskPotential( surfdens=lambda R: 1.5 * numpy.exp(-R), ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), ( "AnyAxisymmetricRazorThinDiskPotential w/ parameters w/ units does not behave as expected" ) # AnyAxisymmetricRazorThinDiskPotential, r in surfdens also has units pot = potential.AnyAxisymmetricRazorThinDiskPotential( surfdens=lambda R: 1.5 * conversion.surfdens_in_msolpc2(vo, ro) * units.Msun / units.pc**2 * numpy.exp(-R / ro / units.kpc), ro=ro, vo=vo, ) pot_nounits = potential.AnyAxisymmetricRazorThinDiskPotential( surfdens=lambda R: 1.5 * numpy.exp(-R), ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), ( "AnyAxisymmetricRazorThinDiskPotential w/ parameters w/ units does not behave as expected" ) # AnyAxisymmetricRazorThinDiskPotential, r in surfdens only has units pot = potential.AnyAxisymmetricRazorThinDiskPotential( surfdens=lambda R: 1.5 * numpy.exp(-R / ro / units.kpc), ro=ro, vo=vo ) pot_nounits = potential.AnyAxisymmetricRazorThinDiskPotential( surfdens=lambda R: 1.5 * numpy.exp(-R), ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), ( "AnyAxisymmetricRazorThinDiskPotential w/ parameters w/ units does not behave as expected" ) # AnySphericalPotential pot = potential.AnySphericalPotential( dens=lambda r: 0.64 / r / (1 + r) ** 3 * conversion.dens_in_msolpc3(vo, ro) * units.Msun / units.pc**3, ro=ro, vo=vo, ) pot_nounits = potential.AnySphericalPotential( dens=lambda r: 0.64 / r / (1 + r) ** 3, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), ( "potential.AnySphericalPotential w/ parameters w/ units does not behave as expected" ) # AnySphericalPotential, r in dens also has units pot = potential.AnySphericalPotential( dens=lambda r: 0.64 / (r / ro / units.kpc) / (1 + r / ro / units.kpc) ** 3 * conversion.dens_in_msolpc3(vo, ro) * units.Msun / units.pc**3, ro=ro, vo=vo, ) pot_nounits = potential.AnySphericalPotential( dens=lambda r: 0.64 / r / (1 + r) ** 3, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), ( "AnyAxisymmetricRazorThinDiskPotential w/ parameters w/ units does not behave as expected" ) # AnySphericalPotential, r in dens only has units pot = potential.AnySphericalPotential( dens=lambda r: 0.64 / (r / ro / units.kpc) / (1 + r / ro / units.kpc) ** 3, ro=ro, vo=vo, ) pot_nounits = potential.AnySphericalPotential( dens=lambda r: 0.64 / r / (1 + r) ** 3, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), ( "AnyAxisymmetricRazorThinDiskPotential w/ parameters w/ units does not behave as expected" ) # If you add one here, don't base it on ChandrasekharDynamicalFrictionForce!! # RotateAndTiltWrapperPotential, zvec, pa wrappot = potential.TriaxialNFWPotential(amp=1.0, a=3.0, b=0.7, c=0.5) pot = potential.RotateAndTiltWrapperPotential( pot=wrappot, zvec=[0, 1.0, 0], galaxy_pa=30.0 * units.deg, ro=ro, vo=vo ) pot_nounits = potential.RotateAndTiltWrapperPotential( pot=wrappot, zvec=[0, 1.0, 0], galaxy_pa=numpy.pi / 6.0, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "RotateAndTiltWrapperPotential w/ pa w/ units does not behave as expected" # RotateAndTiltWrapperPotential, inclination, galaxy_pa, sky_pa wrappot = potential.TriaxialNFWPotential(amp=1.0, a=3.0, b=0.7, c=0.5) pot = potential.RotateAndTiltWrapperPotential( pot=wrappot, galaxy_pa=30.0 * units.deg, inclination=60.0 * units.deg, sky_pa=-45.0 * units.deg, ro=ro, vo=vo, ) pot_nounits = potential.RotateAndTiltWrapperPotential( pot=wrappot, galaxy_pa=numpy.pi / 6.0, inclination=numpy.pi / 3.0, sky_pa=-numpy.pi / 4.0, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot(4.0, 0.0, phi=1.0, use_physical=False) - pot_nounits(4.0, 0.0, phi=1.0, use_physical=False) ) < 10.0**-8.0 ), "RotateAndTiltWrapperPotential w/ pa w/ units does not behave as expected" # If you add one here, don't base it on ChandrasekharDynamicalFrictionForce!! return None def test_potential_paramunits_2d(): # Test that input units for potential parameters other than the amplitude # behave as expected from galpy import potential from galpy.util import conversion ro, vo = 11.0, 180.0 # CosmphiDiskPotential pot = potential.CosmphiDiskPotential( amp=1.0, m=3, phib=20.0 * units.deg, phio=1290.0 * units.km**2 / units.s**2, r1=8.0 * units.kpc, rb=7.0 * units.kpc, ro=ro, vo=vo, ) pot_nounits = potential.CosmphiDiskPotential( amp=1.0, m=3, phib=20.0 / 180.0 * numpy.pi, phio=1290.0 / vo**2.0, r1=8.0 / ro, rb=7.0 / ro, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) - pot_nounits( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) ) < 10.0**-8.0 ), "CosmphiDiskPotential w/ parameters w/ units does not behave as expected" # CosmphiDiskPotential, alternative setup pot = potential.CosmphiDiskPotential( amp=1.0, m=3, cp=1000.0 * units.km**2 / units.s**2.0, sp=300.0 * units.km**2 / units.s**2.0, r1=8.0 * units.kpc, ro=ro, vo=vo, ) pot_nounits = potential.CosmphiDiskPotential( amp=1.0, m=3, cp=1000.0 / vo**2.0, sp=300.0 / vo**2.0, r1=8.0 / ro, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) - pot_nounits( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) ) < 10.0**-8.0 ), "CosmphiDiskPotential w/ parameters w/ units does not behave as expected" # EllipticalDiskPotential pot = potential.EllipticalDiskPotential( amp=1.0, tform=1.0 * units.Gyr, tsteady=3.0 * units.Gyr, phib=20.0 * units.deg, twophio=1290.0 * units.km**2 / units.s**2, r1=8.0 * units.kpc, ro=ro, vo=vo, ) pot_nounits = potential.EllipticalDiskPotential( amp=1.0, tform=1.0 / conversion.time_in_Gyr(vo, ro), tsteady=3.0 / conversion.time_in_Gyr(vo, ro), phib=20.0 / 180.0 * numpy.pi, twophio=1290.0 / vo**2.0, r1=8.0 / ro, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) - pot_nounits( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) ) < 10.0**-8.0 ), "EllipticalDiskPotential w/ parameters w/ units does not behave as expected" # EllipticalDiskPotential, alternative setup pot = potential.EllipticalDiskPotential( amp=1.0, tform=1.0 * units.Gyr, tsteady=3.0 * units.Gyr, cp=1000.0 * units.km**2 / units.s**2.0, sp=300.0 * units.km**2 / units.s**2.0, r1=8.0 * units.kpc, ro=ro, vo=vo, ) pot_nounits = potential.EllipticalDiskPotential( amp=1.0, tform=1.0 / conversion.time_in_Gyr(vo, ro), tsteady=3.0 / conversion.time_in_Gyr(vo, ro), cp=1000.0 / vo**2.0, sp=300.0 / vo**2.0, r1=8.0 / ro, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) - pot_nounits( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) ) < 10.0**-8.0 ), "EllipticalDiskPotential w/ parameters w/ units does not behave as expected" # LopsidedDiskPotential pot = potential.LopsidedDiskPotential( amp=1.0, phib=20.0 * units.deg, phio=1290.0 * units.km**2 / units.s**2, r1=8.0 * units.kpc, ro=ro, vo=vo, ) pot_nounits = potential.LopsidedDiskPotential( amp=1.0, phib=20.0 / 180.0 * numpy.pi, phio=1290.0 / vo**2.0, r1=8.0 / ro, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) - pot_nounits( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) ) < 10.0**-8.0 ), "LopsidedDiskPotential w/ parameters w/ units does not behave as expected" # LopsidedDiskPotential, alternative setup pot = potential.LopsidedDiskPotential( amp=1.0, cp=1000.0 * units.km**2 / units.s**2.0, sp=300.0 * units.km**2 / units.s**2.0, r1=8.0 * units.kpc, ro=ro, vo=vo, ) pot_nounits = potential.LopsidedDiskPotential( amp=1.0, cp=1000.0 / vo**2.0, sp=300.0 / vo**2.0, r1=8.0 / ro, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs( pot( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) - pot_nounits( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) ) < 10.0**-8.0 ), "LopsidedDiskPotential w/ parameters w/ units does not behave as expected" # SteadyLogSpiralPotential pot = potential.SteadyLogSpiralPotential( amp=1.0, m=4, omegas=50.0 * units.km / units.s / units.kpc, A=1700.0 * units.km**2 / units.s**2, gamma=21.0 * units.deg, alpha=-9.0, ro=ro, vo=vo, ) pot_nounits = potential.SteadyLogSpiralPotential( amp=1.0, m=4, omegas=50.0 * ro / vo, A=1700.0 / vo**2.0, gamma=21.0 / 180.0 * numpy.pi, alpha=-9.0, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) - pot_nounits( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) ) < 10.0**-8.0 ), "SteadyLogSpiralPotential w/ parameters w/ units does not behave as expected" # SteadyLogSpiralPotential, alternative setup pot = potential.SteadyLogSpiralPotential( amp=1.0, m=4, omegas=50.0 * units.km / units.s / units.kpc, A=1700.0 * units.km**2 / units.s**2, gamma=21.0 * units.deg, p=10.0 * units.deg, ro=ro, vo=vo, ) pot_nounits = potential.SteadyLogSpiralPotential( amp=1.0, m=4, omegas=50.0 * ro / vo, A=1700.0 / vo**2.0, gamma=21.0 / 180.0 * numpy.pi, p=10.0 / 180.0 * numpy.pi, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) - pot_nounits( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) ) < 10.0**-8.0 ), "SteadyLogSpiralPotential w/ parameters w/ units does not behave as expected" # TransientLogSpiralPotential pot = potential.TransientLogSpiralPotential( amp=1.0, m=4, omegas=50.0 * units.km / units.s / units.kpc, A=1700.0 * units.km**2 / units.s**2, gamma=21.0 * units.deg, alpha=-9.0, to=2.0 * units.Gyr, sigma=1.0 * units.Gyr, ro=ro, vo=vo, ) pot_nounits = potential.TransientLogSpiralPotential( amp=1.0, m=4, omegas=50.0 * ro / vo, A=1700.0 / vo**2.0, gamma=21.0 / 180.0 * numpy.pi, alpha=-9.0, to=2.0 / conversion.time_in_Gyr(vo, ro), sigma=1.0 / conversion.time_in_Gyr(vo, ro), ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) - pot_nounits( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) ) < 10.0**-8.0 ), "TransientLogSpiralPotential w/ parameters w/ units does not behave as expected" # TransientLogSpiralPotential, alternative setup pot = potential.TransientLogSpiralPotential( amp=1.0, m=4, omegas=50.0 * units.km / units.s / units.kpc, A=1700.0 * units.km**2 / units.s**2, gamma=21.0 * units.deg, p=10.0 * units.deg, to=2.0 * units.Gyr, sigma=1.0 * units.Gyr, ro=ro, vo=vo, ) pot_nounits = potential.TransientLogSpiralPotential( amp=1.0, m=4, omegas=50.0 * ro / vo, A=1700.0 / vo**2.0, gamma=21.0 / 180.0 * numpy.pi, p=10.0 / 180.0 * numpy.pi, to=2.0 / conversion.time_in_Gyr(vo, ro), sigma=1.0 / conversion.time_in_Gyr(vo, ro), ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs( pot( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) - pot_nounits( 1.5, phi=0.1, t=2.0 / conversion.time_in_Gyr(vo, ro), use_physical=False ) ) < 10.0**-8.0 ), "TransientLogSpiralPotential w/ parameters w/ units does not behave as expected" return None def test_potential_paramunits_1d(): # Test that input units for potential parameters other than the amplitude # behave as expected from galpy import potential from galpy.util import conversion ro, vo = 10.5, 195.0 # KGPotential pot = potential.KGPotential( amp=1.0, K=40.0 * units.Msun / units.pc**2, F=0.02 * units.Msun / units.pc**3, D=200 * units.pc, ro=ro, vo=vo, ) pot_nounits = potential.KGPotential( amp=1.0, K=40.0 / conversion.surfdens_in_msolpc2(vo, ro) * 2.0 * numpy.pi, F=0.02 / conversion.dens_in_msolpc3(vo, ro) * 4.0 * numpy.pi, D=0.2 / ro, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs(pot(1.5, use_physical=False) - pot_nounits(1.5, use_physical=False)) < 10.0**-8.0 ), "KGPotential w/ parameters w/ units does not behave as expected" # KGPotential, alternative setup pot = potential.KGPotential( amp=1.0, K=40.0 * units.Msun / units.pc**2 * constants.G, F=0.02 * units.Msun / units.pc**3 * constants.G, D=200 * units.pc, ro=ro, vo=vo, ) pot_nounits = potential.KGPotential( amp=1.0, K=40.0 / conversion.surfdens_in_msolpc2(vo, ro), F=0.02 / conversion.dens_in_msolpc3(vo, ro), D=0.2 / ro, ro=ro, vo=vo, ) # Check potential assert ( numpy.fabs(pot(1.5, use_physical=False) - pot_nounits(1.5, use_physical=False)) < 10.0**-8.0 ), "KGPotential w/ parameters w/ units does not behave as expected" # IsothermalDiskPotential pot = potential.IsothermalDiskPotential( amp=1.2, sigma=30.0 * units.km / units.s, ro=ro, vo=vo ) pot_nounits = potential.IsothermalDiskPotential( amp=1.2, sigma=30.0 / vo, ro=ro, vo=vo ) # Check potential assert ( numpy.fabs(pot(1.5, use_physical=False) - pot_nounits(1.5, use_physical=False)) < 10.0**-8.0 ), "IsothermalDiskPotential w/ parameters w/ units does not behave as expected" return None def test_potential_paramunits_1d_wrongunits(): # Test that input units for potential amplitudes behave as expected from galpy import potential ro, vo = 9.0, 210.0 # KGPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.KGPotential( amp=1.0, K=40.0 * units.Msun / units.pc**3, F=0.02 * units.Msun / units.pc**3, D=200 * units.pc, ro=ro, vo=vo, ) with pytest.raises(units.UnitConversionError) as excinfo: potential.KGPotential( amp=1.0, K=40.0 * units.Msun / units.pc**2, F=0.02 * units.Msun / units.pc**2, D=200 * units.pc, ro=ro, vo=vo, ) # IsothermalDiskPotential with pytest.raises(units.UnitConversionError) as excinfo: potential.IsothermalDiskPotential(amp=1.0, sigma=10 * units.kpc, ro=ro, vo=vo) return None def test_potential_method_turnphysicalon(): from galpy import potential # 3D pot = potential.BurkertPotential(ro=7.0 * units.kpc) pot.turn_physical_on() assert isinstance(pot(1.1, 0.1), units.Quantity), ( "Potential method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(pot._ro - 7.0) < 10.0**-10.0, ( "Potential method does not work as expected" ) assert numpy.fabs(pot._vo - 220.0) < 10.0**-10.0, ( "Potential method turn_physical_on does not work as expected" ) pot.turn_physical_on(ro=6.0, vo=210.0) assert isinstance(pot(1.1, 0.1), units.Quantity), ( "Potential method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(pot._ro - 6.0) < 10.0**-10.0, ( "Potential method does not work as expected" ) assert numpy.fabs(pot._vo - 210.0) < 10.0**-10.0, ( "Potential method turn_physical_on does not work as expected" ) pot.turn_physical_on(ro=6.0 * units.kpc, vo=210.0 * units.km / units.s) assert isinstance(pot(1.1, 0.1), units.Quantity), ( "Potential method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(pot._ro - 6.0) < 10.0**-10.0, ( "Potential method does not work as expected" ) assert numpy.fabs(pot._vo - 210.0) < 10.0**-10.0, ( "Potential method turn_physical_on does not work as expected" ) # 2D pot = potential.EllipticalDiskPotential(ro=6.0 * units.kpc) pot.turn_physical_on(ro=6.0, vo=210.0) assert isinstance(pot(1.1, phi=0.1), units.Quantity), ( "Potential method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(pot._ro - 6.0) < 10.0**-10.0, ( "Potential method does not work as expected" ) assert numpy.fabs(pot._vo - 210.0) < 10.0**-10.0, ( "Potential method turn_physical_on does not work as expected" ) pot.turn_physical_on(ro=6.0 * units.kpc, vo=210.0 * units.km / units.s) assert isinstance(pot(1.1, phi=0.1), units.Quantity), ( "Potential method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(pot._ro - 6.0) < 10.0**-10.0, ( "Potential method does not work as expected" ) assert numpy.fabs(pot._vo - 210.0) < 10.0**-10.0, ( "Potential method turn_physical_on does not work as expected" ) # 1D pot = potential.KGPotential(ro=5.0 * units.kpc) pot.turn_physical_on(ro=9, vo=230) assert isinstance(pot(1.1), units.Quantity), ( "Potential method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(pot._ro - 9.0) < 10.0**-10.0, ( "Potential method turn_physical_on does not work as expected" ) assert numpy.fabs(pot._vo - 230.0) < 10.0**-10.0, ( "Potential method turn_physical_on does not work as expected" ) pot.turn_physical_on(ro=9 * units.kpc, vo=230 * units.km / units.s) assert isinstance(pot(1.1), units.Quantity), ( "Potential method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(pot._ro - 9.0) < 10.0**-10.0, ( "Potential method turn_physical_on does not work as expected" ) assert numpy.fabs(pot._vo - 230.0) < 10.0**-10.0, ( "Potential method turn_physical_on does not work as expected" ) return None def test_potential_method_turnphysicaloff(): from galpy import potential # 3D pot = potential.BurkertPotential(ro=7.0 * units.kpc) pot.turn_physical_off() assert isinstance(pot(1.1, 0.1), float), ( "Potential method does not return float when turn_physical_off has been called" ) # 2D pot = potential.EllipticalDiskPotential(ro=6.0 * units.kpc) pot.turn_physical_off() assert isinstance(pot(1.1, phi=0.1), float), ( "Potential method does not return float when turn_physical_off has been called" ) # 1D pot = potential.KGPotential(ro=5.0 * units.kpc) pot.turn_physical_off() assert isinstance(pot(1.1), float), ( "Potential method does not return float when turn_physical_off has been called" ) return None def test_potential_function_turnphysicalon(): from galpy import potential # 3D pot = potential.BurkertPotential(ro=7.0 * units.kpc) potential.turn_physical_on(pot) assert isinstance(potential.evaluatePotentials(pot, 1.1, 0.1), units.Quantity), ( "Potential function does not return Quantity when function turn_physical_on has been called" ) assert numpy.fabs(pot._ro - 7.0) < 10.0**-10.0, ( "Potential method does not work as expected" ) pot = potential.BurkertPotential(ro=7.0 * units.kpc) potential.turn_physical_on([pot]) assert isinstance(potential.evaluatePotentials([pot], 1.1, 0.1), units.Quantity), ( "Potential function does not return Quantity when function turn_physical_on has been called" ) assert numpy.fabs(pot._ro - 7.0) < 10.0**-10.0, ( "Potential method does not work as expected" ) assert numpy.fabs(pot._vo - 220.0) < 10.0**-10.0, ( "Potential function turn_physical_on does not work as expected" ) # 2D pot = potential.EllipticalDiskPotential(ro=6.0 * units.kpc) potential.turn_physical_on(pot) assert isinstance( potential.evaluateplanarPotentials(pot, 1.1, phi=0.1), units.Quantity ), ( "Potential function does not return Quantity when function turn_physical_on has been called" ) potential.turn_physical_on([pot], ro=9.0, vo=230.0) assert isinstance( potential.evaluateplanarPotentials([pot], 1.1, phi=0.1), units.Quantity ), ( "Potential function does not return Quantity when function turn_physical_on has been called" ) assert numpy.fabs(pot._ro - 9.0) < 10.0**-10.0, ( "Potential method does not work as expected" ) assert numpy.fabs(pot._vo - 230.0) < 10.0**-10.0, ( "Potential function turn_physical_on does not work as expected" ) # 1D pot = potential.KGPotential(ro=5.0 * units.kpc) potential.turn_physical_on(pot) assert isinstance(potential.evaluatelinearPotentials(pot, 1.1), units.Quantity), ( "Potential function does not return Quantity when function turn_physical_on has been called" ) assert numpy.fabs(pot._ro - 5.0) < 10.0**-10.0, ( "Potential function turn_physical_on does not work as expected" ) assert numpy.fabs(pot._vo - 220.0) < 10.0**-10.0, ( "Potential function turn_physical_on does not work as expected" ) potential.turn_physical_on([pot], ro=6.0 * units.kpc, vo=250.0 * units.km / units.s) assert isinstance(potential.evaluatelinearPotentials([pot], 1.1), units.Quantity), ( "Potential function does not return Quantity when function turn_physical_on has been called" ) assert numpy.fabs(pot._ro - 6.0) < 10.0**-10.0, ( "Potential function turn_physical_on does not work as expected" ) assert numpy.fabs(pot._vo - 250.0) < 10.0**-10.0, ( "Potential function turn_physical_on does not work as expected" ) return None def test_potential_function_turnphysicaloff(): from galpy import potential # 3D pot = potential.BurkertPotential(ro=7.0 * units.kpc) potential.turn_physical_off(pot) assert isinstance(potential.evaluatePotentials(pot, 1.1, 0.1), float), ( "Potential function does not return float when function turn_physical_off has been called" ) pot = potential.BurkertPotential(ro=7.0 * units.kpc) potential.turn_physical_off([pot]) assert isinstance(potential.evaluatePotentials([pot], 1.1, 0.1), float), ( "Potential function does not return float when function turn_physical_off has been called" ) # 2D pot = potential.EllipticalDiskPotential(ro=6.0 * units.kpc) potential.turn_physical_off(pot) assert isinstance(potential.evaluateplanarPotentials(pot, 1.1, phi=0.1), float), ( "Potential function does not return float when function turn_physical_off has been called" ) potential.turn_physical_off([pot]) assert isinstance(potential.evaluateplanarPotentials([pot], 1.1, phi=0.1), float), ( "Potential function does not return float when function turn_physical_off has been called" ) # 1D pot = potential.KGPotential(ro=5.0 * units.kpc) potential.turn_physical_off(pot) assert isinstance(potential.evaluatelinearPotentials(pot, 1.1), float), ( "Potential function does not return float when function turn_physical_off has been called" ) potential.turn_physical_off([pot]) assert isinstance(potential.evaluatelinearPotentials([pot], 1.1), float), ( "Potential function does not return float when function turn_physical_off has been called" ) return None def test_potential_setup_roAsQuantity(): from galpy import potential # 3D pot = potential.BurkertPotential(ro=7.0 * units.kpc) assert numpy.fabs(pot._ro - 7.0) < 10.0**-10.0, ( "ro in 3D potential setup as Quantity does not work as expected" ) # 2D pot = potential.EllipticalDiskPotential(ro=6.0 * units.kpc) assert numpy.fabs(pot._ro - 6.0) < 10.0**-10.0, ( "ro in 2D potential setup as Quantity does not work as expected" ) # 1D pot = potential.KGPotential(ro=5.0 * units.kpc) assert numpy.fabs(pot._ro - 5.0) < 10.0**-10.0, ( "ro in 1D potential setup as Quantity does not work as expected" ) return None def test_potential_setup_roAsQuantity_oddunits(): from galpy import potential # 3D pot = potential.BurkertPotential(ro=7.0 * units.lyr) assert numpy.fabs(pot._ro - 7.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in 3D potential setup as Quantity does not work as expected" ) # 2D pot = potential.EllipticalDiskPotential(ro=6.0 * units.lyr) assert numpy.fabs(pot._ro - 6.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in 2D potential setup as Quantity does not work as expected" ) # 1D pot = potential.KGPotential(ro=5.0 * units.lyr) assert numpy.fabs(pot._ro - 5.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in 1D potential setup as Quantity does not work as expected" ) return None def test_potential_setup_voAsQuantity(): from galpy import potential # 3D pot = potential.BurkertPotential(vo=210.0 * units.km / units.s) assert numpy.fabs(pot._vo - 210.0) < 10.0**-10.0, ( "vo in 3D potential setup as Quantity does not work as expected" ) # 2D pot = potential.EllipticalDiskPotential(vo=230.0 * units.km / units.s) assert numpy.fabs(pot._vo - 230.0) < 10.0**-10.0, ( "vo in 2D potential setup as Quantity does not work as expected" ) # 1D pot = potential.KGPotential(vo=250.0 * units.km / units.s) assert numpy.fabs(pot._vo - 250.0) < 10.0**-10.0, ( "vo in 1D potential setup as Quantity does not work as expected" ) return None def test_potential_setup_voAsQuantity_oddunits(): from galpy import potential # 3D pot = potential.BurkertPotential(vo=210.0 * units.pc / units.Myr) assert ( numpy.fabs(pot._vo - 210.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "vo in 3D potential setup as Quantity does not work as expected" # 2D pot = potential.EllipticalDiskPotential(vo=230.0 * units.pc / units.Myr) assert ( numpy.fabs(pot._vo - 230.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "vo in 2D potential setup as Quantity does not work as expected" # 1D pot = potential.KGPotential(vo=250.0 * units.pc / units.Myr) assert ( numpy.fabs(pot._vo - 250.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "vo in 1D potential setup as Quantity does not work as expected" return None def test_interpRZPotential_ro(): # Test that ro is correctly propagated to interpRZPotential from galpy.potential import BurkertPotential, interpRZPotential ro = 9.0 # ro on, single pot bp = BurkertPotential(ro=ro) ip = interpRZPotential(bp) assert numpy.fabs(ip._ro - bp._ro) < 10.0**-10.0, ( "ro not correctly propagated to interpRZPotential" ) assert ip._roSet, "roSet not correctly propagated to interpRZPotential" # ro on, list pot ip = interpRZPotential([bp]) assert numpy.fabs(ip._ro - bp._ro) < 10.0**-10.0, ( "ro not correctly propagated to interpRZPotential" ) assert ip._roSet, "roSet not correctly propagated to interpRZPotential" # ro off, single pot bp = BurkertPotential() ip = interpRZPotential(bp) assert numpy.fabs(ip._ro - bp._ro) < 10.0**-10.0, ( "ro not correctly propagated to interpRZPotential" ) assert not ip._roSet, "roSet not correctly propagated to interpRZPotential" # ro off, list pot bp = BurkertPotential() ip = interpRZPotential([bp]) assert numpy.fabs(ip._ro - bp._ro) < 10.0**-10.0, ( "ro not correctly propagated to interpRZPotential" ) assert not ip._roSet, "roSet not correctly propagated to interpRZPotential" return None def test_interpRZPotential_vo(): # Test that vo is correctly propagated to interpRZPotential from galpy.potential import BurkertPotential, interpRZPotential vo = 200.0 # vo on, single pot bp = BurkertPotential(vo=vo) ip = interpRZPotential(bp) assert numpy.fabs(ip._vo - bp._vo) < 10.0**-10.0, ( "vo not correctly propagated to interpRZPotential" ) assert ip._voSet, "voSet not correctly propagated to interpRZPotential" # vo on, list pot ip = interpRZPotential([bp]) assert numpy.fabs(ip._vo - bp._vo) < 10.0**-10.0, ( "vo not correctly propagated to interpRZPotential" ) assert ip._voSet, "voSet not correctly propagated to interpRZPotential" # vo off, single pot bp = BurkertPotential() ip = interpRZPotential(bp) assert numpy.fabs(ip._vo - bp._vo) < 10.0**-10.0, ( "vo not correctly propagated to interpRZPotential" ) assert not ip._voSet, "voSet not correctly propagated to interpRZPotential" # vo off, list pot bp = BurkertPotential() ip = interpRZPotential([bp]) assert numpy.fabs(ip._vo - bp._vo) < 10.0**-10.0, ( "vo not correctly propagated to interpRZPotential" ) assert not ip._voSet, "voSet not correctly propagated to interpRZPotential" return None def test_SCFPotential_from_density(): from galpy import potential a = 5.0 * units.kpc hp = potential.HernquistPotential(amp=2 * 1e11 * units.Msun, a=a) # Spherical sp = potential.SCFPotential.from_density( lambda r, **kw: hp.dens(r, 0.0, **kw), 10, a=a, symmetry="spherical" ) rs = numpy.geomspace(1.0, 100.0, 101) * units.kpc assert numpy.all( numpy.fabs( 1.0 - sp.dens(rs, rs, use_physical=False) / hp.dens(rs, rs, use_physical=False) ) < 1e-10 ), "SCF density does not agree when initialized with density with units" assert numpy.all(numpy.fabs(1.0 - sp.dens(rs, rs) / hp.dens(rs, rs)) < 1e-10), ( "SCF density does not agree when initialized with density with units" ) # Output density should have units of density, can just test for Quantity, other tests ensure that this is a density assert isinstance(sp.dens(1.0, 0.1), units.Quantity), ( "SCF density does not return Quantity when initialized with density with units" ) # Axisymmetry, use non-physical input sp = potential.SCFPotential.from_density( lambda R, z: hp.dens(R, z, use_physical=False), 10, L=3, a=a, symmetry="axisym" ) rs = numpy.geomspace(1.0, 100.0, 101) * units.kpc assert numpy.all( numpy.fabs( 1.0 - sp.dens(rs, rs, use_physical=False) / hp.dens(rs, rs, use_physical=False) ) < 1e-10 ), "SCF density does not agree when initialized with density with units" # Output density should not have units of density assert not isinstance(sp.dens(1.0, 0.1), units.Quantity), ( "SCF density does not return Quantity when initialized with density with units" ) # General sp = potential.SCFPotential.from_density( lambda R, z, phi, **kw: hp.dens(R, z, phi=phi, **kw), 10, L=3, a=a ) rs = numpy.geomspace(1.0, 100.0, 101) * units.kpc assert numpy.all( numpy.fabs( 1.0 - sp.dens(rs, rs, use_physical=False) / hp.dens(rs, rs, use_physical=False) ) < 1e-10 ), "SCF density does not agree when initialized with density with units" assert numpy.all(numpy.fabs(1.0 - sp.dens(rs, rs) / hp.dens(rs, rs)) < 1e-10), ( "SCF density does not agree when initialized with density with units" ) # Output density should have units of density, can just test for Quantity, other tests ensure that this is a density assert isinstance(sp.dens(1.0, 0.1), units.Quantity), ( "SCF density does not return Quantity when initialized with density with units" ) return None def test_actionAngle_method_returntype(): from galpy.actionAngle import ( actionAngleAdiabatic, actionAngleHarmonic, actionAngleHarmonicInverse, actionAngleIsochrone, actionAngleIsochroneApprox, actionAngleIsochroneInverse, actionAngleSpherical, actionAngleStaeckel, ) from galpy.potential import IsochronePotential, MWPotential, PlummerPotential # actionAngleHarmonic ip = IsochronePotential(normalize=5.0, b=10000.0) # Omega = sqrt(4piG density / 3) aA = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=8.0, vo=220.0 ) assert isinstance(aA(-0.2, 0.1), units.Quantity), ( "actionAngleHarmonic method __call__ does not return Quantity when it should" ) for ii in range(2): assert isinstance(aA.actionsFreqs(-0.2, 0.1)[ii], units.Quantity), ( "actionAngleHarmonic method actionsFreqs does not return Quantity when it should" ) for ii in range(3): assert isinstance(aA.actionsFreqsAngles(-0.2, 0.1)[ii], units.Quantity), ( "actionAngleHarmonic method actionsFreqsAngles does not return Quantity when it should" ) # actionAngleIsochrone aA = actionAngleIsochrone(b=0.8, ro=8.0, vo=220.0) for ii in range(3): assert isinstance(aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity), ( "actionAngleIsochrone method __call__ does not return Quantity when it should" ) for ii in range(6): assert isinstance( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochrone method actionsFreqs does not return Quantity when it should" ) for ii in range(9): assert isinstance( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochrone method actionsFreqsAngles does not return Quantity when it should" ) for ii in range(3): assert isinstance( aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochrone method EccZmaxRperiRap does not return Quantity when it should" ) # actionAngleSpherical pot = PlummerPotential(normalize=1.0, b=0.7) aA = actionAngleSpherical(pot=pot, ro=8.0, vo=220.0) for ii in range(3): assert isinstance(aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity), ( "actionAngleIsochrone method __call__ does not return Quantity when it should" ) for ii in range(6): assert isinstance( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochrone method actionsFreqs does not return Quantity when it should" ) for ii in range(9): assert isinstance( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochrone method actionsFreqsAngles does not return Quantity when it should" ) for ii in range(3): assert isinstance( aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochrone method EccZmaxRperiRap does not return Quantity when it should" ) # actionAngleAdiabatic aA = actionAngleAdiabatic(pot=MWPotential, ro=8.0, vo=220.0) for ii in range(3): assert isinstance(aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity), ( "actionAngleIsochrone method __call__ does not return Quantity when it should" ) for ii in range(3): assert isinstance( aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochrone method EccZmaxRperiRap does not return Quantity when it should" ) # actionAngleStaeckel aA = actionAngleStaeckel(pot=MWPotential, delta=0.45, ro=8.0, vo=220.0) for ii in range(3): assert isinstance(aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity), ( "actionAngleIsochrone method __call__ does not return Quantity when it should" ) for ii in range(6): assert isinstance( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochrone method actionsFreqs does not return Quantity when it should" ) for ii in range(9): assert isinstance( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochrone method actionsFreqsAngles does not return Quantity when it should" ) for ii in range(3): assert isinstance( aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochrone method EccZmaxRperiRap does not return Quantity when it should" ) # actionAngleIsochroneApprox aA = actionAngleIsochroneApprox(pot=MWPotential, b=0.8, ro=8.0, vo=220.0) for ii in range(3): assert isinstance(aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity), ( "actionAngleIsochrone method __call__ does not return Quantity when it should" ) for ii in range(6): assert isinstance( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochrone method actionsFreqs does not return Quantity when it should" ) for ii in range(9): assert isinstance( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochrone method actionsFreqsAngles does not return Quantity when it should" ) # actionAngleHarmonicInverse ip = IsochronePotential(normalize=5.0, b=10000.0) # Omega = sqrt(4piG density / 3) aA = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=8.0, vo=220.0 ) for ii in range(2): assert isinstance(aA(0.1, -0.2)[ii], units.Quantity), ( "actionAngleHarmonicInverse method __call__ does not return Quantity when it should" ) for ii in range(3): assert isinstance(aA.xvFreqs(0.1, -0.2)[ii], units.Quantity), ( "actionAngleHarmonicInverse method xvFreqs does not return Quantity when it should" ) assert isinstance(aA.Freqs(0.1), units.Quantity), ( "actionAngleIsochroneInverse method Freqs does not return Quantity when it should" ) # actionAngleIsochroneInverse aA = actionAngleIsochroneInverse(b=0.8, ro=8.0, vo=220.0) for ii in range(6): assert isinstance(aA(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii], units.Quantity), ( "actionAngleIsochroneInverse method __call__ does not return Quantity when it should" ) for ii in range(9): assert isinstance( aA.xvFreqs(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii], units.Quantity ), ( "actionAngleIsochroneInverse method xvFreqs does not return Quantity when it should" ) for ii in range(3): assert isinstance(aA.Freqs(0.1, 1.1, 0.1)[ii], units.Quantity), ( "actionAngleIsochroneInverse method Freqs does not return Quantity when it should" ) return None def test_actionAngle_method_returnunit(): from galpy.actionAngle import ( actionAngleAdiabatic, actionAngleHarmonic, actionAngleHarmonicInverse, actionAngleIsochrone, actionAngleIsochroneApprox, actionAngleIsochroneInverse, actionAngleSpherical, actionAngleStaeckel, ) from galpy.potential import IsochronePotential, MWPotential, PlummerPotential # actionAngleHarmonic ip = IsochronePotential(normalize=5.0, b=10000.0) # Omega = sqrt(4piG density / 3) aA = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=8.0, vo=220.0 ) try: aA(-0.2, 0.1).to(units.kpc * units.km / units.s) except units.UnitConversionError: raise AssertionError( "actionAngle function __call__ does not return Quantity with the right units" ) try: aA.actionsFreqs(-0.2, 0.1)[0].to(units.kpc * units.km / units.s) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqs does not return Quantity with the right units" ) try: aA.actionsFreqs(-0.2, 0.1)[1].to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqs does not return Quantity with the right units" ) try: aA.actionsFreqsAngles(-0.2, 0.1)[0].to(units.kpc * units.km / units.s) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) try: aA.actionsFreqsAngles(-0.2, 0.1)[1].to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) try: aA.actionsFreqsAngles(-0.2, 0.1)[2].to(units.rad) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) # actionAngleIsochrone aA = actionAngleIsochrone(b=0.8, ro=8.0, vo=220.0) for ii in range(3): try: aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc * units.km / units.s) except units.UnitConversionError: raise AssertionError( "actionAngle function __call__ does not return Quantity with the right units" ) for ii in range(3): try: aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to( units.kpc * units.km / units.s ) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqs does not return Quantity with the right units" ) for ii in range(3, 6): try: aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqs does not return Quantity with the right units" ) for ii in range(3): try: aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to( units.kpc * units.km / units.s ) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) for ii in range(3, 6): try: aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) for ii in range(6, 9): try: aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.rad) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) try: aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0].to( units.dimensionless_unscaled ) except units.UnitConversionError: raise AssertionError( "actionAngle function EccZmaxRperiRap does not return Quantity with the right units" ) for ii in range(1, 4): try: aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc) except units.UnitConversionError: raise AssertionError( "actionAngle function EccZmaxRperiRap does not return Quantity with the right units" ) # actionAngleSpherical pot = PlummerPotential(normalize=1.0, b=0.7) aA = actionAngleSpherical(pot=pot, ro=8.0, vo=220.0) for ii in range(3): try: aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc * units.km / units.s) except units.UnitConversionError: raise AssertionError( "actionAngle function __call__ does not return Quantity with the right units" ) for ii in range(3): try: aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to( units.kpc * units.km / units.s ) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqs does not return Quantity with the right units" ) for ii in range(3, 6): try: aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqs does not return Quantity with the right units" ) for ii in range(3): try: aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to( units.kpc * units.km / units.s ) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) for ii in range(3, 6): try: aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) for ii in range(6, 9): try: aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.rad) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) try: aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0].to( units.dimensionless_unscaled ) except units.UnitConversionError: raise AssertionError( "actionAngle function EccZmaxRperiRap does not return Quantity with the right units" ) for ii in range(1, 4): try: aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc) except units.UnitConversionError: raise AssertionError( "actionAngle function EccZmaxRperiRap does not return Quantity with the right units" ) # actionAngleAdiabatic aA = actionAngleAdiabatic(pot=MWPotential, ro=8.0, vo=220.0) for ii in range(3): try: aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc * units.km / units.s) except units.UnitConversionError: raise AssertionError( "actionAngle function __call__ does not return Quantity with the right units" ) try: aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0].to( units.dimensionless_unscaled ) except units.UnitConversionError: raise AssertionError( "actionAngle function EccZmaxRperiRap does not return Quantity with the right units" ) for ii in range(1, 4): try: aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc) except units.UnitConversionError: raise AssertionError( "actionAngle function EccZmaxRperiRap does not return Quantity with the right units" ) # actionAngleStaeckel aA = actionAngleStaeckel(pot=MWPotential, delta=0.45, ro=8.0, vo=220.0) for ii in range(3): try: aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc * units.km / units.s) except units.UnitConversionError: raise AssertionError( "actionAngle function __call__ does not return Quantity with the right units" ) for ii in range(3): try: aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to( units.kpc * units.km / units.s ) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqs does not return Quantity with the right units" ) for ii in range(3, 6): try: aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqs does not return Quantity with the right units" ) for ii in range(3): try: aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to( units.kpc * units.km / units.s ) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) for ii in range(3, 6): try: aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) for ii in range(6, 9): try: aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.rad) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) try: aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0].to( units.dimensionless_unscaled ) except units.UnitConversionError: raise AssertionError( "actionAngle function EccZmaxRperiRap does not return Quantity with the right units" ) for ii in range(1, 4): try: aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc) except units.UnitConversionError: raise AssertionError( "actionAngle function EccZmaxRperiRap does not return Quantity with the right units" ) # actionAngleIsochroneApprox aA = actionAngleIsochroneApprox(pot=MWPotential, b=0.8, ro=8.0, vo=220.0) for ii in range(3): try: aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc * units.km / units.s) except units.UnitConversionError: raise AssertionError( "actionAngle function __call__ does not return Quantity with the right units" ) for ii in range(3): try: aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to( units.kpc * units.km / units.s ) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqs does not return Quantity with the right units" ) for ii in range(3, 6): try: aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqs does not return Quantity with the right units" ) for ii in range(3): try: aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to( units.kpc * units.km / units.s ) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) for ii in range(3, 6): try: aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) for ii in range(6, 9): try: aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.rad) except units.UnitConversionError: raise AssertionError( "actionAngle function actionsFreqsAngles does not return Quantity with the right units" ) # actionAngleHarmonicInverse ip = IsochronePotential(normalize=5.0, b=10000.0) # Omega = sqrt(4piG density / 3) aA = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=8.0, vo=220.0 ) correct_unit = [units.m, units.m / units.s] for ii in range(2): try: aA(0.1, -0.2)[ii].to(correct_unit[ii]) except units.UnitConversionError: raise AssertionError( "actionAngleInverse function __call__ does not return Quantity with the right units" ) correct_unit = [units.m, units.m / units.s, 1 / units.Gyr] for ii in range(3): try: aA.xvFreqs(0.1, -0.2)[ii].to(correct_unit[ii]) except units.UnitConversionError: raise AssertionError( "actionAngleInverse function actionsFreqs does not return Quantity with the right units" ) try: aA.Freqs(0.1).to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "actionAngleInverse function Freqs does not return Quantity with the right units" ) # actionAngleIsochroneInverse aA = actionAngleIsochroneInverse(b=0.8, ro=8.0, vo=220.0) correct_unit = [ units.m, units.m / units.s, units.m / units.s, units.m, units.m / units.s, units.deg, ] for ii in range(6): try: aA(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii].to(correct_unit[ii]) except units.UnitConversionError: raise AssertionError( "actionAngleInverse function __call__ does not return Quantity with the right units" ) correct_unit = [ units.m, units.m / units.s, units.m / units.s, units.m, units.m / units.s, units.deg, 1 / units.Gyr, 1 / units.Gyr, 1 / units.Gyr, ] for ii in range(9): try: aA.xvFreqs(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii].to(correct_unit[ii]) except units.UnitConversionError: raise AssertionError( "actionAngleInverse function actionsFreqs does not return Quantity with the right units" ) for ii in range(3): try: aA.Freqs(0.1, 1.1, 0.1)[ii].to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "actionAngleInverse function Freqs does not return Quantity with the right units" ) return None def test_actionAngle_method_value(): from galpy.actionAngle import ( actionAngleAdiabatic, actionAngleHarmonic, actionAngleHarmonicInverse, actionAngleIsochrone, actionAngleIsochroneApprox, actionAngleIsochroneInverse, actionAngleSpherical, actionAngleStaeckel, ) from galpy.potential import IsochronePotential, MWPotential, PlummerPotential from galpy.util import conversion ro, vo = 9.0, 230.0 # actionAngleHarmonic ip = IsochronePotential(normalize=5.0, b=10000.0) # Omega = sqrt(4piG density / 3) aA = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=ro, vo=vo ) aAnu = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) assert ( numpy.fabs( aA(-0.2, 0.1).to(units.kpc * units.km / units.s).value - aAnu(-0.2, 0.1) * ro * vo ) < 10.0**-8.0 ), "actionAngle function __call__ does not return Quantity with the right value" assert ( numpy.fabs( aA.actionsFreqs(-0.2, 0.1)[0].to(units.kpc * units.km / units.s).value - aAnu.actionsFreqs(-0.2, 0.1)[0] * ro * vo ) < 10.0**-8.0 ), "actionAngle function actionsFreqs does not return Quantity with the right value" assert ( numpy.fabs( aA.actionsFreqs(-0.2, 0.1)[1].to(1 / units.Gyr).value - aAnu.actionsFreqs(-0.2, 0.1)[1] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "actionAngle function actionsFreqs does not return Quantity with the right value" assert ( numpy.fabs( aA.actionsFreqsAngles(-0.2, 0.1)[0].to(units.kpc * units.km / units.s).value - aAnu.actionsFreqsAngles(-0.2, 0.1)[0] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) assert ( numpy.fabs( aA.actionsFreqsAngles(-0.2, 0.1)[1].to(1 / units.Gyr).value - aAnu.actionsFreqsAngles(-0.2, 0.1)[1] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) assert ( numpy.fabs( aA.actionsFreqsAngles(-0.2, 0.1)[2].to(units.rad).value - aAnu.actionsFreqsAngles(-0.2, 0.1)[2] ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) # actionAngleIsochrone aA = actionAngleIsochrone(b=0.8, ro=ro, vo=vo) aAnu = actionAngleIsochrone(b=0.8) for ii in range(3): assert ( numpy.fabs( aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.kpc * units.km / units.s) .value - aAnu(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), "actionAngle function __call__ does not return Quantity with the right value" for ii in range(3): assert ( numpy.fabs( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.kpc * units.km / units.s) .value - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(1 / units.Gyr) .value - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.kpc * units.km / units.s) .value - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(1 / units.Gyr) .value - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) for ii in range(6, 9): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.rad) .value - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) assert ( numpy.fabs( aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0] .to(units.dimensionless_unscaled) .value - aAnu.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0] ) < 10.0**-8.0 ), ( "actionAngle function EccZmaxRperiRap does not return Quantity with the right value" ) for ii in range(1, 4): assert ( numpy.fabs( aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc).value - aAnu.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro ) < 10.0**-8.0 ), ( "actionAngle function EccZmaxRperiRap does not return Quantity with the right value" ) # actionAngleSpherical pot = PlummerPotential(normalize=1.0, b=0.7) aA = actionAngleSpherical(pot=pot, ro=ro, vo=vo) aAnu = actionAngleSpherical(pot=pot) for ii in range(3): assert ( numpy.fabs( aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0, ro=9.0 * units.kpc)[ii] .to(units.kpc * units.km / units.s) .value - aAnu(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * 9.0 * vo ) < 10.0**-8.0 ), "actionAngle function __call__ does not return Quantity with the right value" for ii in range(3): assert ( numpy.fabs( aA.actionsFreqs( 1.1, 0.1, 1.1, 0.1, 0.2, 0.0, vo=230.0 * units.km / units.s )[ii] .to(units.kpc * units.km / units.s) .value - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * 230.0 ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(1 / units.Gyr) .value - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.kpc * units.km / units.s) .value - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(1 / units.Gyr) .value - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) for ii in range(6, 9): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.rad) .value - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) assert ( numpy.fabs( aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0] .to(units.dimensionless_unscaled) .value - aAnu.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0] ) < 10.0**-8.0 ), ( "actionAngle function EccZmaxRperiRap does not return Quantity with the right value" ) for ii in range(1, 4): assert ( numpy.fabs( aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc).value - aAnu.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro ) < 10.0**-8.0 ), ( "actionAngle function EccZmaxRperiRap does not return Quantity with the right value" ) # actionAngleAdiabatic aA = actionAngleAdiabatic(pot=MWPotential, ro=ro, vo=vo) aAnu = actionAngleAdiabatic(pot=MWPotential) for ii in range(3): assert ( numpy.fabs( aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.kpc * units.km / units.s) .value - aAnu(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), "actionAngle function __call__ does not return Quantity with the right value" assert ( numpy.fabs( aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0] .to(units.dimensionless_unscaled) .value - aAnu.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0] ) < 10.0**-8.0 ), ( "actionAngle function EccZmaxRperiRap does not return Quantity with the right value" ) for ii in range(1, 4): assert ( numpy.fabs( aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc).value - aAnu.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro ) < 10.0**-8.0 ), ( "actionAngle function EccZmaxRperiRap does not return Quantity with the right value" ) # actionAngleStaeckel aA = actionAngleStaeckel(pot=MWPotential, delta=0.45, ro=ro, vo=vo) aAnu = actionAngleStaeckel(pot=MWPotential, delta=0.45) for ii in range(3): assert ( numpy.fabs( aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.kpc * units.km / units.s) .value - aAnu(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), "actionAngle function __call__ does not return Quantity with the right value" for ii in range(3): assert ( numpy.fabs( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.kpc * units.km / units.s) .value - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(1 / units.Gyr) .value - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.kpc * units.km / units.s) .value - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(1 / units.Gyr) .value - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) for ii in range(6, 9): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.rad) .value - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) assert ( numpy.fabs( aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0] .to(units.dimensionless_unscaled) .value - aAnu.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0] ) < 10.0**-8.0 ), ( "actionAngle function EccZmaxRperiRap does not return Quantity with the right value" ) for ii in range(1, 4): assert ( numpy.fabs( aA.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii].to(units.kpc).value - aAnu.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro ) < 10.0**-8.0 ), ( "actionAngle function EccZmaxRperiRap does not return Quantity with the right value" ) # actionAngleIsochroneApprox aA = actionAngleIsochroneApprox(pot=MWPotential, b=0.8, ro=ro, vo=vo) aAnu = actionAngleIsochroneApprox(pot=MWPotential, b=0.8) for ii in range(3): assert ( numpy.fabs( aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.kpc * units.km / units.s) .value - aAnu(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), "actionAngle function __call__ does not return Quantity with the right value" for ii in range(3): assert ( numpy.fabs( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.kpc * units.km / units.s) .value - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(1 / units.Gyr) .value - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqs does not return Quantity with the right value" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.kpc * units.km / units.s) .value - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * ro * vo ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(1 / units.Gyr) .value - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) for ii in range(6, 9): assert ( numpy.fabs( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] .to(units.rad) .value - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle function actionsFreqsAngles does not return Quantity with the right value" ) # actionAngleHarmonicInverse ip = IsochronePotential(normalize=5.0, b=10000.0) # Omega = sqrt(4piG density / 3) aA = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=ro * units.kpc, vo=vo * units.km / units.s, ) aAnu = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) correct_unit = [units.kpc, units.km / units.s] correct_fac = [ro, vo] for ii in range(2): assert ( numpy.fabs( aA(0.1, -0.2, ro=ro * units.kpc, vo=vo * units.km / units.s)[ii] .to(correct_unit[ii]) .value - aAnu(0.1, -0.2)[ii] * correct_fac[ii] ) < 10.0**-8.0 ), ( "actionAngleInverse function __call__ does not return Quantity with the right value" ) correct_unit = [units.kpc, units.km / units.s, 1 / units.Gyr] correct_fac = [ro, vo, conversion.freq_in_Gyr(vo, ro)] for ii in range(3): assert ( numpy.fabs( aA.xvFreqs(0.1, -0.2)[ii].to(correct_unit[ii]).value - aAnu.xvFreqs(0.1, -0.2)[ii] * correct_fac[ii] ) < 10.0**-8.0 ), ( "actionAngleInverse function xvFreqs does not return Quantity with the right value" ) assert ( numpy.fabs( aA.Freqs(0.1).to(1 / units.Gyr).value - aAnu.Freqs(0.1) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "actionAngleInverse function Freqs does not return Quantity with the right value" # actionAngleIsochroneInverse aA = actionAngleIsochroneInverse( b=0.8, ro=ro * units.kpc, vo=vo * units.km / units.s ) aAnu = actionAngleIsochroneInverse(b=0.8) correct_unit = [ units.kpc, units.km / units.s, units.km / units.s, units.kpc, units.km / units.s, units.rad, ] correct_fac = [ro, vo, vo, ro, vo, 1.0] for ii in range(6): assert ( numpy.fabs( aA( 0.1, 1.1, 0.1, 0.1, 0.2, 0.0, ro=ro * units.kpc, vo=vo * units.km / units.s, )[ii] .to(correct_unit[ii]) .value - aAnu(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii] * correct_fac[ii] ) < 10.0**-8.0 ), ( "actionAngleInverse function __call__ does not return Quantity with the right value" ) correct_unit = [ units.kpc, units.km / units.s, units.km / units.s, units.kpc, units.km / units.s, units.rad, 1 / units.Gyr, 1 / units.Gyr, 1 / units.Gyr, ] correct_fac = [ ro, vo, vo, ro, vo, 1.0, conversion.freq_in_Gyr(vo, ro), conversion.freq_in_Gyr(vo, ro), conversion.freq_in_Gyr(vo, ro), ] for ii in range(9): assert ( numpy.fabs( aA.xvFreqs(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii].to(correct_unit[ii]).value - aAnu.xvFreqs(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii] * correct_fac[ii] ) < 10.0**-8.0 ), ( "actionAngleInverse function xvFreqs does not return Quantity with the right value" ) for ii in range(3): assert ( numpy.fabs( aA.Freqs(0.1, 1.1, 0.1)[ii].to(1 / units.Gyr).value - aAnu.Freqs(0.1, 1.1, 0.1)[ii] * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), ( "actionAngleInverse function Freqs does not return Quantity with the right value" ) return None def test_actionAngle_setup_roAsQuantity(): from galpy.actionAngle import ( actionAngleAdiabatic, actionAngleHarmonic, actionAngleHarmonicInverse, actionAngleIsochrone, actionAngleIsochroneApprox, actionAngleIsochroneInverse, actionAngleSpherical, actionAngleStaeckel, ) from galpy.potential import IsochronePotential, MWPotential, PlummerPotential # actionAngleHarmonicc ip = IsochronePotential(normalize=5.0, b=10000.0) aA = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=7.0 * units.kpc ) assert numpy.fabs(aA._ro - 7.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleIsochrone aA = actionAngleIsochrone(b=0.8, ro=7.0 * units.kpc) assert numpy.fabs(aA._ro - 7.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleSpherical pot = PlummerPotential(normalize=1.0, b=0.7) aA = actionAngleSpherical(pot=pot, ro=7.0 * units.kpc) assert numpy.fabs(aA._ro - 7.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleAdiabatic aA = actionAngleAdiabatic(pot=MWPotential, ro=9.0 * units.kpc) assert numpy.fabs(aA._ro - 9.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleStaeckel aA = actionAngleStaeckel(pot=MWPotential, delta=0.45, ro=7.0 * units.kpc) assert numpy.fabs(aA._ro - 7.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleIsochroneApprox aA = actionAngleIsochroneApprox(pot=MWPotential, b=0.8, ro=7.0 * units.kpc) assert numpy.fabs(aA._ro - 7.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleHarmonicInverse ip = IsochronePotential(normalize=5.0, b=10000.0) aA = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=7.0 * units.kpc ) assert numpy.fabs(aA._ro - 7.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleIsochroneInverse aA = actionAngleIsochroneInverse(b=0.8, ro=7.0 * units.kpc) assert numpy.fabs(aA._ro - 7.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) return None def test_actionAngle_setup_roAsQuantity_oddunits(): from galpy.actionAngle import ( actionAngleAdiabatic, actionAngleHarmonic, actionAngleHarmonicInverse, actionAngleIsochrone, actionAngleIsochroneApprox, actionAngleIsochroneInverse, actionAngleSpherical, actionAngleStaeckel, ) from galpy.potential import IsochronePotential, MWPotential, PlummerPotential # actionAngleHarmonic ip = IsochronePotential(normalize=5.0, b=10000.0) aA = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=7.0 * units.lyr ) assert numpy.fabs(aA._ro - 7.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleIsochrone aA = actionAngleIsochrone(b=0.8, ro=7.0 * units.lyr) assert numpy.fabs(aA._ro - 7.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleSpherical pot = PlummerPotential(normalize=1.0, b=0.7) aA = actionAngleSpherical(pot=pot, ro=7.0 * units.lyr) assert numpy.fabs(aA._ro - 7.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleAdiabatic aA = actionAngleAdiabatic(pot=MWPotential, ro=7.0 * units.lyr) assert numpy.fabs(aA._ro - 7.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleStaeckel aA = actionAngleStaeckel(pot=MWPotential, delta=0.45, ro=7.0 * units.lyr) assert numpy.fabs(aA._ro - 7.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleIsochroneApprox aA = actionAngleIsochroneApprox(pot=MWPotential, b=0.8, ro=7.0 * units.lyr) assert numpy.fabs(aA._ro - 7.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleHarmonicInverse ip = IsochronePotential(normalize=5.0, b=10000.0) aA = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=7.0 * units.lyr ) assert numpy.fabs(aA._ro - 7.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleIsochroneInverse aA = actionAngleIsochroneInverse(b=0.8, ro=7.0 * units.lyr) assert numpy.fabs(aA._ro - 7.0 * units.lyr.to(units.kpc)) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) return None def test_actionAngle_setup_voAsQuantity(): from galpy.actionAngle import ( actionAngleAdiabatic, actionAngleHarmonic, actionAngleHarmonicInverse, actionAngleIsochrone, actionAngleIsochroneApprox, actionAngleIsochroneInverse, actionAngleSpherical, actionAngleStaeckel, ) from galpy.potential import IsochronePotential, MWPotential, PlummerPotential # actionAngleHarmonic ip = IsochronePotential(normalize=5.0, b=10000.0) aA = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), vo=230.0 * units.km / units.s, ) assert numpy.fabs(aA._vo - 230.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleIsochrone aA = actionAngleIsochrone(b=0.8, vo=230.0 * units.km / units.s) assert numpy.fabs(aA._vo - 230.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleSpherical pot = PlummerPotential(normalize=1.0, b=0.7) aA = actionAngleSpherical(pot=pot, vo=230.0 * units.km / units.s) assert numpy.fabs(aA._vo - 230.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleAdiabatic aA = actionAngleAdiabatic(pot=MWPotential, ro=9.0 * units.kpc) assert numpy.fabs(aA._ro - 9.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleStaeckel aA = actionAngleStaeckel(pot=MWPotential, delta=0.45, vo=230.0 * units.km / units.s) assert numpy.fabs(aA._vo - 230.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleIsochroneApprox aA = actionAngleIsochroneApprox( pot=MWPotential, b=0.8, vo=230.0 * units.km / units.s ) assert numpy.fabs(aA._vo - 230.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleHarmonicInverse ip = IsochronePotential(normalize=5.0, b=10000.0) aA = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), vo=230.0 * units.km / units.s, ) assert numpy.fabs(aA._vo - 230.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleIsochroneInverse aA = actionAngleIsochroneInverse(b=0.8, vo=230.0 * units.km / units.s) assert numpy.fabs(aA._vo - 230.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) return None def test_actionAngle_setup_voAsQuantity_oddunits(): from galpy.actionAngle import ( actionAngleAdiabatic, actionAngleHarmonic, actionAngleHarmonicInverse, actionAngleIsochrone, actionAngleIsochroneApprox, actionAngleIsochroneInverse, actionAngleSpherical, actionAngleStaeckel, ) from galpy.potential import IsochronePotential, MWPotential, PlummerPotential # actionAngleHarmonic ip = IsochronePotential(normalize=5.0, b=10000.0) aA = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), vo=230.0 * units.pc / units.Myr, ) assert ( numpy.fabs(aA._vo - 230.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "ro in actionAngle setup as Quantity does not work as expected" # actionAngleIsochrone aA = actionAngleIsochrone(b=0.8, vo=230.0 * units.pc / units.Myr) assert ( numpy.fabs(aA._vo - 230.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "ro in actionAngle setup as Quantity does not work as expected" # actionAngleSpherical pot = PlummerPotential(normalize=1.0, b=0.7) aA = actionAngleSpherical(pot=pot, vo=230.0 * units.pc / units.Myr) assert ( numpy.fabs(aA._vo - 230.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "ro in actionAngle setup as Quantity does not work as expected" # actionAngleAdiabatic aA = actionAngleAdiabatic(pot=MWPotential, ro=9.0 * units.kpc) assert numpy.fabs(aA._ro - 9.0) < 10.0**-10.0, ( "ro in actionAngle setup as Quantity does not work as expected" ) # actionAngleStaeckel aA = actionAngleStaeckel( pot=MWPotential, delta=0.45, vo=230.0 * units.pc / units.Myr ) assert ( numpy.fabs(aA._vo - 230.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "ro in actionAngle setup as Quantity does not work as expected" # actionAngleIsochroneApprox aA = actionAngleIsochroneApprox( pot=MWPotential, b=0.8, vo=230.0 * units.pc / units.Myr ) assert ( numpy.fabs(aA._vo - 230.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "ro in actionAngle setup as Quantity does not work as expected" # actionAngleHarmonicInverse ip = IsochronePotential(normalize=5.0, b=10000.0) aA = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), vo=230.0 * units.pc / units.Myr, ) assert ( numpy.fabs(aA._vo - 230.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "ro in actionAngle setup as Quantity does not work as expected" # actionAngleIsochroneInverse aA = actionAngleIsochroneInverse(b=0.8, vo=230.0 * units.pc / units.Myr) assert ( numpy.fabs(aA._vo - 230.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "ro in actionAngle setup as Quantity does not work as expected" return None def test_actionAngle_method_turnphysicalon(): from galpy.actionAngle import actionAngleIsochrone aA = actionAngleIsochrone(b=0.8, ro=7.0 * units.kpc, vo=230.0 * units.km / units.s) aA.turn_physical_on() assert isinstance(aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0], units.Quantity), ( "actionAngle method does not return Quantity when turn_physical_on has been called" ) assert isinstance( aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0], units.Quantity ), ( "actionAngle method does not return Quantity when turn_physical_on has been called" ) assert isinstance( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0], units.Quantity ), ( "actionAngle method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(aA._ro - 7.0) < 10.0**-10.0, ( "actionAngle method does not work as expected" ) assert numpy.fabs(aA._vo - 230.0) < 10.0**-10.0, ( "actionAngle method turn_physical_on does not work as expected" ) aA.turn_physical_on(ro=8.0) assert isinstance(aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0], units.Quantity), ( "actionAngle method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(aA._ro - 8.0) < 10.0**-10.0, ( "actionAngle method does not work as expected" ) assert numpy.fabs(aA._vo - 230.0) < 10.0**-10.0, ( "actionAngle method turn_physical_on does not work as expected" ) aA.turn_physical_on(vo=210.0) assert isinstance(aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0], units.Quantity), ( "actionAngle method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(aA._ro - 8.0) < 10.0**-10.0, ( "actionAngle method does not work as expected" ) assert numpy.fabs(aA._vo - 210.0) < 10.0**-10.0, ( "actionAngle method turn_physical_on does not work as expected" ) aA.turn_physical_on(ro=9.0 * units.kpc) assert isinstance(aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0], units.Quantity), ( "actionAngle method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(aA._ro - 9.0) < 10.0**-10.0, ( "actionAngle method does not work as expected" ) assert numpy.fabs(aA._vo - 210.0) < 10.0**-10.0, ( "actionAngle method turn_physical_on does not work as expected" ) aA.turn_physical_on(vo=200.0 * units.km / units.s) assert isinstance(aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0], units.Quantity), ( "actionAngle method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(aA._ro - 9.0) < 10.0**-10.0, ( "actionAngle method does not work as expected" ) assert numpy.fabs(aA._vo - 200.0) < 10.0**-10.0, ( "actionAngle method turn_physical_on does not work as expected" ) return None def test_actionAngle_method_turnphysicaloff(): from galpy.actionAngle import actionAngleIsochrone aA = actionAngleIsochrone(b=0.8, ro=7.0 * units.kpc, vo=230.0 * units.km / units.s) aA.turn_physical_off() assert isinstance(aA(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0][0], float), ( "actionAngle method does not return float when turn_physical_off has been called" ) assert isinstance(aA.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0][0], float), ( "actionAngle method does not return float when turn_physical_off has been called" ) assert isinstance( aA.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[0][0], float ), "actionAngle method does not return float when turn_physical_off has been called" return None def test_actionAngleHarmonic_setup_omega_units(): from galpy.actionAngle import actionAngleHarmonic from galpy.util import conversion ro, vo = 9.0, 230.0 aA = actionAngleHarmonic(omega=0.1 / units.Gyr, ro=ro, vo=vo) aAu = actionAngleHarmonic(omega=0.1 / conversion.freq_in_Gyr(vo, ro)) assert numpy.fabs(aA._omega - aAu._omega) < 10.0**-10.0, ( "omega with units in actionAngleHarmonic setup does not work as expected" ) return None def test_actionAngleHarmonicInverse_setup_omega_units(): from galpy.actionAngle import actionAngleHarmonicInverse from galpy.util import conversion ro, vo = 9.0, 230.0 aA = actionAngleHarmonicInverse(omega=0.1 / units.Gyr, ro=ro, vo=vo) aAu = actionAngleHarmonicInverse(omega=0.1 / conversion.freq_in_Gyr(vo, ro)) assert numpy.fabs(aA._omega - aAu._omega) < 10.0**-10.0, ( "omega with units in actionAngleHarmonic setup does not work as expected" ) return None def test_actionAngleStaeckel_setup_delta_units(): from galpy.actionAngle import actionAngleStaeckel from galpy.potential import MWPotential ro = 9.0 aA = actionAngleStaeckel(pot=MWPotential, delta=0.45 * ro * units.kpc, ro=ro) aAu = actionAngleStaeckel(pot=MWPotential, delta=0.45) assert numpy.fabs(aA._delta - aAu._delta) < 10.0**-10.0, ( "delta with units in actionAngleStaeckel setup does not work as expected" ) return None def test_actionAngleStaeckelGrid_setup_delta_units(): from galpy.actionAngle import actionAngleStaeckelGrid from galpy.potential import MWPotential ro = 9.0 aA = actionAngleStaeckelGrid( pot=MWPotential, delta=0.45 * ro * units.kpc, ro=ro, nE=5, npsi=5, nLz=5 ) aAu = actionAngleStaeckelGrid(pot=MWPotential, delta=0.45, nE=5, npsi=5, nLz=5) assert numpy.fabs(aA._delta - aAu._delta) < 10.0**-10.0, ( "delta with units in actionAngleStaeckel setup does not work as expected" ) return None def test_actionAngleIsochrone_setup_b_units(): from galpy.actionAngle import actionAngleIsochrone ro = 9.0 aA = actionAngleIsochrone(b=0.7 * ro * units.kpc, ro=ro) aAu = actionAngleIsochrone(b=0.7) assert numpy.fabs(aA.b - aAu.b) < 10.0**-10.0, ( "b with units in actionAngleIsochrone setup does not work as expected" ) return None def test_actionAngleIsochroneApprox_setup_b_units(): from galpy.actionAngle import actionAngleIsochroneApprox from galpy.potential import MWPotential ro = 9.0 aA = actionAngleIsochroneApprox(pot=MWPotential, b=0.7 * ro * units.kpc, ro=ro) aAu = actionAngleIsochroneApprox(pot=MWPotential, b=0.7) assert numpy.fabs(aA._aAI.b - aAu._aAI.b) < 10.0**-10.0, ( "b with units in actionAngleIsochroneApprox setup does not work as expected" ) return None def test_actionAngleIsochroneInverse_setup_b_units(): from galpy.actionAngle import actionAngleIsochroneInverse from galpy.potential import MWPotential ro = 9.0 aA = actionAngleIsochroneInverse(pot=MWPotential, b=0.7 * ro * units.kpc, ro=ro) aAu = actionAngleIsochroneInverse(pot=MWPotential, b=0.7) assert numpy.fabs(aA.b - aAu.b) < 10.0**-10.0, ( "b with units in actionAngleIsochroneInverse setup does not work as expected" ) return None def test_actionAngleIsochroneApprix_setup_tintJ_units(): from galpy.actionAngle import actionAngleIsochroneApprox from galpy.potential import MWPotential from galpy.util import conversion ro = 9.0 vo = 230.0 aA = actionAngleIsochroneApprox( pot=MWPotential, b=0.7, tintJ=11.0 * units.Gyr, ro=ro, vo=vo ) aAu = actionAngleIsochroneApprox( pot=MWPotential, b=0.7, tintJ=11.0 / conversion.time_in_Gyr(vo, ro) ) assert numpy.fabs(aA._tintJ - aAu._tintJ) < 10.0**-10.0, ( "tintJ with units in actionAngleIsochroneApprox setup does not work as expected" ) return None def test_actionAngle_method_inputAsQuantity(): from galpy.actionAngle import ( actionAngleAdiabatic, actionAngleHarmonic, actionAngleHarmonicInverse, actionAngleIsochrone, actionAngleIsochroneApprox, actionAngleIsochroneInverse, actionAngleSpherical, actionAngleStaeckel, ) from galpy.potential import IsochronePotential, MWPotential, PlummerPotential ro, vo = 9.0, 230.0 # actionAngleHarmonic ip = IsochronePotential(normalize=5.0, b=10000.0) aA = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=ro, vo=vo ) aAnu = actionAngleHarmonic( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) assert ( numpy.fabs( aA(-0.2 * ro * units.kpc, 0.1 * vo * units.km / units.s, use_physical=False) - aAnu(-0.2, 0.1) ) < 10.0**-8.0 ), ( "actionAngle method __call__ does not return the correct value when input is Quantity" ) assert ( numpy.fabs( aA.actionsFreqs( -0.2 * ro * units.kpc, 0.1 * vo * units.km / units.s, use_physical=False )[0] - aAnu.actionsFreqs(-0.2, 0.1)[0] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqs does not return the correct value when input is Quantity" ) assert ( numpy.fabs( aA.actionsFreqs( -0.2 * ro * units.kpc, 0.1 * vo * units.km / units.s, use_physical=False )[1] - aAnu.actionsFreqs(-0.2, 0.1)[1] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqs does not return the correct value when input is Quantity" ) assert ( numpy.fabs( aA.actionsFreqsAngles( -0.2 * ro * units.kpc, 0.1 * vo * units.km / units.s, use_physical=False )[0] - aAnu.actionsFreqsAngles(-0.2, 0.1)[0] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) assert ( numpy.fabs( aA.actionsFreqsAngles( -0.2 * ro * units.kpc, 0.1 * vo * units.km / units.s, use_physical=False )[1] - aAnu.actionsFreqsAngles(-0.2, 0.1)[1] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) assert ( numpy.fabs( aA.actionsFreqsAngles( -0.2 * ro * units.kpc, 0.1 * vo * units.km / units.s, use_physical=False )[2] - aAnu.actionsFreqsAngles(-0.2, 0.1)[2] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) # actionAngleIsochrone aA = actionAngleIsochrone(b=0.8, ro=ro, vo=vo) aAnu = actionAngleIsochrone(b=0.8) for ii in range(3): assert ( numpy.fabs( aA( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method __call__ does not return the correct value when input is Quantity" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqs( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqs does not return the correct value when input is Quantity" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqs( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqs does not return the correct value when input is Quantity" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqsAngles( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqsAngles( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) for ii in range(6, 9): assert ( numpy.fabs( aA.actionsFreqsAngles( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) for ii in range(4): assert ( numpy.fabs( aA.EccZmaxRperiRap( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method EccZmaxRperiRap does not return the correct value when input is Quantity" ) # actionAngleSpherical pot = PlummerPotential(normalize=1.0, b=0.7) aA = actionAngleSpherical(pot=pot, ro=ro, vo=vo) aAnu = actionAngleSpherical(pot=pot) for ii in range(3): assert ( numpy.fabs( aA( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, ro=ro * units.kpc, )[ii] - aAnu(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method __call__ does not return the correct value when input is Quantity" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqs( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, vo=vo * units.km / units.s, )[ii] - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqs does not return the correct value when input is Quantity" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqs( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqs does not return the correct value when input is Quantity" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqsAngles( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqsAngles( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) for ii in range(6, 9): assert ( numpy.fabs( aA.actionsFreqsAngles( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) # actionAngleAdiabatic aA = actionAngleAdiabatic(pot=MWPotential, ro=ro, vo=vo) aAnu = actionAngleAdiabatic(pot=MWPotential) for ii in range(3): assert ( numpy.fabs( aA( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method __call__ does not return the correct value when input is Quantity" ) for ii in range(4): assert ( numpy.fabs( aA.EccZmaxRperiRap( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method EccZmaxRperiRap does not return the correct value when input is Quantity" ) # actionAngleStaeckel aA = actionAngleStaeckel(pot=MWPotential, delta=0.45, ro=ro, vo=vo) aAnu = actionAngleStaeckel(pot=MWPotential, delta=0.45) for ii in range(3): assert ( numpy.fabs( aA( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method __call__ does not return the correct value when input is Quantity" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqs( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqs does not return the correct value when input is Quantity" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqs( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqs does not return the correct value when input is Quantity" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqsAngles( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqsAngles( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) for ii in range(6, 9): assert ( numpy.fabs( aA.actionsFreqsAngles( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) for ii in range(4): assert ( numpy.fabs( aA.EccZmaxRperiRap( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.EccZmaxRperiRap(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method EccZmaxRperiRap does not return the correct value when input is Quantity" ) # actionAngleIsochroneApprox aA = actionAngleIsochroneApprox(pot=MWPotential, b=0.8, ro=ro, vo=vo) aAnu = actionAngleIsochroneApprox(pot=MWPotential, b=0.8) for ii in range(3): assert ( numpy.fabs( aA( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method __call__ does not return the correct value when input is Quantity" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqs( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqs does not return the correct value when input is Quantity" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqs( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqs(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqs does not return the correct value when input is Quantity" ) for ii in range(3): assert ( numpy.fabs( aA.actionsFreqsAngles( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) for ii in range(3, 6): assert ( numpy.fabs( aA.actionsFreqsAngles( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) for ii in range(6, 9): assert ( numpy.fabs( aA.actionsFreqsAngles( 1.1 * ro * units.kpc, 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 0.1 * ro * units.kpc, 0.2 * vo * units.km / units.s, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.actionsFreqsAngles(1.1, 0.1, 1.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngle method actionsFreqsAngles does not return the correct value when input is Quantity" ) # actionAngleHarmonic ip = IsochronePotential(normalize=5.0, b=10000.0) aA = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0), ro=ro, vo=vo ) aAnu = actionAngleHarmonicInverse( omega=numpy.sqrt(4.0 * numpy.pi * ip.dens(1.2, 0.0) / 3.0) ) actionsUnit = ro * vo * units.kpc * units.km / units.s for ii in range(2): assert ( numpy.fabs( aA(0.1 * actionsUnit, -0.2 * units.rad, use_physical=False)[ii] - aAnu(0.1, -0.2)[ii] ) < 10.0**-8.0 ), ( "actionAngleInverse method __call__ does not return the correct value when input is Quantity" ) for ii in range(3): assert ( numpy.fabs( aA.xvFreqs(0.1 * actionsUnit, -0.2 * units.rad, use_physical=False)[ii] - aAnu.xvFreqs(0.1, -0.2)[ii] ) < 10.0**-8.0 ), ( "actionAngleInverse method xvFreqs does not return the correct value when input is Quantity" ) assert ( numpy.fabs(aA.Freqs(0.1 * actionsUnit, use_physical=False) - aAnu.Freqs(0.1)) < 10.0**-8.0 ), ( "actionAngleInverse method Freqs does not return the correct value when input is Quantity" ) # actionAngleIsochroneInverse aA = actionAngleIsochroneInverse(b=0.8, ro=ro, vo=vo) aAnu = actionAngleIsochroneInverse(b=0.8) actionsUnit = ro * vo * units.kpc * units.km / units.s for ii in range(6): assert ( numpy.fabs( aA( 0.1 * actionsUnit, 1.1 * actionsUnit, 0.1 * actionsUnit, 0.1 * units.rad, 0.2 * units.rad, 0.0 * units.rad, use_physical=False, )[ii] - aAnu(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngleInverse method __call__ does not return the correct value when input is Quantity" ) for ii in range(9): assert ( numpy.fabs( aA.xvFreqs( 0.1 * actionsUnit, 1.1 * actionsUnit, 0.1 * actionsUnit, 0.1 * units.rad, 0.2 * units.rad, 0.0 * units.rad, use_physical=False, )[ii] - aAnu.xvFreqs(0.1, 1.1, 0.1, 0.1, 0.2, 0.0)[ii] ) < 10.0**-8.0 ), ( "actionAngleInverse method xvFreqs does not return the correct value when input is Quantity" ) for ii in range(3): assert ( numpy.fabs( aA.Freqs( 0.1 * actionsUnit, 1.1 * actionsUnit, 0.1 * actionsUnit, use_physical=False, )[ii] - aAnu.Freqs(0.1, 1.1, 0.1)[ii] ) < 10.0**-8.0 ), ( "actionAngleInverse method Freqs does not return the correct value when input is Quantity" ) return None def test_actionAngleIsochroneApprox_method_ts_units(): from galpy.actionAngle import actionAngleIsochroneApprox from galpy.orbit import Orbit from galpy.potential import IsochronePotential from galpy.util import conversion ip = IsochronePotential(normalize=1.0, b=1.2) ro, vo = 7.5, 215.0 aAIA = actionAngleIsochroneApprox(pot=ip, b=0.8, ro=ro, vo=vo) R, vR, vT, z, vz, phi = 1.1, 0.3, 1.2, 0.2, 0.5, 2.0 # Setup an orbit, and integrated it first o = Orbit([R, vR, vT, z, vz, phi]) ts = ( numpy.linspace(0.0, 10.0, 25000) * units.Gyr ) # Integrate for a long time, not the default o.integrate(ts, ip) jiaO = aAIA.actionsFreqs(o, ts=ts) jiaOu = aAIA.actionsFreqs(o, ts=ts.value / conversion.time_in_Gyr(vo, ro)) dOr = numpy.fabs((jiaO[3] - jiaOu[3]) / jiaO[3]) dOp = numpy.fabs((jiaO[4] - jiaOu[4]) / jiaO[4]) dOz = numpy.fabs((jiaO[5] - jiaOu[5]) / jiaO[5]) assert dOr < 10.0**-6.0, "actionAngleIsochroneApprox with ts with units fails" assert dOp < 10.0**-6.0, "actionAngleIsochroneApprox with ts with units fails" assert dOz < 10.0**-6.0, "actionAngleIsochroneApprox with ts with units fails" # Same for actionsFreqsAngles jiaO = aAIA.actionsFreqsAngles(o, ts=ts) jiaOu = aAIA.actionsFreqsAngles(o, ts=ts.value / conversion.time_in_Gyr(vo, ro)) dOr = numpy.fabs((jiaO[3] - jiaOu[3]) / jiaO[3]) dOp = numpy.fabs((jiaO[4] - jiaOu[4]) / jiaO[4]) dOz = numpy.fabs((jiaO[5] - jiaOu[5]) / jiaO[5]) assert dOr < 10.0**-6.0, "actionAngleIsochroneApprox with ts with units fails" assert dOp < 10.0**-6.0, "actionAngleIsochroneApprox with ts with units fails" assert dOz < 10.0**-6.0, "actionAngleIsochroneApprox with ts with units fails" return None def test_actionAngle_inconsistentPotentialUnits_error(): from galpy.actionAngle import ( actionAngleAdiabatic, actionAngleIsochrone, actionAngleIsochroneApprox, actionAngleIsochroneInverse, actionAngleSpherical, actionAngleStaeckel, ) from galpy.potential import IsochronePotential, PlummerPotential # actionAngleIsochrone pot = IsochronePotential(normalize=1.0, ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: actionAngleIsochrone(ip=pot, ro=8.0, vo=220.0) pot = IsochronePotential(normalize=1.0, ro=8.0, vo=230.0) with pytest.raises(AssertionError) as excinfo: actionAngleIsochrone(ip=pot, ro=8.0, vo=220.0) # actionAngleSpherical pot = PlummerPotential(normalize=1.0, b=0.7, ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: actionAngleSpherical(pot=pot, ro=8.0, vo=220.0) pot = PlummerPotential(normalize=1.0, b=0.7, ro=8.0, vo=230.0) with pytest.raises(AssertionError) as excinfo: actionAngleSpherical(pot=pot, ro=8.0, vo=220.0) # actionAngleAdiabatic pot = PlummerPotential(normalize=1.0, b=0.7, ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: actionAngleAdiabatic(pot=[pot], ro=8.0, vo=220.0) pot = PlummerPotential(normalize=1.0, b=0.7, ro=8.0, vo=230.0) with pytest.raises(AssertionError) as excinfo: actionAngleAdiabatic(pot=[pot], ro=8.0, vo=220.0) # actionAngleStaeckel pot = PlummerPotential(normalize=1.0, b=0.7, ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: actionAngleStaeckel(delta=0.45, pot=pot, ro=8.0, vo=220.0) pot = PlummerPotential(normalize=1.0, b=0.7, ro=8.0, vo=230.0) with pytest.raises(AssertionError) as excinfo: actionAngleStaeckel(delta=0.45, pot=pot, ro=8.0, vo=220.0) # actionAngleIsochroneApprox pot = PlummerPotential(normalize=1.0, b=0.7, ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: actionAngleIsochroneApprox(b=0.8, pot=pot, ro=8.0, vo=220.0) pot = PlummerPotential(normalize=1.0, b=0.7, ro=8.0, vo=230.0) with pytest.raises(AssertionError) as excinfo: actionAngleIsochroneApprox(b=0.8, pot=pot, ro=8.0, vo=220.0) # actionAngleIsochroneInverse pot = IsochronePotential(normalize=1.0, ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: actionAngleIsochroneInverse(ip=pot, ro=8.0, vo=220.0) pot = IsochronePotential(normalize=1.0, ro=8.0, vo=230.0) with pytest.raises(AssertionError) as excinfo: actionAngleIsochroneInverse(ip=pot, ro=8.0, vo=220.0) return None def test_actionAngle_inconsistentOrbitUnits_error(): from galpy.actionAngle import ( actionAngleAdiabatic, actionAngleIsochrone, actionAngleIsochroneApprox, actionAngleSpherical, actionAngleStaeckel, ) from galpy.orbit import Orbit from galpy.potential import IsochronePotential, PlummerPotential # actionAngleIsochrone pot = IsochronePotential(normalize=1) aA = actionAngleIsochrone(ip=pot, ro=8.0, vo=220.0) o = Orbit([1.1, 0.2, 1.2, 0.1, 0.2, 0.2], ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: aA(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqs(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqsAngles(o) o = Orbit([1.1, 0.2, 1.2, 0.1, 0.2, 0.2], ro=8.0, vo=230.0) with pytest.raises(AssertionError) as excinfo: aA(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqs(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqsAngles(o) # actionAngleSpherical pot = PlummerPotential(normalize=1.0, b=0.7) aA = actionAngleSpherical(pot=pot, ro=8.0, vo=220.0) o = Orbit([1.1, 0.2, 1.2, 0.1, 0.2, 0.2], ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: aA(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqs(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqsAngles(o) o = Orbit([1.1, 0.2, 1.2, 0.1, 0.2, 0.2], ro=8.0, vo=230.0) with pytest.raises(AssertionError) as excinfo: aA(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqs(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqsAngles(o) # actionAngleAdiabatic aA = actionAngleAdiabatic(pot=[pot], ro=8.0, vo=220.0) o = Orbit([1.1, 0.2, 1.2, 0.1, 0.2, 0.2], ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: aA(o) o = Orbit([1.1, 0.2, 1.2, 0.1, 0.2, 0.2], ro=8.0, vo=230.0) with pytest.raises(AssertionError) as excinfo: aA(o) # actionAngleStaeckel aA = actionAngleStaeckel(delta=0.45, pot=pot, ro=8.0, vo=220.0) o = Orbit([1.1, 0.2, 1.2, 0.1, 0.2, 0.2], ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: aA(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqs(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqsAngles(o) o = Orbit([1.1, 0.2, 1.2, 0.1, 0.2, 0.2], ro=8.0, vo=230.0) with pytest.raises(AssertionError) as excinfo: aA(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqs(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqsAngles(o) # actionAngleIsochroneApprox aA = actionAngleIsochroneApprox(b=0.8, pot=pot, ro=8.0, vo=220.0) o = Orbit([1.1, 0.2, 1.2, 0.1, 0.2, 0.2], ro=7.0, vo=220.0) with pytest.raises(AssertionError) as excinfo: aA(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqs(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqsAngles(o) o = Orbit([1.1, 0.2, 1.2, 0.1, 0.2, 0.2], ro=8.0, vo=230.0) with pytest.raises(AssertionError) as excinfo: aA(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqs(o) with pytest.raises(AssertionError) as excinfo: aA.actionsFreqsAngles(o) return None def test_actionAngle_input_wrongunits(): from galpy.actionAngle import actionAngleSpherical from galpy.potential import PlummerPotential # actionAngleSpherical pot = PlummerPotential(normalize=1.0, b=0.7) aA = actionAngleSpherical(pot=pot, ro=8.0, vo=220.0) with pytest.raises(units.UnitConversionError) as excinfo: aA( 1.0 * units.Gyr, 0.1 * units.km / units.s, 1.1 * units.km / units.s, 0.1 * units.kpc, 0.2 * units.km / units.s, 0.1 * units.rad, ) with pytest.raises(units.UnitConversionError) as excinfo: aA( 1.0 * units.kpc, 0.1 * units.Gyr, 1.1 * units.km / units.s, 0.1 * units.kpc, 0.2 * units.km / units.s, 0.1 * units.rad, ) return None def test_actionAngleInverse_input_wrongunits(): from galpy.actionAngle import actionAngleIsochroneInverse from galpy.potential import IsochronePotential ip = IsochronePotential(normalize=1.0, b=0.7) aAII = actionAngleIsochroneInverse(ip=ip, ro=8.0, vo=220.0) with pytest.raises(units.UnitConversionError) as excinfo: aAII( 1.0 * units.Gyr, 0.1 * units.kpc * units.km / units.s, 1.1 * units.kpc * units.km / units.s, 0.1 * units.rad, 0.2 * units.rad, 0.1 * units.rad, ) with pytest.raises(units.UnitConversionError) as excinfo: aAII( 1.0 * units.Gyr, 0.1 * units.kpc * units.km / units.s, 1.1 * units.kpc * units.km / units.s, 0.1 * units.km, 0.2 * units.rad, 0.1 * units.rad, ) return None def test_estimateDeltaStaeckel_method_returntype(): from galpy.actionAngle import estimateDeltaStaeckel from galpy.potential import MiyamotoNagaiPotential pot = MiyamotoNagaiPotential(normalize=True, ro=8.0, vo=220.0) assert isinstance(estimateDeltaStaeckel(pot, 1.1, 0.1), units.Quantity), ( "estimateDeltaStaeckel function does not return Quantity when it should" ) assert isinstance( estimateDeltaStaeckel(pot, 1.1 * numpy.ones(3), 0.1 * numpy.ones(3)), units.Quantity, ), "estimateDeltaStaeckel function does not return Quantity when it should" return None def test_estimateDeltaStaeckel_method_returnunit(): from galpy.actionAngle import estimateDeltaStaeckel from galpy.potential import MiyamotoNagaiPotential pot = MiyamotoNagaiPotential(normalize=True, ro=8.0, vo=220.0) try: estimateDeltaStaeckel(pot, 1.1, 0.1).to(units.kpc) except units.UnitConversionError: raise AssertionError( "estimateDeltaStaeckel function does not return Quantity with the right units" ) try: estimateDeltaStaeckel(pot, 1.1 * numpy.ones(3), 0.1 * numpy.ones(3)).to( units.kpc ) except units.UnitConversionError: raise AssertionError( "estimateDeltaStaeckel function does not return Quantity with the right units" ) return None def test_estimateDeltaStaeckel_method_value(): from galpy.actionAngle import estimateDeltaStaeckel from galpy.potential import MiyamotoNagaiPotential ro, vo = 9.0, 230.0 pot = MiyamotoNagaiPotential(normalize=True, ro=ro, vo=vo) potu = MiyamotoNagaiPotential(normalize=True) assert ( numpy.fabs( estimateDeltaStaeckel(pot, 1.1 * ro * units.kpc, 0.1 * ro * units.kpc) .to(units.kpc) .value - estimateDeltaStaeckel(potu, 1.1, 0.1) * ro ) < 10.0**-8.0 ), "estimateDeltaStaeckel function does not return Quantity with the right value" assert numpy.all( numpy.fabs( estimateDeltaStaeckel(pot, 1.1 * numpy.ones(3), 0.1 * numpy.ones(3)) .to(units.kpc) .value - estimateDeltaStaeckel(potu, 1.1 * numpy.ones(3), 0.1 * numpy.ones(3)) * ro ) < 10.0**-8.0 ), "estimateDeltaStaeckel function does not return Quantity with the right value" return None def test_estimateBIsochrone_method_returntype(): from galpy.actionAngle import estimateBIsochrone from galpy.potential import MiyamotoNagaiPotential pot = MiyamotoNagaiPotential(normalize=True, ro=8.0, vo=220.0) assert isinstance(estimateBIsochrone(pot, 1.1, 0.1), units.Quantity), ( "estimateBIsochrone function does not return Quantity when it should" ) for ii in range(3): assert isinstance( estimateBIsochrone(pot, 1.1 * numpy.ones(3), 0.1 * numpy.ones(3))[ii], units.Quantity, ), "estimateBIsochrone function does not return Quantity when it should" return None def test_estimateBIsochrone_method_returnunit(): from galpy.actionAngle import estimateBIsochrone from galpy.potential import MiyamotoNagaiPotential pot = MiyamotoNagaiPotential(normalize=True, ro=8.0, vo=220.0) try: estimateBIsochrone(pot, 1.1, 0.1).to(units.kpc) except units.UnitConversionError: raise AssertionError( "estimateBIsochrone function does not return Quantity with the right units" ) for ii in range(3): try: estimateBIsochrone(pot, 1.1 * numpy.ones(3), 0.1 * numpy.ones(3))[ii].to( units.kpc ) except units.UnitConversionError: raise AssertionError( "estimateBIsochrone function does not return Quantity with the right units" ) return None def test_estimateBIsochrone_method_value(): from galpy.actionAngle import estimateBIsochrone from galpy.potential import MiyamotoNagaiPotential ro, vo = 9.0, 230.0 pot = MiyamotoNagaiPotential(normalize=True, ro=ro, vo=vo) potu = MiyamotoNagaiPotential(normalize=True) assert ( numpy.fabs( estimateBIsochrone(pot, 1.1 * ro * units.kpc, 0.1 * ro * units.kpc) .to(units.kpc) .value - estimateBIsochrone(potu, 1.1, 0.1) * ro ) < 10.0**-8.0 ), "estimateBIsochrone function does not return Quantity with the right value" for ii in range(3): assert numpy.all( numpy.fabs( estimateBIsochrone(pot, 1.1 * numpy.ones(3), 0.1 * numpy.ones(3))[ii] .to(units.kpc) .value - estimateBIsochrone(potu, 1.1 * numpy.ones(3), 0.1 * numpy.ones(3))[ii] * ro ) < 10.0**-8.0 ), "estimateBIsochrone function does not return Quantity with the right value" return None def test_df_method_turnphysicalon(): from galpy.df import dehnendf from galpy.orbit import Orbit df = dehnendf(ro=7.0, vo=230.0) df.turn_physical_on() assert isinstance(df(Orbit([1.1, 0.1, 1.1])), units.Quantity), ( "df method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(df._ro - 7.0) < 10.0**-10.0, "df method does not work as expected" assert numpy.fabs(df._vo - 230.0) < 10.0**-10.0, ( "df method turn_physical_on does not work as expected" ) df.turn_physical_on(ro=9.0) assert isinstance(df(Orbit([1.1, 0.1, 1.1])), units.Quantity), ( "df method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(df._ro - 9.0) < 10.0**-10.0, "df method does not work as expected" assert numpy.fabs(df._vo - 230.0) < 10.0**-10.0, ( "df method turn_physical_on does not work as expected" ) df.turn_physical_on(vo=210.0) assert isinstance(df(Orbit([1.1, 0.1, 1.1])), units.Quantity), ( "df method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(df._ro - 9.0) < 10.0**-10.0, "df method does not work as expected" assert numpy.fabs(df._vo - 210.0) < 10.0**-10.0, ( "df method turn_physical_on does not work as expected" ) df.turn_physical_on(ro=10.0 * units.kpc) assert isinstance(df(Orbit([1.1, 0.1, 1.1])), units.Quantity), ( "df method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(df._ro - 10.0) < 10.0**-10.0, ( "df method does not work as expected" ) assert numpy.fabs(df._vo - 210.0) < 10.0**-10.0, ( "df method turn_physical_on does not work as expected" ) df.turn_physical_on(vo=190.0 * units.km / units.s) assert isinstance(df(Orbit([1.1, 0.1, 1.1])), units.Quantity), ( "df method does not return Quantity when turn_physical_on has been called" ) assert numpy.fabs(df._ro - 10.0) < 10.0**-10.0, ( "df method does not work as expected" ) assert numpy.fabs(df._vo - 190.0) < 10.0**-10.0, ( "df method turn_physical_on does not work as expected" ) return None def test_df_method_turnphysicaloff(): from galpy.df import dehnendf from galpy.orbit import Orbit df = dehnendf(ro=7.0, vo=230.0) df.turn_physical_off() assert isinstance(numpy.atleast_1d(df(Orbit([1.1, 0.1, 1.1])))[0], float), ( "df method does not return float when turn_physical_off has been called" ) return None def test_diskdf_method_returntype(): from galpy.df import dehnendf, shudf from galpy.orbit import Orbit df = dehnendf(ro=8.0, vo=220.0) dfs = shudf(ro=8.0, vo=220.0) assert isinstance(df(Orbit([1.1, 0.1, 1.1])), units.Quantity), ( "diskdf method __call__ does not return Quantity when it should" ) assert isinstance(df.targetSigma2(1.2), units.Quantity), ( "diskdf method targetSigma2 does not return Quantity when it should" ) assert isinstance(df.targetSurfacemass(1.2), units.Quantity), ( "diskdf method targetSurfacemass does not return Quantity when it should" ) assert isinstance(df.targetSurfacemassLOS(1.2, 40.0), units.Quantity), ( "diskdf method targetSurfacemassLOS does not return Quantity when it should" ) assert isinstance(df.surfacemassLOS(1.2, 35.0), units.Quantity), ( "diskdf method surfacemassLOS does not return Quantity when it should" ) assert isinstance(df.sampledSurfacemassLOS(1.2), units.Quantity), ( "diskdf method sampledSurfacemassLOS does not return Quantity when it should" ) assert isinstance(df.sampleVRVT(1.1), units.Quantity), ( "diskdf method sampleVRVT does not return Quantity when it should" ) assert isinstance(df.sampleLOS(12.0)[0].R(), units.Quantity), ( "diskdf method sampleLOS does not return Quantity when it should" ) assert isinstance(df.sample()[0].R(), units.Quantity), ( "diskdf method sample does not return Quantity when it should" ) assert isinstance(dfs.sample()[0].R(), units.Quantity), ( "diskdf method sample does not return Quantity when it should" ) assert isinstance(df.asymmetricdrift(0.8), units.Quantity), ( "diskdf method asymmetricdrift does not return Quantity when it should" ) assert isinstance(df.surfacemass(1.1), units.Quantity), ( "diskdf method does not return Quantity when it should" ) assert isinstance(df.sigma2surfacemass(1.2), units.Quantity), ( "diskdf method sigma2surfacemass does not return Quantity when it should" ) assert isinstance(df.oortA(1.2), units.Quantity), ( "diskdf method oortA does not return Quantity when it should" ) assert isinstance(df.oortB(1.2), units.Quantity), ( "diskdf method oortB does not return Quantity when it should" ) assert isinstance(df.oortC(1.2), units.Quantity), ( "diskdf method oortC does not return Quantity when it should" ) assert isinstance(df.oortK(1.2), units.Quantity), ( "diskdf method oortK does not return Quantity when it should" ) assert isinstance(df.sigma2(1.2), units.Quantity), ( "diskdf method sigma2 does not return Quantity when it should" ) assert isinstance(df.sigmaT2(1.2), units.Quantity), ( "diskdf method sigmaT2 does not return Quantity when it should" ) assert isinstance(df.sigmaR2(1.2), units.Quantity), ( "diskdf method sigmaR2 does not return Quantity when it should" ) assert isinstance(df.meanvT(1.2), units.Quantity), ( "diskdf method meanvT does not return Quantity when it should" ) assert isinstance(df.meanvR(1.2), units.Quantity), ( "diskdf method meanvR does not return Quantity when it should" ) assert isinstance(df.vmomentsurfacemass(1.1, 0, 0), units.Quantity), ( "diskdf method vmomentsurfacemass does not return Quantity when it should" ) assert isinstance(df.vmomentsurfacemass(1.1, 1, 0), units.Quantity), ( "diskdf method vmomentsurfacemass does not return Quantity when it should" ) assert isinstance(df.vmomentsurfacemass(1.1, 1, 1), units.Quantity), ( "diskdf method vmomentsurfacemass does not return Quantity when it should" ) return None def test_diskdf_method_returnunit(): from galpy.df import dehnendf from galpy.orbit import Orbit df = dehnendf(ro=8.0, vo=220.0) try: df(Orbit([1.1, 0.1, 1.1])).to(1 / (units.km / units.s) ** 2 / units.kpc**2) except units.UnitConversionError: raise AssertionError( "diskdf method __call__ does not return Quantity with the right units" ) try: df.targetSigma2(1.2).to((units.km / units.s) ** 2) except units.UnitConversionError: raise AssertionError( "diskdf method targetSigma2 does not return Quantity with the right units" ) try: df.targetSurfacemass(1.2).to(units.Msun / units.pc**2) except units.UnitConversionError: raise AssertionError( "diskdf method targetSurfacemass does not return Quantity with the right units" ) try: df.targetSurfacemassLOS(1.2, 30.0).to(units.Msun / units.pc) except units.UnitConversionError: raise AssertionError( "diskdf method targetSurfacemassLOS does not return Quantity with the right units" ) try: df.surfacemassLOS(1.2, 40.0).to(units.Msun / units.pc) except units.UnitConversionError: raise AssertionError( "diskdf method surfacemassLOS does not return Quantity with the right units" ) try: df.sampledSurfacemassLOS(1.2).to(units.pc) except units.UnitConversionError: raise AssertionError( "diskdf method sampledSurfacemassLOS does not return Quantity with the right units" ) try: df.sampleVRVT(1.2).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "diskdf method sampleVRVT does not return Quantity with the right units" ) try: df.asymmetricdrift(1.2).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "diskdf method asymmetricdrift does not return Quantity with the right units" ) try: df.surfacemass(1.2).to(units.Msun / units.pc**2) except units.UnitConversionError: raise AssertionError( "diskdf method surfacemass does not return Quantity with the right units" ) try: df.sigma2surfacemass(1.2).to( units.Msun / units.pc**2 * (units.km / units.s) ** 2 ) except units.UnitConversionError: raise AssertionError( "diskdf method surfacemass does not return Quantity with the right units" ) try: df.oortA(1.2).to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "diskdf method oortA does not return Quantity with the right units" ) try: df.oortB(1.2).to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "diskdf method oortB does not return Quantity with the right units" ) try: df.oortC(1.2).to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "diskdf method oortC does not return Quantity with the right units" ) try: df.oortK(1.2).to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "diskdf method oortK does not return Quantity with the right units" ) try: df.sigma2(1.2).to((units.km / units.s) ** 2) except units.UnitConversionError: raise AssertionError( "diskdf method sigma2 does not return Quantity with the right units" ) try: df.sigmaT2(1.2).to((units.km / units.s) ** 2) except units.UnitConversionError: raise AssertionError( "diskdf method sigmaT2 does not return Quantity with the right units" ) try: df.sigmaR2(1.2).to((units.km / units.s) ** 2) except units.UnitConversionError: raise AssertionError( "diskdf method sigmaR2 does not return Quantity with the right units" ) try: df.meanvR(1.2).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "diskdf method meanvR does not return Quantity with the right units" ) try: df.meanvT(1.2).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "diskdf method meanvT does not return Quantity with the right units" ) try: df.vmomentsurfacemass(1.1, 0, 0).to(units.Msun / units.pc**2) except units.UnitConversionError: raise AssertionError( "diskdf method vmomentsurfacemass does not return Quantity with the right units" ) try: df.vmomentsurfacemass(1.1, 1, 0).to( units.Msun / units.pc**2 * (units.km / units.s) ) except units.UnitConversionError: raise AssertionError( "diskdf method vmomentsurfacemass does not return Quantity with the right units" ) try: df.vmomentsurfacemass(1.1, 1, 1).to( units.Msun / units.pc**2 * (units.km / units.s) ** 2 ) except units.UnitConversionError: raise AssertionError( "diskdf method vmomentsurfacemass does not return Quantity with the right units" ) try: df.vmomentsurfacemass(1.1, 0, 2).to( units.Msun / units.pc**2 * (units.km / units.s) ** 2 ) except units.UnitConversionError: raise AssertionError( "diskdf method vmomentsurfacemass does not return Quantity with the right units" ) return None def test_diskdf_method_value(): from galpy.df import dehnendf from galpy.orbit import Orbit from galpy.util import conversion ro, vo = 7.0, 230.0 df = dehnendf(ro=ro, vo=vo) dfnou = dehnendf() assert ( numpy.fabs( df(Orbit([1.1, 0.1, 1.1])) .to(1 / units.kpc**2 / (units.km / units.s) ** 2) .value - dfnou(Orbit([1.1, 0.1, 1.1])) / vo**2 / ro**2 ) < 10.0**-8.0 ), "diskdf method __call__ does not return correct Quantity" assert ( numpy.fabs( df.targetSigma2(1.2).to((units.km / units.s) ** 2).value - dfnou.targetSigma2(1.2) * vo**2 ) < 10.0**-8.0 ), "diskdf method targetSigma2 does not return correct Quantity" assert ( numpy.fabs( df.targetSurfacemass(1.2).to(units.Msun / units.pc**2).value - dfnou.targetSurfacemass(1.2) * conversion.surfdens_in_msolpc2(vo, ro) ) < 10.0**-8.0 ), "diskdf method targetSurfacemass does not return correct Quantity" assert ( numpy.fabs( df.targetSurfacemassLOS(1.2, 40.0).to(units.Msun / units.pc).value - dfnou.targetSurfacemassLOS(1.2, 40.0) * conversion.surfdens_in_msolpc2(vo, ro) * ro * 1000.0 ) < 10.0**-8.0 ), "diskdf method targetSurfacemassLOS does not return correct Quantity" assert ( numpy.fabs( df.surfacemassLOS(1.2, 35.0).to(units.Msun / units.pc).value - dfnou.surfacemassLOS(1.2, 35.0) * conversion.surfdens_in_msolpc2(vo, ro) * ro * 1000.0 ) < 10.0**-8.0 ), "diskdf method surfacemassLOS does not return correct Quantity" assert ( numpy.fabs( df.asymmetricdrift(0.8).to(units.km / units.s).value - dfnou.asymmetricdrift(0.8) * vo ) < 10.0**-8.0 ), "diskdf method asymmetricdrift does not return correct Quantity" assert ( numpy.fabs( df.surfacemass(1.1).to(units.Msun / units.pc**2).value - dfnou.surfacemass(1.1) * conversion.surfdens_in_msolpc2(vo, ro) ) < 10.0**-8.0 ), "diskdf method does not return correct Quantity" assert ( numpy.fabs( df.sigma2surfacemass(1.2) .to(units.Msun / units.pc**2 * (units.km / units.s) ** 2) .value - dfnou.sigma2surfacemass(1.2) * conversion.surfdens_in_msolpc2(vo, ro) * vo**2 ) < 10.0**-8.0 ), "diskdf method sigma2surfacemass does not return correct Quantity" assert ( numpy.fabs( df.oortA(1.2).to(1 / units.Gyr).value - dfnou.oortA(1.2) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "diskdf method oortA does not return correct Quantity" assert ( numpy.fabs( df.oortB(1.2).to(1 / units.Gyr).value - dfnou.oortB(1.2) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "diskdf method oortB does not return correct Quantity" assert ( numpy.fabs( df.oortC(1.2).to(1 / units.Gyr).value - dfnou.oortC(1.2) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "diskdf method oortC does not return correct Quantity" assert ( numpy.fabs( df.oortK(1.2).to(1 / units.Gyr).value - dfnou.oortK(1.2) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "diskdf method oortK does not return correct Quantity" assert ( numpy.fabs( df.sigma2(1.2).to((units.km / units.s) ** 2).value - dfnou.sigma2(1.2) * vo**2 ) < 10.0**-8.0 ), "diskdf method sigma2 does not return correct Quantity" assert ( numpy.fabs( df.sigmaT2(1.2).to((units.km / units.s) ** 2).value - dfnou.sigmaT2(1.2) * vo**2 ) < 10.0**-8.0 ), "diskdf method sigmaT2 does not return correct Quantity" assert ( numpy.fabs( df.sigmaR2(1.2).to((units.km / units.s) ** 2).value - dfnou.sigmaR2(1.2) * vo**2 ) < 10.0**-8.0 ), "diskdf method sigmaR2 does not return correct Quantity" assert ( numpy.fabs(df.meanvT(1.2).to(units.km / units.s).value - dfnou.meanvT(1.2) * vo) < 10.0**-8.0 ), "diskdf method meanvT does not return correct Quantity" assert ( numpy.fabs(df.meanvR(1.2).to(units.km / units.s).value - dfnou.meanvR(1.2) * vo) < 10.0**-8.0 ), "diskdf method meanvT does not return correct Quantity" assert ( numpy.fabs( df.vmomentsurfacemass(1.1, 0, 0).to(units.Msun / units.pc**2).value - dfnou.vmomentsurfacemass(1.1, 0, 0) * conversion.surfdens_in_msolpc2(vo, ro) ) < 10.0**-8.0 ), "diskdf method vmomentsurfacemass does not return correct Quantity" assert ( numpy.fabs( df.vmomentsurfacemass(1.1, 0, 1) .to(units.Msun / units.pc**2 * (units.km / units.s) ** 1) .value - dfnou.vmomentsurfacemass(1.1, 0, 1) * conversion.surfdens_in_msolpc2(vo, ro) * vo ) < 10.0**-8.0 ), "diskdf method vmomentsurfacemass does not return correct Quantity" assert ( numpy.fabs( df.vmomentsurfacemass(1.1, 1, 1) .to(units.Msun / units.pc**2 * (units.km / units.s) ** 2) .value - dfnou.vmomentsurfacemass(1.1, 1, 1) * conversion.surfdens_in_msolpc2(vo, ro) * vo**2 ) < 10.0**-8.0 ), "diskdf method vmomentsurfacemass does not return correct Quantity" return None def test_diskdf_sample(): # Test that the sampling routines work with Quantity output from galpy.df import dehnendf, shudf ro, vo = 7.0, 230.0 df = dehnendf(ro=ro, vo=vo) dfnou = dehnendf() dfs = shudf(ro=ro, vo=vo) dfsnou = shudf() # sampledSurfacemassLOS numpy.random.seed(1) du = ( df.sampledSurfacemassLOS(11.0 * units.deg, n=1, maxd=10.0 * units.kpc) .to(units.kpc) .value / ro ) numpy.random.seed(1) dnou = dfnou.sampledSurfacemassLOS(11.0 * numpy.pi / 180.0, n=1, maxd=10.0 / ro) assert numpy.fabs(du - dnou) < 10.0**-8.0, ( "diskdf sampling method sampledSurfacemassLOS does not return expected Quantity" ) # sampleVRVT numpy.random.seed(1) du = df.sampleVRVT(1.1, n=1).to(units.km / units.s).value / vo numpy.random.seed(1) dnou = dfnou.sampleVRVT(1.1, n=1) assert numpy.all(numpy.fabs(du - dnou) < 10.0**-8.0), ( "diskdf sampling method sampleVRVT does not return expected Quantity" ) # sampleLOS numpy.random.seed(1) du = df.sampleLOS(11.0 * units.deg, n=1) numpy.random.seed(1) dnou = dfnou.sampleLOS(11.0, n=1, deg=True) assert numpy.all( numpy.fabs(numpy.array(du[0].vxvv) - numpy.array(dnou[0].vxvv)) < 10.0**-8.0 ), "diskdf sampling method sampleLOS does not work as expected with Quantity input" # sample numpy.random.seed(1) du = df.sample(rrange=[4.0 * units.kpc, 12.0 * units.kpc], n=1) numpy.random.seed(1) dnou = dfnou.sample(rrange=[4.0 / ro, 12.0 / ro], n=1) assert numpy.all( numpy.fabs(numpy.array(du[0].vxvv) - numpy.array(dnou[0].vxvv)) < 10.0**-8.0 ), "diskdf sampling method sample does not work as expected with Quantity input" # sample for Shu numpy.random.seed(1) du = dfs.sample(rrange=[4.0 * units.kpc, 12.0 * units.kpc], n=1) numpy.random.seed(1) dnou = dfsnou.sample(rrange=[4.0 / ro, 12.0 / ro], n=1) assert numpy.all( numpy.fabs(numpy.array(du[0].vxvv) - numpy.array(dnou[0].vxvv)) < 10.0**-8.0 ), "diskdf sampling method sample does not work as expected with Quantity input" return None def test_diskdf_method_inputAsQuantity(): # Using the decorator from galpy.df import dehnendf from galpy.util import conversion ro, vo = 7.0, 230.0 df = dehnendf(ro=ro, vo=vo) dfnou = dehnendf() assert ( numpy.fabs( df.targetSigma2(1.2 * ro * units.kpc).to((units.km / units.s) ** 2).value - dfnou.targetSigma2(1.2) * vo**2 ) < 10.0**-8.0 ), "diskdf method targetSigma2 does not return correct Quantity" assert ( numpy.fabs( df.targetSurfacemass(1.2 * ro * units.kpc) .to(units.Msun / units.pc**2) .value - dfnou.targetSurfacemass(1.2) * conversion.surfdens_in_msolpc2(vo, ro) ) < 10.0**-8.0 ), "diskdf method targetSurfacemass does not return correct Quantity" assert ( numpy.fabs( df.asymmetricdrift(0.8 * ro * units.kpc).to(units.km / units.s).value - dfnou.asymmetricdrift(0.8) * vo ) < 10.0**-8.0 ), "diskdf method asymmetricdrift does not return correct Quantity" assert ( numpy.fabs( df.surfacemass(1.1 * ro * units.kpc).to(units.Msun / units.pc**2).value - dfnou.surfacemass(1.1) * conversion.surfdens_in_msolpc2(vo, ro) ) < 10.0**-8.0 ), "diskdf method does not return correct Quantity" assert ( numpy.fabs( df.sigma2surfacemass(1.2 * ro * units.kpc) .to(units.Msun / units.pc**2 * (units.km / units.s) ** 2) .value - dfnou.sigma2surfacemass(1.2) * conversion.surfdens_in_msolpc2(vo, ro) * vo**2 ) < 10.0**-8.0 ), "diskdf method sigma2surfacemass does not return correct Quantity" assert ( numpy.fabs( df.oortA(1.2 * ro * units.kpc).to(1 / units.Gyr).value - dfnou.oortA(1.2) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "diskdf method oortA does not return correct Quantity" assert ( numpy.fabs( df.oortB(1.2 * ro * units.kpc).to(1 / units.Gyr).value - dfnou.oortB(1.2) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "diskdf method oortB does not return correct Quantity" assert ( numpy.fabs( df.oortC(1.2 * ro * units.kpc).to(1 / units.Gyr).value - dfnou.oortC(1.2) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "diskdf method oortC does not return correct Quantity" assert ( numpy.fabs( df.oortK(1.2 * ro * units.kpc).to(1 / units.Gyr).value - dfnou.oortK(1.2) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "diskdf method oortK does not return correct Quantity" assert ( numpy.fabs( df.sigma2(1.2 * ro * units.kpc).to((units.km / units.s) ** 2).value - dfnou.sigma2(1.2) * vo**2 ) < 10.0**-8.0 ), "diskdf method sigma2 does not return correct Quantity" assert ( numpy.fabs( df.sigmaT2(1.2 * ro * units.kpc).to((units.km / units.s) ** 2).value - dfnou.sigmaT2(1.2) * vo**2 ) < 10.0**-8.0 ), "diskdf method sigmaT2 does not return correct Quantity" assert ( numpy.fabs( df.sigmaR2(1.2 * ro * units.kpc).to((units.km / units.s) ** 2).value - dfnou.sigmaR2(1.2) * vo**2 ) < 10.0**-8.0 ), "diskdf method sigmaR2 does not return correct Quantity" assert ( numpy.fabs( df.meanvT(1.2 * ro * units.kpc).to(units.km / units.s).value - dfnou.meanvT(1.2) * vo ) < 10.0**-8.0 ), "diskdf method meanvT does not return correct Quantity" assert ( numpy.fabs( df.meanvR(1.2 * ro * units.kpc).to(units.km / units.s).value - dfnou.meanvR(1.2) * vo ) < 10.0**-8.0 ), "diskdf method meanvT does not return correct Quantity" return None def test_diskdf_method_inputAsQuantity_special(): from galpy.df import dehnendf, shudf from galpy.util import conversion ro, vo = 7.0, 230.0 df = dehnendf(ro=ro, vo=vo) dfnou = dehnendf() dfs = shudf(ro=ro, vo=vo) dfsnou = shudf() assert ( numpy.fabs( df( 0.6 * vo**2.0 * units.km**2 / units.s**2, 1.1 * vo * ro * units.kpc * units.km / units.s, ) .to(1 / units.kpc**2 / (units.km / units.s) ** 2) .value - dfnou(0.6, 1.1) / vo**2 / ro**2 ) < 10.0**-6.0 ), "diskdf method __call__ with Quantity input does not return correct Quantity" assert ( numpy.fabs( dfs( 0.6 * vo**2.0 * units.km**2 / units.s**2, 1.1 * vo * ro * units.kpc * units.km / units.s, ) .to(1 / units.kpc**2 / (units.km / units.s) ** 2) .value - dfsnou(0.6, 1.1) / vo**2 / ro**2 ) < 10.0**-6.0 ), "diskdf method __call__ with Quantity input does not return correct Quantity" assert ( numpy.fabs( df.targetSurfacemassLOS(1.2 * ro * units.kpc, 40.0 * units.deg) .to(units.Msun / units.pc) .value - dfnou.targetSurfacemassLOS(1.2, 40.0) * conversion.surfdens_in_msolpc2(vo, ro) * ro * 1000.0 ) < 10.0**-8.0 ), ( "diskdf method targetSurfacemassLOS with Quantity input does not return correct Quantity" ) assert ( numpy.fabs( df.surfacemassLOS(1.2 * ro * units.kpc, 35.0 * units.deg) .to(units.Msun / units.pc) .value - dfnou.surfacemassLOS(1.2, 35.0) * conversion.surfdens_in_msolpc2(vo, ro) * ro * 1000.0 ) < 10.0**-8.0 ), ( "diskdf method surfacemassLOS does with Quantity input not return correct Quantity" ) assert ( numpy.fabs( df.vmomentsurfacemass( 1.1, 0, 0, ro=9.0 * units.kpc, vo=245.0 * units.km / units.s ) .to(units.Msun / units.pc**2) .value - dfnou.vmomentsurfacemass(1.1, 0, 0) * conversion.surfdens_in_msolpc2(245, 9.0) ) < 10.0**-8.0 ), ( "diskdf method vmomentsurfacemass does with Quantity input not return correct Quantity" ) return None def test_diskdf_setup_roAsQuantity(): from galpy.df import dehnendf ro = 7.0 df = dehnendf(ro=ro * units.kpc) assert numpy.fabs(df._ro - ro) < 10.0**-10.0, ( "ro in diskdf setup as Quantity does not work as expected" ) return None def test_diskdf_setup_roAsQuantity_oddunits(): from galpy.df import dehnendf ro = 7000.0 df = dehnendf(ro=ro * units.lyr) assert numpy.fabs(df._ro - ro * (units.lyr).to(units.kpc)) < 10.0**-10.0, ( "ro in diskdf setup as Quantity does not work as expected" ) return None def test_diskdf_setup_voAsQuantity(): from galpy.df import dehnendf vo = 230.0 df = dehnendf(vo=vo * units.km / units.s) assert numpy.fabs(df._vo - vo) < 10.0**-10.0, ( "vo in diskdf setup as Quantity does not work as expected" ) return None def test_diskdf_setup_voAsQuantity_oddunits(): from galpy.df import dehnendf vo = 230.0 df = dehnendf(vo=vo * units.pc / units.Myr) assert ( numpy.fabs(df._vo - vo * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "vo in diskdf setup as Quantity does not work as expected" return None def test_diskdf_setup_profileAsQuantity(): from galpy.df import dehnendf, shudf from galpy.orbit import Orbit df = dehnendf( ro=8.0, vo=220.0, profileParams=(9.0 * units.kpc, 10.0 * units.kpc, 20.0 * units.km / units.s), ) dfs = shudf( ro=8.0, vo=220.0, profileParams=(9.0 * units.kpc, 10.0 * units.kpc, 20.0 * units.km / units.s), ) assert numpy.fabs(df._surfaceSigmaProfile._params[0] - 9.0 / 8.0) < 10.0**-10.0, ( "hR in diskdf setup as Quantity does not work as expected" ) assert numpy.fabs(df._surfaceSigmaProfile._params[1] - 10.0 / 8.0) < 10.0**-10.0, ( "hsR in diskdf setup as Quantity does not work as expected" ) assert ( numpy.fabs(df._surfaceSigmaProfile._params[2] - 20.0 / 220.0) < 10.0**-10.0 ), "sR in diskdf setup as Quantity does not work as expected" assert numpy.fabs(dfs._surfaceSigmaProfile._params[0] - 9.0 / 8.0) < 10.0**-10.0, ( "hR in diskdf setup as Quantity does not work as expected" ) assert numpy.fabs(dfs._surfaceSigmaProfile._params[1] - 10.0 / 8.0) < 10.0**-10.0, ( "hsR in diskdf setup as Quantity does not work as expected" ) assert ( numpy.fabs(dfs._surfaceSigmaProfile._params[2] - 20.0 / 220.0) < 10.0**-10.0 ), "sR in diskdf setup as Quantity does not work as expected" return None def test_evolveddiskdf_method_returntype(): from galpy.df import dehnendf from galpy.potential import EllipticalDiskPotential, LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0) ep = EllipticalDiskPotential( twophio=0.05, phib=0.0, p=0.0, tform=-150.0, tsteady=125.0 ) idfwarm = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15), ro=8.0, vo=220.0) from galpy.df import evolveddiskdf edfwarm = evolveddiskdf(idfwarm, [lp, ep], to=-150.0) from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.1]) assert isinstance(edfwarm(o), units.Quantity), ( "evolveddiskdf method __call__ does not return Quantity when it should" ) assert isinstance( edfwarm.oortA( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ), units.Quantity, ), "evolveddiskdf method oortA does not return Quantity when it should" assert isinstance( edfwarm.oortB( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ), units.Quantity, ), "evolveddiskdf method oortB does not return Quantity when it should" assert isinstance( edfwarm.oortC( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ), units.Quantity, ), "evolveddiskdf method oortC does not return Quantity when it should" assert isinstance( edfwarm.oortK( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ), units.Quantity, ), "evolveddiskdf method oortK does not return Quantity when it should" assert isinstance( edfwarm.sigmaT2(1.2, grid=True, returnGrid=False, gridpoints=3), units.Quantity ), "evolveddiskdf method sigmaT2 does not return Quantity when it should" assert isinstance( edfwarm.sigmaR2(1.2, grid=True, returnGrid=False, gridpoints=3), units.Quantity ), "evolveddiskdf method sigmaR2 does not return Quantity when it should" assert isinstance( edfwarm.sigmaRT(1.2, grid=True, returnGrid=False, gridpoints=3), units.Quantity ), "evolveddiskdf method sigmaRT does not return Quantity when it should" assert isinstance( edfwarm.vertexdev(1.2, grid=True, returnGrid=False, gridpoints=3), units.Quantity, ), "evolveddiskdf method vertexdev does not return Quantity when it should" assert isinstance( edfwarm.meanvT(1.2, grid=True, returnGrid=False, gridpoints=3), units.Quantity ), "evolveddiskdf method meanvT does not return Quantity when it should" assert isinstance( edfwarm.meanvR(1.2, grid=True, returnGrid=False, gridpoints=3), units.Quantity ), "evolveddiskdf method meanvR does not return Quantity when it should" return None def test_evolveddiskdf_method_returnunit(): from galpy.df import dehnendf from galpy.potential import EllipticalDiskPotential, LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0) ep = EllipticalDiskPotential( twophio=0.05, phib=0.0, p=0.0, tform=-150.0, tsteady=125.0 ) idfwarm = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15), ro=8.0, vo=220.0) from galpy.df import evolveddiskdf edfwarm = evolveddiskdf(idfwarm, [lp, ep], to=-150.0) from galpy.orbit import Orbit try: edfwarm(Orbit([1.1, 0.1, 1.1, 0.2])).to( 1 / (units.km / units.s) ** 2 / units.kpc**2 ) except units.UnitConversionError: raise AssertionError( "evolveddiskdf method __call__ does not return Quantity with the right units" ) try: edfwarm.oortA( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ).to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "evolveddiskdf method oortA does not return Quantity with the right units" ) try: edfwarm.oortB( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ).to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "evolveddiskdf method oortB does not return Quantity with the right units" ) try: edfwarm.oortC( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ).to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "evolveddiskdf method oortC does not return Quantity with the right units" ) try: edfwarm.oortK( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ).to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "evolveddiskdf method oortK does not return Quantity with the right units" ) try: edfwarm.sigmaT2(1.2, grid=True, returnGrid=False, gridpoints=3).to( (units.km / units.s) ** 2 ) except units.UnitConversionError: raise AssertionError( "evolveddiskdf method sigmaT2 does not return Quantity with the right units" ) try: edfwarm.sigmaR2(1.2, grid=True, returnGrid=False, gridpoints=3).to( (units.km / units.s) ** 2 ) except units.UnitConversionError: raise AssertionError( "evolveddiskdf method sigmaR2 does not return Quantity with the right units" ) try: edfwarm.sigmaRT(1.2, grid=True, returnGrid=False, gridpoints=3).to( (units.km / units.s) ** 2 ) except units.UnitConversionError: raise AssertionError( "evolveddiskdf method sigmaRT does not return Quantity with the right units" ) try: edfwarm.vertexdev(1.2, grid=True, returnGrid=False, gridpoints=3).to(units.deg) except units.UnitConversionError: raise AssertionError( "evolveddiskdf method vertexdev does not return Quantity with the right units" ) try: edfwarm.meanvR(1.2, grid=True, returnGrid=False, gridpoints=3).to( units.km / units.s ) except units.UnitConversionError: raise AssertionError( "evolveddiskdf method meanvR does not return Quantity with the right units" ) try: edfwarm.meanvT(1.2, grid=True, returnGrid=False, gridpoints=3).to( units.km / units.s ) except units.UnitConversionError: raise AssertionError( "evolveddiskdf method meanvT does not return Quantity with the right units" ) return None def test_evolveddiskdf_method_value(): from galpy.df import dehnendf from galpy.potential import EllipticalDiskPotential, LogarithmicHaloPotential from galpy.util import conversion lp = LogarithmicHaloPotential(normalize=1.0) ep = EllipticalDiskPotential( twophio=0.05, phib=0.0, p=0.0, tform=-150.0, tsteady=125.0 ) ro, vo = 6.0, 230.0 idfwarm = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15), ro=ro, vo=vo) from galpy.df import evolveddiskdf edfwarm = evolveddiskdf(idfwarm, [lp, ep], to=-150.0) idfwarmnou = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15)) edfwarmnou = evolveddiskdf(idfwarmnou, [lp, ep], to=-150.0) from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.1]) assert ( numpy.fabs( edfwarm(o).to(1 / units.kpc**2 / (units.km / units.s) ** 2).value - edfwarmnou(o) / ro**2 / vo**2 ) < 10.0**-8.0 ), "evolveddiskdf method __call__ does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.oortA( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) .to(1 / units.Gyr) .value - edfwarmnou.oortA( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "evolveddiskdf method oortA does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.oortB( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) .to(1 / units.Gyr) .value - edfwarmnou.oortB( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "evolveddiskdf method oortB does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.oortC( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) .to(1 / units.Gyr) .value - edfwarmnou.oortC( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "evolveddiskdf method oortC does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.oortK( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) .to(1 / units.Gyr) .value - edfwarmnou.oortK( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "evolveddiskdf method oortK does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.sigmaT2(1.2, grid=True, returnGrid=False, gridpoints=3) .to((units.km / units.s) ** 2) .value - edfwarmnou.sigmaT2(1.2, grid=True, returnGrid=False, gridpoints=3) * vo**2 ) < 10.0**-8.0 ), "evolveddiskdf method sigmaT2 does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.sigmaR2(1.2, grid=True, returnGrid=False, gridpoints=3) .to((units.km / units.s) ** 2) .value - edfwarmnou.sigmaR2(1.2, grid=True, returnGrid=False, gridpoints=3) * vo**2 ) < 10.0**-8.0 ), "evolveddiskdf method sigmaR2 does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.sigmaRT(1.2, grid=True, returnGrid=False, gridpoints=3) .to((units.km / units.s) ** 2) .value - edfwarmnou.sigmaRT(1.2, grid=True, returnGrid=False, gridpoints=3) * vo**2 ) < 10.0**-8.0 ), "evolveddiskdf method sigmaRT does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.vertexdev(1.2, grid=True, returnGrid=False, gridpoints=3) .to(units.rad) .value - edfwarmnou.vertexdev(1.2, grid=True, returnGrid=False, gridpoints=3) ) < 10.0**-8.0 ), "evolveddiskdf method vertexdev does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.meanvT(1.2, grid=True, returnGrid=False, gridpoints=3) .to(units.km / units.s) .value - edfwarmnou.meanvT(1.2, grid=True, returnGrid=False, gridpoints=3) * vo ) < 10.0**-8.0 ), "evolveddiskdf method meanvT does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.meanvR(1.2, grid=True, returnGrid=False, gridpoints=3) .to(units.km / units.s) .value - edfwarmnou.meanvR(1.2, grid=True, returnGrid=False, gridpoints=3) * vo ) < 10.0**-8.0 ), "evolveddiskdf method meanvR does not return correct Quantity when it should" return None def test_evolveddiskdf_method_inputAsQuantity(): # Those that use the decorator from galpy.df import dehnendf from galpy.potential import EllipticalDiskPotential, LogarithmicHaloPotential from galpy.util import conversion lp = LogarithmicHaloPotential(normalize=1.0) ep = EllipticalDiskPotential( twophio=0.05, phib=0.0, p=0.0, tform=-150.0, tsteady=125.0 ) ro, vo = 6.0, 230.0 idfwarm = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15), ro=ro, vo=vo) from galpy.df import evolveddiskdf edfwarm = evolveddiskdf(idfwarm, [lp, ep], to=-150.0) idfwarmnou = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15)) edfwarmnou = evolveddiskdf(idfwarmnou, [lp, ep], to=-150.0) from galpy.orbit import Orbit assert ( numpy.fabs( edfwarm.oortA( 1.2 * ro * units.kpc, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) .to(1 / units.Gyr) .value - edfwarmnou.oortA( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "evolveddiskdf method oortA does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.oortB( 1.2 * ro * units.kpc, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) .to(1 / units.Gyr) .value - edfwarmnou.oortB( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "evolveddiskdf method oortB does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.oortC( 1.2 * ro * units.kpc, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) .to(1 / units.Gyr) .value - edfwarmnou.oortC( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "evolveddiskdf method oortC does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.oortK( 1.2 * ro * units.kpc, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) .to(1 / units.Gyr) .value - edfwarmnou.oortK( 1.2, grid=True, returnGrids=False, gridpoints=3, derivRGrid=True, derivphiGrid=True, derivGridpoints=3, ) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "evolveddiskdf method oortK does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.sigmaT2( 1.2 * ro * units.kpc, grid=True, returnGrid=False, gridpoints=3 ) .to((units.km / units.s) ** 2) .value - edfwarmnou.sigmaT2(1.2, grid=True, returnGrid=False, gridpoints=3) * vo**2 ) < 10.0**-8.0 ), "evolveddiskdf method sigmaT2 does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.sigmaR2( 1.2 * ro * units.kpc, grid=True, returnGrid=False, gridpoints=3 ) .to((units.km / units.s) ** 2) .value - edfwarmnou.sigmaR2(1.2, grid=True, returnGrid=False, gridpoints=3) * vo**2 ) < 10.0**-8.0 ), "evolveddiskdf method sigmaR2 does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.sigmaRT( 1.2 * ro * units.kpc, grid=True, returnGrid=False, gridpoints=3 ) .to((units.km / units.s) ** 2) .value - edfwarmnou.sigmaRT(1.2, grid=True, returnGrid=False, gridpoints=3) * vo**2 ) < 10.0**-8.0 ), "evolveddiskdf method sigmaRT does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.vertexdev( 1.2 * ro * units.kpc, grid=True, returnGrid=False, gridpoints=3 ) .to(units.rad) .value - edfwarmnou.vertexdev(1.2, grid=True, returnGrid=False, gridpoints=3) ) < 10.0**-8.0 ), "evolveddiskdf method vertexdev does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.meanvT( 1.2 * ro * units.kpc, grid=True, returnGrid=False, gridpoints=3 ) .to(units.km / units.s) .value - edfwarmnou.meanvT(1.2, grid=True, returnGrid=False, gridpoints=3) * vo ) < 10.0**-8.0 ), "evolveddiskdf method meanvT does not return correct Quantity when it should" assert ( numpy.fabs( edfwarm.meanvR( 1.2 * ro * units.kpc, grid=True, returnGrid=False, gridpoints=3 ) .to(units.km / units.s) .value - edfwarmnou.meanvR(1.2, grid=True, returnGrid=False, gridpoints=3) * vo ) < 10.0**-8.0 ), "evolveddiskdf method meanvR does not return correct Quantity when it should" return None def test_evolveddiskdf_method_inputAsQuantity_special(): from galpy.df import dehnendf from galpy.potential import EllipticalDiskPotential, LogarithmicHaloPotential from galpy.util import conversion lp = LogarithmicHaloPotential(normalize=1.0) ep = EllipticalDiskPotential( twophio=0.05, phib=0.0, p=0.0, tform=-150.0, tsteady=125.0 ) ro, vo = 6.0, 230.0 idfwarm = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15), ro=ro, vo=vo) from galpy.df import evolveddiskdf edfwarm = evolveddiskdf(idfwarm, [lp, ep], to=-150.0) idfwarmnou = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15)) edfwarmnou = evolveddiskdf(idfwarmnou, [lp, ep], to=-150.0) from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.1]) ts = numpy.linspace(0.0, -150.0, 101) assert numpy.all( numpy.fabs( edfwarm(o, ts * conversion.time_in_Gyr(vo, ro) * units.Gyr) .to(1 / units.kpc**2 / (units.km / units.s) ** 2) .value - edfwarmnou(o, ts) / ro**2 / vo**2 ) < 10.0**-8.0 ), "evolveddiskdf method __call__ does not return correct Quantity when it should" return None def test_evolveddiskdf_setup_roAsQuantity(): from galpy.df import dehnendf from galpy.potential import EllipticalDiskPotential, LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0) ep = EllipticalDiskPotential( twophio=0.05, phib=0.0, p=0.0, tform=-150.0, tsteady=125.0 ) ro = 7.0 idfwarm = dehnendf( beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15), ro=ro * units.kpc ) from galpy.df import evolveddiskdf df = evolveddiskdf(idfwarm, [lp, ep], to=-150.0) assert numpy.fabs(df._ro - ro) < 10.0**-10.0, ( "ro in evolveddiskdf setup as Quantity does not work as expected" ) return None def test_evolveddiskdf_setup_roAsQuantity_oddunits(): from galpy.df import dehnendf from galpy.potential import EllipticalDiskPotential, LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0) ep = EllipticalDiskPotential( twophio=0.05, phib=0.0, p=0.0, tform=-150.0, tsteady=125.0 ) ro = 7000.0 idfwarm = dehnendf( beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15), ro=ro * units.lyr ) from galpy.df import evolveddiskdf df = evolveddiskdf(idfwarm, [lp, ep], to=-150.0) assert numpy.fabs(df._ro - ro * (units.lyr).to(units.kpc)) < 10.0**-10.0, ( "ro in evolveddiskdf setup as Quantity does not work as expected" ) return None def test_evolveddiskdf_setup_voAsQuantity(): from galpy.df import dehnendf from galpy.potential import EllipticalDiskPotential, LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0) ep = EllipticalDiskPotential( twophio=0.05, phib=0.0, p=0.0, tform=-150.0, tsteady=125.0 ) vo = 230.0 idfwarm = dehnendf( beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15), vo=vo * units.km / units.s ) from galpy.df import evolveddiskdf df = evolveddiskdf(idfwarm, [lp, ep], to=-150.0) assert numpy.fabs(df._vo - vo) < 10.0**-10.0, ( "vo in evolveddiskdf setup as Quantity does not work as expected" ) return None def test_evolveddiskdf_setup_voAsQuantity_oddunits(): from galpy.df import dehnendf from galpy.potential import EllipticalDiskPotential, LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0) ep = EllipticalDiskPotential( twophio=0.05, phib=0.0, p=0.0, tform=-150.0, tsteady=125.0 ) vo = 230.0 idfwarm = dehnendf( beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15), vo=vo * units.pc / units.Myr ) from galpy.df import evolveddiskdf df = evolveddiskdf(idfwarm, [lp, ep], to=-150.0) assert ( numpy.fabs(df._vo - vo * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "vo in evolveddiskdf setup as Quantity does not work as expected" return None def test_evolveddiskdf_setup_toAsQuantity(): from galpy.df import dehnendf from galpy.potential import EllipticalDiskPotential, LogarithmicHaloPotential from galpy.util import conversion lp = LogarithmicHaloPotential(normalize=1.0) ep = EllipticalDiskPotential( twophio=0.05, phib=0.0, p=0.0, tform=-150.0, tsteady=125.0 ) ro, vo = 7.0, 230.0 idfwarm = dehnendf(beta=0.0, profileParams=(1.0 / 3.0, 1.0, 0.15), vo=vo, ro=ro) from galpy.df import evolveddiskdf df = evolveddiskdf(idfwarm, [lp, ep], to=-3.0 * units.Gyr) assert numpy.fabs(df._to + 3.0 / conversion.time_in_Gyr(vo, ro)) < 10.0**-10.0, ( "to in evolveddiskdf setup as Quantity does not work as expected" ) return None def test_quasiisothermaldf_method_returntype(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.orbit import Orbit from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) qdf = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True, ro=8.0, vo=220.0, ) o = Orbit([1.1, 0.1, 1.1, 0.1, 0.03, 0.4]) R = numpy.array([1.0, 1.1, 1.2, 1.3]) z = numpy.array([-0.1, 0.0, 0.1, 0.2]) assert isinstance(qdf(o), units.Quantity), ( "quasiisothermaldf method __call__ does not return Quantity when it should" ) assert isinstance(qdf.estimate_hr(1.1), units.Quantity), ( "quasiisothermaldf method estimate_hr does not return Quantity when it should" ) assert isinstance(qdf.estimate_hz(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method estimate_hz does not return Quantity when it should" ) assert isinstance(qdf.estimate_hsr(1.1), units.Quantity), ( "quasiisothermaldf method estimate_hsr does not return Quantity when it should" ) assert isinstance(qdf.estimate_hsz(1.1), units.Quantity), ( "quasiisothermaldf method estimate_hsz does not return Quantity when it should" ) assert isinstance(qdf.surfacemass_z(1.1), units.Quantity), ( "quasiisothermaldf method surfacemass_z does not return Quantity when it should" ) assert isinstance(qdf.density(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method density does not return Quantity when it should" ) assert isinstance(qdf.sigmaR2(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method sigmaR2 does not return Quantity when it should" ) assert isinstance(qdf.sigmaT2(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method sigmaT2 does not return Quantity when it should" ) assert isinstance(qdf.sigmaz2(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method sigmaz2 does not return Quantity when it should" ) assert isinstance(qdf.sigmaRz(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method sigmaRz does not return Quantity when it should" ) assert isinstance(qdf.tilt(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method tilt does not return Quantity when it should" ) assert isinstance(qdf.meanvR(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method meanvR does not return Quantity when it should" ) assert isinstance(qdf.meanvT(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method meanvT does not return Quantity when it should" ) assert isinstance(qdf.meanvz(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method meanvz does not return Quantity when it should" ) assert isinstance(qdf.meanjr(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method meanjr does not return Quantity when it should" ) assert isinstance(qdf.meanlz(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method meanlz does not return Quantity when it should" ) assert isinstance(qdf.meanjz(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method meanjz does not return Quantity when it should" ) assert isinstance(qdf.sampleV(1.1, 0.1), units.Quantity), ( "quasiisothermaldf method sampleV does not return Quantity when it should" ) assert isinstance(qdf.sampleV_interpolate(R, z, 0.1, 0.1), units.Quantity), ( "quasiisothermaldf method sampleV_interpolate does not return Quantity when it should" ) assert isinstance(qdf.pvR(0.1, 1.1, 0.1), units.Quantity), ( "quasiisothermaldf method pvR does not return Quantity when it should" ) assert isinstance(qdf.pvT(1.1, 1.1, 0.1), units.Quantity), ( "quasiisothermaldf method pvT does not return Quantity when it should" ) assert isinstance(qdf.pvz(0.1, 1.1, 0.1), units.Quantity), ( "quasiisothermaldf method pvz does not return Quantity when it should" ) assert isinstance(qdf.pvRvT(0.1, 1.1, 1.1, 0.1), units.Quantity), ( "quasiisothermaldf method pvRvT does not return Quantity when it should" ) assert isinstance(qdf.pvRvz(0.1, 0.2, 1.1, 0.1), units.Quantity), ( "quasiisothermaldf method pvRvz does not return Quantity when it should" ) assert isinstance(qdf.pvTvz(1.1, 1.1, 1.1, 0.1), units.Quantity), ( "quasiisothermaldf method pvTvz does not return Quantity when it should" ) assert isinstance(qdf.vmomentdensity(1.1, 0.1, 0, 0, 0, gl=True), units.Quantity), ( "quasiisothermaldf method vmomentdensity does not return Quantity when it should" ) assert isinstance(qdf.vmomentdensity(1.1, 0.1, 1, 0, 0, gl=True), units.Quantity), ( "quasiisothermaldf method vmomentdensity does not return Quantity when it should" ) assert isinstance(qdf.vmomentdensity(1.1, 0.1, 0, 1, 1, gl=True), units.Quantity), ( "quasiisothermaldf method vmomentdensity does not return Quantity when it should" ) assert isinstance(qdf.vmomentdensity(1.1, 0.1, 0, 0, 1, gl=True), units.Quantity), ( "quasiisothermaldf method vmomentdensity does not return Quantity when it should" ) assert isinstance(qdf.vmomentdensity(1.1, 0.1, 1, 1, 0, gl=True), units.Quantity), ( "quasiisothermaldf method vmomentdensity does not return Quantity when it should" ) assert isinstance(qdf.vmomentdensity(1.1, 0.1, 2, 1, 1, gl=True), units.Quantity), ( "quasiisothermaldf method vmomentdensity does not return Quantity when it should" ) assert isinstance(qdf.jmomentdensity(1.1, 0.1, 0, 0, 0, gl=True), units.Quantity), ( "quasiisothermaldf method jmomentdensity does not return Quantity when it should" ) assert isinstance(qdf.jmomentdensity(1.1, 0.1, 1, 0, 0, gl=True), units.Quantity), ( "quasiisothermaldf method jmomentdensity does not return Quantity when it should" ) assert isinstance(qdf.jmomentdensity(1.1, 0.1, 0, 1, 1, gl=True), units.Quantity), ( "quasiisothermaldf method jmomentdensity does not return Quantity when it should" ) assert isinstance(qdf.jmomentdensity(1.1, 0.1, 0, 0, 1, gl=True), units.Quantity), ( "quasiisothermaldf method jmomentdensity does not return Quantity when it should" ) assert isinstance(qdf.jmomentdensity(1.1, 0.1, 1, 1, 0, gl=True), units.Quantity), ( "quasiisothermaldf method jmomentdensity does not return Quantity when it should" ) assert isinstance(qdf.jmomentdensity(1.1, 0.1, 2, 1, 1, gl=True), units.Quantity), ( "quasiisothermaldf method jmomentdensity does not return Quantity when it should" ) return None def test_quasiisothermaldf_method_returnunit(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.orbit import Orbit from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) qdf = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True, ro=8.0, vo=220.0, ) o = Orbit([1.1, 0.1, 1.1, 0.1, 0.03, 0.4]) R = numpy.array([0.6, 0.7, 0.8, 0.9, 1.0]) z = numpy.array([0.0, 0.1, 0.2, 0.3, 0.4]) try: qdf(o).to(1 / (units.km / units.s) ** 3 / units.kpc**3) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method __call__ does not return Quantity with the right units" ) try: qdf.estimate_hr(1.1).to(units.kpc) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method estimate_hr does not return Quantity with the right units" ) try: qdf.estimate_hz(1.1, 0.1).to(units.kpc) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method estimate_hz does not return Quantity with the right units" ) try: qdf.estimate_hsr(1.1).to(units.kpc) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method estimate_hsr does not return Quantity with the right units" ) try: qdf.estimate_hsz(1.1).to(units.kpc) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method estimate_hsz does not return Quantity with the right units" ) try: qdf.surfacemass_z(1.1).to(1 / units.pc**2) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method surfacemass_z does not return Quantity with the right units" ) try: qdf.density(1.1, 0.1).to(1 / units.pc**3) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method density does not return Quantity with the right units" ) try: qdf.sigmaR2(1.1, 0.1).to((units.km / units.s) ** 2) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method sigmaR2 does not return Quantity with the right units" ) try: qdf.sigmaRz(1.1, 0.1).to((units.km / units.s) ** 2) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method sigmaRz does not return Quantity with the right units" ) try: qdf.sigmaT2(1.1, 0.1).to((units.km / units.s) ** 2) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method sigmaT2 does not return Quantity with the right units" ) try: qdf.sigmaz2(1.1, 0.1).to((units.km / units.s) ** 2) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method sigmaz2 does not return Quantity with the right units" ) try: qdf.tilt(1.1, 0.1).to(units.deg) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method tilt does not return Quantity with the right units" ) try: qdf.meanvR(1.1, 0.1).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method meanvR does not return Quantity with the right units" ) try: qdf.meanvT(1.1, 0.1).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method meanvT does not return Quantity with the right units" ) try: qdf.meanvz(1.1, 0.1).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method meanvz does not return Quantity with the right units" ) try: qdf.meanjr(1.1, 0.1).to(units.kpc * (units.km / units.s)) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method meanjr does not return Quantity with the right units" ) try: qdf.meanlz(1.1, 0.1).to(units.kpc * (units.km / units.s)) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method meanlz does not return Quantity with the right units" ) try: qdf.meanjz(1.1, 0.1).to(units.kpc * (units.km / units.s)) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method meanjz does not return Quantity with the right units" ) try: qdf.sampleV(1.1, 0.1).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method sampleV does not return Quantity with the right units" ) try: qdf.sampleV_interpolate(R, z, 0.1, 0.1).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method sampleV_interpolate does not return Quantity with the right units" ) try: qdf.pvR(0.1, 1.1, 0.1).to(1 / (units.km / units.s) / units.pc**3) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method pvR does not return Quantity with the right units" ) try: qdf.pvz(0.1, 1.1, 0.1).to(1 / (units.km / units.s) / units.pc**3) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method pvz does not return Quantity with the right units" ) try: qdf.pvT(1.1, 1.1, 0.1).to(1 / (units.km / units.s) / units.pc**3) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method pvT does not return Quantity with the right units" ) try: qdf.pvRvT(0.1, 1.1, 1.1, 0.1).to(1 / (units.km / units.s) ** 2 / units.pc**3) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method pvRvT does not return Quantity with the right units" ) try: qdf.pvRvz(0.1, 0.2, 1.1, 0.1).to(1 / (units.km / units.s) ** 2 / units.pc**3) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method pvRvz does not return Quantity with the right units" ) try: qdf.pvTvz(1.1, 0.2, 1, 1.1, 0.1).to(1 / (units.km / units.s) ** 2 / units.pc**3) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method pvTvz does not return Quantity with the right units" ) try: qdf.vmomentdensity(1.1, 0.2, 0, 0, 0, gl=True).to(1 / units.pc**3) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method vmomentdensity does not return Quantity with the right units" ) try: qdf.vmomentdensity(1.1, 0.2, 1, 0, 0, gl=True).to( 1 / units.pc**3 * (units.km / units.s) ) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method vmomentdensity does not return Quantity with the right units" ) try: qdf.vmomentdensity(1.1, 0.2, 1, 1, 0, gl=True).to( 1 / units.pc**3 * (units.km / units.s) ** 2 ) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method vmomentdensity does not return Quantity with the right units" ) try: qdf.jmomentdensity(1.1, 0.2, 0, 0, 0, gl=True).to(1 / units.pc**3) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method jmomentdensity does not return Quantity with the right units" ) try: qdf.jmomentdensity(1.1, 0.2, 1, 0, 0, gl=True).to( 1 / units.pc**3 * (units.kpc * units.km / units.s) ) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method jmomentdensity does not return Quantity with the right units" ) try: qdf.jmomentdensity(1.1, 0.2, 1, 1, 0, gl=True).to( 1 / units.pc**3 * (units.kpc * units.km / units.s) ** 2 ) except units.UnitConversionError: raise AssertionError( "quasiisothermaldf method jmomentdensity does not return Quantity with the right units" ) return None def test_quasiisothermaldf_method_value(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.orbit import Orbit from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) ro, vo = 9.0, 210.0 qdf = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True, ro=ro, vo=vo, ) qdfnou = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True ) o = Orbit([1.1, 0.1, 1.1, 0.1, 0.03, 0.4]) assert ( numpy.fabs( qdf(o).to(1 / units.kpc**3 / (units.km / units.s) ** 3).value - qdfnou(o) / ro**3 / vo**3 ) < 10.0**-8.0 ), "quasiisothermaldf method __call__ does not return correct Quantity" assert ( numpy.fabs( qdf.estimate_hr(1.1).to(units.kpc).value - qdfnou.estimate_hr(1.1) * ro ) < 10.0**-8.0 ), "quasiisothermaldf method estimate_hr does not return correct Quantity" assert ( numpy.fabs( qdf.estimate_hz(1.1, 0.1).to(units.kpc).value - qdfnou.estimate_hz(1.1, 0.1) * ro ) < 10.0**-8.0 ), "quasiisothermaldf method estimate_hz does not return correct Quantity" assert ( numpy.fabs( qdf.estimate_hsr(1.1).to(units.kpc).value - qdfnou.estimate_hsr(1.1) * ro ) < 10.0**-8.0 ), "quasiisothermaldf method estimate_hsr does not return correct Quantity" assert ( numpy.fabs( qdf.estimate_hsz(1.1).to(units.kpc).value - qdfnou.estimate_hsz(1.1) * ro ) < 10.0**-8.0 ), "quasiisothermaldf method estimate_hsz does not return correct Quantity" assert ( numpy.fabs( qdf.surfacemass_z(1.1).to(1 / units.kpc**2).value - qdfnou.surfacemass_z(1.1) / ro**2 ) < 10.0**-8.0 ), "quasiisothermaldf method surfacemass_z does not return correct Quantity" assert ( numpy.fabs( qdf.density(1.1, 0.1).to(1 / units.kpc**3).value - qdfnou.density(1.1, 0.1) / ro**3 ) < 10.0**-8.0 ), "quasiisothermaldf method density does not return correct Quantity" assert ( numpy.fabs( qdf.sigmaR2(1.1, 0.1).to((units.km / units.s) ** 2).value - qdfnou.sigmaR2(1.1, 0.1) * vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method sigmaR2 does not return correct Quantity" assert ( numpy.fabs( qdf.sigmaT2(1.1, 0.1).to((units.km / units.s) ** 2).value - qdfnou.sigmaT2(1.1, 0.1) * vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method sigmaT2 does not return correct Quantity" assert ( numpy.fabs( qdf.sigmaz2(1.1, 0.1).to((units.km / units.s) ** 2).value - qdfnou.sigmaz2(1.1, 0.1) * vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method sigmaz2 does not return correct Quantity" assert ( numpy.fabs( qdf.sigmaRz(1.1, 0.1).to((units.km / units.s) ** 2).value - qdfnou.sigmaRz(1.1, 0.1) * vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method sigmaRz does not return correct Quantity" assert ( numpy.fabs(qdf.tilt(1.1, 0.1).to(units.rad).value - qdfnou.tilt(1.1, 0.1)) < 10.0**-8.0 ), "quasiisothermaldf method tilt does not return correct Quantity" assert ( numpy.fabs( qdf.meanvR(1.1, 0.1).to(units.km / units.s).value - qdfnou.meanvR(1.1, 0.1) * vo ) < 10.0**-8.0 ), "quasiisothermaldf method meanvR does not return correct Quantity" assert ( numpy.fabs( qdf.meanvT(1.1, 0.1).to(units.km / units.s).value - qdfnou.meanvT(1.1, 0.1) * vo ) < 10.0**-8.0 ), "quasiisothermaldf method meanvT does not return correct Quantity" assert ( numpy.fabs( qdf.meanvz(1.1, 0.1).to(units.km / units.s).value - qdfnou.meanvz(1.1, 0.1) * vo ) < 10.0**-8.0 ), "quasiisothermaldf method meanvz does not return correct Quantity" # Lower tolerance, because determined through sampling assert ( numpy.fabs( qdf.meanjr(1.1, 0.1, nmc=100000).to(units.kpc * units.km / units.s).value - qdfnou.meanjr(1.1, 0.1, nmc=100000) * ro * vo ) < 10.0 ), "quasiisothermaldf method meanjr does not return correct Quantity" assert ( numpy.fabs( qdf.meanlz(1.1, 0.1, nmc=100000).to(units.kpc * units.km / units.s).value - qdfnou.meanlz(1.1, 0.1, nmc=100000) * ro * vo ) < 100.0 ), "quasiisothermaldf method meanlz does not return correct Quantity" assert ( numpy.fabs( qdf.meanjz(1.1, 0.1, nmc=100000).to(units.kpc * units.km / units.s).value - qdfnou.meanjz(1.1, 0.1, nmc=100000) * ro * vo ) < 10.0 ), "quasiisothermaldf method meanjz does not return correct Quantity" assert ( numpy.fabs( qdf.pvR(0.1, 1.1, 0.1).to(1 / units.kpc**3 / (units.km / units.s)).value - qdfnou.pvR(0.1, 1.1, 0.1) / ro**3 / vo ) < 10.0**-8.0 ), "quasiisothermaldf method pvR does not return correct Quantity" assert ( numpy.fabs( qdf.pvT(1.1, 1.1, 0.1).to(1 / units.kpc**3 / (units.km / units.s)).value - qdfnou.pvT(1.1, 1.1, 0.1) / ro**3 / vo ) < 10.0**-8.0 ), "quasiisothermaldf method pvT does not return correct Quantity" assert ( numpy.fabs( qdf.pvz(0.1, 1.1, 0.1).to(1 / units.kpc**3 / (units.km / units.s)).value - qdfnou.pvz(0.1, 1.1, 0.1) / ro**3 / vo ) < 10.0**-8.0 ), "quasiisothermaldf method pvz does not return correct Quantity" assert ( numpy.fabs( qdf.pvRvT(0.1, 1.1, 1.1, 0.1) .to(1 / units.kpc**3 / (units.km / units.s) ** 2) .value - qdfnou.pvRvT(0.1, 1.1, 1.1, 0.1) / ro**3 / vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method pvRvT does not return correct Quantity" assert ( numpy.fabs( qdf.pvRvz(0.1, 0.2, 1.1, 0.1) .to(1 / units.kpc**3 / (units.km / units.s) ** 2) .value - qdfnou.pvRvz(0.1, 0.2, 1.1, 0.1) / ro**3 / vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method pvRvz does not return correct Quantity" assert ( numpy.fabs( qdf.pvTvz(1.1, 1.1, 1.1, 0.1) .to(1 / units.kpc**3 / (units.km / units.s) ** 2) .value - qdfnou.pvTvz(1.1, 1.1, 1.1, 0.1) / ro**3 / vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method pvTvz does not return correct Quantity" assert ( numpy.fabs( qdf.vmomentdensity( 1.1, 0.1, 0, 0, 0, gl=True, ro=ro * units.kpc, vo=vo * units.km / units.s, ) .to(1 / units.kpc**3 * (units.km / units.s) ** 0) .value - qdfnou.vmomentdensity(1.1, 0.1, 0, 0, 0, gl=True) / ro**3 * vo**0 ) < 10.0**-8.0 ), "quasiisothermaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( qdf.vmomentdensity(1.1, 0.1, 1, 0, 0, gl=True) .to(1 / units.kpc**3 * (units.km / units.s) ** 1) .value - qdfnou.vmomentdensity(1.1, 0.1, 1, 0, 0, gl=True) / ro**3 * vo ) < 10.0**-8.0 ), "quasiisothermaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( qdf.vmomentdensity(1.1, 0.1, 0, 1, 1, gl=True) .to(1 / units.kpc**3 * (units.km / units.s) ** 2) .value - qdfnou.vmomentdensity(1.1, 0.1, 0, 1, 1, gl=True) / ro**3 * vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( qdf.vmomentdensity(1.1, 0.1, 1, 1, 0, gl=True) .to(1 / units.kpc**3 * (units.km / units.s) ** 2) .value - qdfnou.vmomentdensity(1.1, 0.1, 1, 1, 0, gl=True) / ro**3 * vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( qdf.vmomentdensity(1.1, 0.1, 2, 1, 1, gl=True) .to(1 / units.kpc**3 * (units.km / units.s) ** 4) .value - qdfnou.vmomentdensity(1.1, 0.1, 2, 1, 1, gl=True) / ro**3 * vo**4 ) < 10.0**-8.0 ), "quasiisothermaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( qdf.jmomentdensity( 1.1, 0.1, 0, 0, 0, nmc=100000, ro=ro * units.kpc, vo=vo * units.km / units.s, ) .to(1 / units.kpc**3 * (units.kpc * units.km / units.s) ** 0) .value - qdfnou.jmomentdensity(1.1, 0.1, 0, 0, 0, nmc=100000) / ro**3 * (ro * vo) ** 0 ) < 10.0**-4.0 ), "quasiisothermaldf method jmomentdensity does not return correct Quantity" assert ( numpy.fabs( qdf.jmomentdensity(1.1, 0.1, 1, 0, 0, nmc=100000) .to(1 / units.kpc**3 * (units.kpc * units.km / units.s) ** 1) .value - qdfnou.jmomentdensity(1.1, 0.1, 1, 0, 0, nmc=100000) / ro**3 * (ro * vo) ** 1 ) < 10.0**-2.0 ), "quasiisothermaldf method jmomentdensity does not return correct Quantity" assert ( numpy.fabs( qdf.jmomentdensity(1.1, 0.1, 0, 1, 1, nmc=100000) .to(1 / units.kpc**3 * (units.kpc * units.km / units.s) ** 2) .value - qdfnou.jmomentdensity(1.1, 0.1, 0, 1, 1, nmc=100000) / ro**3 * (ro * vo) ** 2 ) < 1.0 ), "quasiisothermaldf method jmomentdensity does not return correct Quantity" assert ( numpy.fabs( qdf.jmomentdensity(1.1, 0.1, 1, 1, 0, nmc=100000) .to(1 / units.kpc**3 * (units.kpc * units.km / units.s) ** 2) .value - qdfnou.jmomentdensity(1.1, 0.1, 1, 1, 0, nmc=100000) / ro**3 * (ro * vo) ** 2 ) < 10.0 ), "quasiisothermaldf method jmomentdensity does not return correct Quantity" assert ( numpy.fabs( qdf.jmomentdensity(1.1, 0.1, 2, 1, 1, nmc=100000) .to(1 / units.kpc**3 * (units.kpc * units.km / units.s) ** 4) .value - qdfnou.jmomentdensity(1.1, 0.1, 2, 1, 1, nmc=100000) / ro**3 * (ro * vo) ** 4 ) < 10000.0 ), "quasiisothermaldf method jmomentdensity does not return correct Quantity" return None def test_quasiisothermaldf_sample(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) ro, vo = 9.0, 210.0 qdf = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True, ro=ro, vo=vo, ) qdfnou = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True ) numpy.random.seed(1) vu = qdf.sampleV(1.1, 0.1, n=1).to(units.km / units.s).value / vo numpy.random.seed(1) vnou = qdfnou.sampleV(1.1, 0.1, n=1) assert numpy.all(numpy.fabs(vu - vnou) < 10.0**-8.0), ( "quasiisothermaldf sampleV does not return correct Quantity" ) # Also when giving vo with units itself numpy.random.seed(1) vu = ( qdf.sampleV(1.1, 0.1, n=1, vo=vo * units.km / units.s) .to(units.km / units.s) .value / vo ) numpy.random.seed(1) vnou = qdfnou.sampleV(1.1, 0.1, n=1) assert numpy.all(numpy.fabs(vu - vnou) < 10.0**-8.0), ( "quasiisothermaldf sampleV does not return correct Quantity" ) return None def test_quasiisothermaldf_interpolate_sample(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) ro, vo = 9.0, 210.0 R = numpy.array([0.6, 0.7, 0.8, 0.9, 1.0]) z = numpy.array([0.0, 0.1, 0.2, 0.3, 0.4]) qdf = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True, ro=ro, vo=vo, ) qdfnou = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True ) numpy.random.seed(1) vu = qdf.sampleV_interpolate(R, z, 0.1, 0.1).to(units.km / units.s).value / vo numpy.random.seed(1) vnou = qdfnou.sampleV_interpolate(R, z, 0.1, 0.1) assert numpy.all(numpy.fabs(vu - vnou) < 10.0**-8.0), ( "quasiisothermaldf sampleV_interpolate does not return correct Quantity" ) # Also when giving vo with units itself numpy.random.seed(1) vu = ( qdf.sampleV_interpolate(R, z, 0.1, 0.1, vo=vo * units.km / units.s) .to(units.km / units.s) .value / vo ) numpy.random.seed(1) vnou = qdfnou.sampleV_interpolate(R, z, 0.1, 0.1) assert numpy.all(numpy.fabs(vu - vnou) < 10.0**-8.0), ( "quasiisothermaldf sampleV_interpolate does not return correct Quantity" ) return None def test_quasiisothermaldf_method_inputAsQuantity(): # Those that use the decorator from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) ro, vo = 9.0, 210.0 qdf = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True, ro=ro, vo=vo, ) qdfnou = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True ) assert ( numpy.fabs( qdf.estimate_hr(1.1 * ro * units.kpc, z=100.0 * units.pc, dR=1.0 * units.pc) .to(units.kpc) .value - qdfnou.estimate_hr(1.1, 0.1 / ro, dR=10.0**-3.0 / ro) * ro ) < 10.0**-8.0 ), "quasiisothermaldf method estimate_hr does not return correct Quantity" assert ( numpy.fabs( qdf.estimate_hz( 1.1 * ro * units.kpc, 0.1 * ro * units.kpc, dz=1.0 * units.pc ) .to(units.kpc) .value - qdfnou.estimate_hz(1.1, 0.1, dz=10.0**-3.0 / ro) * ro ) < 10.0**-8.0 ), "quasiisothermaldf method estimate_hz does not return correct Quantity" assert ( numpy.fabs( qdf.estimate_hsr( 1.1 * ro * units.kpc, z=100.0 * units.pc, dR=1.0 * units.pc ) .to(units.kpc) .value - qdfnou.estimate_hsr(1.1, 0.1 / ro, dR=10.0**-3.0 / ro) * ro ) < 10.0**-8.0 ), "quasiisothermaldf method estimate_hsr does not return correct Quantity" assert ( numpy.fabs( qdf.estimate_hsz( 1.1 * ro * units.kpc, z=100.0 * units.pc, dR=1.0 * units.pc ) .to(units.kpc) .value - qdfnou.estimate_hsz(1.1, 0.1 / ro, dR=10.0**-3.0 / ro) * ro ) < 10.0**-8.0 ), "quasiisothermaldf method estimate_hsz does not return correct Quantity" assert ( numpy.fabs( qdf.surfacemass_z(1.1 * ro * units.kpc, zmax=2.0 * units.kpc) .to(1 / units.kpc**2) .value - qdfnou.surfacemass_z(1.1, zmax=2.0 / ro) / ro**2 ) < 10.0**-8.0 ), "quasiisothermaldf method surfacemass_z does not return correct Quantity" assert ( numpy.fabs( qdf.density(1.1 * ro * units.kpc, 0.1 * ro * units.kpc) .to(1 / units.kpc**3) .value - qdfnou.density(1.1, 0.1) / ro**3 ) < 10.0**-8.0 ), "quasiisothermaldf method density does not return correct Quantity" assert ( numpy.fabs( qdf.sigmaR2(1.1 * ro * units.kpc, 0.1 * ro * units.kpc) .to((units.km / units.s) ** 2) .value - qdfnou.sigmaR2(1.1, 0.1) * vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method sigmaR2 does not return correct Quantity" assert ( numpy.fabs( qdf.sigmaT2(1.1 * ro * units.kpc, 0.1 * ro * units.kpc) .to((units.km / units.s) ** 2) .value - qdfnou.sigmaT2(1.1, 0.1) * vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method sigmaT2 does not return correct Quantity" assert ( numpy.fabs( qdf.sigmaz2(1.1 * ro * units.kpc, 0.1 * ro * units.kpc) .to((units.km / units.s) ** 2) .value - qdfnou.sigmaz2(1.1, 0.1) * vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method sigmaz2 does not return correct Quantity" assert ( numpy.fabs( qdf.sigmaRz(1.1 * ro * units.kpc, 0.1 * ro * units.kpc) .to((units.km / units.s) ** 2) .value - qdfnou.sigmaRz(1.1, 0.1) * vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method sigmaRz does not return correct Quantity" assert ( numpy.fabs( qdf.tilt(1.1 * ro * units.kpc, 0.1 * ro * units.kpc).to(units.rad).value - qdfnou.tilt(1.1, 0.1) ) < 10.0**-8.0 ), "quasiisothermaldf method tilt does not return correct Quantity" assert ( numpy.fabs( qdf.meanvR(1.1 * ro * units.kpc, 0.1 * ro * units.kpc) .to(units.km / units.s) .value - qdfnou.meanvR(1.1, 0.1) * vo ) < 10.0**-8.0 ), "quasiisothermaldf method meanvR does not return correct Quantity" assert ( numpy.fabs( qdf.meanvT(1.1 * ro * units.kpc, 0.1 * ro * units.kpc) .to(units.km / units.s) .value - qdfnou.meanvT(1.1, 0.1) * vo ) < 10.0**-8.0 ), "quasiisothermaldf method meanvT does not return correct Quantity" assert ( numpy.fabs( qdf.meanvz(1.1 * ro * units.kpc, 0.1 * ro * units.kpc) .to(units.km / units.s) .value - qdfnou.meanvz(1.1, 0.1) * vo ) < 10.0**-8.0 ), "quasiisothermaldf method meanvz does not return correct Quantity" # Lower tolerance, because determined through sampling assert ( numpy.fabs( qdf.meanjr(1.1 * ro * units.kpc, 0.1 * ro * units.kpc, nmc=100000) .to(units.kpc * units.km / units.s) .value - qdfnou.meanjr(1.1, 0.1, nmc=100000) * ro * vo ) < 10.0 ), "quasiisothermaldf method meanjr does not return correct Quantity" assert ( numpy.fabs( qdf.meanlz(1.1 * ro * units.kpc, 0.1 * ro * units.kpc, nmc=100000) .to(units.kpc * units.km / units.s) .value - qdfnou.meanlz(1.1, 0.1, nmc=100000) * ro * vo ) < 100.0 ), "quasiisothermaldf method meanlz does not return correct Quantity" assert ( numpy.fabs( qdf.meanjz(1.1 * ro * units.kpc, 0.1 * ro * units.kpc, nmc=100000) .to(units.kpc * units.km / units.s) .value - qdfnou.meanjz(1.1, 0.1, nmc=100000) * ro * vo ) < 10.0 ), "quasiisothermaldf method meanjz does not return correct Quantity" assert ( numpy.fabs( qdf.pvR( 0.1 * vo * units.km / units.s, 1.1 * ro * units.kpc, 0.1 * ro * units.kpc, ) .to(1 / units.kpc**3 / (units.km / units.s)) .value - qdfnou.pvR(0.1, 1.1, 0.1) / ro**3 / vo ) < 10.0**-8.0 ), "quasiisothermaldf method pvR does not return correct Quantity" assert ( numpy.fabs( qdf.pvT( 1.1 * vo * units.km / units.s, 1.1 * ro * units.kpc, 0.1 * ro * units.kpc, ) .to(1 / units.kpc**3 / (units.km / units.s)) .value - qdfnou.pvT(1.1, 1.1, 0.1) / ro**3 / vo ) < 10.0**-8.0 ), "quasiisothermaldf method pvT does not return correct Quantity" assert ( numpy.fabs( qdf.pvz( 0.1 * vo * units.km / units.s, 1.1 * ro * units.kpc, 0.1 * ro * units.kpc, ) .to(1 / units.kpc**3 / (units.km / units.s)) .value - qdfnou.pvz(0.1, 1.1, 0.1) / ro**3 / vo ) < 10.0**-8.0 ), "quasiisothermaldf method pvz does not return correct Quantity" assert ( numpy.fabs( qdf.pvRvT( 0.1 * vo * units.km / units.s, 1.1 * vo * units.km / units.s, 1.1 * ro * units.kpc, 0.1 * ro * units.kpc, ) .to(1 / units.kpc**3 / (units.km / units.s) ** 2) .value - qdfnou.pvRvT(0.1, 1.1, 1.1, 0.1) / ro**3 / vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method pvRvT does not return correct Quantity" assert ( numpy.fabs( qdf.pvRvz( 0.1 * vo * units.km / units.s, 0.2 * vo * units.km / units.s, 1.1 * ro * units.kpc, 0.1 * ro * units.kpc, ) .to(1 / units.kpc**3 / (units.km / units.s) ** 2) .value - qdfnou.pvRvz(0.1, 0.2, 1.1, 0.1) / ro**3 / vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method pvRvz does not return correct Quantity" assert ( numpy.fabs( qdf.pvTvz( 1.1 * vo * units.km / units.s, 0.1 * vo * units.km / units.s, 1.1 * ro * units.kpc, 0.1 * ro * units.kpc, ) .to(1 / units.kpc**3 / (units.km / units.s) ** 2) .value - qdfnou.pvTvz(1.1, 0.1, 1.1, 0.1) / ro**3 / vo**2 ) < 10.0**-8.0 ), "quasiisothermaldf method pvTvz does not return correct Quantity" return None def test_quasiisothermaldf_method_inputAsQuantity_special(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) ro, vo = 9.0, 210.0 qdf = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True, ro=ro, vo=vo, ) qdfnou = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True ) assert ( numpy.fabs( qdf( ( 0.05 * ro * vo * units.kpc * units.km / units.s, 1.1 * ro * vo * units.kpc * units.km / units.s, 0.025 * ro * vo * units.kpc * units.km / units.s, ) ) .to(1 / units.kpc**3 / (units.km / units.s) ** 3) .value - qdfnou((0.05, 1.1, 0.025)) / ro**3 / vo**3 ) < 10.0**-8.0 ), "quasiisothermaldf method __call__ does not return correct Quantity" return None def test_quasiisothermaldf_setup_roAsQuantity(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) ro = 9.0 df = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True, ro=ro * units.kpc, ) assert numpy.fabs(df._ro - ro) < 10.0**-10.0, ( "ro in quasiisothermaldf setup as Quantity does not work as expected" ) return None def test_quasiisothermaldf_setup_roAsQuantity_oddunits(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) ro = 9000.0 df = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True, ro=ro * units.lyr, ) assert numpy.fabs(df._ro - ro * (units.lyr).to(units.kpc)) < 10.0**-10.0, ( "ro in quasiisothermaldf setup as Quantity does not work as expected" ) return None def test_quasiisothermaldf_setup_voAsQuantity(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) vo = 230.0 df = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True, vo=vo * units.km / units.s, ) assert numpy.fabs(df._vo - vo) < 10.0**-10.0, ( "vo in quasiisothermaldf setup as Quantity does not work as expected" ) return None def test_quasiisothermaldf_setup_voAsQuantity_oddunits(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) vo = 230.0 df = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True, vo=vo * units.pc / units.Myr, ) assert ( numpy.fabs(df._vo - vo * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "vo in quasiisothermaldf setup as Quantity does not work as expected" return None def test_test_quasiisothermaldf_setup_profileAsQuantity(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.orbit import Orbit from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) ro, vo = 7.0, 250.0 qdf = quasiisothermaldf( 3.0 * units.kpc, 30.0 * units.km / units.s, 20.0 * units.pc / units.Myr, 10.0 * units.kpc, 8000.0 * units.lyr, pot=MWPotential, aA=aA, cutcounter=True, ro=ro, vo=vo, ) assert numpy.fabs(qdf._hr - 3.0 / ro) < 10.0**-10.0, ( "hr in quasiisothermaldf setup as Quantity does not work as expected" ) assert numpy.fabs(qdf._sr - 30.0 / vo) < 10.0**-10.0, ( "sr in quasiisothermaldf setup as Quantity does not work as expected" ) assert ( numpy.fabs(qdf._sz - 20.0 * (units.pc / units.Myr).to(units.km / units.s) / vo) < 10.0**-10.0 ), "sz in quasiisothermaldf setup as Quantity does not work as expected" assert numpy.fabs(qdf._hsr - 10.0 / ro) < 10.0**-10.0, ( "hr in quasiisothermaldf setup as Quantity does not work as expected" ) assert ( numpy.fabs(qdf._hsz - 8000.0 * (units.lyr).to(units.kpc) / ro) < 10.0**-10.0 ), "hsz in quasiisothermaldf setup as Quantity does not work as expected" return None def test_test_quasiisothermaldf_setup_refrloAsQuantity(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf from galpy.orbit import Orbit from galpy.potential import MWPotential aA = actionAngleAdiabatic(pot=MWPotential, c=True) ro, vo = 7.0, 250.0 qdf = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=MWPotential, aA=aA, cutcounter=True, refr=9.0 * units.kpc, lo=10.0 * units.kpc * units.km / units.s, ro=ro, vo=vo, ) assert numpy.fabs(qdf._refr - 9.0 / ro) < 10.0**-10.0, ( "refr in quasiisothermaldf setup as Quantity does not work as expected" ) assert numpy.fabs(qdf._lo - 10.0 / vo / ro) < 10.0**-10.0, ( "lo in quasiisothermaldf setup as Quantity does not work as expected" ) return None def test_sphericaldf_method_returntype(): from galpy import potential from galpy.df import constantbetaHernquistdf, isotropicHernquistdf from galpy.orbit import Orbit pot = potential.HernquistPotential(amp=2.0, a=1.3, ro=8.0, vo=220.0) dfh = isotropicHernquistdf(pot=pot) dfa = constantbetaHernquistdf(pot=pot, beta=-0.2) o = Orbit([1.1, 0.1, 1.1, 0.1, 0.03, 0.4]) assert isinstance(dfh(o), units.Quantity), ( "sphericaldf method __call__ does not return Quantity when it should" ) assert isinstance(dfh((o.E(pot=pot),)), units.Quantity), ( "sphericaldf method __call__ does not return Quantity when it should" ) assert isinstance( dfh(o.R(), o.vR(), o.vT(), o.z(), o.vz(), o.phi()), units.Quantity ), "sphericaldf method __call__ does not return Quantity when it should" assert isinstance(dfh.dMdE(o.E(pot=pot)), units.Quantity), ( "sphericaldf method dMdE does not return Quantity when it should" ) assert isinstance(dfh.vmomentdensity(1.1, 0, 0), units.Quantity), ( "sphericaldf method vmomentdensity does not return Quantity when it should" ) assert isinstance(dfa.vmomentdensity(1.1, 0, 0), units.Quantity), ( "sphericaldf method vmomentdensity does not return Quantity when it should" ) assert isinstance(dfh.vmomentdensity(1.1, 1, 0), units.Quantity), ( "sphericaldf method vmomentdensity does not return Quantity when it should" ) assert isinstance(dfa.vmomentdensity(1.1, 1, 0), units.Quantity), ( "sphericaldf method vmomentdensity does not return Quantity when it should" ) assert isinstance(dfh.vmomentdensity(1.1, 0, 2), units.Quantity), ( "sphericaldf method vmomentdensity does not return Quantity when it should" ) assert isinstance(dfa.vmomentdensity(1.1, 0, 2), units.Quantity), ( "sphericaldf method vmomentdensity does not return Quantity when it should" ) assert isinstance(dfh.sigmar(1.1), units.Quantity), ( "sphericaldf method sigmar does not return Quantity when it should" ) assert isinstance(dfh.sigmat(1.1), units.Quantity), ( "sphericaldf method sigmar does not return Quantity when it should" ) # beta should not be a quantity assert not isinstance(dfh.beta(1.1), units.Quantity), ( "sphericaldf method beta returns Quantity when it shouldn't" ) return None def test_sphericaldf_method_returnunit(): from galpy import potential from galpy.df import constantbetaHernquistdf, isotropicHernquistdf from galpy.orbit import Orbit pot = potential.HernquistPotential(amp=2.0, a=1.3, ro=8.0, vo=220.0) dfh = isotropicHernquistdf(pot=pot) dfa = constantbetaHernquistdf(pot=pot, beta=-0.2) o = Orbit([1.1, 0.1, 1.1, 0.1, 0.03, 0.4]) try: dfh(o).to(units.Msun / units.kpc**3 / (units.km / units.s) ** 3) except units.UnitConversionError: raise AssertionError( "sphericaldf method __call__ does not return Quantity with the right units" ) try: dfh((o.E(pot=pot),)).to(units.Msun / units.kpc**3 / (units.km / units.s) ** 3) except units.UnitConversionError: raise AssertionError( "sphericaldf method __call__ does not return Quantity with the right units" ) try: dfh(o.R(), o.vR(), o.vT(), o.z(), o.vz(), o.phi()).to( units.Msun / units.kpc**3 / (units.km / units.s) ** 3 ) except units.UnitConversionError: raise AssertionError( "sphericaldf method __call__ does not return Quantity with the right units" ) try: dfh.dMdE(o.E(pot=pot)).to(units.Msun / (units.km / units.s) ** 2) except units.UnitConversionError: raise AssertionError( "sphericaldf method dMdE does not return Quantity with the right units" ) try: dfh.vmomentdensity(1.1, 0, 0).to(units.Msun / units.kpc**3) except units.UnitConversionError: raise AssertionError( "sphericaldf method vmomentdensity does not return Quantity with the right units" ) try: dfa.vmomentdensity(1.1, 0, 0).to(units.Msun / units.kpc**3) except units.UnitConversionError: raise AssertionError( "sphericaldf method vmomentdensity does not return Quantity with the right units" ) try: dfh.vmomentdensity(1.1, 1, 0).to(units.Msun / units.kpc**3 * units.km / units.s) except units.UnitConversionError: raise AssertionError( "sphericaldf method vmomentdensity does not return Quantity with the right units" ) try: dfa.vmomentdensity(1.1, 1, 0).to(units.Msun / units.kpc**3 * units.km / units.s) except units.UnitConversionError: raise AssertionError( "sphericaldf method vmomentdensity does not return Quantity with the right units" ) try: dfh.vmomentdensity(1.1, 0, 2).to( units.Msun / units.kpc**3 * units.km**2 / units.s**2 ) except units.UnitConversionError: raise AssertionError( "sphericaldf method vmomentdensity does not return Quantity with the right units" ) try: dfa.vmomentdensity(1.1, 0, 2).to( units.Msun / units.kpc**3 * units.km**2 / units.s**2 ) except units.UnitConversionError: raise AssertionError( "sphericaldf method vmomentdensity does not return Quantity with the right units" ) try: dfh.sigmar(1.1).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "sphericaldf method sigmar does not return Quantity with the right units" ) try: dfh.sigmat(1.1).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "sphericaldf method sigmar does not return Quantity with the right units" ) return None def test_sphericaldf_method_value(): from galpy import potential from galpy.df import constantbetaHernquistdf, isotropicHernquistdf from galpy.orbit import Orbit from galpy.util import conversion ro, vo = 8.0, 220.0 pot = potential.HernquistPotential(amp=2.0, a=1.3) dfh = isotropicHernquistdf(pot=pot, ro=ro, vo=vo) dfh_nou = isotropicHernquistdf(pot=pot) dfa = constantbetaHernquistdf(pot=pot, beta=-0.2, ro=ro, vo=vo) dfa_nou = constantbetaHernquistdf(pot=pot, beta=-0.2) o = Orbit([1.1, 0.1, 1.1, 0.1, 0.03, 0.4]) assert ( numpy.fabs( dfh(o).to(units.Msun / units.kpc**3 / (units.km / units.s) ** 3).value - dfh_nou(o) * conversion.mass_in_msol(vo, ro) / ro**3 / vo**3 ) < 10.0**-8.0 ), "sphericaldf method __call__ does not return correct Quantity" assert ( numpy.fabs( dfh((o.E(pot=pot),)) .to(units.Msun / units.kpc**3 / (units.km / units.s) ** 3) .value - dfh_nou((o.E(pot=pot),)) * conversion.mass_in_msol(vo, ro) / ro**3 / vo**3 ) < 10.0**-8.0 ), "sphericaldf method __call__ does not return correct Quantity" assert ( numpy.fabs( dfh(o.R(), o.vR(), o.vT(), o.z(), o.vz(), o.phi()) .to(units.Msun / units.kpc**3 / (units.km / units.s) ** 3) .value - dfh_nou(o.R(), o.vR(), o.vT(), o.z(), o.vz(), o.phi()) * conversion.mass_in_msol(vo, ro) / ro**3 / vo**3 ) < 10.0**-8.0 ), "sphericaldf method __call__ does not return correct Quantity" assert ( numpy.fabs( dfh.dMdE(o.E(pot=pot)).to(units.Msun / (units.km / units.s) ** 2).value - dfh_nou.dMdE(o.E(pot=pot)) * conversion.mass_in_msol(vo, ro) / vo**2 ) < 10.0**-8.0 ), "sphericaldf method dMdE does not return correct Quantity" assert ( numpy.fabs( dfh.vmomentdensity(1.1, 0, 0).to(units.Msun / units.kpc**3).value - dfh_nou.vmomentdensity(1.1, 0, 0) * conversion.mass_in_msol(vo, ro) / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( dfa.vmomentdensity(1.1, 0, 0).to(units.Msun / units.kpc**3).value - dfa_nou.vmomentdensity(1.1, 0, 0) * conversion.mass_in_msol(vo, ro) / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( dfh.vmomentdensity(1.1, 1, 0) .to(units.Msun / units.kpc**3 * units.km / units.s) .value - dfh_nou.vmomentdensity(1.1, 1, 0) * conversion.mass_in_msol(vo, ro) * vo / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( dfa.vmomentdensity(1.1, 1, 0) .to(units.Msun / units.kpc**3 * units.km / units.s) .value - dfa_nou.vmomentdensity(1.1, 1, 0) * conversion.mass_in_msol(vo, ro) * vo / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" # One with no quantity output import galpy.util._optional_deps galpy.util._optional_deps._APY_UNITS = False # Hack assert ( numpy.fabs( dfh.vmomentdensity(1.1, 0, 2) - dfh_nou.vmomentdensity(1.1, 0, 2) * conversion.mass_in_msol(vo, ro) * vo**2 / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" galpy.util._optional_deps._APY_UNITS = True # Hack assert ( numpy.fabs( dfh.vmomentdensity(1.1, 0, 2) .to(units.Msun / units.kpc**3 * units.km**2 / units.s**2) .value - dfh_nou.vmomentdensity(1.1, 0, 2) * conversion.mass_in_msol(vo, ro) * vo**2 / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( dfa.vmomentdensity(1.1, 0, 2) .to(units.Msun / units.kpc**3 * units.km**2 / units.s**2) .value - dfa_nou.vmomentdensity(1.1, 0, 2) * conversion.mass_in_msol(vo, ro) * vo**2 / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( dfh.sigmar(1.1).to(units.km / units.s).value - dfh_nou.sigmar(1.1) * vo ) < 10.0**-8.0 ), "sphericaldf method sigmar does not return correct Quantity" assert ( numpy.fabs( dfh.sigmat(1.1).to(units.km / units.s).value - dfh_nou.sigmat(1.1) * vo ) < 10.0**-8.0 ), "sphericaldf method sigmat does not return correct Quantity" return None def test_sphericaldf_method_inputAsQuantity(): from galpy import potential from galpy.df import constantbetaHernquistdf, isotropicHernquistdf from galpy.orbit import Orbit from galpy.util import conversion ro, vo = 8.0, 220.0 pot = potential.HernquistPotential(amp=2.0, a=1.3) dfh = isotropicHernquistdf(pot=pot, ro=ro, vo=vo) dfh_nou = isotropicHernquistdf(pot=pot) dfa = constantbetaHernquistdf(pot=pot, beta=-0.2, ro=ro, vo=vo) dfa_nou = constantbetaHernquistdf(pot=pot, beta=-0.2) o = Orbit([1.1, 0.1, 1.1, 0.1, 0.03, 0.4], ro=ro, vo=vo) assert ( numpy.fabs( dfh((o.E(pot=pot),)) .to(units.Msun / units.kpc**3 / (units.km / units.s) ** 3) .value - dfh_nou((o.E(pot=pot, use_physical=False),)) * conversion.mass_in_msol(vo, ro) / ro**3 / vo**3 ) < 10.0**-8.0 ), "sphericaldf method __call__ does not return correct Quantity" assert ( numpy.fabs( dfh(o.R(), o.vR(), o.vT(), o.z(), o.vz(), o.phi()) .to(units.Msun / units.kpc**3 / (units.km / units.s) ** 3) .value - dfh_nou( o.R(use_physical=False), o.vR(use_physical=False), o.vT(use_physical=False), o.z(use_physical=False), o.vz(use_physical=False), o.phi(use_physical=False), ) * conversion.mass_in_msol(vo, ro) / ro**3 / vo**3 ) < 10.0**-8.0 ), "sphericaldf method __call__ does not return correct Quantity" assert ( numpy.fabs( dfh.dMdE(o.E(pot=pot)).to(units.Msun / (units.km / units.s) ** 2).value - dfh_nou.dMdE(o.E(pot=pot, use_physical=False)) * conversion.mass_in_msol(vo, ro) / vo**2 ) < 10.0**-8.0 ), "sphericaldf method dMdE does not return correct Quantity" assert ( numpy.fabs( dfh.vmomentdensity(1.1 * ro * units.kpc, 0, 0) .to(units.Msun / units.kpc**3) .value - dfh_nou.vmomentdensity(1.1, 0, 0) * conversion.mass_in_msol(vo, ro) / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( dfa.vmomentdensity(1.1 * ro * units.kpc, 0, 0, ro=ro * units.kpc) .to(units.Msun / units.kpc**3) .value - dfa_nou.vmomentdensity(1.1, 0, 0) * conversion.mass_in_msol(vo, ro) / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( dfh.vmomentdensity( 1.1 * ro * units.kpc, 1, 0, ro=ro, vo=vo * units.km / units.s ) .to(units.Msun / units.kpc**3 * units.km / units.s) .value - dfh_nou.vmomentdensity(1.1, 1, 0) * conversion.mass_in_msol(vo, ro) * vo / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( dfa.vmomentdensity(1.1 * ro * units.kpc, 1, 0, vo=vo * units.km / units.s) .to(units.Msun / units.kpc**3 * units.km / units.s) .value - dfa_nou.vmomentdensity(1.1, 1, 0) * conversion.mass_in_msol(vo, ro) * vo / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( dfh.vmomentdensity(1.1 * ro * units.kpc, 0, 2) .to(units.Msun / units.kpc**3 * units.km**2 / units.s**2) .value - dfh_nou.vmomentdensity(1.1, 0, 2) * conversion.mass_in_msol(vo, ro) * vo**2 / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( dfa.vmomentdensity(1.1 * ro * units.kpc, 0, 2) .to(units.Msun / units.kpc**3 * units.km**2 / units.s**2) .value - dfa_nou.vmomentdensity(1.1, 0, 2) * conversion.mass_in_msol(vo, ro) * vo**2 / ro**3 ) < 10.0**-8.0 ), "sphericaldf method vmomentdensity does not return correct Quantity" assert ( numpy.fabs( dfh.sigmar(1.1 * ro * units.kpc).to(units.km / units.s).value - dfh_nou.sigmar(1.1) * vo ) < 10.0**-8.0 ), "sphericaldf method sigmar does not return correct Quantity" assert ( numpy.fabs( dfh.sigmat(1.1 * ro * units.kpc).to(units.km / units.s).value - dfh_nou.sigmat(1.1) * vo ) < 10.0**-8.0 ), "sphericaldf method sigmat does not return correct Quantity" return None def test_sphericaldf_sample(): from galpy import potential from galpy.df import isotropicHernquistdf from galpy.orbit import Orbit ro, vo = 8.0, 220.0 pot = potential.HernquistPotential(amp=2.0, a=1.3) dfh = isotropicHernquistdf(pot=pot, ro=ro, vo=vo) numpy.random.seed(10) sam = dfh.sample(R=1.0 * units.kpc, z=0.0 * units.kpc, phi=10.0 * units.deg, n=2) numpy.random.seed(10) sam_nou = dfh.sample(R=1.0 / ro, z=0.0 / ro, phi=10.0 / 180.0 * numpy.pi, n=2) assert numpy.all( numpy.fabs(sam.r(use_physical=False) - sam_nou.r(use_physical=False)) < 1e-8 ), ( "Sample returned by sphericaldf.sample with input R,z,phi with units does not agree with that returned by sampline with input R,z,phi without units" ) assert numpy.all( numpy.fabs(sam.vr(use_physical=False) - sam_nou.vr(use_physical=False)) < 1e-8 ), ( "Sample returned by sphericaldf.sample with input R,z,phi with units does not agree with that returned by sampline with input R,z,phi without units" ) # Array input arr = numpy.array([1.0, 2.0]) numpy.random.seed(10) sam = dfh.sample( R=arr * units.kpc, z=arr * 0.0 * units.kpc, phi=arr * 10.0 * units.deg, n=len(arr), ) numpy.random.seed(10) sam_nou = dfh.sample( R=arr / ro, z=arr * 0.0 / ro, phi=arr * 10.0 / 180.0 * numpy.pi, n=len(arr) ) assert numpy.all( numpy.fabs(sam.r(use_physical=False) - sam_nou.r(use_physical=False)) < 1e-8 ), ( "Sample returned by sphericaldf.sample with input R,z,phi with units does not agree with that returned by sampline with input R,z,phi without units" ) assert numpy.all( numpy.fabs(sam.vr(use_physical=False) - sam_nou.vr(use_physical=False)) < 1e-8 ), ( "Sample returned by sphericaldf.sample with input R,z,phi with units does not agree with that returned by sampline with input R,z,phi without units" ) # rmin numpy.random.seed(10) sam = dfh.sample(n=2, rmin=1.1 * units.kpc) numpy.random.seed(10) sam_nou = dfh.sample(n=2, rmin=1.1 / ro) assert numpy.all( numpy.fabs(sam.r(use_physical=False) - sam_nou.r(use_physical=False)) < 1e-8 ), ( "Sample returned by sphericaldf.sample with input rmin with units does not agree with that returned by sampline with input rmin without units" ) return None def test_sphericaldf_sample_outputunits(): from galpy import potential from galpy.df import isotropicHernquistdf ro, vo = 8.0, 220.0 pot = potential.HernquistPotential(amp=2.0, a=1.3) dfh = isotropicHernquistdf(pot=pot, ro=ro, vo=vo) dfh_nou = isotropicHernquistdf(pot=pot) numpy.random.seed(10) sam = dfh.sample( R=1.0 * units.kpc, z=0.0 * units.kpc, phi=10.0 * units.deg, n=2, return_orbit=False, ) numpy.random.seed(10) sam_nou = dfh_nou.sample( R=1.0 / ro, z=0.0 / ro, phi=10.0 / 180.0 * numpy.pi, n=2, return_orbit=False ) assert numpy.all(numpy.fabs(sam[0].to_value(units.kpc) / ro - sam_nou[0]) < 1e-8), ( "Sample returned by sphericaldf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all( numpy.fabs(sam[1].to_value(units.km / units.s) / vo - sam_nou[1]) < 1e-8 ), ( "Sample returned by sphericaldf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all( numpy.fabs(sam[2].to_value(units.km / units.s) / vo - sam_nou[2]) < 1e-8 ), ( "Sample returned by sphericaldf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all(numpy.fabs(sam[3].to_value(units.kpc) / ro - sam_nou[3]) < 1e-8), ( "Sample returned by sphericaldf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all( numpy.fabs(sam[4].to_value(units.km / units.s) / vo - sam_nou[4]) < 1e-8 ), ( "Sample returned by sphericaldf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all(numpy.fabs(sam[5].to_value(units.rad) - sam_nou[5]) < 1e-8), ( "Sample returned by sphericaldf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) return None def test_kingdf_setup_wunits(): from galpy.df import kingdf from galpy.util import conversion ro, vo = 9.0, 210.0 dfk = kingdf(W0=3.0, M=4 * 1e4 * units.Msun, rt=10.0 * units.pc, ro=ro, vo=vo) dfk_nou = kingdf( W0=3.0, M=4 * 1e4 / conversion.mass_in_msol(vo, ro), rt=10.0 / ro / 1000, ro=ro, vo=vo, ) assert ( numpy.fabs( dfk.sigmar(1.0 * units.pc, use_physical=False) - dfk_nou.sigmar(1.0 * units.pc, use_physical=False) ) < 1e-8 ), ( "kingdf set up with parameters with units does not agree with kingdf not set up with parameters with units" ) return None def test_sphericaldf_densitymatch_issue677(): from astropy import units from galpy.df import isotropicPlummerdf from galpy.potential import PlummerPotential from galpy.util import conversion # First check that when not using unit-full output, the density of the # PlummerPotential matches that of the Plummer DF potential = PlummerPotential(amp=1e5, b=10.0) df = isotropicPlummerdf(pot=potential) assert ( numpy.fabs( potential.dens(0.0, 0.0, 0.0) / df.vmomentdensity(0.0, 0.0, 0.0) - 1.0 ) < 1e-8 ), "Density of PlummerPotential does not match that of isotropicPlummerdf" # Then check that they continue to match when using unit-full output potential = PlummerPotential(amp=1e5 * units.Msun, b=10.0) df = isotropicPlummerdf(pot=potential) assert ( numpy.fabs( potential.dens(0.0, 0.0, 0.0) / df.vmomentdensity(0.0, 0.0, 0.0) - 1.0 ) < 1e-8 ), ( "Density of PlummerPotential does not match that of isotropicPlummerdf when returning Quantities" ) return None def test_streamdf_method_returntype(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s ro, vo = 9.0, 250.0 sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), ro=ro, vo=vo, nosetup=True, ) assert isinstance(sdf_bovy14.misalignment(), units.Quantity), ( "streamdf method misalignment does not return Quantity when it should" ) assert isinstance(sdf_bovy14.estimateTdisrupt(0.1), units.Quantity), ( "streamdf method estimateTdisrupt does not return Quantity when it should" ) assert isinstance(sdf_bovy14.meanOmega(0.1), units.Quantity), ( "streamdf method meanOmega does not return Quantity when it should" ) assert isinstance(sdf_bovy14.sigOmega(0.1), units.Quantity), ( "streamdf method sigOmega does not return Quantity when it should" ) assert isinstance(sdf_bovy14.meantdAngle(0.1), units.Quantity), ( "streamdf method meantdAngle does not return Quantity when it should" ) assert isinstance(sdf_bovy14.sigtdAngle(0.1), units.Quantity), ( "streamdf method sigtdAngle does not return Quantity when it should" ) assert isinstance(sdf_bovy14.meanangledAngle(0.1), units.Quantity), ( "streamdf method meanangledAngle does not return Quantity when it should" ) assert isinstance(sdf_bovy14.sigangledAngle(0.1), units.Quantity), ( "streamdf method sigangledAngle does not return Quantity when it should" ) return None def test_streamdf_method_returnunit(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s ro, vo = 9.0, 250.0 sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), ro=ro, vo=vo, nosetup=True, ) try: sdf_bovy14.misalignment().to(units.deg) except units.UnitConversionError: raise AssertionError( "streamdf method misalignment does not return Quantity with the right units" ) try: sdf_bovy14.estimateTdisrupt(0.1).to(units.Myr) except units.UnitConversionError: raise AssertionError( "streamdf method estimateTdisrupt does not return Quantity with the right units" ) try: sdf_bovy14.meanOmega(0.1).to(1 / units.Myr) except units.UnitConversionError: raise AssertionError( "streamdf method meanOmega does not return Quantity with the right units" ) try: sdf_bovy14.sigOmega(0.1).to(1 / units.Myr) except units.UnitConversionError: raise AssertionError( "streamdf method sigOmega does not return Quantity with the right units" ) try: sdf_bovy14.meantdAngle(0.1).to(units.Myr) except units.UnitConversionError: raise AssertionError( "streamdf method meantdAngle does not return Quantity with the right units" ) try: sdf_bovy14.sigtdAngle(0.1).to(units.Myr) except units.UnitConversionError: raise AssertionError( "streamdf method sigtdAngle does not return Quantity with the right units" ) try: sdf_bovy14.meanangledAngle(0.1).to(units.rad) except units.UnitConversionError: raise AssertionError( "streamdf method meanangledAngle does not return Quantity with the right units" ) try: sdf_bovy14.sigangledAngle(0.1).to(units.rad) except units.UnitConversionError: raise AssertionError( "streamdf method sigangledAngle does not return Quantity with the right units" ) return None def test_streamdf_method_value(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s ro, vo = 9.0, 250.0 sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), ro=ro, vo=vo, nosetup=True, ) sdf_bovy14_nou = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), nosetup=True, ) assert ( numpy.fabs( sdf_bovy14.misalignment().to(units.rad).value - sdf_bovy14_nou.misalignment() ) < _NUMPY_1_22 * 1e-7 + (1 - _NUMPY_1_22) * 1e-8 ), "streamdf method misalignment does not return correct Quantity" assert ( numpy.fabs( sdf_bovy14.estimateTdisrupt(0.1).to(units.Gyr).value - sdf_bovy14_nou.estimateTdisrupt(0.1) * conversion.time_in_Gyr(vo, ro) ) < _NUMPY_1_22 * 1e-7 + (1 - _NUMPY_1_22) * 1e-8 ), "streamdf method estimateTdisrupt does not return correct Quantity" assert numpy.all( numpy.fabs( sdf_bovy14.meanOmega(0.1).to(1 / units.Gyr).value - sdf_bovy14_nou.meanOmega(0.1) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "streamdf method meanOmega does not return correct Quantity" assert ( numpy.fabs( sdf_bovy14.sigOmega(0.1).to(1 / units.Gyr).value - sdf_bovy14_nou.sigOmega(0.1) * conversion.freq_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "streamdf method sigOmega does not return correct Quantity" assert ( numpy.fabs( sdf_bovy14.meantdAngle(0.1).to(units.Gyr).value - sdf_bovy14_nou.meantdAngle(0.1) * conversion.time_in_Gyr(vo, ro) ) < 10.0**-7.0 ), "streamdf method meantdAngle does not return correct Quantity" assert ( numpy.fabs( sdf_bovy14.sigtdAngle(0.1).to(units.Gyr).value - sdf_bovy14_nou.sigtdAngle(0.1) * conversion.time_in_Gyr(vo, ro) ) < 10.0**-8.0 ), "streamdf method sigtdAngle does not return correct Quantity" assert ( numpy.fabs( sdf_bovy14.meanangledAngle(0.1).to(units.rad).value - sdf_bovy14_nou.meanangledAngle(0.1) ) < 10.0**-8.0 ), "streamdf method meanangledAngle does not return correct Quantity" assert ( numpy.fabs( sdf_bovy14.sigangledAngle(0.1).to(units.rad).value - sdf_bovy14_nou.sigangledAngle(0.1) ) < 10.0**-8.0 ), "streamdf method sigangledAngle does not return correct Quantity" return None def test_streamdf_method_inputAsQuantity(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s ro, vo = 9.0, 250.0 sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), ro=ro, vo=vo, nosetup=True, ) sdf_bovy14_nou = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), nosetup=True, ) assert numpy.fabs( sdf_bovy14.subhalo_encounters( venc=200.0 * units.km / units.s, sigma=150.0 * units.km / units.s, nsubhalo=38.35 / (4.0 * (25.0 * units.kpc) ** 3.0 * numpy.pi / 3.0), bmax=1.0 * units.kpc, yoon=False, ) - sdf_bovy14_nou.subhalo_encounters( venc=200.0 / vo, sigma=150.0 / vo, nsubhalo=38.35 / (4.0 * 25.0**3.0 * numpy.pi / 3.0) * ro**3.0, bmax=1.0 / ro, yoon=False, ) ) < 1e-6 * _NUMPY_1_22 + 1e-8 * (1 - _NUMPY_1_22), ( "streamdf method subhalo_encounters with Quantity input does not return correct Quantity" ) assert numpy.fabs( sdf_bovy14.pOparapar(0.2 / units.Gyr, 30.0 * units.deg) - sdf_bovy14_nou.pOparapar( 0.2 / conversion.freq_in_Gyr(vo, ro), 30.0 * numpy.pi / 180.0 ) ) < 1e-5 * _NUMPY_1_22 + 1e-8 * (1 - _NUMPY_1_22), ( "streamdf method pOparapar with Quantity input does not return correct Quantity" ) return None def test_streamdf_sample(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s ro, vo = 9.0, 250.0 sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), ro=ro, vo=vo, nosetup=True, ) sdf_bovy14_nou = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), nosetup=True, ) # aa numpy.random.seed(1) acfsdt = sdf_bovy14.sample(1, returnaAdt=True) numpy.random.seed(1) acfsdtnou = sdf_bovy14_nou.sample(1, returnaAdt=True) assert numpy.all( numpy.fabs( acfsdt[0].to(1 / units.Gyr).value / conversion.freq_in_Gyr(vo, ro) - acfsdtnou[0] ) < 10.0**-8.0 ), "streamdf sample returnaAdt does not return correct Quantity" assert numpy.all( numpy.fabs(acfsdt[1].to(units.rad).value - acfsdtnou[1]) < 10.0**-8.0 ), "streamdf sample returnaAdt does not return correct Quantity" assert numpy.all( numpy.fabs( acfsdt[2].to(units.Gyr).value / conversion.time_in_Gyr(vo, ro) - acfsdtnou[2] ) < 10.0**-8.0 ), "streamdf sample returnaAdt does not return correct Quantity" # Test others as part of streamgapdf return None def test_streamdf_setup_roAsQuantity(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s ro = 9.0 df = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), ro=ro * units.kpc, nosetup=True, ) assert numpy.fabs(df._ro - ro) < 10.0**-10.0, ( "ro in streamdf setup as Quantity does not work as expected" ) return None def test_streamdf_setup_roAsQuantity_oddunits(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s ro = 9000.0 df = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), ro=ro * units.lyr, nosetup=True, ) assert numpy.fabs(df._ro - ro * (units.lyr).to(units.kpc)) < 10.0**-10.0, ( "ro in quasiisothermaldf setup as Quantity does not work as expected" ) return None def test_streamdf_setup_voAsQuantity(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s vo = 250.0 df = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), vo=vo * units.km / units.s, nosetup=True, ) assert numpy.fabs(df._vo - vo) < 10.0**-10.0, ( "vo in streamdf setup as Quantity does not work as expected" ) return None def test_streamdf_setup_voAsQuantity_oddunits(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s vo = 250.0 df = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), vo=vo * units.pc / units.Myr, nosetup=True, ) assert ( numpy.fabs(df._vo - vo * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "vo in streamdf setup as Quantity does not work as expected" return None def test_streamdf_setup_paramsAsQuantity(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 * units.km / units.s ro, vo = 9.0, 230.0 df = streamdf( sigv, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 * units.Gyr, ro=ro, vo=vo, sigangle=0.01 * units.deg, deltaAngleTrack=170.0 * units.deg, nosetup=True, ) assert numpy.fabs(df._sigv - 0.365 / vo) < 10.0**-10.0, ( "sigv in streamdf setup as Quantity does not work as expected" ) assert ( numpy.fabs(df._tdisrupt - 4.5 / conversion.time_in_Gyr(vo, ro)) < 10.0**-10.0 ), "tdisrupt in streamdf setup as Quantity does not work as expected" assert numpy.fabs(df._sigangle - 0.01 * (units.deg).to(units.rad)) < 10.0**-10.0, ( "sigangle in streamdf setup as Quantity does not work as expected" ) assert ( numpy.fabs(df._deltaAngleTrack - 170.0 * (units.deg).to(units.rad)) < 10.0**-10.0 ), "deltaAngleTrack in streamdf setup as Quantity does not work as expected" return None def test_streamdf_setup_coordtransformparamsAsQuantity(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s ro, vo = 9.0, 230.0 df = streamdf( sigv / vo, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ro=ro, vo=vo, nosetup=True, R0=8.0 * units.kpc, Zsun=25.0 * units.pc, vsun=units.Quantity( [ -10.0 * units.km / units.s, 240.0 * units.pc / units.Myr, 7.0 * units.km / units.s, ] ), ) assert numpy.fabs(df._R0 - 8.0) < 10.0**-10.0, ( "R0 in streamdf setup as Quantity does not work as expected" ) assert numpy.fabs(df._Zsun - 0.025) < 10.0**-10.0, ( "Zsun in streamdf setup as Quantity does not work as expected" ) assert numpy.fabs(df._vsun[0] + 10.0) < 10.0**-10.0, ( "vsun in streamdf setup as Quantity does not work as expected" ) assert ( numpy.fabs(df._vsun[1] - 240.0 * (units.pc / units.Myr).to(units.km / units.s)) < 10.0**-10.0 ), "vsun in streamdf setup as Quantity does not work as expected" assert numpy.fabs(df._vsun[2] - 7.0) < 10.0**-10.0, ( "vsun in streamdf setup as Quantity does not work as expected" ) # Now with vsun as Quantity df = streamdf( sigv / vo, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ro=ro, vo=vo, nosetup=True, R0=8.0 * units.kpc, Zsun=25.0 * units.pc, vsun=units.Quantity([-10.0, 240.0, 7.0], unit=units.km / units.s), ) assert numpy.fabs(df._vsun[0] + 10.0) < 10.0**-10.0, ( "vsun in streamdf setup as Quantity does not work as expected" ) assert numpy.fabs(df._vsun[1] - 240.0) < 10.0**-10.0, ( "vsun in streamdf setup as Quantity does not work as expected" ) assert numpy.fabs(df._vsun[2] - 7.0) < 10.0**-10.0, ( "vsun in streamdf setup as Quantity does not work as expected" ) return None def test_streamdf_RnormWarning(): import warnings from galpy.actionAngle import actionAngleIsochroneApprox # Imports from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions from galpy.util import galpyWarning lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s ro, vo = 9.0, 250.0 with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), Rnorm=ro, nosetup=True, ) raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "WARNING: Rnorm keyword input to streamdf is deprecated in favor of the standard ro keyword" ) if raisedWarning: break assert raisedWarning, "Rnorm warning not raised when it should have been" return None def test_streamdf_VnormWarning(): import warnings from galpy.actionAngle import actionAngleIsochroneApprox # Imports from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions from galpy.util import galpyWarning lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s ro, vo = 9.0, 250.0 with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), Vnorm=vo, nosetup=True, ) raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "WARNING: Vnorm keyword input to streamdf is deprecated in favor of the standard vo keyword" ) if raisedWarning: break assert raisedWarning, "Vnorm warning not raised when it should have been" return None def test_streamgapdf_method_returntype(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamgapdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) prog_unp_peri = Orbit( [ 2.6556151742081835, 0.2183747276300308, 0.67876510797240575, -2.0143395648974671, -0.3273737682604374, 0.24218273922966019, ] ) global sdf_sanders15, sdf_sanders15_nou V0, R0 = 220.0, 8.0 sigv = 0.365 * (10.0 / 2.0) ** (1.0 / 3.0) * units.km / units.s # bare-bones setup, only interested in testing consistency between units # and no units sdf_sanders15 = streamgapdf( sigv, progenitor=prog_unp_peri, pot=lp, aA=aAI, leading=False, nTrackChunks=5, nTrackIterations=1, nTrackChunksImpact=5, sigMeanOffset=4.5, tdisrupt=10.88 * units.Gyr, Vnorm=V0, Rnorm=R0, impactb=0.1 * units.kpc, subhalovel=numpy.array([6.82200571, 132.7700529, 149.4174464]) * units.km / units.s, timpact=0.88 * units.Gyr, impact_angle=-2.34 * units.rad, GM=10.0**8.0 * units.Msun, rs=625.0 * units.pc, ) # Setup nounit version for later sdf_sanders15_nou = streamgapdf( sigv.to(units.km / units.s).value / V0, progenitor=prog_unp_peri, pot=lp, aA=aAI, leading=False, nTrackChunks=5, nTrackIterations=1, nTrackChunksImpact=5, Vnorm=V0, Rnorm=R0, sigMeanOffset=4.5, tdisrupt=10.88 / conversion.time_in_Gyr(V0, R0), impactb=0.1 / R0, subhalovel=numpy.array([6.82200571, 132.7700529, 149.4174464]) / V0, timpact=0.88 / conversion.time_in_Gyr(V0, R0), impact_angle=-2.34, GM=10.0**-2.0 / conversion.mass_in_1010msol(V0, R0), rs=0.625 / R0, ) # turn off units sdf_sanders15_nou._roSet = False sdf_sanders15_nou._voSet = False assert isinstance(sdf_sanders15.meanOmega(0.1), units.Quantity), ( "streamgapdf method meanOmega does not return Quantity when it should" ) return None def test_streamgapdf_method_returnunit(): try: sdf_sanders15.meanOmega(0.1).to(1 / units.Gyr) except units.UnitConversionError: raise AssertionError( "streamdf method meanOmega does not return Quantity with the right units" ) return None def test_streamgapdf_method_value(): from galpy.util import conversion assert numpy.all( numpy.fabs( sdf_sanders15.meanOmega(0.1).to(1 / units.Gyr).value / conversion.freq_in_Gyr(sdf_sanders15._vo, sdf_sanders15._ro) - sdf_sanders15_nou.meanOmega(0.1) ) < 10.0**-8.0 ), "streamgapdf method meanOmega does not return correct Quantity" return None def test_streamgapdf_setup_impactparamsAsQuantity(): assert ( numpy.fabs(sdf_sanders15._impactb - sdf_sanders15_nou._impactb) < 10.0**-8.0 ), "impactb specified as Quantity for streamgapdf does not work as expected" assert ( numpy.fabs(sdf_sanders15._impact_angle - sdf_sanders15_nou._impact_angle) < 10.0**-8.0 ), "impact_angle specified as Quantity for streamgapdf does not work as expected" assert ( numpy.fabs(sdf_sanders15._timpact - sdf_sanders15_nou._timpact) < 10.0**-8.0 ), "timpact specified as Quantity for streamgapdf does not work as expected" assert numpy.all( numpy.fabs(sdf_sanders15._subhalovel - sdf_sanders15_nou._subhalovel) < 10.0**-8.0 ), "subhalovel specified as Quantity for streamgapdf does not work as expected" # GM and rs are not currently stored in streamgapdf, so just check kick assert numpy.all( numpy.fabs(sdf_sanders15._kick_deltav - sdf_sanders15_nou._kick_deltav) < 10.0**-8.0 ), ( "Calculated kick from parameters specified as Quantity for streamgapdf does not work as expected" ) return None def test_streamgapdf_inputAsQuantity(): from galpy.util import conversion assert ( numpy.fabs( sdf_sanders15.pOparapar(0.2 / units.Gyr, 30.0 * units.deg) - sdf_sanders15_nou.pOparapar( 0.2 / conversion.freq_in_Gyr(sdf_sanders15._vo, sdf_sanders15._ro), 30.0 * numpy.pi / 180.0, ) ) < 1e-4 ), ( "streamgapdf method pOparapar with Quantity input does not return correct Quantity" ) return None def test_streamgapdf_sample(): from galpy.util import conversion # RvR numpy.random.seed(1) RvR = sdf_sanders15.sample(1) numpy.random.seed(1) RvRnou = sdf_sanders15_nou.sample(1) assert ( numpy.fabs(RvR[0].to(units.kpc).value / sdf_sanders15._ro - RvRnou[0]) < _NUMPY_1_22 * 1e-4 + (1 - _NUMPY_1_22) * 1e-6 ), "streamgapdf sample RvR does not return a correct Quantity" assert ( numpy.fabs(RvR[3].to(units.kpc).value / sdf_sanders15._ro - RvRnou[3]) < 1e-6 ), "streamgapdf sample RvR does not return a correct Quantity" assert ( numpy.fabs(RvR[1].to(units.km / units.s).value / sdf_sanders15._vo - RvRnou[1]) < 1e-6 ), "streamgapdf sample RvR does not return a correct Quantity" assert ( numpy.fabs(RvR[2].to(units.km / units.s).value / sdf_sanders15._vo - RvRnou[2]) < 1e-6 ), "streamgapdf sample RvR does not return a correct Quantity" assert ( numpy.fabs(RvR[4].to(units.km / units.s).value / sdf_sanders15._vo - RvRnou[4]) < 1e-6 ), "streamgapdf sample RvR does not return a correct Quantity" assert numpy.fabs(RvR[5].to(units.rad).value - RvRnou[5]) < 1e-6, ( "streamgapdf sample RvR does not return a correct Quantity" ) # RvR,dt numpy.random.seed(1) RvRdt = sdf_sanders15.sample(1, returndt=True) numpy.random.seed(1) RvRdtnou = sdf_sanders15_nou.sample(1, returndt=True) assert ( numpy.fabs(RvRdt[0].to(units.kpc).value / sdf_sanders15._ro - RvRdtnou[0]) < 1e-6 ), "streamgapdf sample RvRdt does not return a correct Quantity" assert ( numpy.fabs(RvRdt[3].to(units.kpc).value / sdf_sanders15._ro - RvRdtnou[3]) < 1e-6 ), "streamgapdf sample RvRdt does not return a correct Quantity" assert ( numpy.fabs( RvRdt[1].to(units.km / units.s).value / sdf_sanders15._vo - RvRdtnou[1] ) < 1e-6 ), "streamgapdf sample RvRdt does not return a correct Quantity" assert ( numpy.fabs( RvRdt[2].to(units.km / units.s).value / sdf_sanders15._vo - RvRdtnou[2] ) < 1e-6 ), "streamgapdf sample RvRdt does not return a correct Quantity" assert ( numpy.fabs( RvRdt[4].to(units.km / units.s).value / sdf_sanders15._vo - RvRdtnou[4] ) < 1e-6 ), "streamgapdf sample RvRdt does not return a correct Quantity" assert numpy.fabs(RvRdt[5].to(units.rad).value - RvRdtnou[5]) < 1e-6, ( "streamgapdf sample RvRdt does not return a correct Quantity" ) assert ( numpy.fabs( RvRdt[6].to(units.Gyr).value / conversion.time_in_Gyr(sdf_sanders15._vo, sdf_sanders15._ro) - RvRdtnou[6] ) < 1e-6 ), "streamgapdf sample RvRdt does not return a correct Quantity" # xy numpy.random.seed(1) xy = sdf_sanders15.sample(1, xy=True) numpy.random.seed(1) xynou = sdf_sanders15_nou.sample(1, xy=True) assert ( numpy.fabs(xy[0].to(units.kpc).value / sdf_sanders15._ro - xynou[0]) < 1e-6 ), "streamgapdf sample xy does not return a correct Quantity" assert ( numpy.fabs(xy[1].to(units.kpc).value / sdf_sanders15._ro - xynou[1]) < 1e-6 ), "streamgapdf sample xy does not return a correct Quantity" assert ( numpy.fabs(xy[2].to(units.kpc).value / sdf_sanders15._ro - xynou[2]) < 1e-6 ), "streamgapdf sample xy does not return a correct Quantity" assert ( numpy.fabs(xy[3].to(units.km / units.s).value / sdf_sanders15._vo - xynou[3]) < 1e-6 ), "streamgapdf sample xy does not return a correct Quantity" assert ( numpy.fabs(xy[4].to(units.km / units.s).value / sdf_sanders15._vo - xynou[4]) < 1e-6 ), "streamgapdf sample xy does not return a correct Quantity" assert ( numpy.fabs(xy[5].to(units.km / units.s).value / sdf_sanders15._vo - xynou[5]) < 1e-6 ), "streamgapdf sample xy does not return a correct Quantity" # xydt numpy.random.seed(1) xydt = sdf_sanders15.sample(1, xy=True, returndt=True) numpy.random.seed(1) xydtnou = sdf_sanders15_nou.sample(1, xy=True, returndt=True) assert ( numpy.fabs(xy[0].to(units.kpc).value / sdf_sanders15._ro - xynou[0]) < 1e-6 ), "streamgapdf sample xy does not return a correct Quantity" assert ( numpy.fabs(xy[1].to(units.kpc).value / sdf_sanders15._ro - xynou[1]) < 1e-6 ), "streamgapdf sample xy does not return a correct Quantity" assert ( numpy.fabs(xy[2].to(units.kpc).value / sdf_sanders15._ro - xynou[2]) < 1e-6 ), "streamgapdf sample xy does not return a correct Quantity" assert ( numpy.fabs(xy[3].to(units.km / units.s).value / sdf_sanders15._vo - xynou[3]) < 1e-6 ), "streamgapdf sample xy does not return a correct Quantity" assert ( numpy.fabs(xy[4].to(units.km / units.s).value / sdf_sanders15._vo - xynou[4]) < 1e-6 ), "streamgapdf sample xy does not return a correct Quantity" assert ( numpy.fabs(xy[5].to(units.km / units.s).value / sdf_sanders15._vo - xynou[5]) < 1e-6 ), "streamgapdf sample xy does not return a correct Quantity" assert ( numpy.fabs( xydt[6].to(units.Gyr).value / conversion.time_in_Gyr(sdf_sanders15._vo, sdf_sanders15._ro) - xydtnou[6] ) < 1e-6 ), "streamgapdf sample xydt does not return a correct Quantity" # lb numpy.random.seed(1) lb = sdf_sanders15.sample(1, lb=True) numpy.random.seed(1) lbnou = sdf_sanders15_nou.sample(1, lb=True) assert ( numpy.fabs(lb[0].to(units.deg).value - lbnou[0]) < _NUMPY_1_22 * 1e-4 + (1 - _NUMPY_1_22) * 1e-5 ), "streamgapdf sample lb does not return a correct Quantity" assert ( numpy.fabs(lb[1].to(units.deg).value - lbnou[1]) < _NUMPY_1_22 * 1e-4 + (1 - _NUMPY_1_22) * 1e-5 ), "streamgapdf sample lb does not return a correct Quantity" assert ( numpy.fabs(lb[2].to(units.kpc).value - lbnou[2]) < _NUMPY_1_22 * 1e-5 + (1 - _NUMPY_1_22) * 1e-8 ), "streamgapdf sample lb does not return a correct Quantity" assert ( numpy.fabs(lb[3].to(units.km / units.s).value - lbnou[3]) < _NUMPY_1_22 * 1e-4 + (1 - _NUMPY_1_22) * 1e-5 ), "streamgapdf sample lb does not return a correct Quantity" assert ( numpy.fabs(lb[4].to(units.mas / units.yr).value - lbnou[4]) < _NUMPY_1_22 * 1e-4 + (1 - _NUMPY_1_22) * 1e-5 ), "streamgapdf sample lb does not return a correct Quantity" assert ( numpy.fabs(lb[5].to(units.mas / units.yr).value - lbnou[5]) < _NUMPY_1_22 * 1e-4 + (1 - _NUMPY_1_22) * 1e-5 ), "streamgapdf sample lb does not return a correct Quantity" # lbdt numpy.random.seed(1) lbdt = sdf_sanders15.sample(1, lb=True, returndt=True) numpy.random.seed(1) lbdtnou = sdf_sanders15_nou.sample(1, lb=True, returndt=True) assert ( numpy.fabs(lbdt[0].to(units.deg).value - lbdtnou[0]) < _NUMPY_1_22 * 1e-4 + (1 - _NUMPY_1_22) * 1e-6 ), "streamgapdf sample lbdt does not return a correct Quantity" assert ( numpy.fabs(lbdt[1].to(units.deg).value - lbdtnou[1]) < _NUMPY_1_22 * 1e-4 + (1 - _NUMPY_1_22) * 1e-6 ), "streamgapdf sample lbdt does not return a correct Quantity" assert ( numpy.fabs(lbdt[2].to(units.kpc).value - lbdtnou[2]) < _NUMPY_1_22 * 1e-5 + (1 - _NUMPY_1_22) * 1e-8 ), "streamgapdf sample lbdt does not return a correct Quantity" assert ( numpy.fabs(lbdt[3].to(units.km / units.s).value - lbdtnou[3]) < _NUMPY_1_22 * 1e-4 + (1 - _NUMPY_1_22) * 1e-6 ), "streamgapdf sample lbdt does not return a correct Quantity" assert ( numpy.fabs(lbdt[4].to(units.mas / units.yr).value - lbdtnou[4]) < _NUMPY_1_22 * 1e-4 + (1 - _NUMPY_1_22) * 1e-6 ), "streamgapdf sample lbdt does not return a correct Quantity" assert ( numpy.fabs(lbdt[5].to(units.mas / units.yr).value - lbdtnou[5]) < _NUMPY_1_22 * 1e-4 + (1 - _NUMPY_1_22) * 1e-6 ), "streamgapdf sample lbdt does not return a correct Quantity" assert ( numpy.fabs( lbdt[6].to(units.Gyr).value / conversion.time_in_Gyr(sdf_sanders15._vo, sdf_sanders15._ro) - lbdtnou[6] ) < _NUMPY_1_22 * 1e-6 + (1 - _NUMPY_1_22) * 1e-8 ), "streamgapdf sample lbdt does not return a correct Quantity" return None def test_streamspraydf_setup_paramsAsQuantity(): # Imports from galpy.df import chen24spraydf, fardal15spraydf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions ro, vo = 8.0, 220.0 lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) Mass = 2 * 10.0**4.0 * units.Msun tdisrupt = 4.5 * units.Gyr for streamspraydf in [fardal15spraydf, chen24spraydf]: # Object with physical inputs off spdf_bovy14_nou = streamspraydf( Mass.to_value(units.Msun) / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=tdisrupt.to_value(units.Gyr) / conversion.time_in_Gyr(vo, ro), ) # Object with physical on spdf_bovy14 = streamspraydf( Mass, progenitor=obs, pot=lp, tdisrupt=tdisrupt, ro=ro, vo=vo ) numpy.random.seed(10) sam = spdf_bovy14.sample(n=2) numpy.random.seed(10) sam_nou = spdf_bovy14_nou.sample(n=2) assert numpy.all( numpy.fabs(sam.r(use_physical=False) - sam_nou.r(use_physical=False)) < 1e-8 ), ( "Sample returned by streamspraydf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all( numpy.fabs(sam.vr(use_physical=False) - sam_nou.vr(use_physical=False)) < 1e-8 ), ( "Sample returned by streamspraydf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) return None def test_streamspraydf_sample_orbit(): from galpy import potential from galpy.df import chen24spraydf, fardal15spraydf from galpy.orbit import Orbit from galpy.util import conversion ro, vo = 8.0, 220.0 lp = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) for streamspraydf in [fardal15spraydf, chen24spraydf]: # Object with physical off spdf_bovy14_nou = streamspraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ) # Object with physical on spdf_bovy14 = streamspraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ro=ro, vo=vo, ) numpy.random.seed(10) sam = spdf_bovy14.sample(n=2) numpy.random.seed(10) sam_nou = spdf_bovy14_nou.sample(n=2) assert numpy.all( numpy.fabs(sam.r(use_physical=False) - sam_nou.r(use_physical=False)) < 1e-8 ), ( "Sample returned by streamspraydf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all( numpy.fabs(sam.vr(use_physical=False) - sam_nou.vr(use_physical=False)) < 1e-8 ), ( "Sample returned by streamspraydf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) return None def test_streamspraydf_sample_RvR(): from galpy import potential from galpy.df import chen24spraydf, fardal15spraydf from galpy.orbit import Orbit from galpy.util import conversion ro, vo = 8.0, 220.0 lp = potential.LogarithmicHaloPotential(normalize=1.0, q=0.9) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) for streamspraydf in [fardal15spraydf, chen24spraydf]: # Object with physical off spdf_bovy14_nou = streamspraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ) # Object with physical on spdf_bovy14 = streamspraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ro=ro, vo=vo, ) numpy.random.seed(10) sam, dt = spdf_bovy14.sample(n=2, return_orbit=False, returndt=True) numpy.random.seed(10) sam_nou, dt_nou = spdf_bovy14_nou.sample(n=2, return_orbit=False, returndt=True) assert numpy.all( numpy.fabs(sam[0].to_value(units.kpc) / ro - sam_nou[0]) < 1e-8 ), ( "Sample returned by streamspraydf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all( numpy.fabs(sam[1].to_value(units.km / units.s) / vo - sam_nou[1]) < 1e-8 ), ( "Sample returned by streamspraydf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all( numpy.fabs(sam[2].to_value(units.km / units.s) / vo - sam_nou[2]) < 1e-8 ), ( "Sample returned by streamspraydf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all( numpy.fabs(sam[3].to_value(units.kpc) / ro - sam_nou[3]) < 1e-8 ), ( "Sample returned by streamspraydf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all( numpy.fabs(sam[4].to_value(units.km / units.s) / vo - sam_nou[4]) < 1e-8 ), ( "Sample returned by streamspraydf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all(numpy.fabs(sam[5].to_value(units.rad) - sam_nou[5]) < 1e-8), ( "Sample returned by streamspraydf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) assert numpy.all( numpy.fabs(dt.to_value(units.Gyr) / conversion.time_in_Gyr(vo, ro) - dt_nou) < 1e-8 ), ( "Sample returned by streamspraydf.sample with with unit output is inconsistenty with the same sample sampled without unit output" ) return None def test_df_inconsistentPotentialUnits_error(): from galpy.actionAngle import actionAngleAdiabatic from galpy.df import quasiisothermaldf, streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential ro, vo = 9.0, 220.0 # quasiisothermaldf lp = LogarithmicHaloPotential(normalize=1.0, q=0.9, ro=ro, vo=vo) aA = actionAngleAdiabatic(pot=lp, c=True, ro=ro, vo=vo) with pytest.raises(AssertionError) as excinfo: qdf = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=lp, aA=aA, cutcounter=True, ro=ro * 1.1, vo=vo, ) with pytest.raises(AssertionError) as excinfo: qdf = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=lp, aA=aA, cutcounter=True, ro=ro, vo=vo * 1.1, ) with pytest.raises(AssertionError) as excinfo: qdf = quasiisothermaldf( 1.0 / 3.0, 0.2, 0.1, 1.0, 1.0, pot=lp, aA=aA, cutcounter=True, ro=ro * 1.1, vo=vo * 1.1, ) # streamdf from galpy.actionAngle import actionAngleIsochroneApprox lp = LogarithmicHaloPotential(normalize=1.0, q=0.9, ro=ro, vo=vo) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8, ro=ro, vo=vo) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s with pytest.raises(AssertionError) as excinfo: sdf = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=30.0, ro=ro * 1.1, vo=vo, ) with pytest.raises(AssertionError) as excinfo: sdf = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=30.0, ro=ro, vo=vo * 1.1, ) with pytest.raises(AssertionError) as excinfo: sdf = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=30.0, ro=ro * 1.1, vo=vo * 1.1, ) return None def test_jeans_sigmar_returntype(): from galpy.df import jeans from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) ro, vo = 8.5, 240.0 assert isinstance(jeans.sigmar(lp, 2.0, ro=ro, vo=vo), units.Quantity), ( "jeans.sigmar does not return Quantity when it should" ) return None def test_jeans_sigmar_returnunit(): from galpy.df import jeans from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) ro, vo = 8.5, 240.0 try: jeans.sigmar(lp, 2.0, ro=ro, vo=vo).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "jeans.sigmar does not return Quantity with the right units" ) return None def test_jeans_sigmar_value(): from galpy.df import jeans from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) ro, vo = 8.5, 240.0 assert ( numpy.fabs( jeans.sigmar(lp, 2.0, ro=ro, vo=vo).to(units.km / units.s).value - jeans.sigmar(lp, 2.0) * vo ) < 10.0**-8.0 ), "jeans.sigmar does not return correct Quantity" return None def test_jeans_sigmar_inputAsQuantity(): from galpy.df import jeans from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) ro, vo = 8.5, 240.0 assert ( numpy.fabs( jeans.sigmar(lp, 2.0 * ro * units.kpc, ro=ro, vo=vo) .to(units.km / units.s) .value - jeans.sigmar(lp, 2.0) * vo ) < 10.0**-8.0 ), "jeans.sigmar does not return correct Quantity" return None def test_jeans_sigmar_PotentialInputWithQuantities(): from galpy.df import jeans from galpy.potential import PlummerPotential pp = PlummerPotential(1693 * units.solMass, b=100 * units.pc) pp_nounits = PlummerPotential(1693 * units.solMass, b=100 * units.pc) pp_nounits.turn_physical_off() ro, vo = 8.5, 240.0 assert ( numpy.fabs( jeans.sigmar(pp, 1.0 * units.pc, ro=ro, vo=vo).to_value(units.km / units.s) - jeans.sigmar(pp_nounits, 1e-3 / ro) * vo ) < 10.0**-8.0 ), "jeans.sigmar does not return correct Quantity" return None def test_jeans_sigmalos_returntype(): from galpy.df import jeans from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) ro, vo = 8.5, 240.0 assert isinstance(jeans.sigmalos(lp, 2.0, ro=ro, vo=vo), units.Quantity), ( "jeans.sigmalos does not return Quantity when it should" ) return None def test_jeans_sigmalos_returnunit(): from galpy.df import jeans from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) ro, vo = 8.5, 240.0 try: jeans.sigmalos(lp, 2.0, ro=ro, vo=vo).to(units.km / units.s) except units.UnitConversionError: raise AssertionError( "jeans.sigmalos does not return Quantity with the right units" ) return None def test_jeans_sigmalos_value(): from galpy.df import jeans from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=1.0) ro, vo = 8.5, 240.0 assert ( numpy.fabs( jeans.sigmalos(lp, 2.0, ro=ro, vo=vo).to(units.km / units.s).value - jeans.sigmalos(lp, 2.0) * vo ) < 10.0**-8.0 ), "jeans.sigmalos does not return correct Quantity" return None def test_jeans_sigmalos_PotentialInputWithQuantities(): from galpy.df import jeans from galpy.potential import PlummerPotential pp = PlummerPotential(1693 * units.solMass, b=100 * units.pc) pp_nounits = PlummerPotential(1693 * units.solMass, b=100 * units.pc) pp_nounits.turn_physical_off() ro, vo = 8.5, 240.0 assert ( numpy.fabs( jeans.sigmalos(pp, 1.0 * units.pc, ro=ro, vo=vo).to_value( units.km / units.s ) - jeans.sigmalos(pp_nounits, 1e-3 / ro) * vo ) < 10.0**-8.0 ), "jeans.sigmalos does not return correct Quantity" return None def test_orbitmethodswunits_quantity_issue326(): # Methods that *always* return a number with implied units # (like Orbit.dist), should return always return a Quantity when # apy-units=True in the configuration file (see issue 326) from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.1, 0.2, 0.0]) # First make sure we're testing what we want to test assert not o._roSet, ( "Test of whether or not Orbit methods that should always return a Quantity do so cannot run meaningfully when _roSet is True" ) assert not o._voSet, ( "Test of whether or not Orbit methods that should always return a Quantity do so cannot run meaningfully when _voSet is True" ) # Then test methods assert isinstance(o.ra(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.dec(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.ll(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.bb(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.dist(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.pmra(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.pmdec(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.pmll(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.pmbb(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.vlos(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.helioX(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.helioY(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.helioZ(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.U(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.V(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.W(), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) return None def test_orbitmethodswunits_quantity_overrideusephysical_issue326(): # Methods that *always* return a number with implied units # (like Orbit.dist), should return always return a Quantity when # apy-units=True in the configuration file (see issue 326) # This test: *even* when use_physical=False from galpy.orbit import Orbit o = Orbit([1.0, 0.1, 1.1, 0.1, 0.2, 0.0]) # First make sure we're testing what we want to test assert not o._roSet, ( "Test of whether or not Orbit methods that should always return a Quantity do so cannot run meaningfully when _roSet is True" ) assert not o._voSet, ( "Test of whether or not Orbit methods that should always return a Quantity do so cannot run meaningfully when _voSet is True" ) # Then test methods assert isinstance(o.ra(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.dec(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.ll(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.bb(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.dist(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.pmra(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.pmdec(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.pmll(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.pmbb(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.vlos(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.helioX(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.helioY(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.helioZ(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.U(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.V(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) assert isinstance(o.W(use_physical=False), units.Quantity), ( "Orbit method ra does not return Quantity when called for orbit with _roSet = False / _voSet = False" ) return None def test_SkyCoord_nodoubleunits_issue325(): # make sure that SkyCoord doesn't return distances with units like kpc^2 # which happened before, because it would use a distance with units of # kpc and then again multiply with kpc from galpy.orbit import Orbit o = Orbit(vxvv=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], radec=True) # Check return units of SkyCoord try: o.SkyCoord().ra.to(units.deg) except units.UnitConversionError: raise AssertionError( "Orbit method SkyCoord has the wrong units for the right ascension" ) try: o.SkyCoord().dec.to(units.deg) except units.UnitConversionError: raise AssertionError( "Orbit method SkyCoord has the wrong units for the declination" ) try: o.SkyCoord().distance.to(units.kpc) except units.UnitConversionError: raise AssertionError( "Orbit method SkyCoord has the wrong units for the distance" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_scf.py0000644000175100001660000007706714761352023015664 0ustar00runnerdocker############################TESTS ON POTENTIALS################################ import numpy from galpy import df, potential from galpy.orbit import Orbit from galpy.potential import SCFPotential from galpy.util import coords EPS = 1e-13 ## default epsilon DEFAULT_R = numpy.array([0.5, 1.0, 2.0]) DEFAULT_Z = numpy.array([0.0, 0.125, -0.125, 0.25, -0.25]) DEFAULT_PHI = numpy.array( [0.0, 0.5, -0.5, 1.0, -1.0, numpy.pi, 0.5 + numpy.pi, 1.0 + numpy.pi] ) ##Tests whether invalid coefficients will throw an error at runtime def test_coeffs_toomanydimensions(): Acos = numpy.ones((10, 2, 32, 34)) try: SCFPotential(Acos=Acos) raise Exception("Expected RuntimeError") except RuntimeError: pass def test_coeffs_toolittledimensions(): Acos = numpy.ones((10, 2)) try: SCFPotential(Acos=Acos) raise Exception("Expected RuntimeError") except RuntimeError: pass def test_coeffs_AsinNone_LnotequalM(): Acos = numpy.ones((2, 3, 4)) try: SCFPotential(Acos=Acos) raise Exception("Expected RuntimeError") except RuntimeError: pass def test_coeffs_AsinNotNone_LnotequalM(): Acos = numpy.ones((2, 3, 4)) Asin = numpy.ones((2, 3, 4)) try: SCFPotential(Acos=Acos, Asin=Asin) raise Exception("Expected RuntimeError") except RuntimeError: pass def test_coeffs_AsinNone_Mequals1(): Acos = numpy.zeros((2, 3, 1)) Asin = None SCFPotential(Acos=Acos, Asin=Asin) def test_coeffs_AsinNone_MequalsL(): Acos = numpy.zeros((2, 3, 3)) Asin = None SCFPotential(Acos=Acos, Asin=Asin) def test_coeffs_AsinNone_AcosNotaxisym(): Acos = numpy.ones((2, 3, 3)) try: SCFPotential(Acos=Acos) raise Exception("Expected RuntimeError") except RuntimeError: pass def test_coeffs_AsinShape_notequal_AcosShape(): Acos = numpy.ones((2, 3, 3)) Asin = numpy.ones((2, 2, 2)) try: SCFPotential(Acos=Acos, Asin=Asin) raise Exception("Expected RuntimeError") except RuntimeError: pass def test_coeffs_Acos_L_M_notLowerTriangular(): Acos = numpy.ones((2, 3, 3)) Asin = numpy.zeros((2, 3, 3)) try: SCFPotential(Acos=Acos, Asin=Asin) raise Exception("Expected RuntimeWarning") except RuntimeWarning: pass def test_coeffs_Asin_L_M_notLowerTriangular(): Acos = numpy.zeros((2, 3, 3)) Asin = numpy.ones((2, 3, 3)) try: SCFPotential(Acos=Acos, Asin=Asin) raise Exception("Expected RuntimeWarning") except RuntimeWarning: pass def testAxi_phiIsNone(): R = 1 z = 0 phi = 1.1 scf = SCFPotential() assert scf(R, z, None) == scf(R, z, phi), ( "The axisymmetric potential does not work at phi=None" ) assert scf.dens(R, z, None) == scf.dens(R, z, phi), ( "The axisymmetric density does not work at phi=None" ) assert scf.Rforce(R, z, None) == scf.Rforce(R, z, phi), ( "The axisymmetric Rforce does not work at phi=None" ) assert scf.zforce(R, z, None) == scf.zforce(R, z, phi), ( "The axisymmetric zforce does not work at phi=None" ) assert scf.phitorque(R, z, None) == scf.phitorque(R, z, phi), ( "The axisymmetric phitorque does not work at phi=None" ) ##Tests user inputs as arrays def testArray_RArray(): scf = SCFPotential() array = numpy.linspace(0, 3, 100) ArrayTest(scf, [array, 1.0, 0]) def testArray_zArray(): scf = SCFPotential() array = numpy.linspace(0, 3, 100) ArrayTest(scf, [1.0, array, 0]) def testArray_phiArray(): scf = SCFPotential() array = numpy.linspace(0, 3, 100) ArrayTest(scf, [1.0, 1.0, array]) def testArrayBroadcasting(): scf = SCFPotential() R = numpy.ones((10, 20, 2)) z = numpy.linspace(0, numpy.pi, 10)[:, None, None] phi = numpy.zeros((10, 20))[:, :, None] ArrayTest(scf, [R, z, phi]) ## tests whether scf_compute_spherical computes the correct coefficients for a Hernquist Potential def test_scf_compute_spherical_hernquist(): Acos, Asin = potential.scf_compute_coeffs_spherical(sphericalHernquistDensity, 10) spherical_coeffsTest(Acos, Asin) assert numpy.fabs(Acos[0, 0, 0] - 1.0) < EPS, ( f"Acos(n=0,l=0,m=0) = 1 fails. Found to be Acos(n=0,l=0,m=0) = {Acos[0, 0, 0]}" ) assert numpy.all(numpy.fabs(Acos[1:, 0, 0]) < EPS), "Acos(n>0,l=0,m=0) = 0 fails." ## tests whether scf_compute_spherical computes the correct coefficients for Zeeuw's Potential def test_scf_compute_spherical_zeeuw(): Acos, Asin = potential.scf_compute_coeffs_spherical(rho_Zeeuw, 10) spherical_coeffsTest(Acos, Asin) assert numpy.fabs(Acos[0, 0, 0] - 2 * 3.0 / 4) < EPS, ( f"Acos(n=0,l=0,m=0) = 3/2 fails. Found to be Acos(n=0,l=0,m=0) = {Acos[0, 0, 0]}" ) assert numpy.fabs(Acos[1, 0, 0] - 2 * 1.0 / 12) < EPS, ( f"Acos(n=1,l=0,m=0) = 1/6 fails. Found to be Acos(n=0,l=0,m=0) = {Acos[0, 0, 0]}" ) assert numpy.all(numpy.fabs(Acos[2:, 0, 0]) < EPS), "Acos(n>1,l=0,m=0) = 0 fails." ##Tests that the numerically calculated results from axi_density1 matches with the analytic results def test_scf_compute_axi_density1(): A = potential.scf_compute_coeffs_axi(axi_density1, 10, 10) axi_coeffsTest(A[0], A[1]) analytically_calculated = numpy.array( [ [4.0 / 3, 7.0 * 3 ** (-5 / 2.0), 2 * 11 * 5 ** (-5.0 / 2), 0], [0, 0, 0, 0], [ 0, 11.0 / (3.0 ** (5.0 / 2) * 5 * 7.0 * 2), 1.0 / (2 * 3.0 * 5**0.5 * 7.0), 0, ], ] ) numerically_calculated = A[0][:3, :4, 0] shape = numerically_calculated.shape for n in range(shape[0]): for l in range(shape[1]): assert ( numpy.fabs(numerically_calculated[n, l] - analytically_calculated[n, l]) < EPS ), ( f"Acos(n={n},l={l},0) = {numerically_calculated[n, l]}, whereas it was analytically calculated to be {analytically_calculated[n, l]}" ) # Checks that A at l != 0,1,2 are always zero assert numpy.all(numpy.fabs(A[0][:, 3:, 0]) < 1e-10), "Acos(n,l>2,m=0) = 0 fails." # Checks that A at n odd is always zero assert numpy.all(numpy.fabs(A[0][1::2, :, 0]) < 1e-10), ( "Acos(n odd,l,m=0) = 0 fails." ) # Checks that A = 0 when n != 0 and l = 0 assert numpy.all(numpy.fabs(A[0][1:, 0, 0]) < 1e-10), ( "Acos(n > 1,l=0,m=0) = 0 fails." ) ##Tests that the numerically calculated results from axi_density2 matches with the analytic results def test_scf_compute_axi_density2(): A = potential.scf_compute_coeffs_axi( axi_density2, 10, 10, radial_order=30, costheta_order=12 ) axi_coeffsTest(A[0], A[1]) analytically_calculated = 2 * numpy.array( [ [1.0, 7.0 * 3 ** (-3 / 2.0) / 4.0, 3 * 11 * 5 ** (-5.0 / 2) / 2.0, 0], [0, 0, 0, 0], ##I never did analytically solve for n=1 [ 0, 11.0 / (7 * 5 * 3 ** (3.0 / 2) * 2 ** (3.0)), (7 * 5 ** (0.5) * 2**3.0) ** -1.0, 0, ], ] ) numerically_calculated = A[0][:3, :4, 0] shape = numerically_calculated.shape for n in range(shape[0]): if n == 1: continue for l in range(shape[1]): assert ( numpy.fabs(numerically_calculated[n, l] - analytically_calculated[n, l]) < EPS ), ( f"Acos(n={n},l={l},0) = {numerically_calculated[n, l]}, whereas it was analytically calculated to be {analytically_calculated[n, l]}" ) # Checks that A at l != 0,1,2 are always zero assert numpy.all(numpy.fabs(A[0][:, 3:, 0]) < 1e-10), "Acos(n,l>2,m=0) = 0 fails." # Checks that A = 0 when n = 2,4,..,2*n and l = 0 assert numpy.all(numpy.fabs(A[0][2::2, 0, 0]) < 1e-10), ( "Acos(n > 1,l = 0,m=0) = 0 fails." ) ## Tests how nbody calculation compares to density calculation for scf_compute_coeff in the spherical case def test_scf_compute_spherical_nbody_hernquist(): N = int(1e6) Mh = 11.0 ah = 50.0 / 8.0 m = Mh / N factor = 1.0 nsamp = 10 Norder = 10 hern = potential.HernquistPotential(amp=2 * Mh, a=ah) hern.turn_physical_off() hdf = df.isotropicHernquistdf(hern) numpy.random.seed(1) samples = [hdf.sample(n=N) for i in range(nsamp)] positions = numpy.array( [ [samples[i].x(), samples[i].y(), samples[i].z() * factor] for i in range(nsamp) ] ) c = numpy.zeros((nsamp, Norder, 1, 1)) s = numpy.zeros((nsamp, Norder, 1, 1)) for i in range(nsamp): c[i], s[i] = potential.scf_compute_coeffs_spherical_nbody( positions[i], Norder, mass=m * numpy.ones(N), a=ah ) cc, ss = potential.scf_compute_coeffs_spherical(hern.dens, Norder, a=ah) # Check that the difference between the coefficients is within the standard deviation assert (cc - numpy.mean(c, axis=0) < numpy.std(c, axis=0)).all() # Repeat test for single mass c = numpy.zeros((nsamp, Norder, 1, 1)) s = numpy.zeros((nsamp, Norder, 1, 1)) for i in range(nsamp): c[i], s[i] = potential.scf_compute_coeffs_spherical_nbody( positions[i], Norder, mass=m, a=ah ) assert (cc - numpy.mean(c, axis=0) < numpy.std(c, axis=0)).all() return None ## Tests how nbody calculation compares to density calculation for scf_compute_coeff def test_scf_compute_axi_nbody_twopowertriaxial(): N = int(1e5) Mh = 11.0 ah = 50.0 / 8.0 m = Mh / N zfactor = 2.5 nsamp = 10 Norder = 10 Lorder = 10 hern = potential.HernquistPotential(amp=2 * Mh, a=ah) hern.turn_physical_off() hdf = df.isotropicHernquistdf(hern) numpy.random.seed(1) samp = [hdf.sample(n=N) for i in range(nsamp)] positions = numpy.array( [[samp[i].x(), samp[i].y(), samp[i].z() * zfactor] for i in range(nsamp)] ) # This is an axisymmtric Hernquist profile with the same mass as the above tptp = potential.TwoPowerTriaxialPotential( amp=2.0 * Mh / zfactor, a=ah, alpha=1.0, beta=4.0, b=1.0, c=zfactor ) tptp.turn_physical_off() cc, ss = potential.scf_compute_coeffs_axi(tptp.dens, Norder, Lorder, a=ah) c, s = numpy.zeros((2, nsamp, Norder, Lorder, 1)) for i, p in enumerate(positions): c[i], s[i] = potential.scf_compute_coeffs_axi_nbody( p, Norder, Lorder, mass=m * numpy.ones(N), a=ah ) # Check that the difference between the coefficients is within two standard deviations assert (cc - (numpy.mean(c, axis=0)) <= (2.0 * numpy.std(c, axis=0))).all() # Repeat test for single mass c, s = numpy.zeros((2, nsamp, Norder, Lorder, 1)) for i, p in enumerate(positions): c[i], s[i] = potential.scf_compute_coeffs_axi_nbody( p, Norder, Lorder, mass=m, a=ah ) assert (cc - (numpy.mean(c, axis=0)) <= (2.0 * numpy.std(c, axis=0))).all() return None ## Tests how nbody calculation compares to density calculation for scf_compute_coeff def test_scf_compute_nbody_twopowertriaxial(): N = int(1e5) Mh = 11.0 ah = 50.0 / 8.0 m = Mh / N yfactor = 1.5 zfactor = 2.5 nsamp = 10 Norder = 10 Lorder = 10 hern = potential.HernquistPotential(amp=2 * Mh, a=ah) hern.turn_physical_off() hdf = df.isotropicHernquistdf(hern) numpy.random.seed(2) samp = [hdf.sample(n=N) for i in range(nsamp)] positions = numpy.array( [ [samp[i].x(), samp[i].y() * yfactor, samp[i].z() * zfactor] for i in range(nsamp) ] ) # This is an triaxial Hernquist profile with the same mass as the above tptp = potential.TwoPowerTriaxialPotential( amp=2.0 * Mh / yfactor / zfactor, a=ah, alpha=1.0, beta=4.0, b=yfactor, c=zfactor, ) tptp.turn_physical_off() cc, ss = potential.scf_compute_coeffs(tptp.dens, Norder, Lorder, a=ah) c, s = numpy.zeros((2, nsamp, Norder, Lorder, Lorder)) for i, p in enumerate(positions): c[i], s[i] = potential.scf_compute_coeffs_nbody( p, Norder, Lorder, mass=m * numpy.ones(N), a=ah ) # Check that the difference between the coefficients is within two standard deviations assert (cc - (numpy.mean(c, axis=0)) <= (2.0 * numpy.std(c, axis=0))).all() # Repeat test for single mass c, s = numpy.zeros((2, nsamp, Norder, Lorder, Lorder)) for i, p in enumerate(positions): c[i], s[i] = potential.scf_compute_coeffs_nbody(p, Norder, Lorder, mass=m, a=ah) assert (cc - (numpy.mean(c, axis=0)) <= (2.0 * numpy.std(c, axis=0))).all() return None def test_scf_compute_nfw(): Acos, Asin = potential.scf_compute_coeffs_spherical(rho_NFW, 10) spherical_coeffsTest(Acos, Asin) ##Tests radial order from scf_compute_coeffs_spherical def test_nfw_sphericalOrder(): Acos, Asin = potential.scf_compute_coeffs_spherical(rho_NFW, 10) Acos2, Asin2 = potential.scf_compute_coeffs_spherical(rho_NFW, 10, radial_order=50) assert numpy.all(numpy.fabs(Acos - Acos2) < EPS), ( "Increasing the radial order fails for scf_compute_coeffs_spherical" ) ##Tests radial and costheta order from scf_compute_coeffs_axi def test_axi_density1_axiOrder(): Acos, Asin = potential.scf_compute_coeffs_axi(axi_density1, 10, 10) Acos2, Asin2 = potential.scf_compute_coeffs_axi( axi_density1, 10, 10, radial_order=50, costheta_order=50 ) assert numpy.all(numpy.fabs(Acos - Acos2) < 1e-10), ( "Increasing the radial and costheta order fails for scf_compute_coeffs_axi" ) ##Tests radial, costheta and phi order from scf_compute_coeffs def test_density1_Order(): Acos, Asin = potential.scf_compute_coeffs(density1, 5, 5) Acos2, Asin2 = potential.scf_compute_coeffs( density1, 5, 5, radial_order=19, costheta_order=19, phi_order=19 ) assert numpy.all(numpy.fabs(Acos - Acos2) < 1e-3), ( "Increasing the radial, costheta, and phi order fails for Acos from scf_compute_coeffs" ) assert numpy.all(numpy.fabs(Asin - Asin) < EPS), ( "Increasing the radial, costheta, and phi order fails for Asin from scf_compute_coeffs" ) ## Tests whether scf_compute_axi reduces to scf_compute_spherical for the Hernquist Potential def test_scf_axiHernquistCoeffs_ReducesToSpherical(): Aspherical = potential.scf_compute_coeffs_spherical(sphericalHernquistDensity, 10) Aaxi = potential.scf_compute_coeffs_axi(sphericalHernquistDensity, 10, 10) axi_reducesto_spherical(Aspherical, Aaxi, "Hernquist Potential") ## Tests whether scf_compute_axi reduces to scf_compute_spherical for Zeeuw's Potential def test_scf_axiZeeuwCoeffs_ReducesToSpherical(): Aspherical = potential.scf_compute_coeffs_spherical(rho_Zeeuw, 10) Aaxi = potential.scf_compute_coeffs_axi(rho_Zeeuw, 10, 10) axi_reducesto_spherical(Aspherical, Aaxi, "Zeeuw Potential") ## Tests whether scf_compute reduces to scf_compute_spherical for Hernquist Potential def test_scf_HernquistCoeffs_ReducesToSpherical(): Aspherical = potential.scf_compute_coeffs_spherical(sphericalHernquistDensity, 5) Aaxi = potential.scf_compute_coeffs(sphericalHernquistDensity, 5, 5) reducesto_spherical(Aspherical, Aaxi, "Hernquist Potential") ## Tests whether scf_compute reduces to scf_compute_spherical for Zeeuw's Potential def test_scf_ZeeuwCoeffs_ReducesToSpherical(): Aspherical = potential.scf_compute_coeffs_spherical(rho_Zeeuw, 5) Aaxi = potential.scf_compute_coeffs( rho_Zeeuw, 5, 5, radial_order=20, costheta_order=20 ) reducesto_spherical(Aspherical, Aaxi, "Zeeuw Potential") ## Tests whether scf density matches with Hernquist density def test_densMatches_hernquist(): h = potential.HernquistPotential() Acos, Asin = potential.scf_compute_coeffs_spherical(sphericalHernquistDensity, 10) scf = SCFPotential() assertmsg = "Comparing the density of Hernquist Potential with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(h.dens, scf.dens, assertmsg) ## Tests whether scf density matches with Zeeuw density def test_densMatches_zeeuw(): Acos, Asin = potential.scf_compute_coeffs_spherical(rho_Zeeuw, 10) scf = SCFPotential(amp=1, Acos=Acos, Asin=Asin) assertmsg = "Comparing the density of Zeeuw's perfect ellipsoid with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(rho_Zeeuw, scf.dens, assertmsg) ## Tests whether scf density matches with axi_density1 def test_densMatches_axi_density1(): Acos, Asin = potential.scf_compute_coeffs_axi(axi_density1, 50, 3) scf = SCFPotential(amp=1, Acos=Acos, Asin=Asin) assertmsg = "Comparing axi_density1 with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(axi_density1, scf.dens, assertmsg, eps=1e-3) ## Tests whether scf density matches with axi_density2 def test_densMatches_axi_density2(): Acos, Asin = potential.scf_compute_coeffs_axi(axi_density2, 50, 3) scf = SCFPotential(amp=1, Acos=Acos, Asin=Asin) assertmsg = "Comparing axi_density2 with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(axi_density2, scf.dens, assertmsg, eps=1e-3) ## Tests whether scf density matches with NFW def test_densMatches_nfw(): nfw = potential.NFWPotential() Acos, Asin = potential.scf_compute_coeffs_spherical(rho_NFW, 50, a=50) scf = SCFPotential(amp=1, Acos=Acos, Asin=Asin, a=50) assertmsg = "Comparing nfw with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(nfw.dens, scf.dens, assertmsg, eps=1e-2) ## Tests whether scf potential matches with Hernquist potential def test_potentialMatches_hernquist(): h = potential.HernquistPotential() Acos, Asin = potential.scf_compute_coeffs_spherical(sphericalHernquistDensity, 10) scf = SCFPotential() assertmsg = "Comparing the potential of Hernquist Potential with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(h, scf, assertmsg) ## Tests whether scf Potential matches with NFW def test_potentialMatches_nfw(): nfw = potential.NFWPotential() Acos, Asin = potential.scf_compute_coeffs_spherical(rho_NFW, 50, a=50) scf = SCFPotential(amp=1, Acos=Acos, Asin=Asin, a=50) assertmsg = "Comparing nfw with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(nfw, scf, assertmsg, eps=1e-4) ## Tests whether scf Rforce matches with Hernquist Rforce def test_RforceMatches_hernquist(): h = potential.HernquistPotential() Acos, Asin = potential.scf_compute_coeffs_spherical(sphericalHernquistDensity, 1) scf = SCFPotential(amp=1, Acos=Acos, Asin=Asin) assertmsg = "Comparing the radial force of Hernquist Potential with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(h.Rforce, scf.Rforce, assertmsg) ## Tests whether scf zforce matches with Hernquist zforce def test_zforceMatches_hernquist(): h = potential.HernquistPotential() Acos, Asin = potential.scf_compute_coeffs_spherical(sphericalHernquistDensity, 1) scf = SCFPotential(amp=1, Acos=Acos, Asin=Asin) assertmsg = "Comparing the vertical force of Hernquist Potential with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(h.zforce, scf.zforce, assertmsg) ## Tests whether scf phitorque matches with Hernquist phitorque def test_phitorqueMatches_hernquist(): h = potential.HernquistPotential() Acos, Asin = potential.scf_compute_coeffs_spherical(sphericalHernquistDensity, 1) scf = SCFPotential(amp=1, Acos=Acos, Asin=Asin) assertmsg = "Comparing the azimuth force of Hernquist Potential with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(h.phitorque, scf.phitorque, assertmsg) ## Tests whether scf Rforce matches with NFW Rforce def test_RforceMatches_nfw(): nfw = potential.NFWPotential() Acos, Asin = potential.scf_compute_coeffs_spherical(rho_NFW, 50, a=50) scf = SCFPotential(amp=1, Acos=Acos, Asin=Asin, a=50) assertmsg = "Comparing the radial force of NFW Potential with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(nfw.Rforce, scf.Rforce, assertmsg, eps=1e-3) ## Tests whether scf zforce matches with NFW zforce def test_zforceMatches_nfw(): nfw = potential.NFWPotential() Acos, Asin = potential.scf_compute_coeffs_spherical(rho_NFW, 50, a=50) scf = SCFPotential(amp=1, Acos=Acos, Asin=Asin, a=50) assertmsg = "Comparing the vertical force of NFW Potential with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(nfw.zforce, scf.zforce, assertmsg, eps=1e-3) ## Tests whether scf phitorque matches with NFW Rforce def test_phitorqueMatches_nfw(): nfw = potential.NFWPotential() Acos, Asin = potential.scf_compute_coeffs_spherical(rho_NFW, 10) scf = SCFPotential(amp=1, Acos=Acos, Asin=Asin) assertmsg = "Comparing the azimuth force of NFW Potential with SCF fails at R={0}, Z={1}, phi={2}" compareFunctions(nfw.phitorque, scf.phitorque, assertmsg) # Test that "FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated ..." warning doesn't happen (#461) def test_FutureWarning_multid_indexing(): scf = SCFPotential() array = numpy.linspace(0, 3, 100) # Turn warnings into errors to test for them import warnings with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", FutureWarning) ArrayTest(scf, [array, 1.0, 0]) raisedWarning = False for wa in w: raisedWarning = ( "Using a non-tuple sequence for multidimensional indexing is deprecated" in str(wa.message) ) if raisedWarning: break assert not raisedWarning, ( "SCFPotential should not raise 'FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated ...', but did" ) return None # Test that running with a density in physical units works as expected def test_physical_dens_spherical(): a = 1.3 ro, vo = 7.0, 230.0 hp = potential.HernquistPotential(a=a, ro=ro, vo=vo) Acos, Asin = potential.scf_compute_coeffs_spherical(hp.dens, 10, a=a) sp = potential.SCFPotential(Acos=Acos, Asin=Asin, a=a) rs = numpy.geomspace(0.1, 10.0, 101) assert numpy.all( numpy.fabs( 1.0 - sp.dens(rs, 0.0, use_physical=False) / hp.dens(rs, 0.0, use_physical=False) ) < 1e-10 ), ( "SCF density does not agree with input density when calculated with physical density" ) return None # Test that running with a density in physical units works as expected def test_physical_dens_axi(): a = 1.3 ro, vo = 7.0, 230.0 hp = potential.HernquistPotential(a=a, ro=ro, vo=vo) Acos, Asin = potential.scf_compute_coeffs_axi(hp.dens, 10, 2, a=a) sp = potential.SCFPotential(Acos=Acos, Asin=Asin, a=a) rs = numpy.geomspace(0.1, 10.0, 101) assert numpy.all( numpy.fabs( 1.0 - sp.dens(rs, 0.0, use_physical=False) / hp.dens(rs, 0.0, use_physical=False) ) < 1e-10 ), ( "SCF density does not agree with input density when calculated with physical density" ) return None # Test that running with a density in physical units works as expected def test_physical_dens(): a = 1.3 ro, vo = 7.0, 230.0 hp = potential.HernquistPotential(a=a, ro=ro, vo=vo) Acos, Asin = potential.scf_compute_coeffs(hp.dens, 10, 2, a=a) sp = potential.SCFPotential(Acos=Acos, Asin=Asin, a=a) rs = numpy.geomspace(0.1, 10.0, 101) assert numpy.all( numpy.fabs( 1.0 - sp.dens(rs, 0.0, use_physical=False) / hp.dens(rs, 0.0, use_physical=False) ) < 1e-10 ), ( "SCF density does not agree with input density when calculated with physical density" ) return None # Test that from_density acts as expected def test_from_density_hernquist(): a = 1.3 hp = potential.HernquistPotential(a=a) Acos, Asin = potential.scf_compute_coeffs_spherical(hp.dens, 10, a=a) sp_direct = potential.SCFPotential(Acos=Acos, Asin=Asin, a=a) sp_from = potential.SCFPotential.from_density( hp.dens, 10, a=a, symmetry="spherical" ) rs = numpy.geomspace(0.1, 10.0, 101) assert numpy.all( numpy.fabs( 1.0 - sp_direct.dens(rs, 0.0, use_physical=False) / sp_from.dens(rs, 0.0, use_physical=False) ) < 1e-10 ), "SCF density does not agree between direct init and from_density init" return None # Test that from_density acts as expected def test_from_density_axi(): a = 1.0 Acos, Asin = potential.scf_compute_coeffs_axi( axi_density2, 10, 10, a=a, radial_order=30, costheta_order=12 ) sp_direct = potential.SCFPotential(Acos=Acos, Asin=Asin, a=a) sp_from = potential.SCFPotential.from_density( axi_density2, 10, L=10, a=a, symmetry="axi", radial_order=30, costheta_order=12 ) rs = numpy.geomspace(0.1, 10.0, 101) assert numpy.all( numpy.fabs( 1.0 - sp_direct.dens(rs, rs, use_physical=False) / sp_from.dens(rs, rs, use_physical=False) ) < 1e-10 ), "SCF density does not agree between direct init and from_density init" return None # Test that from_density acts as expected def test_from_density(): a = 1.0 Acos, Asin = potential.scf_compute_coeffs(rho_Zeeuw, 10, 3, a=a) sp_direct = potential.SCFPotential(Acos=Acos, Asin=Asin, a=a) sp_from = potential.SCFPotential.from_density( rho_Zeeuw, 10, L=3, a=a, symmetry=None ) rs = numpy.geomspace(0.1, 10.0, 101) assert numpy.all( numpy.fabs( 1.0 - sp_direct.dens(rs, rs, phi=rs, use_physical=False) / sp_from.dens(rs, rs, phi=rs, use_physical=False) ) < 1e-10 ), "SCF density does not agree between direct init and from_density init" return None ##############GENERIC FUNCTIONS BELOW############### ##This is used to test whether input as arrays works def ArrayTest(scf, params): def compareFunctions(func, result, i): if numpy.isnan(result[i]): return numpy.isnan(func(R[i], z[i], phi[i])) if numpy.isinf(result[i]): return numpy.isinf(func(R[i], z[i], phi[i])) return numpy.all(numpy.fabs(result[i] - func(R[i], z[i], phi[i])) < EPS) potential = scf(*params).flatten() density = scf.dens(*params).flatten() Rforce = scf.Rforce(*params).flatten() zforce = scf.zforce(*params).flatten() phitorque = scf.phitorque(*params).flatten() R, z, phi = params shape = numpy.array(R * z * phi).shape R = (numpy.ones(shape) * R).flatten() z = (numpy.ones(shape) * z).flatten() phi = (numpy.ones(shape) * phi).flatten() message = "{0} at R={1}, z={2}, phi={3} was found to be {4} where it was expected to be equal to {5}" for i in range(len(R)): assert compareFunctions(scf, potential, i), message.format( "Potential", R[i], z[i], phi[i], potential[i], scf(R[i], z[i], phi[i]) ) assert compareFunctions(scf.dens, density, i), message.format( "Density", R[i], z[i], phi[i], density[i], scf.dens(R[i], z[i], phi[i]) ) assert compareFunctions(scf.Rforce, Rforce, i), message.format( "Rforce", R[i], z[i], phi[i], Rforce[i], scf.Rforce(R[i], z[i], phi[i]) ) assert compareFunctions(scf.zforce, zforce, i), message.format( "zforce", R[i], z[i], phi[i], zforce[i], scf.zforce(R[i], z[i], phi[i]) ) assert compareFunctions(scf.phitorque, phitorque, i), message.format( "phitorque", R[i], z[i], phi[i], phitorque[i], scf.phitorque(R[i], z[i], phi[i]), ) ## This is used to compare scf functions with its corresponding galpy function def compareFunctions( galpyFunc, scfFunc, assertmsg, Rs=DEFAULT_R, Zs=DEFAULT_Z, phis=DEFAULT_PHI, eps=EPS ): ##Assert msg must have 3 placeholders ({}) for Rs, Zs, and phis for ii in range(len(Rs)): for jj in range(len(Zs)): for kk in range(len(phis)): e = numpy.divide( galpyFunc(Rs[ii], Zs[jj], phis[kk]) - scfFunc(Rs[ii], Zs[jj], phis[kk]), galpyFunc(Rs[ii], Zs[jj], phis[kk]), ) e = numpy.fabs(numpy.fabs(e)) if galpyFunc(Rs[ii], Zs[jj], phis[kk]) == 0: continue ## Ignoring divide by zero assert e < eps, assertmsg.format(Rs[ii], Zs[jj], phis[kk]) ##General function that tests whether coefficients for a spherical density has the expected property def spherical_coeffsTest(Acos, Asin, eps=EPS): ## We expect Asin to be zero assert Asin is None or numpy.all(numpy.fabs(Asin) < eps), ( "Confirming Asin = 0 fails" ) ## We expect that the only non-zero values occur at (n,l=0,m=0) assert numpy.all(numpy.fabs(Acos[:, 1:, :]) < eps) and numpy.all( numpy.fabs(Acos[:, :, 1:]) < eps ), "Non Zero value found outside (n,l,m) = (n,0,0)" ##General function that tests whether coefficients for an axi symmetric density has the expected property def axi_coeffsTest(Acos, Asin): ## We expect Asin to be zero assert Asin is None or numpy.all(numpy.fabs(Asin) < EPS), ( "Confirming Asin = 0 fails" ) ## We expect that the only non-zero values occur at (n,l,m=0) assert numpy.all(numpy.fabs(Acos[:, :, 1:]) < EPS), ( "Non Zero value found outside (n,l,m) = (n,0,0)" ) ## Tests whether the coefficients of a spherical density computed using the scf_compute_coeffs_axi reduces to ## The coefficients computed using the scf_compute_coeffs_spherical def axi_reducesto_spherical(Aspherical, Aaxi, potentialName): Acos_s, Asin_s = Aspherical Acos_a, Asin_a = Aaxi spherical_coeffsTest(Acos_a, Asin_a, eps=1e-10) n = min(Acos_s.shape[0], Acos_a.shape[0]) assert numpy.all(numpy.fabs(Acos_s[:n, 0, 0] - Acos_a[:n, 0, 0]) < EPS), ( f"The axi symmetric Acos(n,l=0,m=0) does not reduce to the spherical Acos(n,l=0,m=0) for {potentialName}" ) ## Tests whether the coefficients of a spherical density computed using the scf_compute_coeffs reduces to ## The coefficients computed using the scf_compute_coeffs_spherical def reducesto_spherical(Aspherical, A, potentialName): Acos_s, Asin_s = Aspherical Acos, Asin = A spherical_coeffsTest(Acos, Asin, eps=1e-10) n = min(Acos_s.shape[0], Acos.shape[0]) assert numpy.all(numpy.fabs(Acos_s[:n, 0, 0] - Acos[:n, 0, 0]) < EPS), ( f"Acos(n,l=0,m=0) as generated by scf_compute_coeffs does not reduce to the spherical Acos(n,l=0,m=0) for {potentialName}" ) ## Hernquist potential as a function of r def sphericalHernquistDensity(R, z=0, phi=0): h = potential.HernquistPotential() return h.dens(R, z, phi) def rho_Zeeuw(R, z, phi, a=1.0): r, theta, phi = coords.cyl_to_spher(R, z, phi) return 3.0 / (4 * numpy.pi) * numpy.power((a + r), -4.0) * a def rho_NFW(R, z=0, phi=0.0): nfw = potential.NFWPotential() return nfw.dens(R, z, phi) def axi_density1(R, z=0, phi=0.0): r, theta, phi = coords.cyl_to_spher(R, z, phi) h = potential.HernquistPotential() return h.dens(R, z, phi) * (1 + numpy.cos(theta) + numpy.cos(theta) ** 2.0) def axi_density2(R, z=0, phi=0.0): spherical_coords = coords.cyl_to_spher(R, z, phi) theta = spherical_coords[1] return rho_Zeeuw(R, z, phi) * (1 + numpy.cos(theta) + numpy.cos(theta) ** 2) def density1(R, z=0, phi=0.0): r, theta, phi = coords.cyl_to_spher(R, z, phi) h = potential.HernquistPotential(2) return ( h.dens(R, z, phi) * (1 + numpy.cos(theta) + numpy.cos(theta) ** 2.0) * (1 + numpy.cos(phi) + numpy.sin(phi)) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_snapshotpotential.py0000644000175100001660000007061014761352023020653 0ustar00runnerdocker################ TESTS OF THE SNAPSHOTPOTENTIAL CLASS AND RELATED ############# import numpy import pynbody from galpy import potential def test_snapshotKeplerPotential_eval(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.SnapshotRZPotential(s, num_threads=1) kp = potential.KeplerPotential(amp=1.0) # should be the same assert numpy.fabs(sp(1.0, 0.0) - kp(1.0, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) assert numpy.fabs(sp(0.5, 0.0) - kp(0.5, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) assert numpy.fabs(sp(1.0, 0.5) - kp(1.0, 0.5)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) assert numpy.fabs(sp(1.0, -0.5) - kp(1.0, -0.5)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) return None def test_snapshotKeplerPotential_Rforce(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.SnapshotRZPotential(s) kp = potential.KeplerPotential(amp=1.0) # should be the same assert numpy.fabs(sp.Rforce(1.0, 0.0) - kp.Rforce(1.0, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) assert numpy.fabs(sp.Rforce(0.5, 0.0) - kp.Rforce(0.5, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) assert numpy.fabs(sp.Rforce(1.0, 0.5) - kp.Rforce(1.0, 0.5)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) assert numpy.fabs(sp.Rforce(1.0, -0.5) - kp.Rforce(1.0, -0.5)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) return None def test_snapshotKeplerPotential_zforce(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.SnapshotRZPotential(s) kp = potential.KeplerPotential(amp=1.0) # should be the same assert numpy.fabs(sp.zforce(1.0, 0.0) - kp.zforce(1.0, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) assert numpy.fabs(sp.zforce(0.5, 0.0) - kp.zforce(0.5, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) assert numpy.fabs(sp.zforce(1.0, 0.5) - kp.zforce(1.0, 0.5)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) assert numpy.fabs(sp.zforce(1.0, -0.5) - kp.zforce(1.0, -0.5)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) return None def test_snapshotKeplerPotential_hash(): # Test that hashing the previous grid works # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.SnapshotRZPotential(s) kp = potential.KeplerPotential(amp=1.0) # should be the same assert numpy.fabs(sp(1.0, 0.0) - kp(1.0, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) assert numpy.fabs(sp(1.0, 0.0) - kp(1.0, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) return None def test_snapshotKeplerPotential_grid(): # Test that evaluating on a grid works # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 2.0 s["eps"] = 0.0 sp = potential.SnapshotRZPotential(s) kp = potential.KeplerPotential(amp=2.0) # should be the same rs = numpy.arange(3) + 1 zs = 0.1 assert numpy.all(numpy.fabs(sp(rs, zs) - kp(rs, zs)) < 10.0**-8.0), ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) return None def test_snapshotKeplerPotential_eval_array(): # Test evaluating the snapshotPotential with array input # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.SnapshotRZPotential(s) kp = potential.KeplerPotential(amp=1.0) # should be the same rs = numpy.ones(3) * 0.5 + 0.5 zs = (numpy.zeros(3) - 1.0) / 2.0 assert numpy.all(numpy.fabs(sp(rs, zs) - kp(rs, zs)) < 10.0**-8.0), ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) return None def test_snapshotKeplerPotential_Rforce_array(): # Test evaluating the snapshotPotential with array input # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.SnapshotRZPotential(s) kp = potential.KeplerPotential(amp=1.0) # should be the same rs = numpy.ones(3) * 0.5 + 0.5 zs = (numpy.zeros(3) - 1.0) / 2.0 assert numpy.all(numpy.fabs(sp.Rforce(rs, zs) - kp.Rforce(rs, zs)) < 10.0**-8.0), ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) return None def test_snapshotKeplerPotential_zforce_array(): # Test evaluating the snapshotPotential with array input # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.SnapshotRZPotential(s) kp = potential.KeplerPotential(amp=1.0) # should be the same rs = numpy.ones(3) * 0.5 + 0.5 zs = (numpy.zeros(3) - 1.0) / 2.0 assert numpy.all(numpy.fabs(sp.zforce(rs, zs) - kp.zforce(rs, zs)) < 10.0**-8.0), ( "SnapshotRZPotential with single unit mass does not correspond to KeplerPotential" ) return None # Test that using different numbers of azimuths to average over gives the same result def test_snapshotKeplerPotential_eval_naz(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.SnapshotRZPotential(s, num_threads=1) spaz = potential.SnapshotRZPotential(s, num_threads=1, nazimuths=12) assert numpy.fabs(sp(1.0, 0.0) - spaz(1.0, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass for naz=4 does not agree with naz=12" ) assert numpy.fabs(sp(0.5, 0.0) - spaz(0.5, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass for naz=4 does not agree with naz=12" ) assert numpy.fabs(sp(1.0, 0.5) - spaz(1.0, 0.5)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass for naz=4 does not agree with naz=12" ) assert numpy.fabs(sp(1.0, -0.5) - spaz(1.0, -0.5)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass for naz=4 does not agree with naz=12" ) return None def test_snapshotKeplerPotential_Rforce_naz(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.SnapshotRZPotential(s, num_threads=1) spaz = potential.SnapshotRZPotential(s, num_threads=1, nazimuths=12) assert numpy.fabs(sp.Rforce(1.0, 0.0) - spaz.Rforce(1.0, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass for naz=4 does not agree with naz=12" ) assert numpy.fabs(sp.Rforce(0.5, 0.0) - spaz.Rforce(0.5, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass for naz=4 does not agree with naz=12" ) assert numpy.fabs(sp.Rforce(1.0, 0.5) - spaz.Rforce(1.0, 0.5)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass for naz=4 does not agree with naz=12" ) assert numpy.fabs(sp.Rforce(1.0, -0.5) - spaz.Rforce(1.0, -0.5)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass for naz=4 does not agree with naz=12" ) return None def test_snapshotKeplerPotential_zforce_naz(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.SnapshotRZPotential(s, num_threads=1) spaz = potential.SnapshotRZPotential(s, num_threads=1, nazimuths=12) assert numpy.fabs(sp.zforce(1.0, 0.0) - spaz.zforce(1.0, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass for naz=4 does not agree with naz=12" ) assert numpy.fabs(sp.zforce(0.5, 0.0) - spaz.zforce(0.5, 0.0)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass for naz=4 does not agree with naz=12" ) assert numpy.fabs(sp.zforce(1.0, 0.5) - spaz.zforce(1.0, 0.5)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass for naz=4 does not agree with naz=12" ) assert numpy.fabs(sp.zforce(1.0, -0.5) - spaz.zforce(1.0, -0.5)) < 10.0**-8.0, ( "SnapshotRZPotential with single unit mass for naz=4 does not agree with naz=12" ) return None def test_interpsnapshotKeplerPotential_eval(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.InterpSnapshotRZPotential( s, rgrid=(0.01, 2.0, 201), zgrid=(0.0, 0.2, 201), logR=False, interpPot=True, zsym=True, numcores=1, ) kp = potential.KeplerPotential(amp=1.0) # should be the same # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21) zs = numpy.linspace(-0.2, 0.2, 41) for r in rs: for z in zs: assert numpy.fabs((sp(r, z) - kp(r, z)) / kp(r, z)) < 10.0**-10.0, ( f"RZPot interpolation w/ InterpSnapShotPotential of KeplerPotential fails at (R,z) = ({r:g},{z:g})" ) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 10) zs = numpy.linspace(-0.2, 0.2, 20) for r in rs: for z in zs: assert numpy.fabs((sp(r, z) - kp(r, z)) / kp(r, z)) < 10.0**-5.0, ( f"RZPot interpolation w/ InterpSnapShotPotential of KeplerPotential fails at (R,z) = ({r:g},{z:g}) by {numpy.fabs((sp(r, z) - kp(r, z)) / kp(r, z)):g}" ) # Test all at the same time to use vector evaluation mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all(numpy.fabs((sp(mr, mz) - kp(mr, mz)) / kp(mr, mz)) < 10.0**-5.0), ( "RZPot interpolation w/ interpRZPotential fails for vector input" ) return None def test_interpsnapshotKeplerPotential_logR_eval(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.InterpSnapshotRZPotential( s, rgrid=(numpy.log(0.01), numpy.log(20.0), 251), logR=True, zgrid=(0.0, 0.2, 201), interpPot=True, zsym=True, ) kp = potential.KeplerPotential(amp=1.0) # should be the same rs = numpy.linspace(0.02, 16.0, 20) zs = numpy.linspace(-0.15, 0.15, 40) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all(numpy.fabs((sp(mr, mz) - kp(mr, mz)) / kp(mr, mz)) < 10.0**-5.0), ( f"RZPot interpolation w/ interpRZPotential fails for vector input, w/ logR at (R,z) = ({mr[numpy.argmax(numpy.fabs((sp(mr, mz) - kp(mr, mz)) / kp(mr, mz)))]:f},{mz[numpy.argmax(numpy.fabs((sp(mr, mz) - kp(mr, mz)) / kp(mr, mz)))]:f}) by {numpy.amax(numpy.fabs((sp(mr, mz) - kp(mr, mz)) / kp(mr, mz))):g}" ) return None def test_interpsnapshotKeplerPotential_noc_eval(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.InterpSnapshotRZPotential( s, rgrid=(0.01, 2.0, 201), zgrid=(0.0, 0.2, 201), logR=False, interpPot=True, zsym=True, enable_c=False, ) kp = potential.KeplerPotential(amp=1.0) # should be the same # Test all at the same time to use vector evaluation rs = numpy.linspace(0.01, 2.0, 10) zs = numpy.linspace(-0.2, 0.2, 20) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all(numpy.fabs((sp(mr, mz) - kp(mr, mz)) / kp(mr, mz)) < 10.0**-5.0), ( "RZPot interpolation w/ interpRZPotential fails for vector input, without enable_c" ) return None # Test that using different numbers of azimuths to average over gives the same result def test_interpsnapshotKeplerPotential_eval_naz(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 1.0 s["eps"] = 0.0 sp = potential.InterpSnapshotRZPotential( s, rgrid=(0.01, 2.0, 51), zgrid=(0.0, 0.2, 51), logR=False, interpPot=True, zsym=True, numcores=1, ) spaz = potential.InterpSnapshotRZPotential( s, rgrid=(0.01, 2.0, 51), zgrid=(0.0, 0.2, 51), logR=False, interpPot=True, zsym=True, numcores=1, nazimuths=12, ) # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21) zs = numpy.linspace(-0.2, 0.2, 41) for r in rs: for z in zs: assert numpy.fabs((sp(r, z) - spaz(r, z)) / sp(r, z)) < 10.0**-10.0, ( f"RZPot interpolation w/ InterpSnapShotPotential of KeplerPotential with different nazimuths fails at (R,z) = ({r:g},{z:g})" ) # This tests within the grid, with vector evaluation rs = numpy.linspace(0.01, 2.0, 10) zs = numpy.linspace(-0.2, 0.2, 20) mr, mz = numpy.meshgrid(rs, zs) mr = mr.flatten() mz = mz.flatten() assert numpy.all( numpy.fabs((sp(mr, mz) - spaz(mr, mz)) / sp(mr, mz)) < 10.0**-5.0 ), ( "RZPot interpolation w/ interpRZPotential with different nazimimuths fails for vector input" ) return None def test_interpsnapshotKeplerPotential_normalize(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 4.0 s["eps"] = 0.0 sp = potential.InterpSnapshotRZPotential( s, rgrid=(0.01, 3.0, 201), zgrid=(0.0, 0.2, 201), logR=False, interpPot=True, interpepifreq=True, interpverticalfreq=True, zsym=True, ) nkp = potential.KeplerPotential(normalize=1.0) # normalized Kepler ukp = potential.KeplerPotential(amp=4.0) # Un-normalized Kepler # Currently unnormalized assert numpy.fabs(sp.Rforce(1.0, 0.0) + 4.0) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) assert numpy.fabs(sp(1.0, 0.1) - ukp(1.0, 0.1)) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) assert numpy.fabs(sp.epifreq(1.0) - ukp.epifreq(1.0)) < 10.0**-4.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) assert numpy.fabs(sp.verticalfreq(1.0) - ukp.verticalfreq(1.0)) < 10.0**-4.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) # Normalize sp.normalize(R0=1.0) assert numpy.fabs(sp.Rforce(1.0, 0.0) + 1.0) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be normalized doesn't behave as expected" ) assert numpy.fabs(sp(1.0, 0.1) - nkp(1.0, 0.1)) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) assert numpy.fabs(sp.epifreq(1.0) - nkp.epifreq(1.0)) < 10.0**-4.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) assert numpy.fabs(sp.verticalfreq(1.0) - nkp.verticalfreq(1.0)) < 10.0**-4.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) # De normalize sp.denormalize() assert numpy.fabs(sp.Rforce(1.0, 0.0) + 4.0) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be normalized doesn't behave as expected" ) assert numpy.fabs(sp(1.0, 0.1) - ukp(1.0, 0.1)) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) assert numpy.fabs(sp.epifreq(1.0) - ukp.epifreq(1.0)) < 10.0**-4.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) assert numpy.fabs(sp.verticalfreq(1.0) - ukp.verticalfreq(1.0)) < 10.0**-4.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) # Also test when R0 =/= 1 sp.normalize(R0=2.0) assert numpy.fabs(sp.Rforce(1.0, 0.0) + 1.0) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be normalized doesn't behave as expected" ) assert numpy.fabs(sp(1.0, 0.1) - nkp(1.0, 0.1)) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) assert numpy.fabs(sp.epifreq(1.0) - nkp.epifreq(1.0)) < 10.0**-4.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) assert numpy.fabs(sp.verticalfreq(1.0) - nkp.verticalfreq(1.0)) < 10.0**-4.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) # De normalize sp.denormalize() assert numpy.fabs(sp.Rforce(1.0, 0.0) + 4.0) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be normalized doesn't behave as expected" ) assert numpy.fabs(sp(1.0, 0.1) - ukp(1.0, 0.1)) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) assert numpy.fabs(sp.epifreq(1.0) - ukp.epifreq(1.0)) < 10.0**-4.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) assert numpy.fabs(sp.verticalfreq(1.0) - ukp.verticalfreq(1.0)) < 10.0**-4.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) return None def test_interpsnapshotKeplerPotential_noc_normalize(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 4.0 s["eps"] = 0.0 sp = potential.InterpSnapshotRZPotential( s, rgrid=(numpy.log(0.01), numpy.log(3.0), 201), zgrid=(0.0, 0.2, 201), logR=True, interpPot=True, enable_c=False, zsym=True, ) # Currently unnormalized assert numpy.fabs(sp.Rforce(1.0, 0.0) + 4.0) < 10.0**-6.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) # Normalize sp.normalize(R0=1.0) assert numpy.fabs(sp.Rforce(1.0, 0.0) + 1.0) < 10.0**-6.0, ( "InterpSnapShotPotential that is assumed to be normalized doesn't behave as expected" ) # De normalize sp.denormalize() assert numpy.fabs(sp.Rforce(1.0, 0.0) + 4.0) < 10.0**-6.0, ( "InterpSnapShotPotential that is assumed to be normalized doesn't behave as expected" ) # Also test when R0 =/= 1 sp.normalize(R0=2.0) assert numpy.fabs(sp.Rforce(1.0, 0.0) + 1.0) < 10.0**-6.0, ( "InterpSnapShotPotential that is assumed to be normalized doesn't behave as expected" ) # De normalize sp.denormalize() assert numpy.fabs(sp.Rforce(1.0, 0.0) + 4.0) < 10.0**-6.0, ( "InterpSnapShotPotential that is assumed to be normalized doesn't behave as expected" ) return None def test_interpsnapshotKeplerPotential_normalize_units(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 4.0 s["eps"] = 0.0 s["pos"].units = "kpc" s["eps"].units = "kpc" s["vel"].units = "km s**-1" sp = potential.InterpSnapshotRZPotential( s, rgrid=(0.01, 3.0, 201), zgrid=(0.0, 0.2, 201), logR=False, interpPot=True, zsym=True, ) # Currently unnormalized assert numpy.fabs(sp.Rforce(1.0, 0.0) + 4.0) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be unnormalized doesn't behave as expected" ) # Normalize sp.normalize(R0=1.0) assert numpy.fabs(sp.Rforce(1.0, 0.0) + 1.0) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be normalized doesn't behave as expected" ) # De normalize sp.denormalize() assert numpy.fabs(sp.Rforce(1.0, 0.0) + 4.0) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be normalized doesn't behave as expected" ) # Also test when R0 =/= 1 sp.normalize(R0=2.0) assert numpy.fabs(sp.Rforce(1.0, 0.0) + 1.0) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be normalized doesn't behave as expected" ) # De normalize sp.denormalize() assert numpy.fabs(sp.Rforce(1.0, 0.0) + 4.0) < 10.0**-7.0, ( "InterpSnapShotPotential that is assumed to be normalized doesn't behave as expected" ) return None def test_interpsnapshotKeplerPotential_epifreq(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 2.0 s["eps"] = 0.0 sp = potential.InterpSnapshotRZPotential( s, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpPot=True, interpepifreq=True, zsym=True, ) kp = potential.KeplerPotential(normalize=2.0) # should be the same # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21)[1:] for r in rs: assert ( numpy.fabs((sp.epifreq(r) - kp.epifreq(r)) / kp.epifreq(r)) < 10.0**-4.0 ), ( f"RZPot interpolation of epifreq w/ InterpSnapShotPotential of KeplerPotential fails at R = {r:g} by {numpy.fabs((sp.epifreq(r) - kp.epifreq(r)) / kp.epifreq(r)):g}" ) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 10)[1:] for r in rs: assert ( numpy.fabs((sp.epifreq(r) - kp.epifreq(r)) / kp.epifreq(r)) < 10.0**-4.0 ), ( f"RZPot interpolation of epifreq w/ InterpSnapShotPotential of KeplerPotential fails at R = {r:g} by {numpy.fabs((sp.epifreq(r) - kp.epifreq(r)) / kp.epifreq(r)):g}" ) return None def test_interpsnapshotKeplerPotential_verticalfreq(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 2.0 s["eps"] = 0.0 sp = potential.InterpSnapshotRZPotential( s, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpPot=True, interpverticalfreq=True, zsym=True, ) kp = potential.KeplerPotential(normalize=2.0) # should be the same # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21)[1:] for r in rs: assert ( numpy.fabs((sp.verticalfreq(r) - kp.verticalfreq(r)) / kp.verticalfreq(r)) < 10.0**-4.0 ), ( f"RZPot interpolation of verticalfreq w/ InterpSnapShotPotential of KeplerPotential fails at R = {r:g} by {numpy.fabs((sp.verticalfreq(r) - kp.verticalfreq(r)) / kp.verticalfreq(r)):g}" ) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 10)[1:] for r in rs: assert ( numpy.fabs((sp.verticalfreq(r) - kp.verticalfreq(r)) / kp.verticalfreq(r)) < 10.0**-4.0 ), ( f"RZPot interpolation of verticalfreq w/ InterpSnapShotPotential of KeplerPotential fails at R = {r:g} by {numpy.fabs((sp.verticalfreq(r) - kp.verticalfreq(r)) / kp.verticalfreq(r)):g}" ) return None def test_interpsnapshotKeplerPotential_R2deriv(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 2.0 s["eps"] = 0.0 sp = potential.InterpSnapshotRZPotential( s, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpPot=True, interpepifreq=True, zsym=True, ) kp = potential.KeplerPotential(amp=2.0) # should be the same # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21)[1:] zs = numpy.linspace(-0.2, 0.2, 41) for r in rs: for z in zs: assert ( numpy.fabs((sp.R2deriv(r, z) - kp.R2deriv(r, z)) / kp.R2deriv(r, z)) < 10.0**-4.0 ), ( f"RZPot interpolation of R2deriv w/ InterpSnapShotPotential of KeplerPotential fails at (R,z) = ({r:g},{z:g}) by {numpy.fabs((sp.R2deriv(r, z) - kp.R2deriv(r, z)) / kp.R2deriv(r, z)):g}" ) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 10)[1:] zs = numpy.linspace(-0.2, 0.2, 20) for r in rs: for z in zs: assert ( numpy.fabs((sp.R2deriv(r, z) - kp.R2deriv(r, z)) / kp.R2deriv(r, z)) < 10.0**-4.0 ), ( f"RZPot interpolation of R2deriv w/ InterpSnapShotPotential of KeplerPotential fails at (R,z) = ({r:g},{z:g}) by {numpy.fabs((sp.R2deriv(r, z) - kp.R2deriv(r, z)) / kp.R2deriv(r, z)):g}" ) return None def test_interpsnapshotKeplerPotential_z2deriv(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 2.0 s["eps"] = 0.0 sp = potential.InterpSnapshotRZPotential( s, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpPot=True, interpverticalfreq=True, zsym=True, ) kp = potential.KeplerPotential(amp=2.0) # should be the same # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21)[1:] zs = numpy.linspace(-0.2, 0.2, 41) for r in rs: for z in zs: assert ( numpy.fabs((sp.z2deriv(r, z) - kp.z2deriv(r, z)) / kp.z2deriv(r, z)) < 10.0**-4.0 ), ( f"RZPot interpolation of z2deriv w/ InterpSnapShotPotential of KeplerPotential fails at (R,z) = ({r:g},{z:g}) by {numpy.fabs((sp.z2deriv(r, z) - kp.z2deriv(r, z)) / kp.z2deriv(r, z)):g}" ) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 10)[1:] zs = numpy.linspace(-0.2, 0.2, 20) for r in rs: for z in zs: assert ( numpy.fabs((sp.z2deriv(r, z) - kp.z2deriv(r, z)) / kp.z2deriv(r, z)) < 2.0 * 10.0**-4.0 ), ( f"RZPot interpolation of z2deriv w/ InterpSnapShotPotential of KeplerPotential fails at (R,z) = ({r:g},{z:g}) by {numpy.fabs((sp.z2deriv(r, z) - kp.z2deriv(r, z)) / kp.z2deriv(r, z)):g}" ) return None def test_interpsnapshotKeplerpotential_Rzderiv(): # Set up a snapshot with just one unit mass at the origin s = pynbody.new(star=1) s["mass"] = 2.0 s["eps"] = 0.0 sp = potential.InterpSnapshotRZPotential( s, rgrid=(0.01, 2.0, 101), zgrid=(0.0, 0.2, 101), logR=False, interpPot=True, interpepifreq=True, interpverticalfreq=True, zsym=True, ) kp = potential.KeplerPotential(amp=2.0) # should be the same # This just tests on the grid rs = numpy.linspace(0.01, 2.0, 21)[1:] zs = numpy.linspace(-0.2, 0.2, 41) zs = zs[zs != 0.0] # avoid zero # Test, but give small |z| a less constraining for r in rs: for z in zs: assert numpy.fabs( (sp.Rzderiv(r, z) - kp.Rzderiv(r, z)) / kp.Rzderiv(r, z) ) < 10.0**-4.0 * (1.0 + 19.0 * (numpy.fabs(z) < 0.05)), ( f"RZPot interpolation of Rzderiv w/ InterpSnapShotPotential of KeplerPotential fails at (R,z) = ({r:g},{z:g}) by {numpy.fabs((sp.Rzderiv(r, z) - kp.Rzderiv(r, z)) / kp.Rzderiv(r, z)):g}; value is {kp.Rzderiv(r, z):g}" ) # This tests within the grid rs = numpy.linspace(0.01, 2.0, 10)[1:] zs = numpy.linspace(-0.2, 0.2, 20) for r in rs: for z in zs: assert numpy.fabs( (sp.Rzderiv(r, z) - kp.Rzderiv(r, z)) / kp.Rzderiv(r, z) ) < 10.0**-4.0 * (1.0 + 19.0 * (numpy.fabs(z) < 0.05)), ( f"RZPot interpolation of Rzderiv w/ InterpSnapShotPotential of KeplerPotential fails at (R,z) = ({r:g},{z:g}) by {numpy.fabs((sp.Rzderiv(r, z) - kp.Rzderiv(r, z)) / kp.Rzderiv(r, z)):g}" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_sphericaldf.py0000644000175100001660000031770314761352023017367 0ustar00runnerdocker# Tests of spherical distribution functions import platform WIN32 = platform.system() == "Windows" if not WIN32: # Enable 64bit for JAX from jax import config config.update("jax_enable_x64", True) import numpy import pytest from scipy import integrate, special from galpy import potential from galpy.df import ( constantbetadf, constantbetaHernquistdf, eddingtondf, isotropicHernquistdf, isotropicNFWdf, isotropicPlummerdf, jeans, kingdf, osipkovmerrittdf, osipkovmerrittHernquistdf, osipkovmerrittNFWdf, ) from galpy.util import galpyWarning ############################# ISOTROPIC HERNQUIST DF ########################## # Note that we use the Hernquist case to check a bunch of code in the # sphericaldf realm that doesn't need to be checked for each new spherical DF def test_isotropic_hernquist_dens_spherically_symmetric(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp = dfh.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_isotropic_hernquist_dens_massprofile(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp = dfh.sample(n=100000) tol = 5 * 1e-3 check_spherical_massprofile( samp, lambda r: pot.mass(r) / pot.mass( numpy.amax(samp.r()), ), tol, skip=1000, ) return None def test_isotropic_hernquist_singler_is_atsingler(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp = dfh.sample(R=1.3, z=0.0, n=100000) assert numpy.all(numpy.fabs(samp.r() - 1.3) < 1e-8), ( "Sampling a spherical distribution function at a single r does not produce orbits at a single r" ) return None def test_isotropic_hernquist_singler_is_atrandomphi(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp = dfh.sample(R=1.3, z=0.0, n=100000) tol = 1e-2 check_azimuthal_symmetry(samp, 0, tol) check_azimuthal_symmetry(samp, 1, tol) check_azimuthal_symmetry(samp, 2, tol) check_azimuthal_symmetry(samp, 3, tol) check_azimuthal_symmetry(samp, 4, tol) check_azimuthal_symmetry(samp, 5, tol) check_azimuthal_symmetry(samp, 6, tol) return None def test_isotropic_hernquist_singlerphi_is_atsinglephi(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp = dfh.sample(R=1.3, z=0.0, phi=numpy.pi - 0.3, n=100000) assert numpy.all(numpy.fabs(samp.phi() - numpy.pi + 0.3) < 1e-8), ( "Sampling a spherical distribution function at a single r and phi oes not produce orbits at a single phi" ) return None def test_isotropic_hernquist_givenr_are_atgivenr(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) numpy.random.seed(10) r = numpy.linspace(0.1, 10.0, 1001) theta = numpy.random.uniform(size=len(r)) * numpy.pi # n should be ignored in the following samp = dfh.sample(R=r * numpy.sin(theta), z=r * numpy.cos(theta), n=100000) assert len(samp) == len(r), ( "Length of sample with given r array is not equal to length of r" ) assert numpy.all(numpy.fabs(samp.r() - r) < 1e-8), ( "Sampling a spherical distribution function at given r does not produce orbits at these given r" ) assert numpy.all(numpy.fabs(samp.R() - r * numpy.sin(theta)) < 1e-8), ( "Sampling a spherical distribution function at given R does not produce orbits at these given R" ) assert numpy.all(numpy.fabs(samp.z() - r * numpy.cos(theta)) < 1e-8), ( "Sampling a spherical distribution function at given z does not produce orbits at these given z" ) return None def test_isotropic_hernquist_dens_massprofile_forcemassinterpolation(): pot = potential.HernquistPotential(amp=2.3, a=1.3) # Remove the inverse cumulative mass function to force its interpolation class isotropicHernquistdfNoICMF(isotropicHernquistdf): _icmf = property() dfh = isotropicHernquistdfNoICMF(pot=pot) numpy.random.seed(10) samp = dfh.sample(n=100000) tol = 5 * 1e-3 check_spherical_massprofile( samp, lambda r: pot.mass(r) / pot.mass( numpy.amax(samp.r()), ), tol, skip=1000, ) return None def test_isotropic_hernquist_sigmar(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp = dfh.sample(n=300000) tol = 0.05 check_sigmar_against_jeans( samp, pot, tol, beta=0.0, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_isotropic_hernquist_singler_sigmar(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) numpy.random.seed(10) for r in [0.3, 1.3, 2.3]: samp = dfh.sample(R=r, z=0.0, n=100000) tol = 0.01 check_sigmar_against_jeans( samp, pot, tol, beta=0.0, rmin=r - 0.1, rmax=r + 0.1, bins=1 ) return None def test_isotropic_hernquist_beta(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp = dfh.sample(n=1000000) tol = 6 * 1e-2 check_beta( samp, pot, tol, beta=0.0, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_isotropic_hernquist_dens_directint(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) tol = 1e-8 check_dens_directint( dfh, pot, tol, lambda r: pot.dens(r, 0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_isotropic_hernquist_meanvr_directint(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) tol = 1e-8 check_meanvr_directint( dfh, pot, tol, beta=0.0, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_hernquist_sigmar_directint(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) tol = 1e-5 check_sigmar_against_jeans_directint( dfh, pot, tol, beta=0.0, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_hernquist_sigmar_directint_forcevmoment(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) tol = 1e-5 check_sigmar_against_jeans_directint_forcevmoment( dfh, pot, tol, beta=0.0, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_hernquist_beta_directint(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) tol = 1e-8 check_beta_directint( dfh, tol, beta=0.0, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_hernquist_energyoutofbounds(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) assert numpy.all(numpy.fabs(dfh((numpy.arange(0.1, 10.0, 0.1),))) < 1e-8), ( "Evaluating the isotropic Hernquist DF at E > 0 does not give zero" ) assert numpy.all(numpy.fabs(dfh((pot(0, 0) - 1e-4,))) < 1e-8), ( "Evaluating the isotropic Hernquist DF at E < -GM/a does not give zero" ) return None # Check that samples of R,vR,.. are the same as orbit samples def test_isotropic_hernquist_phasespacesamples_vs_orbitsamples(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) numpy.random.seed(10) samp_orbits = dfh.sample(n=1000) # Reset seed such that we should get the same numpy.random.seed(10) samp_RvR = dfh.sample(n=1000, return_orbit=False) assert numpy.all(numpy.fabs(samp_orbits.R() - samp_RvR[0]) < 1e-8), ( "Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits" ) assert numpy.all(numpy.fabs(samp_orbits.vR() - samp_RvR[1]) < 1e-8), ( "Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits" ) assert numpy.all(numpy.fabs(samp_orbits.vT() - samp_RvR[2]) < 1e-8), ( "Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits" ) assert numpy.all(numpy.fabs(samp_orbits.z() - samp_RvR[3]) < 1e-8), ( "Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits" ) assert numpy.all(numpy.fabs(samp_orbits.vz() - samp_RvR[4]) < 1e-8), ( "Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits" ) assert numpy.all(numpy.fabs(samp_orbits.phi() - samp_RvR[5]) < 1e-8), ( "Sampling R,vR,... from spherical DF does not give the same as sampling equivalent orbits" ) return None def test_isotropic_hernquist_diffcalls(): from galpy.orbit import Orbit pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) # R,vR... vs. E R, vR, vT, z, vz, phi = 1.1, 0.3, 0.2, 0.9, -0.2, 2.4 # Calculate E directly assert ( numpy.fabs( dfh(R, vR, vT, z, vz, phi) - dfh((pot(R, z) + 0.5 * (vR**2.0 + vT**2.0 + vz**2.0),)) ) < 1e-8 ), ( "Calling the isotropic Hernquist DF with R,vR,... or E[R,vR,...] does not give the same answer" ) # Also L assert ( numpy.fabs( dfh(R, vR, vT, z, vz, phi) - dfh( ( pot(R, z) + 0.5 * (vR**2.0 + vT**2.0 + vz**2.0), numpy.sqrt(numpy.sum(Orbit([R, vR, vT, z, vz, phi]).L() ** 2.0)), ) ) ) < 1e-8 ), ( "Calling the isotropic Hernquist DF with R,vR,... or E[R,vR,...] does not give the same answer" ) # Also as orbit assert ( numpy.fabs(dfh(R, vR, vT, z, vz, phi) - dfh(Orbit([R, vR, vT, z, vz, phi]))) < 1e-8 ), ( "Calling the isotropic Hernquist DF with R,vR,... or E[R,vR,...] does not give the same answer" ) return None # Test that integrating the differential energy distribution dMdE over all energies equals the total mass def test_isotropic_hernquist_dMdE_integral(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = isotropicHernquistdf(pot=pot) tol = 1e-8 check_dMdE_integral(dfh, tol) return None ############################# ANISOTROPIC HERNQUIST DF ######################## def test_anisotropic_hernquist_dens_spherically_symmetric(): pot = potential.HernquistPotential(amp=2.3, a=1.3) betas = [-0.7, -0.5, -0.4, 0.0, 0.3, 0.5] for beta in betas: dfh = constantbetaHernquistdf(pot=pot, beta=beta) numpy.random.seed(10) samp = dfh.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_anisotropic_hernquist_dens_massprofile(): pot = potential.HernquistPotential(amp=2.3, a=1.3) betas = [-0.7, -0.5, -0.4, 0.0, 0.3, 0.5] for beta in betas: dfh = constantbetaHernquistdf(pot=pot, beta=beta) numpy.random.seed(10) samp = dfh.sample(n=100000) tol = 5 * 1e-3 check_spherical_massprofile( samp, lambda r: pot.mass(r) / pot.mass(numpy.amax(samp.r())), tol, skip=1000 ) return None def test_anisotropic_hernquist_sigmar(): pot = potential.HernquistPotential(amp=2.3, a=1.3) betas = [-0.7, -0.5, -0.4, 0.0, 0.3, 0.5] for beta in betas: dfh = constantbetaHernquistdf(pot=pot, beta=beta) numpy.random.seed(10) samp = dfh.sample(n=300000) tol = 0.05 check_sigmar_against_jeans( samp, pot, tol, beta=beta, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_anisotropic_hernquist_beta(): pot = potential.HernquistPotential(amp=2.3, a=1.3) betas = [-0.7, -0.5, -0.4, 0.0, 0.3, 0.5] for beta in betas: dfh = constantbetaHernquistdf(pot=pot, beta=beta) numpy.random.seed(10) samp = dfh.sample(n=1000000) tol = 8 * 1e-2 * (beta > -0.7) + 0.12 * (beta == -0.7) check_beta( samp, pot, tol, beta=beta, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_anisotropic_hernquist_dens_directint(): pot = potential.HernquistPotential(amp=2.3, a=1.3) betas = [-0.7, -0.5, -0.4, 0.0, 0.3, 0.5] for beta in betas: dfh = constantbetaHernquistdf(pot=pot, beta=beta) tol = 1e-7 check_dens_directint( dfh, pot, tol, lambda r: pot.dens(r, 0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_anisotropic_hernquist_meanvr_directint(): pot = potential.HernquistPotential(amp=2.3, a=1.3) betas = [-0.7, -0.5, -0.4, 0.0, 0.3, 0.5] for beta in betas: dfh = constantbetaHernquistdf(pot=pot, beta=beta) tol = 1e-8 check_meanvr_directint( dfh, pot, tol, beta=beta, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_anisotropic_hernquist_sigmar_directint(): pot = potential.HernquistPotential(amp=2.3, a=1.3) betas = [-0.7, -0.5, -0.4, 0.0, 0.3, 0.5] for beta in betas: dfh = constantbetaHernquistdf(pot=pot, beta=beta) tol = 1e-5 check_sigmar_against_jeans_directint( dfh, pot, tol, beta=beta, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_anisotropic_hernquist_beta_directint(): pot = potential.HernquistPotential(amp=2.3, a=1.3) betas = [-0.7, -0.5, -0.4, 0.0, 0.3, 0.5] for beta in betas: dfh = constantbetaHernquistdf(pot=pot, beta=beta) tol = 1e-8 check_beta_directint( dfh, tol, beta=beta, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None # Test that integrating the differential energy distribution dMdE over all energies equals the total mass def test_anisotropic_hernquist_dMdE_integral(): pot = potential.HernquistPotential(amp=2.3, a=1.3) betas = [-0.7, -0.5, -0.4, 0.0, 0.3, 0.5] for beta in betas: dfh = constantbetaHernquistdf(pot=pot, beta=beta) tol = 1e-7 check_dMdE_integral(dfh, tol) return None # Also test the specific case of beta=0.5, where dMdE is particularly easy (An & Evans 2006) def test_anisotropic_hernquist_dMdE_betap05(): pot = potential.HernquistPotential(amp=2.3, a=1.3) dfh = constantbetaHernquistdf(pot=pot, beta=0.5) tol = 1e-10 def dMdE_betap05_analytic(E, dfh): if not hasattr(dfh, "_rphi"): dfh._rphi = dfh._setup_rphi_interpolator() rE = dfh._rphi(E) return 4.0 * numpy.pi**3.0 * rE**2.0 * dfh.fE(E) E = numpy.linspace(0.99 * pot(0, 0), pot(numpy.inf, 0) + 1e-6, 1001) assert numpy.all(numpy.fabs(dMdE_betap05_analytic(E, dfh) - dfh.dMdE(E)) < tol), ( "Anisotropic Hernquist DF dMdE for beta=0.5 does not agree with analytic expression" ) return None def test_anisotropic_hernquist_energyoutofbounds(): pot = potential.HernquistPotential(amp=2.3, a=1.3) betas = [-0.7, -0.5, -0.4, 0.0, 0.3, 0.5] for beta in betas: dfh = constantbetaHernquistdf(pot=pot, beta=beta) assert numpy.all(numpy.fabs(dfh((numpy.arange(0.1, 10.0, 0.1), 1.1))) < 1e-8), ( "Evaluating the anisotropic Hernquist DF at E > 0 does not give zero" ) assert numpy.all(numpy.fabs(dfh((pot(0, 0) - 1e-4, 1.1))) < 1e-8), ( "Evaluating the anisotropic Hernquist DF at E < -GM/a does not give zero" ) return None def test_anisotropic_hernquist_diffcalls(): from galpy.orbit import Orbit pot = potential.HernquistPotential(amp=2.3, a=1.3) betas = [-0.7, -0.5, -0.4, 0.0, 0.3, 0.5] for beta in betas: dfh = constantbetaHernquistdf(pot=pot, beta=beta) # R,vR... vs. E R, vR, vT, z, vz, phi = 1.1, 0.3, 0.2, 0.9, -0.2, 2.4 # Calculate E directly and L from Orbit assert ( numpy.fabs( dfh(R, vR, vT, z, vz, phi) - dfh( ( pot(R, z) + 0.5 * (vR**2.0 + vT**2.0 + vz**2.0), numpy.sqrt( numpy.sum(Orbit([R, vR, vT, z, vz, phi]).L() ** 2.0) ), ) ) ) < 1e-8 ), ( "Calling the anisotropic Hernquist DF with R,vR,... or E[R,vR,...] does not give the same answer" ) # Also as orbit assert ( numpy.fabs(dfh(R, vR, vT, z, vz, phi) - dfh(Orbit([R, vR, vT, z, vz, phi]))) < 1e-8 ), ( "Calling the anisotropic Hernquist DF with R,vR,... or E[R,vR,...] does not give the same answer" ) return None ########################### OSIPKOV-MERRITT HERNQUIST DF ###################### def test_osipkovmerritt_hernquist_dens_spherically_symmetric(): pot = potential.HernquistPotential(amp=2.3, a=1.3) ras = [0.3, 2.3, 5.7] for ra in ras: dfh = osipkovmerrittHernquistdf(pot=pot, ra=ra) numpy.random.seed(10) samp = dfh.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_osipkovmerritt_hernquist_dens_massprofile(): pot = potential.HernquistPotential(amp=2.3, a=1.3) ras = [0.3, 2.3, 5.7] for ra in ras: dfh = osipkovmerrittHernquistdf(pot=pot, ra=ra) numpy.random.seed(10) samp = dfh.sample(n=100000) tol = 5 * 1e-3 check_spherical_massprofile( samp, lambda r: pot.mass(r) / pot.mass(numpy.amax(samp.r())), tol, skip=1000 ) return None def test_osipkovmerritt_hernquist_sigmar(): pot = potential.HernquistPotential(amp=2.3, a=1.3) ras = [0.3, 2.3, 5.7] for ra in ras: dfh = osipkovmerrittHernquistdf(pot=pot, ra=ra) numpy.random.seed(10) samp = dfh.sample(n=100000) tol = 0.05 check_sigmar_against_jeans( samp, pot, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_osipkovmerritt_hernquist_beta(): pot = potential.HernquistPotential(amp=2.3, a=1.3) ras = [0.3, 2.3, 5.7] for ra in ras: dfh = osipkovmerrittHernquistdf(pot=pot, ra=ra) numpy.random.seed(10) samp = dfh.sample(n=1000000) tol = 0.06 check_beta( samp, pot, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_osipkovmerritt_hernquist_dens_directint(): pot = potential.HernquistPotential(amp=2.3, a=1.3) ras = [0.3, 2.3, 5.7] for ra in ras: dfh = osipkovmerrittHernquistdf(pot=pot, ra=ra) tol = 1e-5 check_dens_directint( dfh, pot, tol, lambda r: pot.dens(r, 0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=6, ) return None def test_osipkovmerritt_hernquist_meanvr_directint(): pot = potential.HernquistPotential(amp=2.3, a=1.3) ras = [0.3, 2.3, 5.7] for ra in ras: dfh = osipkovmerrittHernquistdf(pot=pot, ra=ra) tol = 1e-8 check_meanvr_directint( dfh, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=6 ) return None def test_osipkovmerritt_hernquist_sigmar_directint(): pot = potential.HernquistPotential(amp=2.3, a=1.3) ras = [0.3, 2.3, 5.7] for ra in ras: dfh = osipkovmerrittHernquistdf(pot=pot, ra=ra) tol = 1e-4 check_sigmar_against_jeans_directint( dfh, pot, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=6, ) return None def test_osipkovmerritt_hernquist_beta_directint(): pot = potential.HernquistPotential(amp=2.3, a=1.3) ras = [0.3, 2.3, 5.7] for ra in ras: dfh = osipkovmerrittHernquistdf(pot=pot, ra=ra) tol = 1e-8 check_beta_directint( dfh, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=6, ) return None # Test that the differential energy distribution approaches the isotropic one at # E=Emin and the radial one at E=Emax def test_osipkovmerritt_hernquist_dMdE_limits(): pot = potential.HernquistPotential(amp=2.3, a=1.3) ras = [2.3, 5.7] dfi = isotropicHernquistdf(pot=pot) dfr = constantbetaHernquistdf(pot=pot, beta=0.99) for ra in ras: dfh = osipkovmerrittHernquistdf(pot=pot, ra=ra) toli, tolr = 1e-3, 1e-1 assert ( numpy.fabs( dfh.dMdE(pot(0.0, 0.0) + 0.01) / dfi.dMdE(pot(0.0, 0.0) + 0.01) - 1.0 ) < toli ), "Osipkov-Merritt Hernquist DF does not approach the isotropic DF at E=Emin" assert ( numpy.fabs( dfh.dMdE(pot(numpy.inf, 0.0) - 0.05) / dfr.dMdE(pot(numpy.inf, 0.0) - 0.05) - 1.0 ) < tolr ), "Osipkov-Merritt Hernquist DF does not approach the radial DF at E=Emax" return None # Also do the dMdE test for the general f(E,L) case that we implemented in the # anisotropicsphericaldf superclass; need to call the super.dMdE method for this def test_osipkovmerritt_hernquist_dMdE_limits_generalimplementation(): from galpy.df.osipkovmerrittdf import _osipkovmerrittdf pot = potential.HernquistPotential(amp=2.3, a=1.3) ras = [2.3, 5.7] dfi = isotropicHernquistdf(pot=pot) dfr = constantbetaHernquistdf(pot=pot, beta=0.99) for ra in ras: dfh = osipkovmerrittHernquistdf(pot=pot, ra=ra) toli, tolr = 1e-3, 1e-1 assert ( numpy.fabs( super(_osipkovmerrittdf, dfh)._dMdE([pot(0.0, 0.0) + 0.01]) / dfi.dMdE(pot(0.0, 0.0) + 0.01) - 1.0 ) < toli ), "Osipkov-Merritt Hernquist DF does not approach the isotropic DF at E=Emin" assert ( numpy.fabs( super(_osipkovmerrittdf, dfh)._dMdE([pot(numpy.inf, 0.0) - 0.05]) / dfr.dMdE(pot(numpy.inf, 0.0) - 0.05) - 1.0 ) < tolr ), "Osipkov-Merritt Hernquist DF does not approach the radial DF at E=Emax" return None def test_osipkovmerritt_hernquist_Qoutofbounds(): pot = potential.HernquistPotential(amp=2.3, a=1.3) ras = [0.3, 2.3, 5.7] for ra in ras: dfh = osipkovmerrittHernquistdf(pot=pot, ra=ra) assert numpy.all(numpy.fabs(dfh((numpy.arange(0.1, 10.0, 0.1), 1.1))) < 1e-8), ( "Evaluating the Osipkov-Merritt Hernquist DF at E > 0 does not give zero" ) # The next one is not actually a physical orbit... assert numpy.all(numpy.fabs(dfh((pot(0, 0) - 1e-1, 0.1))) < 1e-8), ( "Evaluating the Osipkov-Merritt Hernquist DF at E < -GM/a does not give zero" ) assert numpy.all(numpy.fabs(dfh((-1e-4, 1.1))) < 1e-8), ( "Evaluating the Osipkov-Merritt Hernquist DF at Q < 0 does not give zero" ) return None def test_osipkovmerritt_hernquist_diffcalls(): from galpy.orbit import Orbit pot = potential.HernquistPotential(amp=2.3, a=1.3) ras = [0.3, 2.3, 5.7] for ra in ras: dfh = osipkovmerrittHernquistdf(pot=pot, ra=ra) # R,vR... vs. E R, vR, vT, z, vz, phi = 1.1, 0.3, 0.2, 0.9, -0.2, 2.4 # Calculate E directly and L from Orbit assert ( numpy.fabs( dfh(R, vR, vT, z, vz, phi) - dfh( ( pot(R, z) + 0.5 * (vR**2.0 + vT**2.0 + vz**2.0), numpy.sqrt( numpy.sum(Orbit([R, vR, vT, z, vz, phi]).L() ** 2.0) ), ) ) ) < 1e-8 ), ( "Calling the Osipkov-Merritt anisotropic Hernquist DF with R,vR,... or E[R,vR,...] does not give the same answer" ) # Also as orbit assert ( numpy.fabs(dfh(R, vR, vT, z, vz, phi) - dfh(Orbit([R, vR, vT, z, vz, phi]))) < 1e-8 ), ( "Calling the Osipkov-Merritt isotropic Hernquist DF with R,vR,... or E[R,vR,...] does not give the same answer" ) return None ############################## OSIPKOV-MERRITT NFW DF ######################### def test_osipkovmerritt_nfw_dens_spherically_symmetric(): pot = potential.NFWPotential(amp=2.3, a=1.3) ras = [2.3, 5.7] for ra in ras: dfh = osipkovmerrittNFWdf(pot=pot, ra=ra) numpy.random.seed(10) samp = dfh.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_osipkovmerritt_nfw_dens_massprofile(): pot = potential.NFWPotential(amp=2.3, a=1.3) ras = [2.3, 5.7] for ra in ras: dfh = osipkovmerrittNFWdf(pot=pot, ra=ra) numpy.random.seed(10) samp = dfh.sample(n=100000) tol = 7 * 1e-3 check_spherical_massprofile( samp, lambda r: pot.mass(r) / pot.mass(numpy.amax(samp.r())), tol, skip=1000 ) return None def test_osipkovmerritt_nfw_sigmar(): pot = potential.NFWPotential(amp=2.3, a=1.3) ras = [2.3, 5.7] for ra in ras: dfh = osipkovmerrittNFWdf(pot=pot, ra=ra) numpy.random.seed(10) samp = dfh.sample(n=1000000) tol = 0.17 check_sigmar_against_jeans( samp, pot, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_osipkovmerritt_nfw_beta(): pot = potential.NFWPotential(amp=2.3, a=1.3) ras = [2.3, 5.7] for ra in ras: dfh = osipkovmerrittNFWdf(pot=pot, ra=ra) numpy.random.seed(10) samp = dfh.sample(n=3000000) tol = 0.15 check_beta( samp, pot, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_osipkovmerritt_nfw_dens_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) ras = [2.3, 5.7] for ra in ras: dfh = osipkovmerrittNFWdf(pot=pot, ra=ra, rmax=numpy.inf) tol = 0.01 # 1% check_dens_directint( dfh, pot, tol, lambda r: pot.dens(r, 0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=6, ) return None def test_osipkovmerritt_nfw_meanvr_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) ras = [2.3, 5.7] for ra in ras: dfh = osipkovmerrittNFWdf(pot=pot, ra=ra, rmax=numpy.inf) tol = 1e-8 check_meanvr_directint( dfh, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=6 ) return None def test_osipkovmerritt_nfw_sigmar_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) ras = [2.3, 5.7] for ra in ras: dfh = osipkovmerrittNFWdf(pot=pot, ra=ra, rmax=numpy.inf) tol = 1e-2 # 1% check_sigmar_against_jeans_directint( dfh, pot, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=6, ) return None def test_osipkovmerritt_nfw_beta_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) ras = [2.3, 5.7] for ra in ras: dfh = osipkovmerrittNFWdf(pot=pot, ra=ra, rmax=numpy.inf) tol = 1e-8 check_beta_directint( dfh, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=6, ) return None def test_osipkovmerritt_nfw_Qoutofbounds(): pot = potential.NFWPotential(amp=2.3, a=1.3) ras = [2.3, 5.7] for ra in ras: dfh = osipkovmerrittNFWdf(pot=pot, ra=ra) assert numpy.all(numpy.fabs(dfh((numpy.arange(0.1, 10.0, 0.1), 1.1))) < 1e-8), ( "Evaluating the Osipkov-Merritt NFW DF at E > 0 does not give zero" ) # The next one is not actually a physical orbit... assert numpy.all(numpy.fabs(dfh((pot(0, 0) - 1e-1, 0.1))) < 1e-8), ( "Evaluating the Osipkov-Merritt NFW DF at E < -GM/a does not give zero" ) assert numpy.all(numpy.fabs(dfh((-1e-4, 1.1))) < 1e-8), ( "Evaluating the Osipkov-Merritt NFW DF at Q < 0 does not give zero" ) return None ############################# ISOTROPIC PLUMMER DF ############################ def test_isotropic_plummer_dens_spherically_symmetric(): pot = potential.PlummerPotential(amp=2.3, b=1.3) dfp = isotropicPlummerdf(pot=pot) numpy.random.seed(10) samp = dfp.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_isotropic_plummer_dens_massprofile(): pot = potential.PlummerPotential(amp=2.3, b=1.3) dfp = isotropicPlummerdf(pot=pot) numpy.random.seed(10) samp = dfp.sample(n=100000) tol = 5 * 1e-3 check_spherical_massprofile( samp, lambda r: pot.mass(r) / pot.mass(numpy.amax(samp.r())), tol, skip=1000 ) return None def test_isotropic_plummer_sigmar(): pot = potential.PlummerPotential(amp=2.3, b=1.3) dfp = isotropicPlummerdf(pot=pot) numpy.random.seed(10) samp = dfp.sample(n=1000000) tol = 0.05 check_sigmar_against_jeans( samp, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_plummer_beta(): pot = potential.PlummerPotential(amp=2.3, b=1.3) dfp = isotropicPlummerdf(pot=pot) numpy.random.seed(10) samp = dfp.sample(n=1000000) tol = 6 * 1e-2 check_beta(samp, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31) return None def test_isotropic_plummer_dens_directint(): pot = potential.PlummerPotential(amp=2.3, b=1.3) dfp = isotropicPlummerdf(pot=pot) tol = 1e-7 check_dens_directint( dfp, pot, tol, lambda r: pot.dens(r, 0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_isotropic_plummer_meanvr_directint(): pot = potential.PlummerPotential(amp=2.3, b=1.3) dfp = isotropicPlummerdf(pot=pot) tol = 1e-8 check_meanvr_directint( dfp, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_plummer_sigmar_directint(): pot = potential.PlummerPotential(amp=2.3, b=1.3) dfp = isotropicPlummerdf(pot=pot) tol = 1e-5 check_sigmar_against_jeans_directint( dfp, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_plummer_beta_directint(): pot = potential.PlummerPotential(amp=2.3, b=1.3) dfp = isotropicPlummerdf(pot=pot) tol = 1e-8 check_beta_directint( dfp, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None # Test that integrating the differential energy distribution dMdE over all energies equals the total mass def test_isotropic_plummer_dMdE_integral(): pot = potential.PlummerPotential(amp=2.3, b=1.3) dfp = isotropicPlummerdf(pot=pot) tol = 1e-8 check_dMdE_integral(dfp, tol) return None def test_isotropic_plummer_energyoutofbounds(): pot = potential.PlummerPotential(amp=2.3, b=1.3) dfp = isotropicPlummerdf(pot=pot) assert numpy.all(numpy.fabs(dfp((numpy.arange(0.1, 10.0, 0.1), 1.1))) < 1e-8), ( "Evaluating the isotropic Plummer DF at E > 0 does not give zero" ) assert numpy.all(numpy.fabs(dfp((pot(0, 0) - 1e-4, 1.1))) < 1e-8), ( "Evaluating the isotropic Plummer DF at E < Phi(0) does not give zero" ) return None ############################# ISOTROPIC NFW DF ############################ def test_isotropic_nfw_dens_spherically_symmetric(): pot = potential.NFWPotential(amp=2.3, a=1.3) dfp = isotropicNFWdf(pot=pot) numpy.random.seed(10) samp = dfp.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_isotropic_nfw_dens_massprofile(): pot = potential.NFWPotential(amp=2.3, a=1.3) dfp = isotropicNFWdf(pot=pot) numpy.random.seed(10) samp = dfp.sample(n=100000) tol = 7 * 1e-3 check_spherical_massprofile( samp, lambda r: pot.mass(r) / pot.mass(numpy.amax(samp.r())), tol, skip=1000 ) return None def test_isotropic_nfw_sigmar(): pot = potential.NFWPotential(amp=2.3, a=1.3) dfp = isotropicNFWdf(pot=pot) numpy.random.seed(10) samp = dfp.sample(n=1000000) tol = 0.08 check_sigmar_against_jeans( samp, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_nfw_beta(): pot = potential.NFWPotential(amp=2.3, a=1.3) dfp = isotropicNFWdf(pot=pot) numpy.random.seed(10) samp = dfp.sample(n=3000000) tol = 8 * 1e-2 check_beta(samp, pot, tol, rmin=pot._scale / 5.0, rmax=pot._scale * 10.0, bins=31) return None def test_isotropic_nfw_dens_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) dfp = isotropicNFWdf(pot=pot) tol = 1e-2 # only approx, normally 1e-7 check_dens_directint( dfp, pot, tol, lambda r: pot.dens(r, 0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_isotropic_nfw_meanvr_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) dfp = isotropicNFWdf(pot=pot) tol = 1e-8 check_meanvr_directint( dfp, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_nfw_sigmar_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) dfp = isotropicNFWdf(pot=pot) tol = 1e-3 # only approx. normally 1e-5 check_sigmar_against_jeans_directint( dfp, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_nfw_beta_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) dfp = isotropicNFWdf(pot=pot) tol = 1e-8 check_beta_directint( dfp, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_nfw_energyoutofbounds(): pot = potential.NFWPotential(amp=2.3, a=1.3) dfp = isotropicNFWdf(pot=pot) assert numpy.all(numpy.fabs(dfp((numpy.arange(0.1, 10.0, 0.1), 1.1))) < 1e-8), ( "Evaluating the isotropic NFW DF at E > 0 does not give zero" ) assert numpy.all(numpy.fabs(dfp((pot(0, 0) - 1e-4, 1.1))) < 1e-8), ( "Evaluating the isotropic NFW DF at E < Phi(0) does not give zero" ) return None def test_isotropic_nfw_widrow_against_improved(): # Test that using the Widrow (2000) prescription gives almost the same f(E) pot = potential.NFWPotential(amp=2.3, a=1.3) dfp = isotropicNFWdf(pot=pot) dfpw = isotropicNFWdf(pot=pot, widrow=True) Es = numpy.linspace(-dfp._Etildemax * 0.999, 0, 101, endpoint=False) assert numpy.all(numpy.fabs(1.0 - dfp.fE(Es) / dfpw.fE(Es)) < 1e-2), ( "isotropic NFW with widrow=True does not agree on f(E) with widrow=False" ) return None ################################# EDDINGTON DF ################################ # For the following tests, we use a DehnenCoreSphericalPotential def test_isotropic_eddington_selfconsist_dehnencore_dens_spherically_symmetric(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot) numpy.random.seed(10) samp = dfp.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_isotropic_eddington_selfconsist_dehnencore_dens_massprofile(): # Do one with pot as list pot = [potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15)] dfp = eddingtondf(pot=pot) numpy.random.seed(10) samp = dfp.sample(n=100000) tol = 5 * 1e-3 check_spherical_massprofile( samp, lambda r: potential.mass(pot, r) / potential.mass(pot, numpy.amax(samp.r())), tol, skip=1000, ) return None def test_isotropic_eddington_selfconsist_dehnencore_sigmar(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot) numpy.random.seed(10) samp = dfp.sample(n=1000000) tol = 0.08 check_sigmar_against_jeans( samp, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_eddington_selfconsist_dehnencore_beta(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot) numpy.random.seed(10) samp = dfp.sample(n=3000000) tol = 8 * 1e-2 check_beta(samp, pot, tol, rmin=pot._scale / 5.0, rmax=pot._scale * 10.0, bins=31) return None def test_isotropic_eddington_selfconsist_dehnencore_dens_directint(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot) tol = 1e-2 # only approx, normally 1e-7 check_dens_directint( dfp, pot, tol, lambda r: pot.dens(r, 0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_isotropic_eddington_selfconsist_dehnencore_meanvr_directint(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot) tol = 1e-8 check_meanvr_directint( dfp, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_eddington_selfconsist_dehnencore_sigmar_directint(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot) tol = 1e-3 # only approx. normally 1e-5 check_sigmar_against_jeans_directint( dfp, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_eddington_selfconsist_dehnencore_beta_directint(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot) tol = 1e-8 check_beta_directint( dfp, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_eddington_selfconsist_dehnencore_energyoutofbounds(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot) assert numpy.all(numpy.fabs(dfp((numpy.arange(0.1, 10.0, 0.1), 1.1))) < 1e-8), ( "Evaluating the eddington DF at E > 0 does not give zero" ) assert numpy.all(numpy.fabs(dfp((pot(0, 0) - 1e-4, 1.1))) < 1e-8), ( "Evaluating the isotropic NFW DF at E < Phi(0) does not give zero" ) return None # For the following tests, we use a DehnenCoreSphericalPotential embedded in # an NFW halo def test_isotropic_eddington_dehnencore_in_nfw_dens_spherically_symmetric(): pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot, denspot=denspot) numpy.random.seed(10) samp = dfp.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_isotropic_eddington_dehnencore_in_nfw_dens_massprofile(): pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot, denspot=denspot) numpy.random.seed(10) samp = dfp.sample(n=100000) tol = 5 * 1e-3 check_spherical_massprofile( samp, lambda r: denspot.mass(r) / denspot.mass(numpy.amax(samp.r())), tol, skip=1000, ) return None def test_isotropic_eddington_dehnencore_in_nfw_sigmar(): # Use list pot = [potential.NFWPotential(amp=2.3, a=1.3)] denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot, denspot=denspot) numpy.random.seed(10) samp = dfp.sample(n=1000000) tol = 0.08 check_sigmar_against_jeans( samp, pot, tol, dens=lambda r: denspot.dens(r, 0, use_physical=False), rmin=pot[0]._scale / 10.0, rmax=pot[0]._scale * 10.0, bins=31, ) return None def test_isotropic_eddington_dehnencore_in_nfw_beta(): pot = potential.NFWPotential(amp=2.3, a=1.3) # Use list denspot = [potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15)] dfp = eddingtondf(pot=pot, denspot=denspot) numpy.random.seed(10) samp = dfp.sample(n=3000000) tol = 8 * 1e-2 check_beta(samp, pot, tol, rmin=pot._scale / 5.0, rmax=pot._scale * 10.0, bins=31) return None def test_isotropic_eddington_dehnencore_in_nfw_dens_directint(): # Lists for all! pot = [potential.NFWPotential(amp=2.3, a=1.3)] denspot = [potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15)] dfp = eddingtondf(pot=pot, denspot=denspot) tol = 1e-2 # only approx, normally 1e-7 check_dens_directint( dfp, pot, tol, lambda r: potential.evaluateDensities(denspot, r, 0), rmin=pot[0]._scale / 10.0, rmax=pot[0]._scale * 10.0, bins=31, ) return None def test_isotropic_eddington_dehnencore_in_nfw_meanvr_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot, denspot=denspot) tol = 1e-8 check_meanvr_directint( dfp, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_eddington_dehnencore_in_nfw_sigmar_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot, denspot=denspot) tol = 1e-3 # only approx. normally 1e-5 check_sigmar_against_jeans_directint( dfp, pot, tol, dens=lambda r: denspot.dens(r, 0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_isotropic_eddington_dehnencore_in_nfw_beta_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot, denspot=denspot) tol = 1e-8 check_beta_directint( dfp, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31 ) return None def test_isotropic_eddington_dehnencore_in_nfw_energyoutofbounds(): pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) dfp = eddingtondf(pot=pot, denspot=denspot) assert numpy.all(numpy.fabs(dfp((numpy.arange(0.1, 10.0, 0.1), 1.1))) < 1e-8), ( "Evaluating the isotropic NFW DF at E > 0 does not give zero" ) assert numpy.all(numpy.fabs(dfp((pot(0, 0) - 1e-4, 1.1))) < 1e-8), ( "Evaluating the isotropic NFW DF at E < Phi(0) does not give zero" ) return None # For the following test use the PowerSphericalPotential to make sure that # sampling works properly for potentials which do not have vectorized masses def test_eddington_powerspherical_massprofile(): rmin = 0.1 # PowerSphericalPotential behaves best w/ rmin rmax = 5.0 # Mass profile divergent pot = potential.PowerSphericalPotential(amp=1.3, alpha=1.4) dfp = eddingtondf(pot=pot, rmax=rmax) numpy.random.seed(10) samp = dfp.sample(n=10000, rmin=rmin) tol = 5 * 1e-2 check_spherical_massprofile( samp, lambda r: (pot.mass(r) - pot.mass(rmin)) / (pot.mass(numpy.amax(samp.r())) - pot.mass(rmin)), tol, skip=1000, ) return None ############# TEST OF dMdE AGAINST KNOWN HERNQUIST FORMULA ############ def test_eddington_hernquist_dMdE(): # Test that dMdE for an isotropic Hernquist model is correct by comparing to the exact solution in isotropicHernquist pot = potential.HernquistPotential(amp=1.3, a=2.3) dfe = eddingtondf(pot=pot) dfi = isotropicHernquistdf(pot) Emin = pot(0.0, 0.0) Emax = pot(numpy.inf, 0.0) E = numpy.linspace(0.99 * Emin, Emax - 0.001, 1001) assert numpy.all(numpy.fabs(dfe.dMdE(E) / dfi.dMdE(E) - 1.0) < 1e-4), ( "dMdE for isotropic Hernquist DF does not agree with exact solution" ) return None ############# FURTHER TESTS OF EDDINGTONDF FOR DIFFERENT POTENTIALS############ # If you implement the required potential derivatives _ddensdr and the 2nd; # add your potential to the tests here def test_eddington_differentpotentials_dens_directint(): pots = [ potential.PowerSphericalPotential(amp=1.3, alpha=1.9), potential.PlummerPotential(amp=2.3, b=1.3), potential.PowerSphericalPotentialwCutoff(amp=1.3, alpha=1.9, rc=1.2), ] tols = [1e-3 for pot in pots] for pot, tol in zip(pots, tols): dfh = eddingtondf(pot=pot) check_dens_directint( dfh, pot, tol, lambda r: pot.dens(r, 0), rmin=pot._scale / 10.0 if hasattr(pot, "_scale") else 0.1, rmax=pot._scale * 10.0 if hasattr(pot, "_scale") else 10.0, bins=11, ) return None def test_eddington_differentpotentials_dMdE_integral(): # Don't do PowerSphericalPotential, because it does not have a finite mass pots = [ potential.PlummerPotential(amp=2.3, b=1.3), potential.PowerSphericalPotentialwCutoff(amp=1.3, alpha=1.9, rc=1.2), ] tols = [1e-6 for pot in pots] for pot, tol in zip(pots, tols): dfh = eddingtondf(pot=pot) check_dMdE_integral(dfh, tol) return None ################################# KING DF ##################################### def test_king_dens_spherically_symmetric(): dfk = kingdf(W0=3.0, M=2.3, rt=1.76) numpy.random.seed(10) samp = dfk.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_king_dens_massprofile(): pot = potential.KingPotential(W0=3.0, M=2.3, rt=1.76) dfk = kingdf(W0=3.0, M=2.3, rt=1.76) numpy.random.seed(10) samp = dfk.sample(n=100000) tol = 1e-2 check_spherical_massprofile( samp, lambda r: pot.mass(r) / pot.mass(numpy.amax(samp.r())), tol, skip=4000 ) return None def test_king_sigmar(): W0s = [1.0, 3.0, 9.0] for W0 in W0s: pot = potential.KingPotential(W0=W0, M=2.3, rt=1.76) dfk = kingdf(W0=W0, M=2.3, rt=1.76) numpy.random.seed(10) samp = dfk.sample(n=1000000) # lower tolerance closer to rt because fewer stars there tol = 0.1 check_sigmar_against_jeans( samp, pot, tol, beta=0.0, rmin=dfk._scale / 10.0, rmax=dfk.rt * 0.7, bins=31 ) tol = 0.2 check_sigmar_against_jeans( samp, pot, tol, beta=0.0, rmin=dfk.rt * 0.8, rmax=dfk.rt * 0.95, bins=5 ) return None def test_king_beta(): pot = potential.KingPotential(W0=3.0, M=2.3, rt=1.76) dfk = kingdf(W0=3.0, M=2.3, rt=1.76) numpy.random.seed(10) samp = dfk.sample(n=1000000) tol = 6 * 1e-2 # lower tolerance closer to rt because fewer stars there tol = 0.135 check_beta(samp, pot, tol, beta=0.0, rmin=dfk._scale / 10.0, rmax=dfk.rt, bins=31) return None def test_king_dens_directint(): pot = potential.KingPotential(W0=3.0, M=2.3, rt=1.76) dfk = kingdf(W0=3.0, M=2.3, rt=1.76) tol = 0.02 check_dens_directint( dfk, pot, tol, lambda r: dfk.dens(r), rmin=dfk._scale / 10.0, rmax=dfk.rt * 0.7, bins=31, ) return None def test_king_sigmar_directint(): pot = potential.KingPotential(W0=3.0, M=2.3, rt=1.76) dfk = kingdf(W0=3.0, M=2.3, rt=1.76) tol = 0.05 # Jeans isn't that accurate for this rather difficult case check_sigmar_against_jeans_directint( dfk, pot, tol, beta=0.0, rmin=dfk._scale / 10.0, rmax=dfk.rt * 0.7, bins=31 ) return None def test_king_beta_directint(): dfk = kingdf(W0=3.0, M=2.3, rt=1.76) tol = 1e-8 check_beta_directint( dfk, tol, beta=0.0, rmin=dfk._scale / 10.0, rmax=dfk.rt * 0.7, bins=31 ) return None ############################### OSIPKOV-MERRITT DF ############################ # For the following tests, we use a DehnenCoreSphericalPotential osipkovmerritt_dfs_selfconsist = None # reuse in other tests def test_osipkovmerritt_selfconsist_dehnencore_dens_spherically_symmetric(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] global osipkovmerritt_dfs_selfconsist osipkovmerritt_dfs_selfconsist = [] for ra in ras: dfh = osipkovmerrittdf(pot=pot, ra=ra) osipkovmerritt_dfs_selfconsist.append(dfh) numpy.random.seed(10) samp = dfh.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_osipkovmerritt_selfconsist_dehnencore_dens_massprofile(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras, osipkovmerritt_dfs_selfconsist): numpy.random.seed(10) samp = dfh.sample(n=100000) tol = 5 * 1e-3 check_spherical_massprofile( samp, lambda r: pot.mass(r) / pot.mass(numpy.amax(samp.r())), tol, skip=1000 ) return None def test_osipkovmerritt_selfconsist_dehnencore_sigmar(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras, osipkovmerritt_dfs_selfconsist): numpy.random.seed(10) samp = dfh.sample(n=300000) tol = 0.1 check_sigmar_against_jeans( samp, pot, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_osipkovmerritt_selfconsist_dehnencore_beta(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras, osipkovmerritt_dfs_selfconsist): numpy.random.seed(10) samp = dfh.sample(n=300000) tol = 0.1 # rmin larger than usual to avoid low number sampling check_beta( samp, pot, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 3.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_osipkovmerritt_selfconsist_dehnencore_dens_directint(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras[:1], osipkovmerritt_dfs_selfconsist[:1]): tol = 1e-4 check_dens_directint( dfh, pot, tol, lambda r: pot.dens(r, 0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=3, ) return None def test_osipkovmerritt_selfconsist_dehnencore_meanvr_directint(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras[1:], osipkovmerritt_dfs_selfconsist[1:]): tol = 1e-8 check_meanvr_directint( dfh, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=3 ) return None def test_osipkovmerritt_selfconsist_dehnencore_sigmar_directint(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras[:1], osipkovmerritt_dfs_selfconsist[:1]): tol = 1e-4 check_sigmar_against_jeans_directint( dfh, pot, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=3, ) return None def test_osipkovmerritt_selfconsist_dehnencore_beta_directint(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras[1:], osipkovmerritt_dfs_selfconsist[1:]): tol = 1e-8 check_beta_directint( dfh, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=3, ) return None def test_osipkovmerritt_selfconsist_dehnencore_Qoutofbounds(): pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras, osipkovmerritt_dfs_selfconsist): assert numpy.all(numpy.fabs(dfh((numpy.arange(0.1, 10.0, 0.1), 1.1))) < 1e-8), ( "Evaluating the Osipkov-Merritt DF at E > 0 does not give zero" ) # The next one is not actually a physical orbit... assert numpy.all(numpy.fabs(dfh((pot(0, 0) - 1e-1, 0.1))) < 1e-8), ( "Evaluating the Osipkov-Merritt DF at E < -GM/a does not give zero" ) assert numpy.all(numpy.fabs(dfh((-1e-4, 1.1))) < 1e-8), ( "Evaluating the Osipkov-Merritt DF at Q < 0 does not give zero" ) return None # For the following tests, we use a DehnenCoreSphericalPotential embedded in # an NFW halo osipkovmerritt_dfs_dehnencore_in_nfw = None # reuse in other tests def test_osipkovmerritt_dehnencore_in_nfw_dens_spherically_symmetric(): pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] global osipkovmerritt_dfs_dehnencore_in_nfw osipkovmerritt_dfs_dehnencore_in_nfw = [] for ra in ras: dfh = osipkovmerrittdf(pot=pot, denspot=denspot, ra=ra) osipkovmerritt_dfs_dehnencore_in_nfw.append(dfh) numpy.random.seed(10) samp = dfh.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_osipkovmerritt_dehnencore_in_nfw_dens_massprofile(): pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras, osipkovmerritt_dfs_dehnencore_in_nfw): numpy.random.seed(10) samp = dfh.sample(n=100000) tol = 5 * 1e-3 check_spherical_massprofile( samp, lambda r: denspot.mass(r) / denspot.mass(numpy.amax(samp.r())), tol, skip=1000, ) return None def test_osipkovmerritt_dehnencore_in_nfw_sigmar(): # Use list pot = [potential.NFWPotential(amp=2.3, a=1.3)] denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras[:1], osipkovmerritt_dfs_dehnencore_in_nfw[:1]): numpy.random.seed(10) samp = dfh.sample(n=300000) tol = 0.07 # rmin larger than usual to avoid low number sampling check_sigmar_against_jeans( samp, pot, tol, dens=lambda r: denspot.dens(r, 0), beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot[0]._scale / 3.0, rmax=pot[0]._scale * 10.0, bins=31, ) return None def test_osipkovmerritt_dehnencore_in_nfw_beta(): # Use list pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = [potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15)] ras = [2.3, 5.7] for ra, dfh in zip(ras[1:], osipkovmerritt_dfs_dehnencore_in_nfw[1:]): numpy.random.seed(10) samp = dfh.sample(n=300000) tol = 0.07 # rmin larger than usual to avoid low number sampling check_beta( samp, pot, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 3.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_osipkovmerritt_dehnencore_in_nfw_dens_directint(): # Use list for both pot = [potential.NFWPotential(amp=2.3, a=1.3)] denspot = [potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15)] ras = [2.3, 5.7] for ra, dfh in zip(ras[:1], osipkovmerritt_dfs_dehnencore_in_nfw[:1]): tol = 3e-4 check_dens_directint( dfh, pot, tol, lambda r: denspot[0].dens(r, 0), rmin=pot[0]._scale / 10.0, rmax=pot[0]._scale * 10.0, bins=3, ) return None def test_osipkovmerritt_dehnencore_in_nfw_meanvr_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras[1:], osipkovmerritt_dfs_dehnencore_in_nfw[1:]): tol = 1e-8 check_meanvr_directint( dfh, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=3 ) return None def test_osipkovmerritt_dehnencore_in_nfw_sigmar_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras[:1], osipkovmerritt_dfs_dehnencore_in_nfw[:1]): tol = 2e-4 check_sigmar_against_jeans_directint( dfh, pot, tol, dens=lambda r: denspot.dens(r, 0), beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=3, ) return None def test_osipkovmerritt_dehnencore_in_nfw_beta_directint(): pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras[1:], osipkovmerritt_dfs_dehnencore_in_nfw[1:]): tol = 1e-8 check_beta_directint( dfh, tol, beta=lambda r: 1.0 / (1.0 + ra**2.0 / r**2.0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=3, ) return None def test_osipkovmerritt_dehnencore_in_nfw_Qoutofbounds(): pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) ras = [2.3, 5.7] for ra, dfh in zip(ras, osipkovmerritt_dfs_dehnencore_in_nfw): assert numpy.all(numpy.fabs(dfh((numpy.arange(0.1, 10.0, 0.1), 1.1))) < 1e-8), ( "Evaluating the Osipkov-Merritt DF at E > 0 does not give zero" ) # The next one is not actually a physical orbit... assert numpy.all(numpy.fabs(dfh((pot(0, 0) - 1e-1, 0.1))) < 1e-8), ( "Evaluating the Osipkov-Merritt DF at E < -GM/a does not give zero" ) assert numpy.all(numpy.fabs(dfh((-1e-4, 1.1))) < 1e-8), ( "Evaluating the Osipkov-Merritt DF at Q < 0 does not give zero" ) return None ################################ CONSTANT-BETA DF ############################# # Test against the known analytical solution for Hernquist def test_constantbetadf_against_hernquist(): if WIN32: return None # skip on Windows, because no JAX pot = potential.HernquistPotential(amp=2.3, a=1.3) betas = [-1.7, -0.7, -0.5, -0.4, 0.0, 0.3, 0.52] for beta in betas: dfh = constantbetaHernquistdf(pot=pot, beta=beta) cdfh = constantbetadf(pot=pot, beta=beta, rmax=numpy.inf) # Check that both give the same answer for a given position R, vR, vT, z, vz, phi = 1.1, 0.3, 0.2, 0.9, -0.2, 2.4 assert ( numpy.fabs(dfh(R, vR, vT, z, vz, phi) - cdfh(R, vR, vT, z, vz, phi)) < 1e-5 ), ( "constantbetadf version of Hernquist does not agree with constantbetaHernquistdf" ) return None # For the following tests, we use a DehnenCoreSphericalPotential constantbeta_dfs_selfconsist = None # reuse in other tests @pytest.fixture(scope="module") def setup_constantbeta_dfs_selfconsist(): if WIN32: return None # skip on Windows, because no JAX pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) twobetas = [-1] constantbeta_dfs_selfconsist = [] for twobeta in twobetas: dfh = constantbetadf(pot=pot, twobeta=twobeta) constantbeta_dfs_selfconsist.append(dfh) return constantbeta_dfs_selfconsist def test_constantbeta_selfconsist_dehnencore_dens_spherically_symmetric( setup_constantbeta_dfs_selfconsist, ): if WIN32: return None # skip on Windows, because no JAX pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) twobetas = [-1] constantbeta_dfs_selfconsist = setup_constantbeta_dfs_selfconsist for twobeta, dfh in zip(twobetas, constantbeta_dfs_selfconsist): numpy.random.seed(10) samp = dfh.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_constantbeta_selfconsist_dehnencore_dens_massprofile( setup_constantbeta_dfs_selfconsist, ): if WIN32: return None # skip on Windows, because no JAX pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) twobetas = [-1] constantbeta_dfs_selfconsist = setup_constantbeta_dfs_selfconsist for twobeta, dfh in zip(twobetas, constantbeta_dfs_selfconsist): numpy.random.seed(10) samp = dfh.sample(n=100000) tol = 5 * 1e-3 check_spherical_massprofile( samp, lambda r: pot.mass(r) / pot.mass(numpy.amax(samp.r())), tol, skip=1000 ) return None def test_constantbeta_selfconsist_dehnencore_sigmar(setup_constantbeta_dfs_selfconsist): if WIN32: return None # skip on Windows, because no JAX pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) twobetas = [-1] constantbeta_dfs_selfconsist = setup_constantbeta_dfs_selfconsist for twobeta, dfh in zip(twobetas, constantbeta_dfs_selfconsist): numpy.random.seed(10) samp = dfh.sample(n=1000000) tol = 0.1 check_sigmar_against_jeans( samp, pot, tol, beta=twobeta / 2.0, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_constantbeta_selfconsist_dehnencore_beta(setup_constantbeta_dfs_selfconsist): if WIN32: return None # skip on Windows, because no JAX pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) twobetas = [-1] constantbeta_dfs_selfconsist = setup_constantbeta_dfs_selfconsist for twobeta, dfh in zip(twobetas, constantbeta_dfs_selfconsist): numpy.random.seed(10) samp = dfh.sample(n=1000000) tol = 0.1 # rmin larger than usual to avoid low number sampling check_beta( samp, pot, tol, beta=twobeta / 2.0, rmin=pot._scale / 3.0, rmax=pot._scale * 10.0, bins=31, ) return None def test_constantbeta_selfconsist_dehnencore_dens_directint( setup_constantbeta_dfs_selfconsist, ): if WIN32: return None # skip on Windows, because no JAX pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) twobetas = [-1] constantbeta_dfs_selfconsist = setup_constantbeta_dfs_selfconsist for twobeta, dfh in zip(twobetas, constantbeta_dfs_selfconsist): tol = 1e-4 check_dens_directint( dfh, pot, tol, lambda r: pot.dens(r, 0), rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=3, ) return None def test_constantbeta_selfconsist_dehnencore_meanvr_directint( setup_constantbeta_dfs_selfconsist, ): if WIN32: return None # skip on Windows, because no JAX pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) twobetas = [-1] constantbeta_dfs_selfconsist = setup_constantbeta_dfs_selfconsist for twobeta, dfh in zip(twobetas, constantbeta_dfs_selfconsist): tol = 1e-8 check_meanvr_directint( dfh, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=3 ) return None def test_constantbeta_selfconsist_dehnencore_sigmar_directint( setup_constantbeta_dfs_selfconsist, ): if WIN32: return None # skip on Windows, because no JAX pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) twobetas = [-1] constantbeta_dfs_selfconsist = setup_constantbeta_dfs_selfconsist for twobeta, dfh in zip(twobetas, constantbeta_dfs_selfconsist): tol = 1e-4 check_sigmar_against_jeans_directint( dfh, pot, tol, beta=twobeta / 2.0, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=3, ) return None # We don't do this test, because it is trivially satisfied by # any f(E,L) = L^(-2beta) f1(E) # def test_constantbeta_selfconsist_dehnencore_beta_directint(): # if WIN32: return None # skip on Windows, because no JAX # pot= potential.DehnenCoreSphericalPotential(amp=2.5,a=1.15) # twobetas= [-1] # for twobeta,dfh in zip(twobetas,constantbeta_dfs_selfconsist): # tol= 1e-8 # check_beta_directint(dfh,tol,beta=twobeta/2., # rmin=pot._scale/10., # rmax=pot._scale*10., # bins=3) # return None # Test that integrating the differential energy distribution dMdE over all energies equals the total mass def test_constantbeta_selfconsist_dehnencore_dMdE_integral( setup_constantbeta_dfs_selfconsist, ): if WIN32: return None # skip on Windows, because no JAX pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) twobetas = [-1] constantbeta_dfs_selfconsist = setup_constantbeta_dfs_selfconsist for twobeta, dfh in zip(twobetas, constantbeta_dfs_selfconsist): tol = 1e-10 check_dMdE_integral(dfh, tol) return None def test_constantbeta_selfconsist_dehnencore_Qoutofbounds( setup_constantbeta_dfs_selfconsist, ): if WIN32: return None # skip on Windows, because no JAX pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) twobetas = [-1] constantbeta_dfs_selfconsist = setup_constantbeta_dfs_selfconsist for twobeta, dfh in zip(twobetas, constantbeta_dfs_selfconsist): assert numpy.all(numpy.fabs(dfh((numpy.arange(0.1, 10.0, 0.1), 1.1))) < 1e-8), ( "Evaluating the constant-beta DF at E > 0 does not give zero" ) # The next one is not actually a physical orbit... assert numpy.all(numpy.fabs(dfh((pot(0, 0) - 1e-1, 0.1))) < 1e-8), ( "Evaluating the constant-beta DF at E < -GM/a does not give zero" ) assert numpy.all(numpy.fabs(dfh((-1e-4, 1.1))) < 1e-8), ( "Evaluating the constantbeta DF at Q < 0 does not give zero" ) return None # Also some tests with rmin in sampling def test_constantbeta_selfconsist_dehnencore_rmin_inbounds( setup_constantbeta_dfs_selfconsist, ): if WIN32: return None # skip on Windows, because no JAX pot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) twobetas = [-1] constantbeta_dfs_selfconsist = setup_constantbeta_dfs_selfconsist rmin = 0.5 for twobeta, dfh in zip(twobetas, constantbeta_dfs_selfconsist): samp = dfh.sample(n=1000000, rmin=rmin) assert numpy.min(samp.r()) >= rmin, "Sample minimum r less than rmin" # Change rmin samp = dfh.sample(n=1000000, rmin=rmin + 1.0) assert numpy.min(samp.r()) >= rmin + 1.0, "Sample minimum r less than rmin" return None # For the following tests, we use a DehnenCoreSphericalPotential embedded in # an NFW halo constantbeta_dfs_dehnencore_in_nfw = None # reuse in other tests def test_constantbeta_dehnencore_in_nfw_dens_spherically_symmetric(): if WIN32: return None # skip on Windows, because no JAX pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) betas = [0.25] global constantbeta_dfs_dehnencore_in_nfw constantbeta_dfs_dehnencore_in_nfw = [] for beta in betas: dfh = constantbetadf(pot=pot, denspot=denspot, beta=beta) constantbeta_dfs_dehnencore_in_nfw.append(dfh) numpy.random.seed(10) samp = dfh.sample(n=100000) # Check spherical symmetry for different harmonics l,m tol = 1e-2 check_spherical_symmetry(samp, 0, 0, tol) check_spherical_symmetry(samp, 1, 0, tol) check_spherical_symmetry(samp, 1, -1, tol) check_spherical_symmetry(samp, 1, 1, tol) check_spherical_symmetry(samp, 2, 0, tol) check_spherical_symmetry(samp, 2, -1, tol) check_spherical_symmetry(samp, 2, -2, tol) check_spherical_symmetry(samp, 2, 1, tol) check_spherical_symmetry(samp, 2, 2, tol) # and some higher order ones check_spherical_symmetry(samp, 3, 1, tol) check_spherical_symmetry(samp, 9, -6, tol) return None def test_constantbeta_dehnencore_in_nfw_dens_massprofile(): if WIN32: return None # skip on Windows, because no JAX pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) betas = [0.25] for beta, dfh in zip(betas, constantbeta_dfs_dehnencore_in_nfw): numpy.random.seed(10) samp = dfh.sample(n=100000) tol = 5 * 1e-3 check_spherical_massprofile( samp, lambda r: denspot.mass(r) / denspot.mass(numpy.amax(samp.r())), tol, skip=1000, ) return None def test_constantbeta_dehnencore_in_nfw_sigmar(): if WIN32: return None # skip on Windows, because no JAX # Use list pot = [potential.NFWPotential(amp=2.3, a=1.3)] denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) betas = [0.25] for beta, dfh in zip(betas, constantbeta_dfs_dehnencore_in_nfw): numpy.random.seed(10) samp = dfh.sample(n=1000000) tol = 0.07 # rmin larger than usual to avoid low number sampling check_sigmar_against_jeans( samp, pot, tol, dens=lambda r: denspot.dens(r, 0), beta=beta, rmin=pot[0]._scale / 3.0, rmax=pot[0]._scale * 10.0, bins=31, ) return None def test_constantbeta_dehnencore_in_nfw_beta(): if WIN32: return None # skip on Windows, because no JAX # Use list pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = [potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15)] betas = [0.25] for beta, dfh in zip(betas, constantbeta_dfs_dehnencore_in_nfw): numpy.random.seed(10) samp = dfh.sample(n=1000000) tol = 0.07 # rmin larger than usual to avoid low number sampling check_beta( samp, pot, tol, beta=beta, rmin=pot._scale / 3.0, rmax=pot._scale * 10.0, bins=31, ) return None # Here in this case so it gets run before fE is changed for directint tests def test_constantbeta_dehnencore_in_nfw_Qoutofbounds(): if WIN32: return None # skip on Windows, because no JAX pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) betas = [0.25] for beta, dfh in zip(betas, constantbeta_dfs_dehnencore_in_nfw): assert numpy.all(numpy.fabs(dfh((numpy.arange(0.1, 10.0, 0.1), 1.1))) < 1e-8), ( "Evaluating the constantbeta DF at E > 0 does not give zero" ) # The next one is not actually a physical orbit... assert numpy.all(numpy.fabs(dfh((pot(0, 0) - 1e-1, 0.1))) < 1e-8), ( "Evaluating the constantbeta DF at E < -GM/a does not give zero" ) assert numpy.all(numpy.fabs(dfh((-1e-4, 1.1))) < 1e-8), ( "Evaluating the constantbeta DF at Q < 0 does not give zero" ) return None def test_constantbeta_dehnencore_in_nfw_dens_directint(): if WIN32: return None # skip on Windows, because no JAX # Use list for both pot = [potential.NFWPotential(amp=2.3, a=1.3)] denspot = [potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15)] betas = [0.25] for beta, dfh in zip(betas, constantbeta_dfs_dehnencore_in_nfw): dfh.fE = lambda x: dfh._fE_interp(x) tol = 3e-4 check_dens_directint( dfh, pot, tol, lambda r: denspot[0].dens(r, 0), rmin=pot[0]._scale / 10.0, rmax=pot[0]._scale * 10.0, bins=3, ) return None def test_constantbeta_dehnencore_in_nfw_meanvr_directint(): if WIN32: return None # skip on Windows, because no JAX pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) betas = [0.25] for beta, dfh in zip(betas, constantbeta_dfs_dehnencore_in_nfw): dfh.fE = lambda x: dfh._fE_interp(x) tol = 1e-8 check_meanvr_directint( dfh, pot, tol, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=3 ) return None def test_constantbeta_dehnencore_in_nfw_sigmar_directint(): if WIN32: return None # skip on Windows, because no JAX pot = potential.NFWPotential(amp=2.3, a=1.3) denspot = potential.DehnenCoreSphericalPotential(amp=2.5, a=1.15) betas = [0.25] for beta, dfh in zip(betas, constantbeta_dfs_dehnencore_in_nfw): dfh.fE = lambda x: dfh._fE_interp(x) tol = 2e-4 check_sigmar_against_jeans_directint( dfh, pot, tol, dens=lambda r: denspot.dens(r, 0), beta=beta, rmin=pot._scale / 10.0, rmax=pot._scale * 10.0, bins=3, ) return None # def test_constantbeta_dehnencore_in_nfw_beta_directint(): # if WIN32: return None # skip on Windows, because no JAX # pot= potential.NFWPotential(amp=2.3,a=1.3) # denspot= potential.DehnenCoreSphericalPotential(amp=2.5,a=1.15) # betas= [0.25] # for beta,dfh in zip(betas,constantbeta_dfs_dehnencore_in_nfw): # dfh.fE= lambda x: dfh._fE_interp(x) # tol= 1e-8 # check_beta_directint(dfh,tol,beta=beta, # rmin=pot._scale/10., # rmax=pot._scale*10., # bins=3) # return None ############ FURTHER TESTS OF CONSTANTBETADF FOR DIFFERENT POTENTIALS########## # If you implement the required potential derivatives and force in JAX, # add your potential to the tests here; use a quick twobeta (odd int)! def test_constantbeta_differentpotentials_dens_directint(): if WIN32: return None # skip on Windows, because no JAX # Combinations of potentials and betas pots = [ potential.HernquistPotential(amp=2.3, a=1.3), potential.PowerSphericalPotential(amp=1.3, alpha=1.9), potential.PlummerPotential(amp=2.3, b=1.3), potential.PowerSphericalPotentialwCutoff(amp=1.3, alpha=1.9, rc=1.2), ] twobetas = [-1 for pot in pots] tols = [1e-3 for pot in pots] for pot, twobeta, tol in zip(pots, twobetas, tols): dfh = constantbetadf(pot=pot, twobeta=twobeta) check_dens_directint( dfh, pot, tol, lambda r: pot.dens(r, 0), rmin=pot._scale / 10.0 if hasattr(pot, "_scale") else 0.1, rmax=pot._scale * 10.0 if hasattr(pot, "_scale") else 10.0, bins=11, ) return None ################# INTERPOLATED POTENTIALS IN DFS ############################## # Eddington DFs with interpolated potentials def test_eddington_interpolatedpotentials_dens_directint(): # Some potentials pots = [ potential.HernquistPotential(amp=1.3, a=0.8), ] tols = [1e-3 for pot in pots] rmins = [0.1] for pot, tol, rmin in zip(pots, tols, rmins): ipot = potential.interpSphericalPotential( rforce=pot, rgrid=numpy.geomspace(0.001, 100.0, 10001) ) # Make sure to use the actual galpy potential for denspot dfh = eddingtondf(pot=ipot, denspot=pot) check_dens_directint( dfh, pot, tol, lambda r: pot.dens(r, 0), rmin=rmin, rmax=10.0, bins=5 ) return None def test_eddington_interpolatedpotentials_meanvr_directint(): # Some potentials pots = [potential.PlummerPotential(amp=2.3, b=1.3)] tols = [1e-3 for pot in pots] rmins = [0.2] for pot, tol, rmin in zip(pots, tols, rmins): ipot = potential.interpSphericalPotential( rforce=pot, rgrid=numpy.geomspace(0.001, 100.0, 10001) ) # Make sure to use the actual galpy potential for denspot dfh = eddingtondf(pot=ipot, denspot=pot) check_meanvr_directint(dfh, pot, tol, rmin=rmin, rmax=10.0, bins=5) return None def test_eddington_interpolatedpotentials_sigmar(): # Some potentials, make sure to use something stable for denspot denspot = potential.HernquistPotential(amp=1.3, a=0.8) pots = [potential.NFWPotential(amp=1.3, a=1.5)] tols = [5e-2 for pot in pots] rmins = [0.2] for pot, tol, rmin in zip(pots, tols, rmins): ipot = potential.interpSphericalPotential( rforce=pot, rgrid=numpy.geomspace(0.001, 100.0, 10001) ) # Make sure to use the actual galpy potential for denspot dfh = eddingtondf(pot=ipot, denspot=denspot) numpy.random.seed(10) samp = dfh.sample(n=1000000) # rmin larger than usual to avoid low number sampling check_sigmar_against_jeans( samp, pot, tol, dens=lambda r: denspot.dens(r, 0), rmin=rmin, rmax=10.0, bins=31, ) return None # Constant beta DFs with interpolated potentials def test_constantbeta_interpolatedpotentials_dens_directint(): if WIN32: return None # skip on Windows, because no JAX # Combinations of potentials and betas pots = [potential.HernquistPotential(amp=1.3, a=0.8)] twobetas = [-1, 1] tols = [1e-2 for pot in pots] rmins = [0.1] # Also test interpolated spherical potentials as source potential for pot, tol, rmin in zip(pots, tols, rmins): # Important for rgrid to extend far beyond the test range ipot = potential.interpSphericalPotential( rforce=pot, rgrid=numpy.geomspace(0.001, 100.0, 10001) ) for twobeta in twobetas: # Make sure to use the actual galpy potential for denspot dfh = constantbetadf(pot=ipot, denspot=pot, twobeta=twobeta) check_dens_directint( dfh, pot, tol, lambda r: pot.dens(r, 0), rmin=rmin, rmax=10.0, bins=5 ) return None def test_constantbeta_interpolatedpotentials_beta(): if WIN32: return None # skip on Windows, because no JAX # Combinations of potentials and betas, make sure to use something stable for denspot denspot = potential.HernquistPotential(amp=1.3, a=0.8) pots = [potential.NFWPotential(amp=1.3, a=1.5)] twobetas = [-1, 1] tols = [5e-2 for pot in pots] rmins = [0.2] for pot, tol, rmin in zip(pots, tols, rmins): ipot = potential.interpSphericalPotential( rforce=pot, rgrid=numpy.geomspace(0.001, 100.0, 10001) ) for twobeta in twobetas: # Make sure to use the actual galpy potential for denspot dfh = constantbetadf(pot=ipot, denspot=denspot, twobeta=twobeta) numpy.random.seed(10) samp = dfh.sample(n=2000000) check_beta(samp, pot, tol, beta=twobeta / 2, rmin=rmin, rmax=10.0, bins=31) return None # Test errors and warnings are raised correctly when using interpSphericalPotential def test_constantbeta_interpolatedpotentials_beta_lt_neg05(): if WIN32: return None # skip on Windows, because no JAX pot = potential.HernquistPotential(amp=1.3, a=0.8) ipot = potential.interpSphericalPotential(rforce=pot) with pytest.raises(RuntimeError) as excinfo: dfh = constantbetadf(pot=ipot, denspot=pot, twobeta=-2) assert ( str(excinfo.value) == "constantbetadf with beta < -0.5 is not supported for use with interpSphericalPotential." ), ( "Error message when beta < -0.5 while using interpSphericalPotential is incorrect" ) def test_eddington_interpolatedpotentials_rmin(): pot = potential.HernquistPotential(amp=1.3, a=0.8) rmin = 0.2 ipot = potential.interpSphericalPotential( rforce=pot, rgrid=numpy.geomspace(rmin, 100.0, 10001) ) dfh = eddingtondf(pot=ipot, denspot=pot, rmax=10.0) with pytest.warns(galpyWarning) as record: samp = dfh.sample(n=100) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "Interpolated potential grid rmin is larger than the rmin to be used for the v_vesc_interpolator grid. This may adversely affect the generated samples. Proceed with care!" ) assert raisedWarning, ( "Using an interpolated potential with rmin smaller than the rmin to be used for the v_vesc_interpolator grid should have raised a warning, but didn't" ) def test_eddington_interpolatedpotentials_rmax(): pot = potential.HernquistPotential(amp=1.3, a=0.8) rmax = 10.0 ipot = potential.interpSphericalPotential( rforce=pot, rgrid=numpy.geomspace(0.001, rmax, 10001) ) with pytest.warns(galpyWarning) as record: dfh = eddingtondf(pot=ipot, denspot=pot) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "The interpolated potential's rmax is smaller than the DF's rmax" ) assert raisedWarning, ( "Using an interpolated potential with rmax smaller than the DF's rmax should have raised a warning, but didn't" ) ########################### TESTS OF ERRORS AND WARNINGS####################### def test_isotropic_hernquist_nopot(): with pytest.raises(AssertionError) as excinfo: dfh = isotropicHernquistdf() assert str(excinfo.value) == "pot= must be potential.HernquistPotential", ( "Error message when not supplying the potential is incorrect" ) return None def test_isotropic_hernquist_wrongpot(): pot = potential.JaffePotential(amp=2.0, a=1.3) with pytest.raises(AssertionError) as excinfo: dfh = isotropicHernquistdf(pot=pot) assert str(excinfo.value) == "pot= must be potential.HernquistPotential", ( "Error message when not supplying the potential is incorrect" ) return None def test_anisotropic_hernquist_nopot(): with pytest.raises(AssertionError) as excinfo: dfh = constantbetaHernquistdf() assert str(excinfo.value) == "pot= must be potential.HernquistPotential", ( "Error message when not supplying the potential is incorrect" ) return None def test_anisotropic_hernquist_wrongpot(): pot = potential.JaffePotential(amp=2.0, a=1.3) with pytest.raises(AssertionError) as excinfo: dfh = constantbetaHernquistdf(pot=pot) assert str(excinfo.value) == "pot= must be potential.HernquistPotential", ( "Error message when not supplying the potential is incorrect" ) return None def test_anisotropic_hernquist_negdf(): pot = potential.HernquistPotential(amp=2.3, a=1.3) # beta > 0.5 has negative DF parts dfh = constantbetaHernquistdf(pot=pot, beta=0.7) with pytest.warns(galpyWarning) as record: samp = dfh.sample(n=100) raisedWarning = False for rec in record: # check that the message matches raisedWarning += ( str(rec.message.args[0]) == "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!" ) assert raisedWarning, ( "Using an anisotropic Hernquist DF that has negative parts should have raised a warning, but didn't" ) ############################# TESTS OF UNIT HANDLING########################### # Test that setting up a DF with unit conversion parameters that are # incompatible with that of the underlying potential fails def test_isotropic_hernquist_incompatibleunits(): pot = potential.HernquistPotential(amp=2.0, a=1.3, ro=9.0, vo=210.0) with pytest.raises(RuntimeError): dfh = isotropicHernquistdf(pot=pot, ro=8.0, vo=210.0) with pytest.raises(RuntimeError): dfh = isotropicHernquistdf(pot=pot, ro=9.0, vo=230.0) with pytest.raises(RuntimeError): dfh = isotropicHernquistdf(pot=pot, ro=8.0, vo=230.0) return None # Test that setting up a DF with unit conversion parameters that are # incompatible between the potential and the density fails def test_eddington_pot_denspot_incompatibleunits(): pot = potential.HernquistPotential(amp=2.0, a=1.3, ro=9.0, vo=210.0) denspot = potential.NFWPotential(amp=2.0, a=1.3, ro=8.0, vo=200.0) with pytest.raises(RuntimeError): denspot = potential.NFWPotential(amp=2.0, a=1.3, ro=8.0, vo=210.0) dfh = eddingtondf(pot=pot, denspot=denspot) with pytest.raises(RuntimeError): denspot = potential.NFWPotential(amp=2.0, a=1.3, ro=9.0, vo=230.0) dfh = eddingtondf(pot=pot, denspot=denspot) with pytest.raises(RuntimeError): denspot = potential.NFWPotential(amp=2.0, a=1.3, ro=8.0, vo=230.0) dfh = eddingtondf(pot=pot, denspot=denspot) return None # Test that the unit system is correctly transferred def test_isotropic_hernquist_unittransfer(): from galpy.util import conversion ro, vo = 9.0, 210.0 pot = potential.HernquistPotential(amp=2.0, a=1.3, ro=ro, vo=vo) dfh = isotropicHernquistdf(pot=pot) phys = conversion.get_physical(dfh, include_set=True) assert phys["roSet"], ( "sphericaldf's ro not set when that of the underlying potential is set" ) assert phys["voSet"], ( "sphericaldf's vo not set when that of the underlying potential is set" ) assert numpy.fabs(phys["ro"] - ro) < 1e-8, ( "Potential's unit system not correctly transferred to sphericaldf's" ) assert numpy.fabs(phys["vo"] - vo) < 1e-8, ( "Potential's unit system not correctly transferred to sphericaldf's" ) # Following should not be on pot = potential.HernquistPotential(amp=2.0, a=1.3) dfh = isotropicHernquistdf(pot=pot) phys = conversion.get_physical(dfh, include_set=True) assert not phys["roSet"], ( "sphericaldf's ro set when that of the underlying potential is not set" ) assert not phys["voSet"], ( "sphericaldf's vo set when that of the underlying potential is not set" ) return None # Test that output orbits from sampling correctly have units on or off def test_isotropic_hernquist_unitsofsamples(): from galpy.util import conversion ro, vo = 9.0, 210.0 pot = potential.HernquistPotential(amp=2.0, a=1.3, ro=ro, vo=vo) dfh = isotropicHernquistdf(pot=pot) samp = dfh.sample(n=100) assert conversion.get_physical(samp, include_set=True)["roSet"], ( "Orbit samples from spherical DF with units on do not have units on" ) assert conversion.get_physical(samp, include_set=True)["voSet"], ( "Orbit samples from spherical DF with units on do not have units on" ) assert ( numpy.fabs(conversion.get_physical(samp, include_set=True)["ro"] - ro) < 1e-8 ), "Orbit samples from spherical DF with units on do not have correct ro" assert ( numpy.fabs(conversion.get_physical(samp, include_set=True)["vo"] - vo) < 1e-8 ), "Orbit samples from spherical DF with units on do not have correct vo" # Also test a case where they should be off pot = potential.HernquistPotential(amp=2.0, a=1.3) dfh = isotropicHernquistdf(pot=pot) samp = dfh.sample(n=100) assert not conversion.get_physical(samp, include_set=True)["roSet"], ( "Orbit samples from spherical DF with units off do not have units off" ) assert not conversion.get_physical(samp, include_set=True)["voSet"], ( "Orbit samples from spherical DF with units off do not have units off" ) return None ############################### HELPER FUNCTIONS ############################## def check_spherical_symmetry(samp, l, m, tol): """Check for spherical symmetry by Monte Carlo integration of the spherical harmonic Y_lm over the sample, should be zero unless l=m=0""" thetas, phis = numpy.arctan2(samp.R(), samp.z()), samp.phi() assert ( numpy.fabs( numpy.sum(special.lpmv(m, l, numpy.cos(thetas)) * numpy.cos(m * phis)) / samp.size - (l == 0) * (m == 0) ) < tol ), ( f"Sample does not appear to be spherically symmetric, fails spherical harmonics test for (l,m) = ({l},{m})" ) return None def check_azimuthal_symmetry(samp, m, tol): """Check for spherical symmetry by Monte Carlo integration of the spherical harmonic Y_lm over the sample, should be zero unless l=m=0""" thetas, phis = numpy.arctan2(samp.R(), samp.z()), samp.phi() assert numpy.fabs(numpy.sum(numpy.cos(m * phis)) / samp.size - (m == 0)) < tol, ( f"Sample does not appear to be azimuthally symmetric, fails Fourier test for m = {m}" ) return None def check_spherical_massprofile(samp, mass_profile, tol, skip=100): """Check that the cumulative distribution of radii follows the cumulative mass profile (normalized such that total mass = 1)""" rs = samp.r() cumul_rs = numpy.sort(rs) cumul_mass = numpy.linspace(0.0, 1.0, len(rs)) for ii in range(len(rs) // skip - 1): indx = (ii + 1) * skip assert numpy.fabs(cumul_mass[indx] - mass_profile(cumul_rs[indx])) < tol, ( "Mass profile of samples does not agree with analytical one" ) return None def check_sigmar_against_jeans( samp, pot, tol, beta=0.0, dens=None, rmin=None, rmax=None, bins=31 ): """Check that sigma_r(r) obtained from a sampling agrees with that coming from the Jeans equation Does this by logarithmically binning in r between rmin and rmax""" vrs = (samp.vR() * samp.R() + samp.vz() * samp.z()) / samp.r() logrs = numpy.log(samp.r()) if rmin is None: rmin = numpy.exp(numpy.amin(logrs)) if rmax is None: rmax = numpy.exp(numpy.amax(logrs)) w, e = numpy.histogram( logrs, range=[numpy.log(rmin), numpy.log(rmax)], bins=bins, weights=numpy.ones_like(logrs), ) mv2, _ = numpy.histogram( logrs, range=[numpy.log(rmin), numpy.log(rmax)], bins=bins, weights=vrs**2.0 ) samp_sigr = numpy.sqrt(mv2 / w) brs = numpy.exp((numpy.roll(e, -1) + e)[:-1] / 2.0) for ii, br in enumerate(brs): assert ( numpy.fabs( samp_sigr[ii] / jeans.sigmar(pot, br, beta=beta, dens=dens) - 1.0 ) < tol ), ( "sigma_r(r) from samples does not agree with that obtained from the Jeans equation" ) return None def check_beta(samp, pot, tol, beta=0.0, rmin=None, rmax=None, bins=31): """Check that beta(r) obtained from a sampling agrees with the expected value Does this by logarithmically binning in r between rmin and rmax""" vrs = (samp.vR() * samp.R() + samp.vz() * samp.z()) / samp.r() vthetas = (samp.z() * samp.vR() - samp.R() * samp.vz()) / samp.r() vphis = samp.vT() logrs = numpy.log(samp.r()) if rmin is None: rmin = numpy.exp(numpy.amin(logrs)) if rmax is None: rmax = numpy.exp(numpy.amax(logrs)) w, e = numpy.histogram( logrs, range=[numpy.log(rmin), numpy.log(rmax)], bins=bins, weights=numpy.ones_like(logrs), ) mvr2, _ = numpy.histogram( logrs, range=[numpy.log(rmin), numpy.log(rmax)], bins=bins, weights=vrs**2.0 ) mvt2, _ = numpy.histogram( logrs, range=[numpy.log(rmin), numpy.log(rmax)], bins=bins, weights=vthetas**2.0, ) mvp2, _ = numpy.histogram( logrs, range=[numpy.log(rmin), numpy.log(rmax)], bins=bins, weights=vphis**2.0 ) samp_sigr = numpy.sqrt(mvr2 / w) samp_sigt = numpy.sqrt(mvt2 / w) samp_sigp = numpy.sqrt(mvp2 / w) samp_beta = 1.0 - (samp_sigt**2.0 + samp_sigp**2.0) / 2.0 / samp_sigr**2.0 brs = numpy.exp((numpy.roll(e, -1) + e)[:-1] / 2.0) if not callable(beta): beta_func = lambda r: beta else: beta_func = beta assert numpy.all(numpy.fabs(samp_beta - beta_func(brs)) < tol), ( f"beta(r) from samples does not agree with the expected value for beta = {beta}" ) return None def check_dens_directint(dfi, pot, tol, dens, rmin=None, rmax=None, bins=31): """Check that the density obtained from integrating over the DF agrees with the expected density""" rs = numpy.linspace(rmin, rmax, bins) intdens = numpy.array([dfi.vmomentdensity(r, 0, 0) for r in rs]) expdens = numpy.array([dens(r) for r in rs]) assert numpy.all(numpy.fabs(intdens / expdens - 1.0) < tol), ( "Density from direct integration is not equal to the expected value" ) return None def check_meanvr_directint(dfi, pot, tol, beta=0.0, rmin=None, rmax=None, bins=31): """Check that the mean v_r(r) obtained from integrating over the DF agrees with the expected zero""" rs = numpy.linspace(rmin, rmax, bins) intmvr = numpy.array( [dfi.vmomentdensity(r, 1, 0) / dfi.vmomentdensity(r, 0, 0) for r in rs] ) assert numpy.all(numpy.fabs(intmvr) < tol), ( "mean v_r(r) from direct integration is not zero" ) return None def check_sigmar_against_jeans_directint( dfi, pot, tol, beta=0.0, dens=None, rmin=None, rmax=None, bins=31 ): """Check that sigma_r(r) obtained from integrating over the DF agrees with that coming from the Jeans equation""" rs = numpy.linspace(rmin, rmax, bins) intsr = numpy.array([dfi.sigmar(r, use_physical=False) for r in rs]) jeanssr = numpy.array( [jeans.sigmar(pot, r, beta=beta, dens=dens, use_physical=False) for r in rs] ) assert numpy.all(numpy.fabs(intsr / jeanssr - 1) < tol), ( "sigma_r(r) from direct integration does not agree with that obtained from the Jeans equation" ) return None def check_sigmar_against_jeans_directint_forcevmoment( dfi, pot, tol, beta=0.0, rmin=None, rmax=None, bins=31 ): """Check that sigma_r(r) obtained from integrating over the DF agrees with that coming from the Jeans equation, using the general sphericaldf class' vmomentdensity""" from galpy.df.sphericaldf import sphericaldf rs = numpy.linspace(rmin, rmax, bins) intsr = numpy.array( [ numpy.sqrt( sphericaldf._vmomentdensity(dfi, r, 2, 0) / sphericaldf._vmomentdensity(dfi, r, 0, 0) ) for r in rs ] ) jeanssr = numpy.array( [jeans.sigmar(pot, r, beta=beta, use_physical=False) for r in rs] ) assert numpy.all(numpy.fabs(intsr / jeanssr - 1) < tol), ( "sigma_r(r) from direct integration does not agree with that obtained from the Jeans equation" ) return None def check_beta_directint(dfi, tol, beta=0.0, rmin=None, rmax=None, bins=31): """Check that beta(r) obtained from integrating over the DF agrees with the expected behavior""" rs = numpy.linspace(rmin, rmax, bins) intbeta = numpy.array([dfi.beta(r) for r in rs]) if not callable(beta): beta_func = lambda r: beta else: beta_func = beta assert numpy.all(numpy.fabs(intbeta - beta_func(rs)) < tol), ( "beta(r) from direct integration does not agree with the expected value" ) return None def check_dMdE_integral(dfi, tol, Emax=None): # Check that the integral of dMdE over all energies equals the total mass total_mass = dfi._pot.mass(numpy.inf) # Integrate dMdE over all energies Emin = dfi._pot(0.0, 0.0) Emax = dfi._pot(numpy.inf, 0) if Emax is None else Emax assert ( numpy.fabs(integrate.quad(lambda E: dfi.dMdE(E), Emin, Emax)[0] - total_mass) < tol ), ( f"Integral of dMdE over all energies does not equal total mass for potential {dfi._pot.__class__.__name__}" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_streamdf.py0000644000175100001660000033114614761352023016705 0ustar00runnerdockerimport platform WIN32 = platform.system() == "Windows" import numpy import pytest from scipy import integrate, interpolate from galpy.util import coords # Exact setup from Bovy (2014); should reproduce those results (which have been # sanity checked @pytest.fixture(scope="module") def bovy14_setup(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s # For custom_transform theta, dec_ngp, ra_ngp = coords.get_epoch_angles(2000.0) T = numpy.dot( numpy.array( [ [numpy.cos(ra_ngp), -numpy.sin(ra_ngp), 0.0], [numpy.sin(ra_ngp), numpy.cos(ra_ngp), 0.0], [0.0, 0.0, 1.0], ] ), numpy.dot( numpy.array( [ [-numpy.sin(dec_ngp), 0.0, numpy.cos(dec_ngp)], [0.0, 1.0, 0.0], [numpy.cos(dec_ngp), 0.0, numpy.sin(dec_ngp)], ] ), numpy.array( [ [numpy.cos(theta), numpy.sin(theta), 0.0], [numpy.sin(theta), -numpy.cos(theta), 0.0], [0.0, 0.0, 1.0], ] ), ), ).T sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), custom_transform=T, ) return sdf_bovy14 # Trailing setup @pytest.fixture(scope="module") def bovy14_trailing_setup(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) lp_false = LogarithmicHaloPotential(normalize=1.0, q=0.8) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) # This is the trailing of the stream that is going the opposite direction obs = Orbit( [1.56148083, -0.35081535, 1.15481504, 0.88719443, 0.47713334, 0.12019596] ) sigv = 0.365 # km/s sdft_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, multi=True, # test multi leading=False, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), nTrackIterations=0, sigangle=0.657, ) return sdft_bovy14 def test_progenitor_coordtransformparams(): # Test related to #189: test that the streamdf setup throws a warning when the given coordinate transformation parameters differ from those of the given progenitor orbit from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions from galpy.util import galpyWarning lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) # odeint to make sure that the C integration warning isn't thrown aAI = actionAngleIsochroneApprox(pot=lp, b=0.8, integrate_method="odeint") obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596], ro=8.5, vo=235.0, zo=0.1, solarmotion=[0.0, -10.0, 0.0], ) sigv = 0.365 # km/s # Turn warnings into errors to test for them import warnings with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) # Test w/ diff Rnorm sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), nosetup=True, # won't look at track Rnorm=10.0, ) # Should raise warning bc of Rnorm, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "Warning: progenitor's ro does not agree with streamdf's ro and R0; this may have unexpected consequences when projecting into observables" ) if raisedWarning: break assert raisedWarning, ( "streamdf setup does not raise warning when progenitor's ro is different from ro" ) # Test w/ diff R0 with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), nosetup=True, # won't look at track R0=10.0, ) # Should raise warning bc of R0, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "Warning: progenitor's ro does not agree with streamdf's ro and R0; this may have unexpected consequences when projecting into observables" ) if raisedWarning: break assert raisedWarning, ( "streamdf setup does not raise warning when progenitor's ro is different from R0" ) # Test w/ diff Vnorm with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), nosetup=True, # won't look at track Rnorm=8.5, R0=8.5, Vnorm=220.0, ) # Should raise warning bc of Vnorm, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "Warning: progenitor's vo does not agree with streamdf's vo; this may have unexpected consequences when projecting into observables" ) if raisedWarning: break assert raisedWarning, ( "streamdf setup does not raise warning when progenitor's vo is different from vo" ) # Test w/ diff zo with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), nosetup=True, # won't look at track Rnorm=8.5, R0=8.5, Vnorm=235.0, Zsun=0.025, ) # Should raise warning bc of zo, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "Warning: progenitor's zo does not agree with streamdf's Zsun; this may have unexpected consequences when projecting into observables" ) if raisedWarning: break assert raisedWarning, ( "streamdf setup does not raise warning when progenitor's zo is different from Zsun" ) # Test w/ diff vsun with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), nosetup=True, # won't look at track Rnorm=8.5, R0=8.5, Vnorm=235.0, Zsun=0.1, vsun=[0.0, 220.0, 0.0], ) # Should raise warning bc of vsun, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "Warning: progenitor's solarmotion does not agree with streamdf's vsun (after accounting for vo); this may have unexpected consequences when projecting into observables" ) if raisedWarning: break assert raisedWarning, ( "streamdf setup does not raise warning when progenitor's solarmotion is different from vsun" ) return None # Exact setup from Bovy (2014); should reproduce those results (which have been # sanity checked def test_bovy14_setup(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup assert not sdf_bovy14 is None, "bovy14 streamdf setup did not work" return None def test_bovy14_freqratio(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test the frequency ratio assert (sdf_bovy14.freqEigvalRatio() - 30.0) ** 2.0 < 10.0**0.0, ( "streamdf model from Bovy (2014) does not give a frequency ratio of about 30" ) assert (sdf_bovy14.freqEigvalRatio(isotropic=True) - 34.0) ** 2.0 < 10.0**0.0, ( "streamdf model from Bovy (2014) does not give an isotropic frequency ratio of about 34" ) return None def test_bovy14_misalignment(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test the misalignment assert (sdf_bovy14.misalignment() / numpy.pi * 180.0 + 0.5) ** 2.0 < 10.0**-2.0, ( "streamdf model from Bovy (2014) does not give a misalighment of about -0.5 degree" ) assert ( sdf_bovy14.misalignment(isotropic=True) / numpy.pi * 180.0 - 1.3 ) ** 2.0 < 10.0**-2.0, ( "streamdf model from Bovy (2014) does not give an isotropic misalighment of about 1.3 degree" ) return None def test_bovy14_track_prog_diff(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the stream and the progenitor are close together, for both leading and trailing check_track_prog_diff(sdf_bovy14, "R", "Z", 0.1) check_track_prog_diff(sdf_bovy14, "R", "Z", 0.8, phys=True) # do 1 with phys check_track_prog_diff(sdf_bovy14, "R", "X", 0.1) check_track_prog_diff(sdf_bovy14, "R", "Y", 0.1) check_track_prog_diff(sdf_bovy14, "R", "vZ", 0.03) check_track_prog_diff(sdf_bovy14, "R", "vZ", 6.6, phys=True) # do 1 with phys check_track_prog_diff(sdf_bovy14, "R", "vX", 0.05) check_track_prog_diff(sdf_bovy14, "R", "vY", 0.05) check_track_prog_diff(sdf_bovy14, "R", "vT", 0.05) check_track_prog_diff(sdf_bovy14, "R", "vR", 0.05) check_track_prog_diff(sdf_bovy14, "ll", "bb", 0.3) check_track_prog_diff(sdf_bovy14, "ll", "dist", 0.5) check_track_prog_diff(sdf_bovy14, "ll", "vlos", 4.0) check_track_prog_diff(sdf_bovy14, "ll", "pmll", 0.3) check_track_prog_diff(sdf_bovy14, "ll", "pmbb", 0.25) return None def test_bovy14_track_spread(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the spreads are small check_track_spread(sdf_bovy14, "R", "Z", 0.01, 0.005) check_track_spread(sdf_bovy14, "R", "Z", 0.08, 0.04, phys=True) # do 1 with phys check_track_spread( sdf_bovy14, "R", "Z", 0.01, 0.005, interp=False ) # do 1 with interp check_track_spread(sdf_bovy14, "X", "Y", 0.01, 0.005) check_track_spread(sdf_bovy14, "X", "Y", 0.08, 0.04, phys=True) # do 1 with phys check_track_spread(sdf_bovy14, "R", "phi", 0.01, 0.005) check_track_spread(sdf_bovy14, "vR", "vT", 0.005, 0.005) check_track_spread(sdf_bovy14, "vR", "vT", 1.1, 1.1, phys=True) # do 1 with phys check_track_spread(sdf_bovy14, "vR", "vZ", 0.005, 0.005) check_track_spread(sdf_bovy14, "vX", "vY", 0.005, 0.005) delattr(sdf_bovy14, "_allErrCovs") # to test that this is re-generated check_track_spread(sdf_bovy14, "ll", "bb", 0.5, 0.5) check_track_spread(sdf_bovy14, "dist", "vlos", 0.5, 5.0) check_track_spread(sdf_bovy14, "pmll", "pmbb", 0.5, 0.5) # These should all exist, so return None assert sdf_bovy14._interpolate_stream_track() is None, ( "_interpolate_stream_track does not return None, even though it should be set up" ) assert sdf_bovy14._interpolate_stream_track_aA() is None, ( "_interpolate_stream_track_aA does not return None, even though it should be set up" ) delattr(sdf_bovy14, "_interpolatedObsTrackAA") delattr(sdf_bovy14, "_interpolatedThetasTrack") # Re-build assert sdf_bovy14._interpolate_stream_track_aA() is None, ( "Re-building interpolated AA track does not return None" ) return None def test_closest_trackpoint(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Check that we can find the closest trackpoint properly check_closest_trackpoint(sdf_bovy14, 50) check_closest_trackpoint(sdf_bovy14, 230, usev=True) check_closest_trackpoint(sdf_bovy14, 330, usev=True, xy=False) check_closest_trackpoint(sdf_bovy14, 40, xy=False) check_closest_trackpoint(sdf_bovy14, 4, interp=False) check_closest_trackpoint(sdf_bovy14, 6, interp=False, usev=True, xy=False) return None def test_closest_trackpointLB(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Check that we can find the closest trackpoint properly in LB check_closest_trackpointLB(sdf_bovy14, 50) check_closest_trackpointLB(sdf_bovy14, 230, usev=True) check_closest_trackpointLB(sdf_bovy14, 4, interp=False) check_closest_trackpointLB(sdf_bovy14, 8, interp=False, usev=True) check_closest_trackpointLB(sdf_bovy14, -1, interp=False, usev=False) check_closest_trackpointLB(sdf_bovy14, -2, interp=False, usev=True) check_closest_trackpointLB(sdf_bovy14, -3, interp=False, usev=True) return None def test_closest_trackpointaA(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Check that we can find the closest trackpoint properly in AA check_closest_trackpointaA(sdf_bovy14, 50) check_closest_trackpointaA(sdf_bovy14, 4, interp=False) return None def test_pOparapar(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that integrating pOparapar gives density_par dens_frompOpar_close = integrate.quad( lambda x: sdf_bovy14.pOparapar(x, 0.1), sdf_bovy14._meandO - 4.0 * numpy.sqrt(sdf_bovy14._sortedSigOEig[2]), sdf_bovy14._meandO + 4.0 * numpy.sqrt(sdf_bovy14._sortedSigOEig[2]), )[0] dens_fromOpar_half = integrate.quad( lambda x: sdf_bovy14.pOparapar(x, 1.1), sdf_bovy14._meandO - 4.0 * numpy.sqrt(sdf_bovy14._sortedSigOEig[2]), sdf_bovy14._meandO + 4.0 * numpy.sqrt(sdf_bovy14._sortedSigOEig[2]), )[0] assert ( numpy.fabs( dens_fromOpar_half / dens_frompOpar_close - sdf_bovy14.density_par(1.1) ) < 10.0**-4.0 ), ( "density from integrating pOparapar not equal to that from density_par for Bovy14 stream" ) return None def test_density_par_valueerror(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the code throws a ValueError if coord is not understood with pytest.raises(ValueError) as excinfo: sdf_bovy14.density_par(0.1, coord="xi") return None def test_density_par(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the density is close to 1 close to the progenitor and close to zero far from the progenitor assert numpy.fabs(sdf_bovy14.density_par(0.1) - 1.0) < 10.0**-2.0, ( "density near progenitor not close to 1 for Bovy14 stream" ) assert numpy.fabs(sdf_bovy14.density_par(0.5) - 1.0) < 10.0**-2.0, ( "density near progenitor not close to 1 for Bovy14 stream" ) assert numpy.fabs(sdf_bovy14.density_par(1.8) - 0.0) < 10.0**-2.0, ( "density far progenitor not close to 0 for Bovy14 stream" ) return None def test_density_phi(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the density in phi is correctly computed, by doing this by hand def dens_phi(apar): dapar = 10.0**-9.0 X, Y, Z = ( sdf_bovy14._interpTrackX(apar), sdf_bovy14._interpTrackY(apar), sdf_bovy14._interpTrackZ(apar), ) R, phi, z = coords.rect_to_cyl(X, Y, Z) dX, dY, dZ = ( sdf_bovy14._interpTrackX(apar + dapar), sdf_bovy14._interpTrackY(apar + dapar), sdf_bovy14._interpTrackZ(apar + dapar), ) dR, dphi, dz = coords.rect_to_cyl(dX, dY, dZ) jac = numpy.fabs((dphi - phi) / dapar) return sdf_bovy14.density_par(apar) / jac apar = 0.1 assert ( numpy.fabs(dens_phi(apar) / sdf_bovy14.density_par(apar, coord="phi") - 1.0) < 10.0**-2.0 ), "density near progenitor in phi is incorrect" apar = 0.5 assert ( numpy.fabs(dens_phi(apar) / sdf_bovy14.density_par(apar, coord="phi") - 1.0) < 10.0**-2.0 ), "density near progenitor in phi is incorrect" apar = 1.8 assert ( numpy.fabs(dens_phi(apar) / sdf_bovy14.density_par(apar, coord="phi") - 1.0) < 10.0**-2.0 ), "density far from progenitor in phi is incorrect" return None def test_density_ll_and_customra(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the density in ll is correctly computed, by doing this by hand # custom should be the same for this setup (see above) def dens_ll(apar): dapar = 10.0**-9.0 X, Y, Z = ( sdf_bovy14._interpTrackX(apar) * sdf_bovy14._ro, sdf_bovy14._interpTrackY(apar) * sdf_bovy14._ro, sdf_bovy14._interpTrackZ(apar) * sdf_bovy14._ro, ) X, Y, Z = coords.galcenrect_to_XYZ( X, Y, Z, Xsun=sdf_bovy14._R0, Zsun=sdf_bovy14._Zsun ) l, b, d = coords.XYZ_to_lbd(X, Y, Z, degree=True) dX, dY, dZ = ( sdf_bovy14._interpTrackX(apar + dapar) * sdf_bovy14._ro, sdf_bovy14._interpTrackY(apar + dapar) * sdf_bovy14._ro, sdf_bovy14._interpTrackZ(apar + dapar) * sdf_bovy14._ro, ) dX, dY, dZ = coords.galcenrect_to_XYZ( dX, dY, dZ, Xsun=sdf_bovy14._R0, Zsun=sdf_bovy14._Zsun ) dl, db, dd = coords.XYZ_to_lbd(dX, dY, dZ, degree=True) jac = numpy.fabs((dl - l) / dapar) return sdf_bovy14.density_par(apar) / jac apar = 0.1 assert ( numpy.fabs(dens_ll(apar) / sdf_bovy14.density_par(apar, coord="ll") - 1.0) < 10.0**-2.0 ), "density near progenitor in ll is incorrect" assert ( numpy.fabs(dens_ll(apar) / sdf_bovy14.density_par(apar, coord="customra") - 1.0) < 10.0**-2.0 ), "density near progenitor in ll is incorrect" apar = 0.5 assert ( numpy.fabs(dens_ll(apar) / sdf_bovy14.density_par(apar, coord="ll") - 1.0) < 10.0**-2.0 ), "density near progenitor in ll is incorrect" assert ( numpy.fabs(dens_ll(apar) / sdf_bovy14.density_par(apar, coord="customra") - 1.0) < 10.0**-2.0 ), "density near progenitor in ll is incorrect" apar = 1.8 assert ( numpy.fabs(dens_ll(apar) / sdf_bovy14.density_par(apar, coord="ll") - 1.0) < 10.0**-2.0 ), "density far from progenitor in ll is incorrect" assert ( numpy.fabs(dens_ll(apar) / sdf_bovy14.density_par(apar, coord="customra") - 1.0) < 10.0**-2.0 ), "density far from progenitor in ll is incorrect" return None def test_density_ra(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the density in ra is correctly computed, by doing this by hand def dens_ra(apar): dapar = 10.0**-9.0 X, Y, Z = ( sdf_bovy14._interpTrackX(apar) * sdf_bovy14._ro, sdf_bovy14._interpTrackY(apar) * sdf_bovy14._ro, sdf_bovy14._interpTrackZ(apar) * sdf_bovy14._ro, ) X, Y, Z = coords.galcenrect_to_XYZ( X, Y, Z, Xsun=sdf_bovy14._R0, Zsun=sdf_bovy14._Zsun ) l, b, d = coords.XYZ_to_lbd(X, Y, Z, degree=True) ra, dec = coords.lb_to_radec(l, b, degree=True) dX, dY, dZ = ( sdf_bovy14._interpTrackX(apar + dapar) * sdf_bovy14._ro, sdf_bovy14._interpTrackY(apar + dapar) * sdf_bovy14._ro, sdf_bovy14._interpTrackZ(apar + dapar) * sdf_bovy14._ro, ) dX, dY, dZ = coords.galcenrect_to_XYZ( dX, dY, dZ, Xsun=sdf_bovy14._R0, Zsun=sdf_bovy14._Zsun ) dl, db, dd = coords.XYZ_to_lbd(dX, dY, dZ, degree=True) dra, ddec = coords.lb_to_radec(dl, db, degree=True) jac = numpy.fabs((dra - ra) / dapar) return sdf_bovy14.density_par(apar) / jac apar = 0.1 assert ( numpy.fabs(dens_ra(apar) / sdf_bovy14.density_par(apar, coord="ra") - 1.0) < 10.0**-2.0 ), "density near progenitor in ra is incorrect" apar = 0.5 assert ( numpy.fabs(dens_ra(apar) / sdf_bovy14.density_par(apar, coord="ra") - 1.0) < 10.0**-2.0 ), "density near progenitor in ra is incorrect" apar = 1.8 assert ( numpy.fabs(dens_ra(apar) / sdf_bovy14.density_par(apar, coord="ra") - 1.0) < 10.0**-2.0 ), "density far from progenitor in ra is incorrect" return None def test_density_ll_wsampling(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the density computed using density_par is correct using a # random sample numpy.random.seed(1) def ll(apar): """Quick function that returns l for a given apar""" X, Y, Z = ( sdf_bovy14._interpTrackX(apar) * sdf_bovy14._ro, sdf_bovy14._interpTrackY(apar) * sdf_bovy14._ro, sdf_bovy14._interpTrackZ(apar) * sdf_bovy14._ro, ) X, Y, Z = coords.galcenrect_to_XYZ( X, Y, Z, Xsun=sdf_bovy14._R0, Zsun=sdf_bovy14._Zsun ) l, b, d = coords.XYZ_to_lbd(X, Y, Z, degree=True) return l LB = sdf_bovy14.sample(n=10000, lb=True) apar1, apar2 = 0.1, 0.6 dens1 = float(numpy.sum((LB[0] > ll(apar1)) * (LB[0] < ll(apar1) + 2.0))) dens2 = float(numpy.sum((LB[0] > ll(apar2)) * (LB[0] < ll(apar2) + 2.0))) dens1_calc = sdf_bovy14.density_par(apar1, coord="ll") dens2_calc = sdf_bovy14.density_par(apar2, coord="ll") assert numpy.fabs(dens1 / dens2 - dens1_calc / dens2_calc) < 0.1, ( "density in ll computed using density_par does not agree with density from random sample" ) return None def test_length(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the length is correct according to its definition thresh = 0.2 assert ( numpy.fabs( sdf_bovy14.density_par(sdf_bovy14.length(threshold=thresh)) / sdf_bovy14.density_par(0.1) - thresh ) < 10.0**-3.0 ), "Stream length does not conform to its definition" thresh = 0.05 assert ( numpy.fabs( sdf_bovy14.density_par(sdf_bovy14.length(threshold=thresh)) / sdf_bovy14.density_par(0.1) - thresh ) < 10.0**-3.0 ), "Stream length does not conform to its definition" return None def test_length_valueerror(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup thresh = 0.00001 with pytest.raises(ValueError) as excinfo: assert ( numpy.fabs( sdf_bovy14.density_par(sdf_bovy14.length(threshold=thresh)) / sdf_bovy14.density_par(0.1) - thresh ) < 10.0**-3.0 ), "Stream length does not conform to its definition" return None def test_length_ang(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that this is roughly correct def dphidapar(apar): dapar = 10.0**-9.0 X, Y, Z = ( sdf_bovy14._interpTrackX(apar) * sdf_bovy14._ro, sdf_bovy14._interpTrackY(apar) * sdf_bovy14._ro, sdf_bovy14._interpTrackZ(apar) * sdf_bovy14._ro, ) X, Y, Z = coords.galcenrect_to_XYZ( X, Y, Z, Xsun=sdf_bovy14._R0, Zsun=sdf_bovy14._Zsun ) l, b, d = coords.XYZ_to_lbd(X, Y, Z, degree=True) dX, dY, dZ = ( sdf_bovy14._interpTrackX(apar + dapar) * sdf_bovy14._ro, sdf_bovy14._interpTrackY(apar + dapar) * sdf_bovy14._ro, sdf_bovy14._interpTrackZ(apar + dapar) * sdf_bovy14._ro, ) dX, dY, dZ = coords.galcenrect_to_XYZ( dX, dY, dZ, Xsun=sdf_bovy14._R0, Zsun=sdf_bovy14._Zsun ) dl, db, dd = coords.XYZ_to_lbd(dX, dY, dZ, degree=True) jac = numpy.fabs((dl - l) / dapar) return jac thresh = 0.2 assert ( numpy.fabs( sdf_bovy14.length(threshold=thresh) * dphidapar(0.3) - sdf_bovy14.length(threshold=thresh, ang=True) ) < 10.0 ), "Length in angular coordinates does not conform to rough expectation" # Dangerous hack to test case where l decreases along the stream sdf_bovy14._interpolatedObsTrackLB[:, :2] *= -1.0 thresh = 0.2 assert ( numpy.fabs( sdf_bovy14.length(threshold=thresh) * dphidapar(0.3) - sdf_bovy14.length(threshold=thresh, ang=True) ) < 10.0 ), "Length in angular coordinates does not conform to rough expectation" # Go back sdf_bovy14._interpolatedObsTrackLB[:, :2] *= -1.0 return None def test_length_phys(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that this is roughly correct def dxdapar(apar): dapar = 10.0**-9.0 X, Y, Z = ( sdf_bovy14._interpTrackX(apar), sdf_bovy14._interpTrackY(apar), sdf_bovy14._interpTrackZ(apar), ) dX, dY, dZ = ( sdf_bovy14._interpTrackX(apar + dapar), sdf_bovy14._interpTrackY(apar + dapar), sdf_bovy14._interpTrackZ(apar + dapar), ) jac = numpy.sqrt( ((dX - X) / dapar) ** 2.0 + ((dY - Y) / dapar) ** 2.0 + ((dZ - Z) / dapar) ** 2.0 ) return jac * sdf_bovy14._ro thresh = 0.2 assert ( numpy.fabs( sdf_bovy14.length(threshold=thresh) * dxdapar(0.3) - sdf_bovy14.length(threshold=thresh, phys=True) ) < 1.0 ), "Length in physical coordinates does not conform to rough expectation" return None def test_meanOmega(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that meanOmega is close to constant and the mean Omega close to the progenitor assert numpy.all( numpy.fabs(sdf_bovy14.meanOmega(0.1) - sdf_bovy14._progenitor_Omega) < 10.0**-2.0 ), "meanOmega near progenitor not close to mean Omega for Bovy14 stream" assert numpy.all( numpy.fabs(sdf_bovy14.meanOmega(0.5) - sdf_bovy14._progenitor_Omega) < 10.0**-2.0 ), "meanOmega near progenitor not close to mean Omega for Bovy14 stream" return None def test_meanOmega_oned(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that meanOmega is close to constant and the mean Omega close to the progenitor assert numpy.fabs(sdf_bovy14.meanOmega(0.1, oned=True)) < 10.0**-2.0, ( "One-dimensional meanOmega near progenitor not close to zero for Bovy14 stream" ) assert numpy.fabs(sdf_bovy14.meanOmega(0.5, oned=True)) < 10.0**-2.0, ( "Oned-dimensional meanOmega near progenitor not close to zero for Bovy14 stream" ) return None def test_sigOmega_constant(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that sigOmega is close to constant close to the progenitor assert ( numpy.fabs(sdf_bovy14.sigOmega(0.1) - sdf_bovy14.sigOmega(0.5)) < 10.0**-4.0 ), "sigOmega near progenitor not close to constant for Bovy14 stream" return None def test_sigOmega_small(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that sigOmega is smaller than the total spread assert sdf_bovy14.sigOmega(0.1) < numpy.sqrt(sdf_bovy14._sortedSigOEig[2]), ( "sigOmega near progenitor not smaller than the total Omega spread" ) assert sdf_bovy14.sigOmega(0.5) < numpy.sqrt(sdf_bovy14._sortedSigOEig[2]), ( "sigOmega near progenitor not smaller than the total Omega spread" ) assert sdf_bovy14.sigOmega(1.2) < numpy.sqrt(sdf_bovy14._sortedSigOEig[2]), ( "sigOmega near progenitor not smaller than the total Omega spread" ) return None def test_meantdAngle(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the mean td for a given angle is close to what's expected assert ( numpy.fabs( (sdf_bovy14.meantdAngle(0.1) - 0.1 / sdf_bovy14._meandO) / sdf_bovy14.meantdAngle(0.1) ) < 10.0**-1.5 ), "mean td close to the progenitor is not dangle/dO" assert ( numpy.fabs( (sdf_bovy14.meantdAngle(0.4) - 0.4 / sdf_bovy14._meandO) / sdf_bovy14.meantdAngle(0.1) ) < 10.0**-0.9 ), "mean td close to the progenitor is not dangle/dO" assert numpy.fabs(sdf_bovy14.meantdAngle(0.0) - 0.0) < 10.0**-0.9, ( "mean td at the progenitor is not 0" ) assert ( numpy.fabs(sdf_bovy14.meantdAngle(10.0) - sdf_bovy14._tdisrupt) < 10.0**-0.9 ), "mean td far from the progenitor is not tdisrupt" return None def test_sigtdAngle(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the sigma of td for a given angle is small assert sdf_bovy14.sigtdAngle(0.1) < 0.2 * 0.1 / sdf_bovy14._meandO, ( "sigma of td close to the progenitor is not small" ) assert sdf_bovy14.sigtdAngle(0.5) > 0.2 * 0.1 / sdf_bovy14._meandO, ( "sigma of td in the middle of the stream is not large" ) # Spread at the progenitor should be zero assert sdf_bovy14.sigtdAngle(0.0) < 1e-5, ( "sigma of td at the progenitor is not zero" ) return None def test_ptdAngle(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the probability distribution for p(td|angle) is reasonable # at 0.1 da = 0.1 expected_max = da / sdf_bovy14._meandO assert sdf_bovy14.ptdAngle(expected_max, da) > sdf_bovy14.ptdAngle( 2.0 * expected_max, da ), "ptdAngle does not peak close to where it is expected to peak" assert sdf_bovy14.ptdAngle(expected_max, da) > sdf_bovy14.ptdAngle( 0.5 * expected_max, da ), "ptdAngle does not peak close to where it is expected to peak" # at 0.6 da = 0.6 expected_max = da / sdf_bovy14._meandO assert sdf_bovy14.ptdAngle(expected_max, da) > sdf_bovy14.ptdAngle( 2.0 * expected_max, da ), "ptdAngle does not peak close to where it is expected to peak" assert sdf_bovy14.ptdAngle(expected_max, da) > sdf_bovy14.ptdAngle( 0.5 * expected_max, da ), "ptdAngle does not peak close to where it is expected to peak" # Now test that the mean and sigma calculated with a simple Riemann sum agrees with meantdAngle da = 0.2 ts = numpy.linspace(0.0, 100.0, 1001) pts = sdf_bovy14.ptdAngle(ts, da) assert ( numpy.fabs( (numpy.sum(ts * pts) / numpy.sum(pts) - sdf_bovy14.meantdAngle(da)) / sdf_bovy14.meantdAngle(da) ) < 10.0**-2.0 ), ( "mean td at angle 0.2 calculated with Riemann sum does not agree with that calculated by meantdAngle" ) assert ( numpy.fabs( ( numpy.sqrt( numpy.sum(ts**2.0 * pts) / numpy.sum(pts) - (numpy.sum(ts * pts) / numpy.sum(pts)) ** 2.0 ) - sdf_bovy14.sigtdAngle(da) ) / sdf_bovy14.sigtdAngle(da) ) < 10.0**-1.5 ), ( "sig td at angle 0.2 calculated with Riemann sum does not agree with that calculated by meantdAngle" ) return None def test_meanangledAngle(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the mean perpendicular angle at a given angle is zero da = 0.1 assert numpy.fabs(sdf_bovy14.meanangledAngle(da, smallest=False)) < 10.0**-2, ( "mean perpendicular angle not zero" ) assert numpy.fabs(sdf_bovy14.meanangledAngle(da, smallest=True)) < 10.0**-2, ( "mean perpendicular angle not zero" ) da = 1.1 assert numpy.fabs(sdf_bovy14.meanangledAngle(da, smallest=False)) < 10.0**-2, ( "mean perpendicular angle not zero" ) assert numpy.fabs(sdf_bovy14.meanangledAngle(da, smallest=True)) < 10.0**-2, ( "mean perpendicular angle not zero" ) da = 0.0 assert numpy.fabs(sdf_bovy14.meanangledAngle(da, smallest=False)) < 10.0**-2, ( "mean perpendicular angle not zero" ) assert numpy.fabs(sdf_bovy14.meanangledAngle(da, smallest=True)) < 10.0**-2, ( "mean perpendicular angle not zero" ) return None def test_sigangledAngle(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the spread in perpendicular angle is much smaller than 1 (the typical spread in the parallel angle) da = 0.1 assert ( sdf_bovy14.sigangledAngle(da, assumeZeroMean=True, smallest=False, simple=False) < 1.0 / sdf_bovy14.freqEigvalRatio() ), "spread in perpendicular angle is not small" assert ( sdf_bovy14.sigangledAngle(da, assumeZeroMean=True, smallest=True, simple=False) < 1.0 / sdf_bovy14.freqEigvalRatio() ), "spread in perpendicular angle is not small" da = 1.1 assert ( sdf_bovy14.sigangledAngle(da, assumeZeroMean=True, smallest=False, simple=False) < 1.0 / sdf_bovy14.freqEigvalRatio() ), "spread in perpendicular angle is not small" assert ( sdf_bovy14.sigangledAngle(da, assumeZeroMean=True, smallest=True, simple=False) < 1.0 / sdf_bovy14.freqEigvalRatio() ), "spread in perpendicular angle is not small" # w/o assuming zeroMean da = 0.1 assert ( sdf_bovy14.sigangledAngle( da, assumeZeroMean=False, smallest=False, simple=False ) < 1.0 / sdf_bovy14.freqEigvalRatio() ), "spread in perpendicular angle is not small" assert ( sdf_bovy14.sigangledAngle(da, assumeZeroMean=False, smallest=True, simple=False) < 1.0 / sdf_bovy14.freqEigvalRatio() ), "spread in perpendicular angle is not small" # w/o assuming zeroMean, at da=0 da = 0.0 assert ( sdf_bovy14.sigangledAngle( da, assumeZeroMean=False, smallest=False, simple=False ) < 1.0 / sdf_bovy14.freqEigvalRatio() ), "spread in perpendicular angle is not small" assert ( sdf_bovy14.sigangledAngle(da, assumeZeroMean=False, smallest=True, simple=False) < 1.0 / sdf_bovy14.freqEigvalRatio() ), "spread in perpendicular angle is not small" # simple estimate da = 0.1 assert ( sdf_bovy14.sigangledAngle(da, assumeZeroMean=False, smallest=False, simple=True) < 1.0 / sdf_bovy14.freqEigvalRatio() ), "spread in perpendicular angle is not small" assert ( sdf_bovy14.sigangledAngle(da, assumeZeroMean=False, smallest=True, simple=True) < 1.0 / sdf_bovy14.freqEigvalRatio() ), "spread in perpendicular angle is not small" return None def test_pangledAngle(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Sanity check pangledAngle, does it peak near zero? Does the mean agree with meandAngle, does the sigma agree with sigdAngle? da = 0.1 assert sdf_bovy14.pangledAngle(0.0, da, smallest=False) > sdf_bovy14.pangledAngle( 0.1, da, smallest=False ), "pangledAngle does not peak near zero" assert sdf_bovy14.pangledAngle(0.0, da, smallest=False) > sdf_bovy14.pangledAngle( -0.1, da, smallest=False ), "pangledAngle does not peak near zero" # also for smallest assert sdf_bovy14.pangledAngle(0.0, da, smallest=True) > sdf_bovy14.pangledAngle( 0.1, da, smallest=False ), "pangledAngle does not peak near zero" assert sdf_bovy14.pangledAngle(0.0, da, smallest=True) > sdf_bovy14.pangledAngle( -0.1, da, smallest=False ), "pangledAngle does not peak near zero" dangles = numpy.linspace(-0.01, 0.01, 201) pdangles = ( numpy.array( [sdf_bovy14.pangledAngle(pda, da, smallest=False) for pda in dangles] ) ).flatten() assert ( numpy.fabs(numpy.sum(dangles * pdangles) / numpy.sum(pdangles)) < 10.0**-2.0 ), ( "mean calculated using Riemann sum of pangledAngle does not agree with actual mean" ) acsig = sdf_bovy14.sigangledAngle( da, assumeZeroMean=True, smallest=False, simple=False ) assert ( numpy.fabs( ( numpy.sqrt(numpy.sum(dangles**2.0 * pdangles) / numpy.sum(pdangles)) - acsig ) / acsig ) < 10.0**-2.0 ), ( "sigma calculated using Riemann sum of pangledAngle does not agree with actual sigma" ) # also for smallest pdangles = ( numpy.array( [sdf_bovy14.pangledAngle(pda, da, smallest=True) for pda in dangles] ) ).flatten() assert ( numpy.fabs(numpy.sum(dangles * pdangles) / numpy.sum(pdangles)) < 10.0**-2.0 ), ( "mean calculated using Riemann sum of pangledAngle does not agree with actual mean" ) acsig = sdf_bovy14.sigangledAngle( da, assumeZeroMean=True, smallest=True, simple=False ) assert ( numpy.fabs( ( numpy.sqrt(numpy.sum(dangles**2.0 * pdangles) / numpy.sum(pdangles)) - acsig ) / acsig ) < 10.0**-1.95 ), ( "sigma calculated using Riemann sum of pangledAngle does not agree with actual sigma" ) return None def test_bovy14_approxaA_inv(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the approximate action-angle conversion near the track works, ie, that the inverse gives the initial point # Point on track, interpolated RvR = sdf_bovy14._interpolatedObsTrack[22, :] check_approxaA_inv( sdf_bovy14, -5.0, RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5], interp=True ) # Point on track, not interpolated RvR = sdf_bovy14._interpolatedObsTrack[152, :] check_approxaA_inv( sdf_bovy14, -3.0, RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5], interp=False ) # Point near track, interpolated RvR = sdf_bovy14._interpolatedObsTrack[22, :] * (1.0 + 10.0**-2.0) check_approxaA_inv( sdf_bovy14, -2.0, RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5], interp=True ) # Point near track, not interpolated RvR = sdf_bovy14._interpolatedObsTrack[152, :] * (1.0 + 10.0**-2.0) check_approxaA_inv( sdf_bovy14, -2.0, RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5], interp=False ) # Point near end of track, interpolated RvR = sdf_bovy14._interpolatedObsTrack[-23, :] check_approxaA_inv( sdf_bovy14, -2.0, RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5], interp=True ) # Point near end of track, not interpolated RvR = sdf_bovy14._interpolatedObsTrack[-23, :] check_approxaA_inv( sdf_bovy14, -2.0, RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5], interp=False ) # Now find some trackpoints close to where angles wrap, to test that wrapping is covered properly everywhere dphi = ( numpy.roll(sdf_bovy14._interpolatedObsTrack[:, 5], -1) - sdf_bovy14._interpolatedObsTrack[:, 5] ) indx = dphi < 0.0 RvR = sdf_bovy14._interpolatedObsTrack[indx, :][0, :] * (1.0 + 10.0**-2.0) check_approxaA_inv( sdf_bovy14, -2.0, RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5], interp=False ) return None def test_bovy14_gaussApprox_onemissing(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test the Gaussian approximation # First, test near an interpolated point, without using interpolation (non-trivial) tol = -3.0 trackp = 110 XvX = list(sdf_bovy14._interpolatedObsTrackXY[trackp, :].flatten()) # X XvX[0] = None meanp, varp = sdf_bovy14.gaussApprox(XvX, interp=False) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackXY[trackp, 0]) < 10.0**tol ), "gaussApprox along track does not work for X" # Y XvX = list(sdf_bovy14._interpolatedObsTrackXY[trackp, :].flatten()) XvX[1] = None meanp, varp = sdf_bovy14.gaussApprox(XvX, interp=False) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackXY[trackp, 1]) < 10.0**tol ), "gaussApprox along track does not work for Y" # Z XvX = list(sdf_bovy14._interpolatedObsTrackXY[trackp, :].flatten()) XvX[2] = None meanp, varp = sdf_bovy14.gaussApprox(XvX, interp=False) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackXY[trackp, 2]) < 10.0**tol ), "gaussApprox along track does not work for Z" # vX XvX = list(sdf_bovy14._interpolatedObsTrackXY[trackp, :].flatten()) XvX[3] = None meanp, varp = sdf_bovy14.gaussApprox(XvX, interp=False) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackXY[trackp, 3]) < 10.0**tol ), "gaussApprox along track does not work for vX" # vY XvX = list(sdf_bovy14._interpolatedObsTrackXY[trackp, :].flatten()) XvX[4] = None meanp, varp = sdf_bovy14.gaussApprox(XvX, interp=False) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackXY[trackp, 4]) < 10.0**tol ), "gaussApprox along track does not work for vY" # vZ XvX = list(sdf_bovy14._interpolatedObsTrackXY[trackp, :].flatten()) XvX[5] = None meanp, varp = sdf_bovy14.gaussApprox(XvX, interp=False) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackXY[trackp, 5]) < 10.0**tol ), "gaussApprox along track does not work for vZ" return None def test_bovy14_gaussApprox_threemissing(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test the Gaussian approximation # First, test near an interpolated point, without using interpolation (non-trivial) tol = -3.0 trackp = 110 XvX = list(sdf_bovy14._interpolatedObsTrackXY[trackp, :].flatten()) # X,vX,vY XvX[0] = None XvX[3] = None XvX[4] = None meanp, varp = sdf_bovy14.gaussApprox(XvX, interp=False) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackXY[trackp, 0]) < 10.0**tol ), "gaussApprox along track does not work for X" assert ( numpy.fabs(meanp[1] - sdf_bovy14._interpolatedObsTrackXY[trackp, 3]) < 10.0**tol ), "gaussApprox along track does not work for vX" assert ( numpy.fabs(meanp[2] - sdf_bovy14._interpolatedObsTrackXY[trackp, 4]) < 10.0**tol ), "gaussApprox along track does not work for vY" # Y,Z,vZ XvX = list(sdf_bovy14._interpolatedObsTrackXY[trackp, :].flatten()) XvX[1] = None XvX[2] = None XvX[5] = None meanp, varp = sdf_bovy14.gaussApprox(XvX, interp=False) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackXY[trackp, 1]) < 10.0**tol ), "gaussApprox along track does not work for Y" assert ( numpy.fabs(meanp[1] - sdf_bovy14._interpolatedObsTrackXY[trackp, 2]) < 10.0**tol ), "gaussApprox along track does not work for Z" assert ( numpy.fabs(meanp[2] - sdf_bovy14._interpolatedObsTrackXY[trackp, 5]) < 10.0**tol ), "gaussApprox along track does not work for vZ" return None def test_bovy14_gaussApprox_fivemissing(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test the Gaussian approximation # Test near an interpolation point tol = -3.0 trackp = 110 XvX = list(sdf_bovy14._interpolatedObsTrackXY[trackp, :].flatten()) # X,Z,vX,vY,vZ XvX[0] = None XvX[2] = None XvX[3] = None XvX[4] = None XvX[5] = None meanp, varp = sdf_bovy14.gaussApprox(XvX, interp=False, cindx=1) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackXY[trackp, 0]) < 10.0**tol ), "gaussApprox along track does not work for X" assert ( numpy.fabs(meanp[1] - sdf_bovy14._interpolatedObsTrackXY[trackp, 2]) < 10.0**tol ), "gaussApprox along track does not work for Z" assert ( numpy.fabs(meanp[2] - sdf_bovy14._interpolatedObsTrackXY[trackp, 3]) < 10.0**tol ), "gaussApprox along track does not work for vX" assert ( numpy.fabs(meanp[3] - sdf_bovy14._interpolatedObsTrackXY[trackp, 4]) < 10.0**tol ), "gaussApprox along track does not work for vY" assert ( numpy.fabs(meanp[4] - sdf_bovy14._interpolatedObsTrackXY[trackp, 5]) < 10.0**tol ), "gaussApprox along track does not work for vZ" # Y,Z,vX,vY,vZ XvX = list(sdf_bovy14._interpolatedObsTrackXY[trackp, :].flatten()) XvX[1] = None XvX[2] = None XvX[3] = None XvX[4] = None XvX[5] = None meanp, varp = sdf_bovy14.gaussApprox(XvX, interp=False, cindx=1) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackXY[trackp, 1]) < 10.0**tol ), "gaussApprox along track does not work for Y" assert ( numpy.fabs(meanp[1] - sdf_bovy14._interpolatedObsTrackXY[trackp, 2]) < 10.0**tol ), "gaussApprox along track does not work for Z" assert ( numpy.fabs(meanp[2] - sdf_bovy14._interpolatedObsTrackXY[trackp, 3]) < 10.0**tol ), "gaussApprox along track does not work for vX" assert ( numpy.fabs(meanp[3] - sdf_bovy14._interpolatedObsTrackXY[trackp, 4]) < 10.0**tol ), "gaussApprox along track does not work for vY" assert ( numpy.fabs(meanp[4] - sdf_bovy14._interpolatedObsTrackXY[trackp, 5]) < 10.0**tol ), "gaussApprox along track does not work for vZ" return None def test_bovy14_gaussApprox_interp(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Tests of Gaussian approximation when using interpolation tol = -10.0 trackp = 234 XvX = list(sdf_bovy14._interpolatedObsTrackXY[trackp, :].flatten()) XvX[1] = None XvX[2] = None meanp, varp = sdf_bovy14.gaussApprox(XvX, interp=True) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackXY[trackp, 1]) < 10.0**tol ), "Gaussian approximation when using interpolation does not work as expected for Y" assert ( numpy.fabs(meanp[1] - sdf_bovy14._interpolatedObsTrackXY[trackp, 2]) < 10.0**tol ), "Gaussian approximation when using interpolation does not work as expected for Y" # also w/ default (which should be interp=True) meanp, varp = sdf_bovy14.gaussApprox(XvX) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackXY[trackp, 1]) < 10.0**tol ), "Gaussian approximation when using interpolation does not work as expected for Y" assert ( numpy.fabs(meanp[1] - sdf_bovy14._interpolatedObsTrackXY[trackp, 2]) < 10.0**tol ), "Gaussian approximation when using interpolation does not work as expected for Y" return None def test_bovy14_gaussApproxLB_onemissing(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test the Gaussian approximation # First, test near an interpolated point, without using interpolation (non-trivial) tol = -2.0 trackp = 102 LB = list(sdf_bovy14._interpolatedObsTrackLB[trackp, :].flatten()) # l LB[0] = None meanp, varp = sdf_bovy14.gaussApprox(LB, interp=False, lb=True) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackLB[trackp, 0]) < 10.0**tol ), "gaussApprox along track does not work for l" # b LB = list(sdf_bovy14._interpolatedObsTrackLB[trackp, :].flatten()) LB[1] = None meanp, varp = sdf_bovy14.gaussApprox(LB, interp=False, lb=True) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackLB[trackp, 1]) < 10.0**tol ), "gaussApprox along track does not work for b" # d LB = list(sdf_bovy14._interpolatedObsTrackLB[trackp, :].flatten()) LB[2] = None meanp, varp = sdf_bovy14.gaussApprox(LB, interp=False, lb=True) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackLB[trackp, 2]) < 10.0**tol ), "gaussApprox along track does not work for d" # vlos LB = list(sdf_bovy14._interpolatedObsTrackLB[trackp, :].flatten()) LB[3] = None meanp, varp = sdf_bovy14.gaussApprox(LB, interp=False, lb=True) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackLB[trackp, 3]) < 10.0**tol ), "gaussApprox along track does not work for vlos" # pmll LB = list(sdf_bovy14._interpolatedObsTrackLB[trackp, :].flatten()) LB[4] = None meanp, varp = sdf_bovy14.gaussApprox(LB, interp=False, lb=True) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackLB[trackp, 4]) < 10.0**tol ), "gaussApprox along track does not work for pmll" # pmbb LB = list(sdf_bovy14._interpolatedObsTrackLB[trackp, :].flatten()) LB[5] = None meanp, varp = sdf_bovy14.gaussApprox(LB, interp=False, lb=True) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackLB[trackp, 5]) < 10.0**tol ), "gaussApprox along track does not work for pmbb" return None def test_bovy14_gaussApproxLB_threemissing(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test the Gaussian approximation # First, test near an interpolated point, without using interpolation (non-trivial) tol = -1.8 trackp = 102 LB = list(sdf_bovy14._interpolatedObsTrackLB[trackp, :].flatten()) # l,vlos,pmll LB[0] = None LB[3] = None LB[4] = None meanp, varp = sdf_bovy14.gaussApprox(LB, interp=False, lb=True) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackLB[trackp, 0]) < 10.0**tol ), "gaussApprox along track does not work for l" assert ( numpy.fabs(meanp[1] - sdf_bovy14._interpolatedObsTrackLB[trackp, 3]) < 10.0**tol ), "gaussApprox along track does not work for vlos" assert ( numpy.fabs(meanp[2] - sdf_bovy14._interpolatedObsTrackLB[trackp, 4]) < 10.0**tol ), "gaussApprox along track does not work for pmll" # b,d,pmbb LB = list(sdf_bovy14._interpolatedObsTrackLB[trackp, :].flatten()) LB[1] = None LB[2] = None LB[5] = None meanp, varp = sdf_bovy14.gaussApprox(LB, interp=False, lb=True) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackLB[trackp, 1]) < 10.0**tol ), "gaussApprox along track does not work for b" assert ( numpy.fabs(meanp[1] - sdf_bovy14._interpolatedObsTrackLB[trackp, 2]) < 10.0**tol ), "gaussApprox along track does not work for d" assert ( numpy.fabs(meanp[2] - sdf_bovy14._interpolatedObsTrackLB[trackp, 5]) < 10.0**tol ), "gaussApprox along track does not work for pmbb" return None def test_bovy14_gaussApproxLB_fivemissing(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test the Gaussian approximation # Test near an interpolation point tol = -1.98 # vlos just doesn't make -2. trackp = 102 LB = list(sdf_bovy14._interpolatedObsTrackLB[trackp, :].flatten()) # X,Z,vX,vY,vZ LB[0] = None LB[2] = None LB[3] = None LB[4] = None LB[5] = None meanp, varp = sdf_bovy14.gaussApprox(LB, interp=False, cindx=1, lb=True) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackLB[trackp, 0]) < 10.0**tol ), "gaussApprox along track does not work for l" assert ( numpy.fabs(meanp[1] - sdf_bovy14._interpolatedObsTrackLB[trackp, 2]) < 10.0**tol ), "gaussApprox along track does not work for d" assert ( numpy.fabs(meanp[2] - sdf_bovy14._interpolatedObsTrackLB[trackp, 3]) < 10.0**tol ), "gaussApprox along track does not work for vlos" assert ( numpy.fabs(meanp[3] - sdf_bovy14._interpolatedObsTrackLB[trackp, 4]) < 10.0**tol ), "gaussApprox along track does not work for pmll" assert ( numpy.fabs(meanp[4] - sdf_bovy14._interpolatedObsTrackLB[trackp, 5]) < 10.0**tol ), "gaussApprox along track does not work for pmbb" # b,d,vlos,pmll,pmbb LB = list(sdf_bovy14._interpolatedObsTrackLB[trackp, :].flatten()) LB[1] = None LB[2] = None LB[3] = None LB[4] = None LB[5] = None meanp, varp = sdf_bovy14.gaussApprox(LB, interp=False, cindx=1, lb=True) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackLB[trackp, 1]) < 10.0**tol ), "gaussApprox along track does not work for b" assert ( numpy.fabs(meanp[1] - sdf_bovy14._interpolatedObsTrackLB[trackp, 2]) < 10.0**tol ), "gaussApprox along track does not work for d" assert ( numpy.fabs(meanp[2] - sdf_bovy14._interpolatedObsTrackLB[trackp, 3]) < 10.0**tol ), "gaussApprox along track does not work for vlos" assert ( numpy.fabs(meanp[3] - sdf_bovy14._interpolatedObsTrackLB[trackp, 4]) < 10.0**tol ), "gaussApprox along track does not work for pmll" assert ( numpy.fabs(meanp[4] - sdf_bovy14._interpolatedObsTrackLB[trackp, 5]) < 10.0**tol ), "gaussApprox along track does not work for pmbb" return None def test_bovy14_gaussApproxLB_interp(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Tests of Gaussian approximation when using interpolation tol = -10.0 trackp = 234 LB = list(sdf_bovy14._interpolatedObsTrackLB[trackp, :].flatten()) LB[1] = None LB[2] = None meanp, varp = sdf_bovy14.gaussApprox(LB, interp=True, lb=True) assert ( numpy.fabs(meanp[0] - sdf_bovy14._interpolatedObsTrackLB[trackp, 1]) < 10.0**tol ), "Gaussian approximation when using interpolation does not work as expected for b" assert ( numpy.fabs(meanp[1] - sdf_bovy14._interpolatedObsTrackLB[trackp, 2]) < 10.0**tol ), "Gaussian approximation when using interpolation does not work as expected for d" return None def test_bovy14_callMargXZ(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Example from the tutorial and paper meanp, varp = sdf_bovy14.gaussApprox([None, None, 2.0 / 8.0, None, None, None]) xs = ( numpy.linspace(-3.0 * numpy.sqrt(varp[0, 0]), 3.0 * numpy.sqrt(varp[0, 0]), 11) + meanp[0] ) logps = numpy.array( [sdf_bovy14.callMarg([x, None, 2.0 / 8.0, None, None, None]) for x in xs] ) ps = numpy.exp(logps) ps /= numpy.sum(ps) * (xs[1] - xs[0]) * 8.0 # Test that the mean is close to the approximation assert numpy.fabs(numpy.sum(xs * ps) / numpy.sum(ps) - meanp[0]) < 10.0**-2.0, ( "mean of full PDF calculation does not agree with Gaussian approximation to the level at which this is expected for p(X|Z)" ) assert ( numpy.fabs( numpy.sqrt( numpy.sum(xs**2.0 * ps) / numpy.sum(ps) - (numpy.sum(xs * ps) / numpy.sum(ps)) ** 2.0 ) - numpy.sqrt(varp[0, 0]) ) < 10.0**-2.0 ), ( "sigma of full PDF calculation does not agree with Gaussian approximation to the level at which this is expected for p(X|Z)" ) # Test that the mean is close to the approximation, when explicitly setting sigma and ngl logps = numpy.array( [ sdf_bovy14.callMarg( [x, None, 2.0 / 8.0, None, None, None], ngl=6, nsigma=3.1 ) for x in xs ] ) ps = numpy.exp(logps) ps /= numpy.sum(ps) * (xs[1] - xs[0]) * 8.0 assert numpy.fabs(numpy.sum(xs * ps) / numpy.sum(ps) - meanp[0]) < 10.0**-2.0, ( "mean of full PDF calculation does not agree with Gaussian approximation to the level at which this is expected for p(X|Z)" ) assert ( numpy.fabs( numpy.sqrt( numpy.sum(xs**2.0 * ps) / numpy.sum(ps) - (numpy.sum(xs * ps) / numpy.sum(ps)) ** 2.0 ) - numpy.sqrt(varp[0, 0]) ) < 10.0**-2.0 ), ( "sigma of full PDF calculation does not agree with Gaussian approximation to the level at which this is expected for p(X|Z)" ) return None def test_bovy14_callMargDPMLL(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # p(D|pmll) meanp, varp = sdf_bovy14.gaussApprox([None, None, None, None, 8.0, None], lb=True) xs = ( numpy.linspace(-3.0 * numpy.sqrt(varp[1, 1]), 3.0 * numpy.sqrt(varp[1, 1]), 11) + meanp[1] ) logps = numpy.array( [sdf_bovy14.callMarg([None, x, None, None, 8.0, None], lb=True) for x in xs] ) ps = numpy.exp(logps) ps /= numpy.sum(ps) * (xs[1] - xs[0]) # Test that the mean is close to the approximation assert numpy.fabs(numpy.sum(xs * ps) / numpy.sum(ps) - meanp[1]) < 10.0**-2.0, ( "mean of full PDF calculation does not agree with Gaussian approximation to the level at which this is expected for p(D|pmll)" ) assert ( numpy.fabs( numpy.sqrt( numpy.sum(xs**2.0 * ps) / numpy.sum(ps) - (numpy.sum(xs * ps) / numpy.sum(ps)) ** 2.0 ) - numpy.sqrt(varp[1, 1]) ) < 10.0**-1.0 ), ( "sigma of full PDF calculation does not agree with Gaussian approximation to the level at which this is expected for p(D|pmll)" ) # Test options assert ( numpy.fabs( sdf_bovy14.callMarg([None, meanp[1], None, None, 8.0, None], lb=True) - sdf_bovy14.callMarg( [None, meanp[1], None, None, 8.0, None], lb=True, ro=sdf_bovy14._ro, vo=sdf_bovy14._vo, R0=sdf_bovy14._R0, Zsun=sdf_bovy14._Zsun, vsun=sdf_bovy14._vsun, ) ) < 10.0**-10.0 ), "callMarg with ro, etc. options set to default does not agree with default" cindx = sdf_bovy14.find_closest_trackpointLB( None, meanp[1], None, None, 8.0, None, usev=True ) assert ( numpy.fabs( sdf_bovy14.callMarg([None, meanp[1], None, None, 8.0, None], lb=True) - sdf_bovy14.callMarg( [None, meanp[1], None, None, 8.0, None], lb=True, cindx=cindx, interp=True, ) ) < 10.0**10.0 ), "callMarg with cindx set does not agree with it set to default" if cindx % 100 > 50: cindx = cindx // 100 + 1 else: cindx = cindx // 100 assert ( numpy.fabs( sdf_bovy14.callMarg( [None, meanp[1], None, None, 8.0, None], lb=True, interp=False ) - sdf_bovy14.callMarg( [None, meanp[1], None, None, 8.0, None], lb=True, interp=False, cindx=cindx, ) ) < 10.0**10.0 ), "callMarg with cindx set does not agree with it set to default" # Same w/o interpolation return None def test_bovy14_callMargVLOSPMBB(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # p(vlos|pmbb) meanp, varp = sdf_bovy14.gaussApprox([None, None, None, None, None, 5.0], lb=True) xs = ( numpy.linspace(-3.0 * numpy.sqrt(varp[3, 3]), 3.0 * numpy.sqrt(varp[3, 3]), 11) + meanp[3] ) logps = numpy.array( [sdf_bovy14.callMarg([None, None, None, x, None, 5.0], lb=True) for x in xs] ) ps = numpy.exp(logps - numpy.amax(logps)) ps /= numpy.sum(ps) * (xs[1] - xs[0]) # Test that the mean is close to the approximation assert numpy.fabs(numpy.sum(xs * ps) / numpy.sum(ps) - meanp[3]) < 5.0, ( "mean of full PDF calculation does not agree with Gaussian approximation to the level at which this is expected for p(D|pmll)" ) return None def test_callArgs(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Tests of _parse_call_args from galpy.orbit import Orbit # Just checking that different types of inputs give the same result trackp = 191 RvR = sdf_bovy14._interpolatedObsTrack[trackp, :].flatten() OA = sdf_bovy14._interpolatedObsTrackAA[trackp, :].flatten() # RvR vs. array of OA s = numpy.ones(2) assert numpy.all( numpy.fabs( sdf_bovy14(RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5]) - sdf_bovy14( OA[0] * s, OA[1] * s, OA[2] * s, OA[3] * s, OA[4] * s, OA[5] * s, aAInput=True, ) ) < 10.0**-8.0 ), "__call__ w/ R,vR,... and equivalent O,theta,... does not give the same result" # RvR vs. OA assert ( numpy.fabs( sdf_bovy14(RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5]) - sdf_bovy14(OA[0], OA[1], OA[2], OA[3], OA[4], OA[5], aAInput=True) ) < 10.0**-8.0 ), "__call__ w/ R,vR,... and equivalent O,theta,... does not give the same result" # RvR vs. orbit assert ( numpy.fabs( sdf_bovy14(RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5]) - sdf_bovy14(Orbit([RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5]])) ) < 10.0**-8.0 ), "__call__ w/ R,vR,... and equivalent orbit does not give the same result" # RvR vs. list of orbit assert ( numpy.fabs( sdf_bovy14(RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5]) - sdf_bovy14([Orbit([RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5]])]) ) < 10.0**-8.0 ), "__call__ w/ R,vR,... and equivalent list of orbit does not give the same result" # RvR w/ and w/o log assert ( numpy.fabs( sdf_bovy14(RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5]) - numpy.log( sdf_bovy14(RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5], log=False) ) ) < 10.0**-8.0 ), "__call__ w/ R,vR,... log and not log does not give the same result" # RvR w/ explicit interp assert ( numpy.fabs( sdf_bovy14(RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5]) - sdf_bovy14(RvR[0], RvR[1], RvR[2], RvR[3], RvR[4], RvR[5], interp=True) ) < 10.0**-8.0 ), "__call__ w/ R,vR,... w/ explicit interp does not give the same result as w/o" # RvR w/o phi should raise error try: sdf_bovy14(RvR[0], RvR[1], RvR[2], RvR[3], RvR[4]) except OSError: pass else: raise AssertionError("__call__ w/o phi does not raise IOError") return None def test_bovy14_sample(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup numpy.random.seed(1) RvR = sdf_bovy14.sample(n=1000) # Sanity checks # Range in Z indx = (RvR[3] > 4.0 / 8.0) * (RvR[3] < 5.0 / 8.0) meanp, varp = sdf_bovy14.gaussApprox([None, None, 4.5 / 8.0, None, None, None]) # mean assert ( numpy.fabs( numpy.sqrt(meanp[0] ** 2.0 + meanp[1] ** 2.0) - numpy.mean(RvR[0][indx]) ) < 10.0**-2.0 ), "Sample track does not lie in the same location as the track" assert numpy.fabs(meanp[4] - numpy.mean(RvR[4][indx])) < 10.0**-2.0, ( "Sample track does not lie in the same location as the track" ) # variance, use smaller range RvR = sdf_bovy14.sample(n=10000) indx = (RvR[3] > 4.4 / 8.0) * (RvR[3] < 4.6 / 8.0) assert ( numpy.fabs(numpy.sqrt(varp[4, 4]) / numpy.std(RvR[4][indx]) - 1.0) < 10.0**0.0 ), "Sample spread not similar to track spread" # Test that t is returned RvRdt = sdf_bovy14.sample(n=10, returndt=True) assert len(RvRdt) == 7, "dt not returned with returndt in sample" return None def test_bovy14_sampleXY(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup XvX = sdf_bovy14.sample(n=1000, xy=True) # Sanity checks # Range in Z indx = (XvX[2] > 4.0 / 8.0) * (XvX[2] < 5.0 / 8.0) meanp, varp = sdf_bovy14.gaussApprox([None, None, 4.5 / 8.0, None, None, None]) # mean assert numpy.fabs(meanp[0] - numpy.mean(XvX[0][indx])) < 10.0**-2.0, ( "Sample track does not lie in the same location as the track" ) assert numpy.fabs(meanp[1] - numpy.mean(XvX[1][indx])) < 10.0**-2.0, ( "Sample track does not lie in the same location as the track" ) assert numpy.fabs(meanp[3] - numpy.mean(XvX[4][indx])) < 10.0**-2.0, ( "Sample track does not lie in the same location as the track" ) # variance, use smaller range XvX = sdf_bovy14.sample(n=10000) indx = (XvX[3] > 4.4 / 8.0) * (XvX[3] < 4.6 / 8.0) assert ( numpy.fabs(numpy.sqrt(varp[0, 0]) / numpy.std(XvX[0][indx]) - 1.0) < 10.0**0.0 ), "Sample spread not similar to track spread" # Test that t is returned XvXdt = sdf_bovy14.sample(n=10, returndt=True, xy=True) assert len(XvXdt) == 7, "dt not returned with returndt in sample" return None def test_bovy14_sampleLB(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup LB = sdf_bovy14.sample(n=1000, lb=True) # Sanity checks # Range in l indx = (LB[0] > 212.5) * (LB[0] < 217.5) meanp, varp = sdf_bovy14.gaussApprox([215, None, None, None, None, None], lb=True) # mean assert numpy.fabs((meanp[0] - numpy.mean(LB[1][indx])) / meanp[0]) < 10.0**-2.0, ( "Sample track does not lie in the same location as the track" ) assert numpy.fabs((meanp[1] - numpy.mean(LB[2][indx])) / meanp[1]) < 10.0**-2.0, ( "Sample track does not lie in the same location as the track" ) assert numpy.fabs((meanp[3] - numpy.mean(LB[4][indx])) / meanp[3]) < 10.0**-2.0, ( "Sample track does not lie in the same location as the track" ) # variance, use smaller range LB = sdf_bovy14.sample(n=10000, lb=True) indx = (LB[0] > 214.0) * (LB[0] < 216.0) assert ( numpy.fabs(numpy.sqrt(varp[0, 0]) / numpy.std(LB[1][indx]) - 1.0) < 10.0**0.0 ), "Sample spread not similar to track spread" # Test that t is returned LBdt = sdf_bovy14.sample(n=10, returndt=True, lb=True) assert len(LBdt) == 7, "dt not returned with returndt in sample" return None def test_bovy14_sampleA(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup AA = sdf_bovy14.sample(n=1000, returnaAdt=True) # Sanity checks indx = (AA[0][0] > 0.5625) * (AA[0][0] < 0.563) assert numpy.fabs(numpy.mean(AA[0][2][indx]) - 0.42525) < 10.0**-1.0, ( "Sample's vertical frequency at given radial frequency is not as expected" ) # Sanity check w/ time AA = sdf_bovy14.sample(n=10000, returnaAdt=True) daperp = numpy.sqrt( numpy.sum( (AA[1] - numpy.tile(sdf_bovy14._progenitor_angle, (10000, 1)).T) ** 2.0, axis=0, ) ) indx = (daperp > 0.24) * (daperp < 0.26) assert ( numpy.fabs( (numpy.mean(AA[2][indx]) - sdf_bovy14.meantdAngle(0.25)) / numpy.mean(AA[2][indx]) ) < 10.0**-2.0 ), "mean stripping time along sample not as expected" return None def test_subhalo_encounters(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that subhalo_encounters acts as expected # linear in sigma assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(sigma=300.0 / 220.0) / sdf_bovy14.subhalo_encounters(sigma=100.0 / 220.0) - 3 ) < 10.0**-8.0 ), "subhalo_encounters does not act linearly with sigma" assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(sigma=200.0 / 220.0) / sdf_bovy14.subhalo_encounters(sigma=100.0 / 220.0) - 2 ) < 10.0**-8.0 ), "subhalo_encounters does not act linearly with sigma" assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(sigma=50.0 / 220.0, yoon=True) / sdf_bovy14.subhalo_encounters(sigma=100.0 / 220.0, yoon=True) - 0.5 ) < 10.0**-8.0 ), "subhalo_encounters does not act linearly with sigma" # linear in bmax assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(bmax=1.5) / sdf_bovy14.subhalo_encounters(bmax=0.5) - 3 ) < 10.0**-8.0 ), "subhalo_encounters does not act linearly with bmax" assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(bmax=1.0) / sdf_bovy14.subhalo_encounters(bmax=0.5) - 2 ) < 10.0**-8.0 ), "subhalo_encounters does not act linearly with bmax" assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(bmax=0.25, yoon=True) / sdf_bovy14.subhalo_encounters(bmax=0.5, yoon=True) - 0.5 ) < 10.0**-8.0 ), "subhalo_encounters does not act linearly with bmax" # except when bmax is tiny, then it shouldn't matter assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(bmax=10.0**-7.0) / sdf_bovy14.subhalo_encounters(bmax=10.0**-6.0) - 1.0 ) < 10.0**-5.0 ), "subhalo_encounters does not become insensitive to bmax at small bmax" # linear in nsubhalo assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(nsubhalo=1.5) / sdf_bovy14.subhalo_encounters(nsubhalo=0.5) - 3 ) < 10.0**-8.0 ), "subhalo_encounters does not act linearly with nsubhalo" assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(nsubhalo=1.0) / sdf_bovy14.subhalo_encounters(nsubhalo=0.5) - 2 ) < 10.0**-8.0 ), "subhalo_encounters does not act linearly with nsubhalo" assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(nsubhalo=0.25, yoon=True) / sdf_bovy14.subhalo_encounters(nsubhalo=0.5, yoon=True) - 0.5 ) < 10.0**-8.0 ), "subhalo_encounters does not act linearly with nsubhalo" # For nsubhalo = 0.3 should have O(10) impacts (wow, that actually worked!) assert ( numpy.fabs( numpy.log10( sdf_bovy14.subhalo_encounters( nsubhalo=0.3, bmax=1.5 / 8.0, sigma=120.0 / 220.0 ) ) - 1.0 ) < 0.5 ), "Number of subhalo impacts does not behave as expected for reasonable inputs" # Except if you're Yoon et al., then it's more like 30 assert ( numpy.fabs( numpy.log10( sdf_bovy14.subhalo_encounters( nsubhalo=0.3, bmax=1.5 / 8.0, sigma=120.0 / 220.0, yoon=True ) ) - 1.5 ) < 0.5 ), "Number of subhalo impacts does not behave as expected for reasonable inputs" return None def test_subhalo_encounters_venc(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the dependence on venc of subhalo_encounters is correct def expected_venc(venc, sigma): return 1.0 - numpy.exp(-(venc**2.0) / 2.0 / sigma**2.0) sigma = 150.0 / 220.0 assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(venc=100.0 / 220.0, sigma=sigma) / sdf_bovy14.subhalo_encounters(sigma=sigma) / expected_venc(100.0 / 220.0, sigma) - 1.0 ) < 10.0**-8.0 ), "subhalo_encounters venc behavior is not correct" assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(venc=200.0 / 220.0, sigma=sigma) / sdf_bovy14.subhalo_encounters(sigma=sigma) / expected_venc(200.0 / 220.0, sigma) - 1.0 ) < 10.0**-8.0 ), "subhalo_encounters venc behavior is not correct" assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(venc=300.0 / 220.0, sigma=sigma) / sdf_bovy14.subhalo_encounters(sigma=sigma) / expected_venc(300.0 / 220.0, sigma) - 1.0 ) < 10.0**-8.0 ), "subhalo_encounters venc behavior is not correct" # Should go to 1 assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(venc=30000.0 / 220.0, sigma=sigma) / sdf_bovy14.subhalo_encounters(sigma=sigma) - 1.0 ) < 10.0**-4.0 ), "subhalo_encounters venc behavior is not correct" return None def test_subhalo_encounters_venc_yoon(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the dependence on venc of subhalo_encounters is correct # in the Yoon et al. case def expected_venc(venc, sigma): return 1.0 - (1.0 + venc**2.0 / 4.0 / sigma**2.0) * numpy.exp( -(venc**2.0) / 4.0 / sigma**2.0 ) sigma = 150.0 / 220.0 assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(venc=100.0 / 220.0, sigma=sigma, yoon=True) / sdf_bovy14.subhalo_encounters(sigma=sigma, yoon=True) / expected_venc(100.0 / 220.0, sigma) - 1.0 ) < 10.0**-8.0 ), "subhalo_encounters venc behavior is not correct" assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(venc=200.0 / 220.0, sigma=sigma, yoon=True) / sdf_bovy14.subhalo_encounters(sigma=sigma, yoon=True) / expected_venc(200.0 / 220.0, sigma) - 1.0 ) < 10.0**-8.0 ), "subhalo_encounters venc behavior is not correct" assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(venc=300.0 / 220.0, sigma=sigma, yoon=True) / sdf_bovy14.subhalo_encounters(sigma=sigma, yoon=True) / expected_venc(300.0 / 220.0, sigma) - 1.0 ) < 10.0**-8.0 ), "subhalo_encounters venc behavior is not correct" # Should go to 1 assert ( numpy.fabs( sdf_bovy14.subhalo_encounters(venc=30000.0 / 220.0, sigma=sigma, yoon=True) / sdf_bovy14.subhalo_encounters(sigma=sigma, yoon=True) - 1.0 ) < 10.0**-4.0 ), "subhalo_encounters venc behavior is not correct" return None def test_bovy14_oppositetrailing_setup_errors(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) lp_false = LogarithmicHaloPotential(normalize=1.0, q=0.8) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) # This is the trailing of the stream that is going the opposite direction obs = Orbit( [1.56148083, -0.35081535, 1.15481504, 0.88719443, 0.47713334, 0.12019596] ) sigv = 0.365 # km/s # Provoke some errors try: sdft_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp_false, aA=aAI, leading=False ) # expl set iterations except OSError: pass else: raise AssertionError( "streamdf setup w/ potential neq actionAngle-potential did not raise IOError" ) # Warning when deltaAngleTrack is too large (turn warning into error for testing; not using catch_warnings, bc we need this to actually fail [setup doesn't work for such a large deltaAngleTrack]) import warnings warnings.simplefilter("error") try: sdft_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=False, deltaAngleTrack=100.0, ) # much too big deltaAngle except: pass else: raise AssertionError( "streamdf setup w/ deltaAngleTrack too large did not raise warning" ) warnings.simplefilter("default") return None def test_bovy14_oppositetrailing_setup_errors(bovy14_trailing_setup): sdft_bovy14 = bovy14_trailing_setup assert not sdft_bovy14 is None, "bovy14 streamdf setup did not work" return None def test_calcaAJac(): from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df.streamdf import calcaAJac from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) R, vR, vT, z, vz, phi = ( 1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596, ) jac = calcaAJac([R, vR, vT, z, vz, phi], aAI, dxv=10**-8.0 * numpy.ones(6)) assert numpy.fabs((numpy.fabs(numpy.linalg.det(jac)) - R) / R) < 10.0**-2.0, ( "Determinant of (x,v) -> (J,theta) transformation is not equal to 1" ) # Now w/ frequencies jac = calcaAJac( [R, vR, vT, z, vz, phi], aAI, dxv=10**-8.0 * numpy.ones(6), actionsFreqsAngles=True, ) # extract (J,theta) Jajac = jac[ numpy.array( [True, True, True, False, False, False, True, True, True], dtype="bool" ), :, ] assert numpy.fabs((numpy.fabs(numpy.linalg.det(Jajac)) - R) / R) < 10.0**-2.0, ( "Determinant of (x,v) -> (J,theta) transformation is not equal to 1, when calculated with actionsFreqsAngles" ) # extract (O,theta) Oajac = jac[ numpy.array( [False, False, False, True, True, True, True, True, True], dtype="bool" ), :, ] OJjac = calcaAJac( [R, vR, vT, z, vz, phi], aAI, dxv=10**-8.0 * numpy.ones(6), dOdJ=True ) assert ( numpy.fabs( ( numpy.fabs(numpy.linalg.det(Oajac)) - R * numpy.fabs(numpy.linalg.det(OJjac)) ) / R / numpy.fabs(numpy.linalg.det(OJjac)) ) < 10.0**-2.0 ), "Determinant of (x,v) -> (O,theta) is not equal to that of dOdJ" OJjac = calcaAJac( [R, vR, vT, z, vz, phi], aAI, dxv=10**-8.0 * numpy.ones(6), freqs=True ) assert ( numpy.fabs( (numpy.fabs(numpy.linalg.det(Oajac)) - numpy.fabs(numpy.linalg.det(OJjac))) / numpy.fabs(numpy.linalg.det(OJjac)) ) < 10.0**-2.0 ), ( "Determinant of (x,v) -> (O,theta) is not equal to that calculated w/ actionsFreqsAngles" ) return None def test_calcaAJacLB(): from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df.streamdf import calcaAJac from galpy.potential import LogarithmicHaloPotential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) R, vR, vT, z, vz, phi = ( 1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596, ) # First convert these to l,b,d,vlos,pmll,pmbb XYZ = coords.galcencyl_to_XYZ(R * 8.0, phi, z * 8.0, Xsun=8.0, Zsun=0.02) l, b, d = coords.XYZ_to_lbd(XYZ[0], XYZ[1], XYZ[2], degree=True) vXYZ = coords.galcencyl_to_vxvyvz( vR * 220.0, vT * 220.0, vz * 220.0, phi=phi, vsun=[10.0, 240.0, -10.0] ) vlos, pmll, pmbb = coords.vxvyvz_to_vrpmllpmbb( vXYZ[0], vXYZ[1], vXYZ[2], l, b, d, degree=True ) jac = calcaAJac( [ l, b, d, vlos, pmll, pmbb, ], aAI, dxv=10**-8.0 * numpy.ones(6), lb=True, R0=8.0, Zsun=0.02, vsun=[10.0, 240.0, -10.0], ro=8.0, vo=220.0, ) lbdjac = numpy.fabs( numpy.linalg.det(coords.lbd_to_XYZ_jac(l, b, d, vlos, pmll, pmbb, degree=True)) ) assert ( numpy.fabs( (numpy.fabs(numpy.linalg.det(jac)) * 8.0**3.0 * 220.0**3.0 - lbdjac) / lbdjac ) < 10.0**-2.0 ), "Determinant of (x,v) -> (J,theta) transformation is not equal to 1" return None def test_estimateTdisrupt(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup from galpy.util import conversion td = numpy.log10( sdf_bovy14.estimateTdisrupt(1.0) * conversion.time_in_Gyr(220.0, 8.0) ) assert (td > 0.0) * (td < 1.0), "estimate of disruption time is not a few Gyr" return None def test_plotting(bovy14_setup, bovy14_trailing_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup sdft_bovy14 = bovy14_trailing_setup # Check plotting routines check_track_plotting(sdf_bovy14, "R", "Z") check_track_plotting(sdf_bovy14, "R", "Z", phys=True) # do 1 with phys check_track_plotting(sdf_bovy14, "R", "Z", interp=False) # do 1 w/o interp check_track_plotting(sdf_bovy14, "R", "X", spread=0) check_track_plotting(sdf_bovy14, "R", "Y", spread=0) check_track_plotting(sdf_bovy14, "R", "phi") check_track_plotting(sdf_bovy14, "R", "vZ") check_track_plotting(sdf_bovy14, "R", "vZ", phys=True) # do 1 with phys check_track_plotting(sdf_bovy14, "R", "vZ", interp=False) # do 1 w/o interp check_track_plotting(sdf_bovy14, "R", "vX", spread=0) check_track_plotting(sdf_bovy14, "R", "vY", spread=0) check_track_plotting(sdf_bovy14, "R", "vT") check_track_plotting(sdf_bovy14, "R", "vR") check_track_plotting(sdf_bovy14, "ll", "bb") check_track_plotting(sdf_bovy14, "ll", "bb", interp=False) # do 1 w/o interp check_track_plotting(sdf_bovy14, "ll", "dist") check_track_plotting(sdf_bovy14, "ll", "vlos") check_track_plotting(sdf_bovy14, "ll", "pmll") delattr(sdf_bovy14, "_ObsTrackLB") # rm, to test that this gets recalculated check_track_plotting(sdf_bovy14, "ll", "pmbb") # Also test plotCompareTrackAAModel sdf_bovy14.plotCompareTrackAAModel() sdft_bovy14.plotCompareTrackAAModel() # has multi return None def test_2ndsetup(): # Test related to #195: when we re-do the setup with the same progenitor, we should get the same from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), nosetup=True, ) # won't look at track rsdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), nosetup=True, ) # won't look at track assert numpy.fabs(sdf_bovy14.misalignment() - rsdf_bovy14.misalignment()) < 0.01, ( "misalignment not the same when setting up the same streamdf w/ a previously used progenitor" ) assert ( numpy.fabs(sdf_bovy14.freqEigvalRatio() - rsdf_bovy14.freqEigvalRatio()) < 0.01 ), ( "freqEigvalRatio not the same when setting up the same streamdf w/ a previously used progenitor" ) return None def test_bovy14_trackaa(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup # Test that the explicitly-calculated frequencies along the track are close to those that the track is based on (Fardal test, #194) from galpy.orbit import Orbit aastream = sdf_bovy14._ObsTrackAA # freqs and angles that the track is based on RvR = sdf_bovy14._ObsTrack # the track in R,vR,... aastream_expl = numpy.reshape( numpy.array( [sdf_bovy14._aA.actionsFreqsAngles(Orbit(trvr))[3:] for trvr in RvR] ), aastream.shape, ) # frequencies, compare to offset between track and progenitor (spread in freq ~ 1/6 that diff, so as long as we're smaller than that we're fine) assert numpy.all( numpy.fabs( (aastream[:, :3] - aastream_expl[:, :3]) / (aastream[0, :3] - sdf_bovy14._progenitor_Omega) ) < 0.05 ), ( "Explicitly calculated frequencies along the track do not agree with the frequencies on which the track is based for bovy14 setup" ) # angles assert numpy.all( numpy.fabs((aastream[:, 3:] - aastream_expl[:, 3:]) / 2.0 / numpy.pi) < 0.001 ), ( "Explicitly calculated angles along the track do not agree with the angles on which the track is based for bovy14 setup" ) return None def test_fardalpot_trackaa(): # Test that the explicitly-calculated frequencies along the track are close to those that the track is based on (Fardal test, #194); used to fail for the potential suggested by Fardal # First setup this specific streamdf instance from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import FlattenedPowerPotential, IsochronePotential from galpy.util import conversion # for unit conversions # test nested list of potentials pot = [ IsochronePotential(b=0.8, normalize=0.8), [FlattenedPowerPotential(alpha=-0.7, q=0.6, normalize=0.2)], ] aAI = actionAngleIsochroneApprox(pot=pot, b=0.9) obs = Orbit([1.10, 0.32, -1.15, 1.10, 0.31, 3.0]) sigv = 1.3 # km/s sdf_fardal = streamdf( sigv / 220.0, progenitor=obs, pot=pot, aA=aAI, leading=True, nTrackChunks=21, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), ) # First test that the misalignment is indeed large assert numpy.fabs(sdf_fardal.misalignment() / numpy.pi * 180.0) > 4.0, ( "misalignment in Fardal test is not large" ) # Now run the test aastream = sdf_fardal._ObsTrackAA # freqs and angles that the track is based on RvR = sdf_fardal._ObsTrack # the track in R,vR,... aastream_expl = numpy.reshape( numpy.array( [sdf_fardal._aA.actionsFreqsAngles(Orbit(trvr))[3:] for trvr in RvR] ), aastream.shape, ) # frequencies, compare to offset between track and progenitor (spread in freq ~ 1/6 that diff, so as long as we're smaller than that we're fine) # print numpy.fabs((aastream[:,:3]-aastream_expl[:,:3])/(aastream[0,:3]-sdf_fardal._progenitor_Omega)) # print numpy.fabs((aastream[:,3:]-aastream_expl[:,3:])/2./numpy.pi) assert numpy.all( numpy.fabs( (aastream[:, :3] - aastream_expl[:, :3]) / (aastream[0, :3] - sdf_fardal._progenitor_Omega) ) < 0.05 ), ( "Explicitly calculated frequencies along the track do not agree with the frequencies on which the track is based for Fardal setup" ) # angles assert numpy.all( numpy.fabs((aastream[:, 3:] - aastream_expl[:, 3:]) / 2.0 / numpy.pi) < 0.001 ), ( "Explicitly calculated angles along the track do not agree with the angles on which the track is based for Fardal setup" ) return None def test_fardalwmwpot_trackaa(): # Test that the explicitly-calculated frequencies along the track are close to those that the track is based on (Fardal test, #194) # First setup this specific streamdf instance from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import MWPotential2014 from galpy.util import conversion # for unit conversions aAI = actionAngleIsochroneApprox(pot=MWPotential2014, b=0.6) obs = Orbit([1.10, 0.32, -1.15, 1.10, 0.31, 3.0]) sigv = 1.3 # km/s sdf_fardal = streamdf( sigv / 220.0, progenitor=obs, pot=MWPotential2014, aA=aAI, leading=True, multi=True, nTrackChunks=21, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), ) # First test that the misalignment is indeed large assert numpy.fabs(sdf_fardal.misalignment() / numpy.pi * 180.0) > 1.0, ( "misalignment in Fardal test is not large enough" ) # Now run the test aastream = sdf_fardal._ObsTrackAA # freqs and angles that the track is based on RvR = sdf_fardal._ObsTrack # the track in R,vR,... aastream_expl = numpy.reshape( numpy.array( [sdf_fardal._aA.actionsFreqsAngles(Orbit(trvr))[3:] for trvr in RvR] ), aastream.shape, ) # frequencies, compare to offset between track and progenitor (spread in freq ~ 1/6 that diff, so as long as we're smaller than that we're fine) # print numpy.fabs((aastream[:,:3]-aastream_expl[:,:3])/(aastream[0,:3]-sdf_fardal._progenitor_Omega)) # print numpy.fabs((aastream[:,3:]-aastream_expl[:,3:])/2./numpy.pi) assert numpy.all( numpy.fabs( (aastream[:, :3] - aastream_expl[:, :3]) / (aastream[0, :3] - sdf_fardal._progenitor_Omega) ) < 0.05 ), ( "Explicitly calculated frequencies along the track do not agree with the frequencies on which the track is based for Fardal setup" ) # angles assert numpy.all( numpy.fabs((aastream[:, 3:] - aastream_expl[:, 3:]) / 2.0 / numpy.pi) < 0.001 ), ( "Explicitly calculated angles along the track do not agree with the angles on which the track is based for Fardal setup" ) return None def test_setup_progIsTrack(): # Test that setting up with progIsTrack=True gives a track that is very close to the given progenitor, such that it works as it should # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596], ro=8.0, vo=220.0, ) sigv = 0.365 # km/s sdfp = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), progIsTrack=True, ) assert numpy.all(numpy.fabs(obs.vxvv[0] - sdfp._ObsTrack[0, :]) < 10.0**-3.0), ( "streamdf setup with progIsTrack does not return a track that is close to the given orbit at the start" ) # Integrate the orbit a little bit and test at a further point obs.integrate(numpy.linspace(0.0, 2.0, 10001), lp) indx = numpy.argmin(numpy.fabs(sdfp._interpolatedObsTrack[:, 0] - 1.75)) oindx = numpy.argmin(numpy.fabs(obs.orbit[0, :, 0] - 1.75)) assert numpy.all( numpy.fabs(sdfp._interpolatedObsTrack[indx, :5] - obs.orbit[0, oindx, :5]) < 10.0**-2.0 ), ( "streamdf setup with progIsTrack does not return a track that is close to the given orbit somewhat further from the start" ) return None def test_bovy14_useTM_poterror(): if WIN32: return None # skip on Windows, because no TM # Test that setting up the stream model with useTM, but a different # actionAngleTorus potential raises a IOError # Imports from galpy.actionAngle import actionAngleIsochroneApprox, actionAngleTorus from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) elp = LogarithmicHaloPotential(normalize=1.0, q=0.8) aAT = actionAngleTorus(pot=elp, tol=0.001) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s with pytest.raises(IOError) as excinfo: sdftm = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, useTM=aAT, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), ) return None def test_bovy14_useTM(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup if WIN32: return None # skip on Windows, because no TM # Test that setting up with useTM is very close to the Bovy (2014) setup # Imports from scipy import interpolate from galpy.actionAngle import actionAngleIsochroneApprox, actionAngleTorus from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) aAT = actionAngleTorus(pot=lp, tol=0.001) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s sdftm = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, useTM=aAT, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), ) sindx = numpy.argsort(sdftm._interpolatedObsTrackLB[:, 0]) interpb = interpolate.InterpolatedUnivariateSpline( sdftm._interpolatedObsTrackLB[sindx, 0], sdftm._interpolatedObsTrackLB[sindx, 1], k=3, ) assert numpy.all( numpy.fabs( interpb(sdf_bovy14._interpolatedObsTrackLB[:, 0]) - sdf_bovy14._interpolatedObsTrackLB[:, 1] ) < 0.1 ), "stream track computed with useTM not close to that without in b" interpD = interpolate.InterpolatedUnivariateSpline( sdftm._interpolatedObsTrackLB[sindx, 0], sdftm._interpolatedObsTrackLB[sindx, 2], k=3, ) assert numpy.all( numpy.fabs( interpD(sdf_bovy14._interpolatedObsTrackLB[:, 0]) - sdf_bovy14._interpolatedObsTrackLB[:, 2] ) < 0.04 ), "stream track computed with useTM not close to that without in distance" interpV = interpolate.InterpolatedUnivariateSpline( sdftm._interpolatedObsTrackLB[sindx, 0], sdftm._interpolatedObsTrackLB[sindx, 3], k=3, ) assert numpy.all( numpy.fabs( interpV(sdf_bovy14._interpolatedObsTrackLB[:, 0]) - sdf_bovy14._interpolatedObsTrackLB[:, 3] ) < 0.6 ), ( "stream track computed with useTM not close to that without in line-of-sight velocity" ) return None def test_bovy14_useTM_useTMHessian(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup if WIN32: return None # skip on Windows, because no TM # Test that setting up with useTM is very close to the Bovy (2014) setup # Imports from scipy import interpolate from galpy.actionAngle import actionAngleIsochroneApprox, actionAngleTorus from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) aAT = actionAngleTorus(pot=lp, tol=0.001) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s sdftm = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, useTM=aAT, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), useTMHessian=True, multi=2, ) sindx = numpy.argsort(sdftm._interpolatedObsTrackLB[:, 0]) interpb = interpolate.InterpolatedUnivariateSpline( sdftm._interpolatedObsTrackLB[sindx, 0], sdftm._interpolatedObsTrackLB[sindx, 1], k=3, ) # Only compare part closest to the progenitor, where this works cindx = sdf_bovy14._interpolatedObsTrackLB[:, 0] < 215.0 assert numpy.all( numpy.fabs( interpb(sdf_bovy14._interpolatedObsTrackLB[cindx, 0]) - sdf_bovy14._interpolatedObsTrackLB[cindx, 1] ) < 0.75 ), ( "stream track computed with useTM and useTMHessian not close to that without in b by %g" % ( numpy.amax( numpy.fabs( interpb(sdf_bovy14._interpolatedObsTrackLB[cindx, 0]) - sdf_bovy14._interpolatedObsTrackLB[cindx, 1] ) ) ) ) interpD = interpolate.InterpolatedUnivariateSpline( sdftm._interpolatedObsTrackLB[sindx, 0], sdftm._interpolatedObsTrackLB[sindx, 2], k=3, ) assert numpy.all( numpy.fabs( interpD(sdf_bovy14._interpolatedObsTrackLB[cindx, 0]) - sdf_bovy14._interpolatedObsTrackLB[cindx, 2] ) < 0.2 ), ( "stream track computed with useTM and useTMHessian not close to that without in distance" ) interpV = interpolate.InterpolatedUnivariateSpline( sdftm._interpolatedObsTrackLB[sindx, 0], sdftm._interpolatedObsTrackLB[sindx, 3], k=3, ) assert numpy.all( numpy.fabs( interpV(sdf_bovy14._interpolatedObsTrackLB[cindx, 0]) - sdf_bovy14._interpolatedObsTrackLB[cindx, 3] ) < 4.0 ), ( "stream track computed with useTM and useTMHessian not close to that without in line-of-sight velocity" ) return None def test_bovy14_useTM_approxConstTrackFreq(bovy14_setup): # Load the streamdf object sdf_bovy14 = bovy14_setup if WIN32: return None # skip on Windows, because no TM # Test that setting up with useTM is very close to the Bovy (2014) setup # Imports from scipy import interpolate from galpy.actionAngle import actionAngleIsochroneApprox, actionAngleTorus from galpy.df import streamdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) aAT = actionAngleTorus(pot=lp, tol=0.001) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) sigv = 0.365 # km/s sdftm = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, useTM=aAT, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(220.0, 8.0), approxConstTrackFreq=True, ) sindx = numpy.argsort(sdftm._interpolatedObsTrackLB[:, 0]) interpb = interpolate.InterpolatedUnivariateSpline( sdftm._interpolatedObsTrackLB[sindx, 0], sdftm._interpolatedObsTrackLB[sindx, 1], k=3, ) # Only compare part closest to the progenitor, where this should be a good approx. cindx = sdf_bovy14._interpolatedObsTrackLB[:, 0] < 215.0 assert numpy.all( numpy.fabs( interpb(sdf_bovy14._interpolatedObsTrackLB[cindx, 0]) - sdf_bovy14._interpolatedObsTrackLB[cindx, 1] ) < 0.1 ), ( "stream track computed with useTM and approxConstTrackFreq not close to that without in b" ) interpD = interpolate.InterpolatedUnivariateSpline( sdftm._interpolatedObsTrackLB[sindx, 0], sdftm._interpolatedObsTrackLB[sindx, 2], k=3, ) assert numpy.all( numpy.fabs( interpD(sdf_bovy14._interpolatedObsTrackLB[cindx, 0]) - sdf_bovy14._interpolatedObsTrackLB[cindx, 2] ) < 0.04 ), ( "stream track computed with useTM and approxConstTrackFreq not close to that without in distance" ) interpV = interpolate.InterpolatedUnivariateSpline( sdftm._interpolatedObsTrackLB[sindx, 0], sdftm._interpolatedObsTrackLB[sindx, 3], k=3, ) assert numpy.all( numpy.fabs( interpV(sdf_bovy14._interpolatedObsTrackLB[cindx, 0]) - sdf_bovy14._interpolatedObsTrackLB[cindx, 3] ) < 0.6 ), ( "stream track computed with useTM and approxConstTrackFreq not close to that without in line-of-sight velocity" ) return None def check_track_prog_diff(sdf, d1, d2, tol, phys=False): observe = [sdf._R0, 0.0, sdf._Zsun] observe.extend(sdf._vsun) # Test that the stream and the progenitor are close together in Z trackR = sdf._parse_track_dim( d1, interp=True, phys=phys ) # bit hacky to use private function trackZ = sdf._parse_track_dim( d2, interp=True, phys=phys ) # bit hacky to use private function ts = sdf._progenitor.t[sdf._progenitor.t < sdf._trackts[-1]] progR = sdf._parse_progenitor_dim( d1, ts, ro=sdf._ro, vo=sdf._vo, obs=observe, phys=phys ) progZ = sdf._parse_progenitor_dim( d2, ts, ro=sdf._ro, vo=sdf._vo, obs=observe, phys=phys ) # Interpolate progenitor, st we can put it on the same grid as the stream interpProgZ = interpolate.InterpolatedUnivariateSpline(progR, progZ, k=3) maxdevZ = numpy.amax(numpy.fabs(interpProgZ(trackR) - trackZ)) assert maxdevZ < tol, ( f"Stream track deviates more from progenitor track in {d2} vs. {d1} than expected; max. deviation = {maxdevZ:f}" ) return None def check_track_spread(sdf, d1, d2, tol1, tol2, phys=False, interp=True): # Check that the spread around the track is small addx, addy = sdf._parse_track_spread(d1, d2, interp=interp, phys=phys) assert numpy.amax(addx) < tol1, ( f"Stream track spread is larger in {d1} than expected; max. deviation = {numpy.amax(addx):f}" ) assert numpy.amax(addy) < tol2, ( f"Stream track spread is larger in {d2} than expected; max. deviation = {numpy.amax(addy):f}" ) return None def check_track_plotting(sdf, d1, d2, phys=False, interp=True, spread=2, ls="-"): # Test that we can plot the stream track if not phys and d1 == "R" and d2 == "Z": # one w/ default sdf.plotTrack(d1=d1, d2=d2, interp=interp, spread=spread) sdf.plotProgenitor(d1=d1, d2=d2) else: sdf.plotTrack( d1=d1, d2=d2, interp=interp, spread=spread, scaleToPhysical=phys, ls="none", color="k", lw=2.0, marker=".", ) sdf.plotProgenitor(d1=d1, d2=d2, scaleToPhysical=phys) return None def check_closest_trackpoint(sdf, trackp, usev=False, xy=True, interp=True): # Check that the closest trackpoint (close )to a trackpoint is the trackpoint if interp: if xy: RvR = sdf._interpolatedObsTrackXY[trackp, :] else: RvR = sdf._interpolatedObsTrack[trackp, :] else: if xy: RvR = sdf._ObsTrackXY[trackp, :] else: RvR = sdf._ObsTrack[trackp, :] R = RvR[0] vR = RvR[1] vT = RvR[2] z = RvR[3] vz = RvR[4] phi = RvR[5] indx = sdf.find_closest_trackpoint( R, vR, vT, z, vz, phi, interp=interp, xy=xy, usev=usev ) assert indx == trackp, "Closest trackpoint to a trackpoint is not that trackpoint" # Same test for a slight offset doff = 10.0**-5.0 indx = sdf.find_closest_trackpoint( R + doff, vR + doff, vT + doff, z + doff, vz + doff, phi + doff, interp=interp, xy=xy, usev=usev, ) assert indx == trackp, ( "Closest trackpoint to close to a trackpoint is not that trackpoint (%i,%i)" % ( indx, trackp, ) ) return None def check_closest_trackpointLB(sdf, trackp, usev=False, interp=True): # Check that the closest trackpoint (close )to a trackpoint is the trackpoint if trackp == -1: # in this case, rm some coordinates trackp = 1 if interp: RvR = sdf._interpolatedObsTrackLB[trackp, :] else: RvR = sdf._ObsTrackLB[trackp, :] R = RvR[0] vR = None vT = RvR[2] z = None vz = RvR[4] phi = None elif trackp == -2: # in this case, rm some coordinates trackp = 1 if interp: RvR = sdf._interpolatedObsTrackLB[trackp, :] else: RvR = sdf._ObsTrackLB[trackp, :] R = None vR = RvR[1] vT = None z = RvR[3] vz = None phi = RvR[5] elif trackp == -3: # in this case, rm some coordinates trackp = 1 if interp: RvR = sdf._interpolatedObsTrackLB[trackp, :] else: RvR = sdf._ObsTrackLB[trackp, :] R = RvR[0] vR = RvR[1] vT = None z = None vz = None phi = None else: if interp: RvR = sdf._interpolatedObsTrackLB[trackp, :] else: RvR = sdf._ObsTrackLB[trackp, :] R = RvR[0] vR = RvR[1] vT = RvR[2] z = RvR[3] vz = RvR[4] phi = RvR[5] indx = sdf.find_closest_trackpointLB( R, vR, vT, z, vz, phi, interp=interp, usev=usev ) assert indx == trackp, ( "Closest trackpoint to a trackpoint is not that trackpoint in LB" ) # Same test for a slight offset doff = 10.0**-5.0 if not R is None: R = R + doff if not vR is None: vR = vR + doff if not vT is None: vT = vT + doff if not z is None: z = z + doff if not vz is None: vz = vz + doff if not phi is None: phi = phi + doff indx = sdf.find_closest_trackpointLB( R, vR, vT, z, vz, phi, interp=interp, usev=usev ) assert indx == trackp, ( "Closest trackpoint to close to a trackpoint is not that trackpoint in LB (%i,%i)" % (indx, trackp) ) return None def check_closest_trackpointaA(sdf, trackp, interp=True): # Check that the closest trackpoint (close )to a trackpoint is the trackpoint if interp: RvR = sdf._interpolatedObsTrackAA[trackp, :] else: RvR = sdf._ObsTrackAA[trackp, :] # These aren't R,vR etc., but frequencies and angles R = RvR[0] vR = RvR[1] vT = RvR[2] z = RvR[3] vz = RvR[4] phi = RvR[5] indx = sdf._find_closest_trackpointaA(R, vR, vT, z, vz, phi, interp=interp) assert indx == trackp, ( "Closest trackpoint to a trackpoint is not that trackpoint for AA" ) # Same test for a slight offset doff = 10.0**-5.0 indx = sdf._find_closest_trackpointaA( R + doff, vR + doff, vT + doff, z + doff, vz + doff, phi + doff, interp=interp ) assert indx == trackp, ( "Closest trackpoint to close to a trackpoint is not that trackpoint for AA (%i,%i)" % (indx, trackp) ) return None def check_approxaA_inv(sdf, tol, R, vR, vT, z, vz, phi, interp=True): # Routine to test that approxaA works # Calculate frequency-angle coords Oa = sdf._approxaA(R, vR, vT, z, vz, phi, interp=interp) # Now go back to real space RvR = sdf._approxaAInv( Oa[0, 0], Oa[1, 0], Oa[2, 0], Oa[3, 0], Oa[4, 0], Oa[5, 0], interp=interp ).flatten() if phi > numpy.pi: phi -= 2.0 * numpy.pi if phi < -numpy.pi: phi += 2.0 * numpy.pi # print numpy.fabs((RvR[0]-R)/R), numpy.fabs((RvR[1]-vR)/vR), numpy.fabs((RvR[2]-vT)/vT), numpy.fabs((RvR[3]-z)/z), numpy.fabs((RvR[4]-vz)/vz), numpy.fabs((RvR[5]-phi)/phi) assert numpy.fabs((RvR[0] - R) / R) < 10.0**tol, ( "R after _approxaA and _approxaAInv does not agree with initial R; relative difference = %g" % (numpy.fabs((RvR[0] - R) / R)) ) assert numpy.fabs((RvR[1] - vR) / vR) < 10.0**tol, ( "vR after _approxaA and _approxaAInv does not agree with initial vR" ) assert numpy.fabs((RvR[2] - vT) / vT) < 10.0**tol, ( "vT after _approxaA and _approxaAInv does not agree with initial vT" ) assert numpy.fabs((RvR[3] - z) / z) < 10.0**tol, ( "z after _approxaA and _approxaAInv does not agree with initial z" ) assert numpy.fabs((RvR[4] - vz) / vz) < 10.0**tol, ( "vz after _approxaA and _approxaAInv does not agree with initial vz" ) assert numpy.fabs((RvR[5] - phi) / numpy.pi) < 10.0**tol, ( "phi after _approxaA and _approxaAInv does not agree with initial phi; relative difference = %g" % (numpy.fabs((RvR[5] - phi) / phi)) ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_streamgapdf.py0000644000175100001660000014011614761352023017370 0ustar00runnerdocker# Tests of streamgapdf implementation, impulse tests moved to # test_streamgapdf_impulse.py import numpy import pytest from scipy import integrate sdf_sanders15 = None # so we can set this up and then use in other tests sdf_sanders15_unp = None # so we can set this up and then use in other tests sdfl_sanders15 = None # so we can set this up and then use in other tests sdfl_sanders15_unp = None # so we can set this up and then use in other tests @pytest.fixture(scope="module") def setup_sanders15_trailing(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf, streamgapdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) prog_unp_peri = Orbit( [ 2.6556151742081835, 0.2183747276300308, 0.67876510797240575, -2.0143395648974671, -0.3273737682604374, 0.24218273922966019, ] ) V0, R0 = 220.0, 8.0 sigv = 0.365 * (10.0 / 2.0) ** (1.0 / 3.0) # km/s sdf_sanders15 = streamgapdf( sigv / V0, progenitor=prog_unp_peri, pot=lp, aA=aAI, leading=False, nTrackChunks=26, nTrackIterations=1, sigMeanOffset=4.5, tdisrupt=10.88 / conversion.time_in_Gyr(V0, R0), Vnorm=V0, Rnorm=R0, impactb=0.0, subhalovel=numpy.array([6.82200571, 132.7700529, 149.4174464]) / V0, timpact=0.88 / conversion.time_in_Gyr(V0, R0), impact_angle=-2.34, GM=10.0**-2.0 / conversion.mass_in_1010msol(V0, R0), rs=0.625 / R0, ) # Also setup the unperturbed model sdf_sanders15_unp = streamdf( sigv / V0, progenitor=prog_unp_peri, pot=lp, aA=aAI, leading=False, nTrackChunks=26, nTrackIterations=1, sigMeanOffset=4.5, tdisrupt=10.88 / conversion.time_in_Gyr(V0, R0), Vnorm=V0, Rnorm=R0, ) return sdf_sanders15, sdf_sanders15_unp @pytest.fixture(scope="module") def setup_sanders15_leading(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamdf, streamgapdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential, PlummerPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) prog_unp_peri = Orbit( [ 2.6556151742081835, 0.2183747276300308, 0.67876510797240575, -2.0143395648974671, -0.3273737682604374, 0.24218273922966019, ] ) V0, R0 = 220.0, 8.0 sigv = 0.365 * (10.0 / 2.0) ** (1.0 / 3.0) # km/s # Use a Potential object for the impact pp = PlummerPotential( amp=10.0**-2.0 / conversion.mass_in_1010msol(V0, R0), b=0.625 / R0 ) import warnings from galpy.util import galpyWarning with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", galpyWarning) sdfl_sanders15 = streamgapdf( sigv / V0, progenitor=prog_unp_peri, pot=lp, aA=aAI, leading=True, nTrackChunks=26, nTrackChunksImpact=29, nTrackIterations=1, sigMeanOffset=4.5, tdisrupt=10.88 / conversion.time_in_Gyr(V0, R0), Vnorm=V0, Rnorm=R0, impactb=0.0, subhalovel=numpy.array([49.447319, 116.179436, 155.104156]) / V0, timpact=0.88 / conversion.time_in_Gyr(V0, R0), impact_angle=2.09, subhalopot=pp, nKickPoints=290, deltaAngleTrackImpact=4.5, multi=True, ) # test multi # Should raise warning bc of deltaAngleTrackImpact, might raise others raisedWarning = False for wa in w: raisedWarning = ( str(wa.message) == "WARNING: deltaAngleTrackImpact angle range large compared to plausible value" ) if raisedWarning: break assert raisedWarning, ( "deltaAngleTrackImpact warning not raised when it should have been" ) # Also setup the unperturbed model sdfl_sanders15_unp = streamdf( sigv / V0, progenitor=prog_unp_peri, pot=lp, aA=aAI, leading=True, nTrackChunks=26, nTrackIterations=1, sigMeanOffset=4.5, tdisrupt=10.88 / conversion.time_in_Gyr(V0, R0), Vnorm=V0, Rnorm=R0, ) return sdfl_sanders15, sdfl_sanders15_unp # Put seed in first function, so the seed gets set even if other test files # were run first def test_setupimpact_error(): numpy.random.seed(1) # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamgapdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) prog_unp_peri = Orbit( [ 2.6556151742081835, 0.2183747276300308, 0.67876510797240575, -2.0143395648974671, -0.3273737682604374, 0.24218273922966019, ] ) V0, R0 = 220.0, 8.0 sigv = 0.365 * (10.0 / 2.0) ** (1.0 / 3.0) # km/s with pytest.raises(IOError) as excinfo: dumb = streamgapdf( sigv / V0, progenitor=prog_unp_peri, pot=lp, aA=aAI, leading=False, nTrackChunks=26, nTrackIterations=1, sigMeanOffset=4.5, tdisrupt=10.88 / conversion.time_in_Gyr(V0, R0), Vnorm=V0, Rnorm=R0, impactb=0.0, subhalovel=numpy.array([6.82200571, 132.7700529, 149.4174464]) / V0, timpact=0.88 / conversion.time_in_Gyr(V0, R0), impact_angle=-2.34, ) # Should be including these: # GM=10.**-2.\ # /conversion.mass_in_1010msol(V0,R0), # rs=0.625/R0) return None def test_leadingwtrailingimpact_error(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamgapdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) prog_unp_peri = Orbit( [ 2.6556151742081835, 0.2183747276300308, 0.67876510797240575, -2.0143395648974671, -0.3273737682604374, 0.24218273922966019, ] ) V0, R0 = 220.0, 8.0 sigv = 0.365 * (10.0 / 2.0) ** (1.0 / 3.0) # km/s with pytest.raises(ValueError) as excinfo: dumb = streamgapdf( sigv / V0, progenitor=prog_unp_peri, pot=lp, aA=aAI, leading=True, nTrackChunks=26, nTrackIterations=1, sigMeanOffset=4.5, tdisrupt=10.88 / conversion.time_in_Gyr(V0, R0), Vnorm=V0, Rnorm=R0, impactb=0.0, subhalovel=numpy.array([6.82200571, 132.7700529, 149.4174464]) / V0, timpact=0.88 / conversion.time_in_Gyr(V0, R0), impact_angle=-2.34, GM=10.0**-2.0 / conversion.mass_in_1010msol(V0, R0), rs=0.625 / R0, ) return None def test_trailingwleadingimpact_error(): # Imports from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import streamgapdf from galpy.orbit import Orbit from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion # for unit conversions lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) prog_unp_peri = Orbit( [ 2.6556151742081835, 0.2183747276300308, 0.67876510797240575, -2.0143395648974671, -0.3273737682604374, 0.24218273922966019, ] ) V0, R0 = 220.0, 8.0 sigv = 0.365 * (10.0 / 2.0) ** (1.0 / 3.0) # km/s with pytest.raises(ValueError) as excinfo: dumb = streamgapdf( sigv / V0, progenitor=prog_unp_peri, pot=lp, aA=aAI, leading=False, nTrackChunks=26, nTrackIterations=1, sigMeanOffset=4.5, tdisrupt=10.88 / conversion.time_in_Gyr(V0, R0), Vnorm=V0, Rnorm=R0, impactb=0.0, subhalovel=numpy.array([6.82200571, 132.7700529, 149.4174464]) / V0, timpact=0.88 / conversion.time_in_Gyr(V0, R0), impact_angle=2.34, GM=10.0**-2.0 / conversion.mass_in_1010msol(V0, R0), rs=0.625 / R0, ) return None # Exact setup from Section 5 of Sanders, Bovy, and Erkal (2015); should reproduce those results (which have been checked against a simulation) def test_sanders15_setup(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing assert not sdf_sanders15 is None, "sanders15 streamgapdf setup did not work" assert not sdf_sanders15_unp is None, ( "sanders15 unperturbed streamdf setup did not work" ) return None def test_sanders15_leading_setup(setup_sanders15_leading): # Load the streamgapdf objects sdfl_sanders15, sdfl_sanders15_unp = setup_sanders15_leading assert not sdfl_sanders15 is None, "sanders15 trailing streamdf setup did not work" assert not sdfl_sanders15_unp is None, ( "sanders15 unperturbed streamdf setup did not work" ) return None # Some very basic tests def test_nTrackIterations(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing assert sdf_sanders15.nTrackIterations == 1, "nTrackIterations should have been 1" return None def test_nTrackChunks(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing assert sdf_sanders15._nTrackChunks == 26, "nTrackChunks should have been 26" return None def test_deltaAngleTrackImpact(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing assert numpy.fabs(sdf_sanders15._deltaAngleTrackImpact - 4.31) < 0.01, ( "deltaAngleTrackImpact should have been ~4.31" ) return None # Tests of the track near the impact def test_trackNearImpact(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Sanity checks against numbers taken from plots of the simulation # Make sure we're near 14.5 assert ( numpy.fabs(sdf_sanders15._gap_ObsTrack[14, 0] * sdf_sanders15._ro - 14.5) < 0.2 ), "14th point along track near the impact is not near 14.5 kpc" assert ( numpy.fabs(sdf_sanders15._gap_ObsTrack[14, 1] * sdf_sanders15._vo - 80) < 3.0 ), ( "Point along the track near impact near R=14.5 does not have the correct radial velocity" ) assert ( numpy.fabs(sdf_sanders15._gap_ObsTrack[14, 2] * sdf_sanders15._vo - 220.0) < 3.0 ), ( "Point along the track near impact near R=14.5 does not have the correct tangential velocity" ) assert ( numpy.fabs(sdf_sanders15._gap_ObsTrack[14, 3] * sdf_sanders15._ro - 0.0) < 1.0 ), ( "Point along the track near impact near R=14.5 does not have the correct vertical height" ) assert ( numpy.fabs(sdf_sanders15._gap_ObsTrack[14, 4] * sdf_sanders15._vo - 200.0) < 5.0 ), ( "Point along the track near impact near R=14.5 does not have the correct vertical velocity" ) # Another one! assert ( numpy.fabs(sdf_sanders15._gap_ObsTrack[27, 0] * sdf_sanders15._ro - 16.25) < 0.2 ), "27th point along track near the impact is not near 16.25 kpc" assert ( numpy.fabs(sdf_sanders15._gap_ObsTrack[27, 1] * sdf_sanders15._vo + 130) < 3.0 ), ( "Point along the track near impact near R=16.25 does not have the correct radial velocity" ) assert ( numpy.fabs(sdf_sanders15._gap_ObsTrack[27, 2] * sdf_sanders15._vo - 200.0) < 3.0 ), ( "Point along the track near impact near R=16.25 does not have the correct tangential velocity" ) assert ( numpy.fabs(sdf_sanders15._gap_ObsTrack[27, 3] * sdf_sanders15._ro + 12.0) < 1.0 ), ( "Point along the track near impact near R=16.25 does not have the correct vertical height" ) assert ( numpy.fabs(sdf_sanders15._gap_ObsTrack[27, 4] * sdf_sanders15._vo - 25.0) < 5.0 ), ( "Point along the track near impact near R=16.25 does not have the correct vertical velocity" ) assert numpy.fabs(sdf_sanders15._gap_ObsTrack[27, 5] - 1.2) < 0.2, ( "Point along the track near impact near R=16.25 does not have the correct azimuth" ) return None def test_interpolatedTrackNearImpact(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Sanity checks against numbers taken from plots of the simulation # Make sure we're near X=-10.9 theta = 2.7 assert ( numpy.fabs(sdf_sanders15._kick_interpTrackX(theta) * sdf_sanders15._ro + 10.9) < 0.2 ), "Point along track near the impact at theta=2.7 is not near X=-10.9 kpc" assert ( numpy.fabs(sdf_sanders15._kick_interpTrackY(theta) * sdf_sanders15._ro - 6.0) < 0.5 ), "Point along track near the impact at theta=2.7 is not near Y=6. kpc" assert ( numpy.fabs(sdf_sanders15._kick_interpTrackZ(theta) * sdf_sanders15._ro + 5.0) < 0.5 ), "Point along track near the impact at theta=2.7 is not near Z=5. kpc" assert ( numpy.fabs(sdf_sanders15._kick_interpTrackvX(theta) * sdf_sanders15._vo + 180.0) < 5 ), "Point along track near the impact at theta=2.7 is not near vX=-180 km/s" assert ( numpy.fabs(sdf_sanders15._kick_interpTrackvY(theta) * sdf_sanders15._vo + 190.0) < 5.0 ), "Point along track near the impact at theta=2.7 is not near vY=190 km/s" assert ( numpy.fabs(sdf_sanders15._kick_interpTrackvZ(theta) * sdf_sanders15._vo - 170.0) < 5.0 ), "Point along track near the impact at theta=2.7 is not near vZ=170 km/s" return None # Test the calculation of the kicks in dv def test_kickdv(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Closest one to the impact point, should be close to zero tIndx = numpy.argmin( numpy.fabs( sdf_sanders15._kick_interpolatedThetasTrack - sdf_sanders15._impact_angle ) ) assert numpy.all( numpy.fabs(sdf_sanders15._kick_deltav[tIndx] * sdf_sanders15._vo) < 0.3 ), "Kick near the impact point not close to zero" # The peak, size and location assert ( numpy.fabs( numpy.amax(numpy.fabs(sdf_sanders15._kick_deltav[:, 0] * sdf_sanders15._vo)) - 0.35 ) < 0.06 ), "Peak dvx incorrect" assert ( sdf_sanders15._kick_interpolatedThetasTrack[ numpy.argmax(sdf_sanders15._kick_deltav[:, 0] * sdf_sanders15._vo) ] - sdf_sanders15._impact_angle < 0.0 ), "Location of peak dvx incorrect" assert ( numpy.fabs( numpy.amax(numpy.fabs(sdf_sanders15._kick_deltav[:, 1] * sdf_sanders15._vo)) - 0.35 ) < 0.06 ), "Peak dvy incorrect" assert ( sdf_sanders15._kick_interpolatedThetasTrack[ numpy.argmax(sdf_sanders15._kick_deltav[:, 1] * sdf_sanders15._vo) ] - sdf_sanders15._impact_angle > 0.0 ), "Location of peak dvy incorrect" assert ( numpy.fabs( numpy.amax(numpy.fabs(sdf_sanders15._kick_deltav[:, 2] * sdf_sanders15._vo)) - 1.8 ) < 0.06 ), "Peak dvz incorrect" assert ( sdf_sanders15._kick_interpolatedThetasTrack[ numpy.argmax(sdf_sanders15._kick_deltav[:, 2] * sdf_sanders15._vo) ] - sdf_sanders15._impact_angle > 0.0 ), "Location of peak dvz incorrect" # Close to zero far from impact point tIndx = numpy.argmin( numpy.fabs( sdf_sanders15._kick_interpolatedThetasTrack - sdf_sanders15._impact_angle - 1.5 ) ) assert numpy.all( numpy.fabs(sdf_sanders15._kick_deltav[tIndx] * sdf_sanders15._vo) < 0.3 ), "Kick far the impact point not close to zero" return None # Test the calculation of the kicks in dO def test_kickdO(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing from galpy.util import conversion # Closest one to the impact point, should be close to zero tIndx = numpy.argmin( numpy.fabs( sdf_sanders15._kick_interpolatedThetasTrack - sdf_sanders15._impact_angle ) ) assert numpy.all( numpy.fabs( sdf_sanders15._kick_dOap[tIndx, :3] * conversion.freq_in_Gyr(sdf_sanders15._vo, sdf_sanders15._ro) ) < 0.03 ), "Kick near the impact point not close to zero" # The peak, size and location assert ( numpy.fabs( numpy.amax( numpy.fabs( sdf_sanders15._kick_dOap[:, 0] * conversion.freq_in_Gyr(sdf_sanders15._vo, sdf_sanders15._ro) ) ) - 0.085 ) < 0.01 ), "Peak dOR incorrect" assert ( sdf_sanders15._kick_interpolatedThetasTrack[ numpy.argmax(sdf_sanders15._kick_dOap[:, 0]) ] - sdf_sanders15._impact_angle < 0.0 ), "Location of peak dOR incorrect" assert ( numpy.fabs( numpy.amax( numpy.fabs( sdf_sanders15._kick_dOap[:, 1] * conversion.freq_in_Gyr(sdf_sanders15._vo, sdf_sanders15._ro) ) ) - 0.07 ) < 0.01 ), "Peak dOp incorrect" assert ( sdf_sanders15._kick_interpolatedThetasTrack[ numpy.argmax(sdf_sanders15._kick_dOap[:, 1]) ] - sdf_sanders15._impact_angle < 0.0 ), "Location of peak dvy incorrect" assert ( numpy.fabs( numpy.amax( numpy.fabs( sdf_sanders15._kick_dOap[:, 2] * conversion.freq_in_Gyr(sdf_sanders15._vo, sdf_sanders15._ro) ) ) - 0.075 ) < 0.01 ), "Peak dOz incorrect" assert ( sdf_sanders15._kick_interpolatedThetasTrack[ numpy.argmax(sdf_sanders15._kick_dOap[:, 2]) ] - sdf_sanders15._impact_angle < 0.0 ), "Location of peak dOz incorrect" # Close to zero far from impact point tIndx = numpy.argmin( numpy.fabs( sdf_sanders15._kick_interpolatedThetasTrack - sdf_sanders15._impact_angle - 1.5 ) ) assert numpy.all( numpy.fabs( sdf_sanders15._kick_dOap[tIndx, :3] * conversion.freq_in_Gyr(sdf_sanders15._vo, sdf_sanders15._ro) ) < 0.03 ), "Kick far the impact point not close to zero" return None def test_kickda(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # All angle kicks should be small, just test that they are smaller than dO/O close to the impact nIndx = ( numpy.fabs( sdf_sanders15._kick_interpolatedThetasTrack - sdf_sanders15._impact_angle ) < 0.75 ) assert numpy.all( numpy.fabs(sdf_sanders15._kick_dOap[nIndx, 3:]) < 2.0 * ( numpy.fabs( sdf_sanders15._kick_dOap[nIndx, :3] / sdf_sanders15._progenitor_Omega ) ) ), "angle kicks not smaller than the frequency kicks" return None # Test the interpolation of the kicks def test_interpKickdO(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing from galpy.util import conversion freqConv = conversion.freq_in_Gyr(sdf_sanders15._vo, sdf_sanders15._ro) # Bunch of spot checks at some interesting angles # Impact angle theta = sdf_sanders15._impact_angle assert numpy.fabs(sdf_sanders15._kick_interpdOpar(theta) * freqConv) < 10.0**-4.0, ( "Frequency kick at the impact point is not zero" ) assert ( numpy.fabs(sdf_sanders15._kick_interpdOperp0(theta) * freqConv) < 10.0**-4.0 ), "Frequency kick at the impact point is not zero" assert ( numpy.fabs(sdf_sanders15._kick_interpdOperp1(theta) * freqConv) < 10.0**-4.0 ), "Frequency kick at the impact point is not zero" assert numpy.fabs(sdf_sanders15._kick_interpdOr(theta) * freqConv) < 10.0**-4.0, ( "Frequency kick at the impact point is not zero" ) assert numpy.fabs(sdf_sanders15._kick_interpdOp(theta) * freqConv) < 10.0**-4.0, ( "Frequency kick at the impact point is not zero" ) assert numpy.fabs(sdf_sanders15._kick_interpdOz(theta) * freqConv) < 10.0**-4.0, ( "Frequency kick at the impact point is not zero" ) # random one theta = sdf_sanders15._impact_angle - 0.25 assert ( numpy.fabs(sdf_sanders15._kick_interpdOpar(theta) * freqConv + 0.07) < 0.002 ), "Frequency kick near the impact point is not zero" assert numpy.fabs(sdf_sanders15._kick_interpdOperp0(theta) * freqConv) < 0.002, ( "Frequency kick near the impact point is not zero" ) assert numpy.fabs(sdf_sanders15._kick_interpdOperp1(theta) * freqConv) < 0.003, ( "Frequency kick near the impact point is not zero" ) assert numpy.fabs(sdf_sanders15._kick_interpdOr(theta) * freqConv - 0.05) < 0.01, ( "Frequency kick near the impact point is not zero" ) assert numpy.fabs(sdf_sanders15._kick_interpdOp(theta) * freqConv - 0.035) < 0.01, ( "Frequency kick near the impact point is not zero" ) assert numpy.fabs(sdf_sanders15._kick_interpdOz(theta) * freqConv - 0.04) < 0.01, ( "Frequency kick near the impact point is not zero" ) # One beyond ._deltaAngleTrackImpact theta = sdf_sanders15._deltaAngleTrackImpact + 0.1 assert ( numpy.fabs(sdf_sanders15._kick_interpdOpar(theta) * freqConv) < 10.0**-16.0 ), "Frequency kick beyond ._deltaAngleTrackImpact is not zero" assert ( numpy.fabs(sdf_sanders15._kick_interpdOperp0(theta) * freqConv) < 10.0**-16.0 ), "Frequency kick beyond ._deltaAngleTrackImpact is not zero" assert ( numpy.fabs(sdf_sanders15._kick_interpdOperp1(theta) * freqConv) < 10.0**-16.0 ), "Frequency kick beyond ._deltaAngleTrackImpact is not zero" assert numpy.fabs(sdf_sanders15._kick_interpdOr(theta) * freqConv) < 10.0**-16.0, ( "Frequency kick beyond ._deltaAngleTrackImpact is not zero" ) assert numpy.fabs(sdf_sanders15._kick_interpdOp(theta) * freqConv) < 10.0**-16.0, ( "Frequency kick beyond ._deltaAngleTrackImpact is not zero" ) assert numpy.fabs(sdf_sanders15._kick_interpdOz(theta) * freqConv) < 10.0**-16.0, ( "Frequency kick beyond ._deltaAngleTrackImpact is not zero" ) assert numpy.fabs(sdf_sanders15._kick_interpdar(theta)) < 10.0**-16.0, ( "Angle kick beyond ._deltaAngleTrackImpact is not zero" ) assert numpy.fabs(sdf_sanders15._kick_interpdap(theta)) < 10.0**-16.0, ( "Angle kick beyond ._deltaAngleTrackImpact is not zero" ) assert numpy.fabs(sdf_sanders15._kick_interpdaz(theta)) < 10.0**-16.0, ( "Angle kick beyond ._deltaAngleTrackImpact is not zero" ) return None def test_interpKickda(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing thetas = numpy.linspace(-0.75, 0.75, 10) + sdf_sanders15._impact_angle assert numpy.all( numpy.fabs(sdf_sanders15._kick_interpdar(thetas)) < 2.0 * numpy.fabs( sdf_sanders15._kick_interpdOr(thetas) / sdf_sanders15._progenitor_Omegar ) ), ( "Interpolated angle kick not everywhere smaller than the frequency kick after one period" ) return None # Test the sampling of present-day perturbed points based on the model def test_sample(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Sample stars from the model and compare them to the stream xv_mock_per = sdf_sanders15.sample(n=100000, xy=True).T # Rough gap-density check ingap = numpy.sum( (xv_mock_per[:, 0] * sdf_sanders15._ro > 4.0) * (xv_mock_per[:, 0] * sdf_sanders15._ro < 5.0) ) edgegap = numpy.sum( (xv_mock_per[:, 0] * sdf_sanders15._ro > 1.0) * (xv_mock_per[:, 0] * sdf_sanders15._ro < 2.0) ) outgap = numpy.sum( (xv_mock_per[:, 0] * sdf_sanders15._ro > -2.5) * (xv_mock_per[:, 0] * sdf_sanders15._ro < -1.5) ) assert numpy.fabs(ingap / float(edgegap) - 0.015 / 0.05) < 0.05, ( "gap density versus edge of the gap is incorrect" ) assert numpy.fabs(ingap / float(outgap) - 0.015 / 0.02) < 0.2, ( "gap density versus outside of the gap is incorrect" ) # Test track of the stream tIndx = ( (xv_mock_per[:, 0] * sdf_sanders15._ro > 4.0) * (xv_mock_per[:, 0] * sdf_sanders15._ro < 5.0) * (xv_mock_per[:, 1] * sdf_sanders15._ro < 5.0) ) assert ( numpy.fabs(numpy.median(xv_mock_per[tIndx, 1]) * sdf_sanders15._ro + 12.25) < 0.1 ), "Location of mock track is incorrect near the gap" assert ( numpy.fabs(numpy.median(xv_mock_per[tIndx, 2]) * sdf_sanders15._ro - 3.8) < 0.1 ), "Location of mock track is incorrect near the gap" assert ( numpy.fabs(numpy.median(xv_mock_per[tIndx, 3]) * sdf_sanders15._vo - 255.0) < 2.0 ), "Location of mock track is incorrect near the gap" assert ( numpy.fabs(numpy.median(xv_mock_per[tIndx, 4]) * sdf_sanders15._vo - 20.0) < 2.0 ), "Location of mock track is incorrect near the gap" assert ( numpy.fabs(numpy.median(xv_mock_per[tIndx, 5]) * sdf_sanders15._vo + 185.0) < 2.0 ), "Location of mock track is incorrect near the gap" return None # Test the sampling of present-day perturbed-unperturbed points # (like in the paper) def test_sample_offset(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Sample stars from the model and compare them to the stream numpy.random.seed(1) xv_mock_per = sdf_sanders15.sample(n=100000, xy=True).T numpy.random.seed(1) # should give same points xv_mock_unp = sdf_sanders15_unp.sample(n=100000, xy=True).T # Test perturbation as a function of unperturbed X tIndx = ( (xv_mock_unp[:, 0] * sdf_sanders15._ro > 0.0) * (xv_mock_unp[:, 0] * sdf_sanders15._ro < 1.0) * (xv_mock_unp[:, 1] * sdf_sanders15._ro < 5.0) ) assert ( numpy.fabs( numpy.median(xv_mock_per[tIndx, 0] - xv_mock_unp[tIndx, 0]) * sdf_sanders15._ro + 0.65 ) < 0.1 ), "Location of perturbed mock track is incorrect near the gap" assert ( numpy.fabs( numpy.median(xv_mock_per[tIndx, 1] - xv_mock_unp[tIndx, 1]) * sdf_sanders15._ro - 0.1 ) < 0.1 ), "Location of perturbed mock track is incorrect near the gap" assert ( numpy.fabs( numpy.median(xv_mock_per[tIndx, 2] - xv_mock_unp[tIndx, 2]) * sdf_sanders15._ro - 0.4 ) < 0.1 ), "Location of perturbed mock track is incorrect near the gap" assert ( numpy.fabs( numpy.median(xv_mock_per[tIndx, 3] - xv_mock_unp[tIndx, 3]) * sdf_sanders15._vo ) < 0.5 ), "Location of perturbed mock track is incorrect near the gap" assert ( numpy.fabs( numpy.median(xv_mock_per[tIndx, 4] - xv_mock_unp[tIndx, 4]) * sdf_sanders15._vo + 7.0 ) < 0.5 ), "Location of perturbed mock track is incorrect near the gap" assert ( numpy.fabs( numpy.median(xv_mock_per[tIndx, 5] - xv_mock_unp[tIndx, 5]) * sdf_sanders15._vo - 4.0 ) < 0.5 ), "Location of perturbed mock track is incorrect near the gap" return None # Test the sampling of present-day perturbed-unperturbed points # (like in the paper, but for the leading stream impact) def test_sample_offset_leading(setup_sanders15_leading): # Load the streamgapdf objects sdfl_sanders15, sdfl_sanders15_unp = setup_sanders15_leading # Sample stars from the model and compare them to the stream numpy.random.seed(1) xv_mock_per = sdfl_sanders15.sample(n=100000, xy=True).T numpy.random.seed(1) # should give same points xv_mock_unp = sdfl_sanders15_unp.sample(n=100000, xy=True).T # Test perturbation as a function of unperturbed X tIndx = ( (xv_mock_unp[:, 0] * sdfl_sanders15._ro > 13.0) * (xv_mock_unp[:, 0] * sdfl_sanders15._ro < 14.0) * (xv_mock_unp[:, 1] * sdfl_sanders15._ro > 5.0) ) assert ( numpy.fabs( numpy.median(xv_mock_per[tIndx, 0] - xv_mock_unp[tIndx, 0]) * sdfl_sanders15._ro + 0.5 ) < 0.1 ), "Location of perturbed mock track is incorrect near the gap" assert ( numpy.fabs( numpy.median(xv_mock_per[tIndx, 1] - xv_mock_unp[tIndx, 1]) * sdfl_sanders15._ro - 0.3 ) < 0.1 ), "Location of perturbed mock track is incorrect near the gap" assert ( numpy.fabs( numpy.median(xv_mock_per[tIndx, 2] - xv_mock_unp[tIndx, 2]) * sdfl_sanders15._ro - 0.45 ) < 0.1 ), "Location of perturbed mock track is incorrect near the gap" assert ( numpy.fabs( numpy.median(xv_mock_per[tIndx, 3] - xv_mock_unp[tIndx, 3]) * sdfl_sanders15._vo + 2.0 ) < 0.5 ), "Location of perturbed mock track is incorrect near the gap" assert ( numpy.fabs( numpy.median(xv_mock_per[tIndx, 4] - xv_mock_unp[tIndx, 4]) * sdfl_sanders15._vo + 7.0 ) < 0.5 ), "Location of perturbed mock track is incorrect near the gap" assert ( numpy.fabs( numpy.median(xv_mock_per[tIndx, 5] - xv_mock_unp[tIndx, 5]) * sdfl_sanders15._vo - 6.0 ) < 0.5 ), "Location of perturbed mock track is incorrect near the gap" return None # Tests of the density and meanOmega functions def test_pOparapar(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Test that integrating pOparapar gives density_par dens_frompOpar_close = integrate.quad( lambda x: sdf_sanders15.pOparapar(x, 0.3), sdf_sanders15._meandO - 10.0 * numpy.sqrt(sdf_sanders15._sortedSigOEig[2]), sdf_sanders15._meandO + 10.0 * numpy.sqrt(sdf_sanders15._sortedSigOEig[2]), )[0] # This is actually in the gap! dens_fromOpar_half = integrate.quad( lambda x: sdf_sanders15.pOparapar(x, 2.6), sdf_sanders15._meandO - 10.0 * numpy.sqrt(sdf_sanders15._sortedSigOEig[2]), sdf_sanders15._meandO + 10.0 * numpy.sqrt(sdf_sanders15._sortedSigOEig[2]), )[0] assert ( numpy.fabs( dens_fromOpar_half / dens_frompOpar_close - sdf_sanders15.density_par(2.6) / sdf_sanders15.density_par(0.3) ) < 10.0**-4.0 ), ( "density from integrating pOparapar not equal to that from density_par for Sanders15 stream" ) return None def test_density_apar_approx(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Test that the approximate density agrees with the direct integration # Need to do this relatively to another density, because there is an # overall offset apar = 2.6 assert ( numpy.fabs( sdf_sanders15.density_par(apar, approx=False) / sdf_sanders15.density_par(apar, approx=True) / sdf_sanders15.density_par(0.3, approx=False) * sdf_sanders15.density_par(0.3, approx=True) - 1.0 ) < 10.0**-3.0 ), "Approximate density does not agree with direct integration" apar = 2.3 assert ( numpy.fabs( sdf_sanders15.density_par(apar, approx=False) / sdf_sanders15.density_par(apar, approx=True) / sdf_sanders15.density_par(0.3, approx=False) * sdf_sanders15.density_par(0.3, approx=True) - 1.0 ) < 10.0**-3.0 ), "Approximate density does not agree with direct integration" return None def test_density_apar_approx_higherorder(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Test that the approximate density agrees with the direct integration # Need to do this relatively to another density, because there is an # overall offset apar = 2.6 assert ( numpy.fabs( sdf_sanders15.density_par(apar, approx=False) / sdf_sanders15.density_par(apar, approx=True, higherorder=True) / sdf_sanders15.density_par(0.3, approx=False) * sdf_sanders15.density_par(0.3, approx=True, higherorder=True) - 1.0 ) < 10.0**-3.0 ), "Approximate density does not agree with direct integration" apar = 2.3 assert ( numpy.fabs( sdf_sanders15.density_par(apar, approx=False) / sdf_sanders15.density_par(apar, approx=True, higherorder=True) / sdf_sanders15.density_par(0.3, approx=False) * sdf_sanders15.density_par(0.3, approx=True, higherorder=True) - 1.0 ) < 10.0**-3.0 ), "Approximate density does not agree with direct integration" return None def test_minOpar(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Test that for Opar < minOpar, p(Opar,apar) is in fact zero! apar = 0.3 dO = 10.0**-4.0 assert ( numpy.fabs(sdf_sanders15.pOparapar(sdf_sanders15.minOpar(apar) - dO, apar)) < 10.0**-16.0 ), "Probability for Opar < minOpar is not zero" apar = 2.6 dO = 10.0**-4.0 assert ( numpy.fabs(sdf_sanders15.pOparapar(sdf_sanders15.minOpar(apar) - dO, apar)) < 10.0**-16.0 ), "Probability for Opar < minOpar is not zero" return None def test_meanOmega_approx(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Test that the approximate meanOmega agrees with the direct integration # Need to do this relatively to another density, because there is an # overall offset apar = 2.6 assert ( numpy.fabs( sdf_sanders15.meanOmega(apar, approx=False, oned=True) / sdf_sanders15.meanOmega(apar, approx=True, oned=True) - 1.0 ) < 10.0**-3.0 ), "Approximate meanOmega does not agree with direct integration" apar = 2.3 assert ( numpy.fabs( sdf_sanders15.meanOmega(apar, approx=False, oned=True) / sdf_sanders15.meanOmega(apar, approx=True, oned=True) - 1.0 ) < 10.0**-3.0 ), "Approximate meanOmega does not agree with direct integration" return None def test_meanOmega_approx_higherorder(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Test that the approximate meanOmega agrees with the direct integration # Need to do this relatively to another density, because there is an # overall offset apar = 2.6 assert ( numpy.fabs( sdf_sanders15.meanOmega(apar, approx=False, oned=True) / sdf_sanders15.meanOmega(apar, approx=True, higherorder=True, oned=True) - 1.0 ) < 10.0**-3.0 ), "Approximate meanOmega does not agree with direct integration" apar = 2.3 assert ( numpy.fabs( sdf_sanders15.meanOmega(apar, approx=False, oned=True) / sdf_sanders15.meanOmega(apar, approx=True, higherorder=True, oned=True) - 1.0 ) < 10.0**-3.0 ), "Approximate meanOmega does not agree with direct integration" return None def test_hernquist(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Test that Hernquist kicks are similar to Plummer kicks, but are # different in understood ways (...) from galpy.util import conversion # Switch to Hernquist V0, R0 = 220.0, 8.0 impactb = 0.0 subhalovel = numpy.array([6.82200571, 132.7700529, 149.4174464]) / V0 impact_angle = -2.34 GM = 10.0**-2.0 / conversion.mass_in_1010msol(V0, R0) rs = 0.625 / R0 sdf_sanders15._determine_deltav_kick( impact_angle, impactb, subhalovel, GM, rs, None, 3, True ) hernquist_kicks = sdf_sanders15._kick_deltav # Back to Plummer sdf_sanders15._determine_deltav_kick( impact_angle, impactb, subhalovel, GM, rs, None, 3, False ) # Repeat some of the deltav tests from above # Closest one to the impact point, should be close to zero tIndx = numpy.argmin( numpy.fabs( sdf_sanders15._kick_interpolatedThetasTrack - sdf_sanders15._impact_angle ) ) assert numpy.all(numpy.fabs(hernquist_kicks[tIndx] * sdf_sanders15._vo) < 0.4), ( "Kick near the impact point not close to zero for Hernquist" ) # The peak, size and location # Peak should be slightly less (guessed these correct!) assert ( numpy.fabs( numpy.amax(numpy.fabs(hernquist_kicks[:, 0] * sdf_sanders15._vo)) - 0.25 ) < 0.06 ), "Peak dvx incorrect" assert ( sdf_sanders15._kick_interpolatedThetasTrack[ numpy.argmax(hernquist_kicks[:, 0] * sdf_sanders15._vo) ] - sdf_sanders15._impact_angle < 0.0 ), "Location of peak dvx incorrect" assert ( numpy.fabs( numpy.amax(numpy.fabs(hernquist_kicks[:, 1] * sdf_sanders15._vo)) - 0.25 ) < 0.06 ), "Peak dvy incorrect" assert ( sdf_sanders15._kick_interpolatedThetasTrack[ numpy.argmax(hernquist_kicks[:, 1] * sdf_sanders15._vo) ] - sdf_sanders15._impact_angle > 0.0 ), "Location of peak dvy incorrect" assert ( numpy.fabs( numpy.amax(numpy.fabs(hernquist_kicks[:, 2] * sdf_sanders15._vo)) - 1.3 ) < 0.06 ), "Peak dvz incorrect" assert ( sdf_sanders15._kick_interpolatedThetasTrack[ numpy.argmax(hernquist_kicks[:, 2] * sdf_sanders15._vo) ] - sdf_sanders15._impact_angle > 0.0 ), "Location of peak dvz incorrect" # Close to zero far from impact point tIndx = numpy.argmin( numpy.fabs( sdf_sanders15._kick_interpolatedThetasTrack - sdf_sanders15._impact_angle - 1.5 ) ) assert numpy.all(numpy.fabs(hernquist_kicks[tIndx] * sdf_sanders15._vo) < 0.3), ( "Kick far the impact point not close to zero" ) return None def test_determine_deltav_valueerrort(setup_sanders15_trailing): # Load the streamgapdf objects sdf_sanders15, sdf_sanders15_unp = setup_sanders15_trailing # Test that modeling leading (trailing) impact for trailing (leading) arm # raises a ValueError when using _determine_deltav_kick from galpy.util import conversion # Switch to Hernquist V0, R0 = 220.0, 8.0 impactb = 0.0 subhalovel = numpy.array([6.82200571, 132.7700529, 149.4174464]) / V0 impact_angle = -2.34 GM = 10.0**-2.0 / conversion.mass_in_1010msol(V0, R0) rs = 0.625 / R0 # Can't do minus impact angle! with pytest.raises(ValueError) as excinfo: sdf_sanders15._determine_deltav_kick( -impact_angle, impactb, subhalovel, GM, rs, None, 3, True ) return None # Test the routine that rotates vectors to an arbitrary vector def test_rotate_to_arbitrary_vector(): from galpy.df.streamgapdf import _rotate_to_arbitrary_vector tol = -10.0 v = numpy.array([[1.0, 0.0, 0.0]]) # Rotate to 90 deg off ma = _rotate_to_arbitrary_vector(v, [0, 1.0, 0]) assert numpy.fabs(ma[0, 0, 1] + 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 1, 0] - 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 2, 2] - 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 0, 0]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 0, 2]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 1, 1]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 1, 2]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 2, 0]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 2, 1]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) # Rotate to 90 deg off ma = _rotate_to_arbitrary_vector(v, [0, 0, 1.0]) assert numpy.fabs(ma[0, 0, 2] + 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 2, 0] - 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 1, 1] - 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 0, 0]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 0, 1]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 2, 2]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 2, 1]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 1, 0]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 1, 2]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) # Rotate to same should be unit matrix ma = _rotate_to_arbitrary_vector(v, v[0]) assert numpy.all(numpy.fabs(numpy.diag(ma[0]) - 1.0) < 10.0**tol), ( "Rotation matrix to same vector is not unity" ) assert numpy.fabs(numpy.sum(ma**2.0) - 3.0) < 10.0**tol, ( "Rotation matrix to same vector is not unity" ) # Rotate to -same should be -unit matrix ma = _rotate_to_arbitrary_vector(v, -v[0]) assert numpy.all(numpy.fabs(numpy.diag(ma[0]) + 1.0) < 10.0**tol), ( "Rotation matrix to minus same vector is not minus unity" ) assert numpy.fabs(numpy.sum(ma**2.0) - 3.0) < 10.0**tol, ( "Rotation matrix to minus same vector is not minus unity" ) return None # Test that the rotation routine works for multiple vectors def test_rotate_to_arbitrary_vector_multi(): from galpy.df.streamgapdf import _rotate_to_arbitrary_vector tol = -10.0 v = numpy.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]) # Rotate to 90 deg off ma = _rotate_to_arbitrary_vector(v, [0, 0, 1.0]) assert numpy.fabs(ma[0, 0, 2] + 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 2, 0] - 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 1, 1] - 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 0, 0]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 0, 1]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 2, 2]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 2, 1]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 1, 0]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 1, 2]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) # 2nd assert numpy.fabs(ma[1, 1, 2] + 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[1, 2, 1] - 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[1, 0, 0] - 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[1, 0, 1]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[1, 0, 2]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[1, 1, 0]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[1, 1, 1]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[1, 2, 0]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[1, 2, 2]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) return None # Test the inverse of the routine that rotates vectors to an arbitrary vector def test_rotate_to_arbitrary_vector_inverse(): from galpy.df.streamgapdf import _rotate_to_arbitrary_vector tol = -10.0 v = numpy.array([[1.0, 0.0, 0.0]]) # Rotate to random vector and back a = numpy.random.uniform(size=3) a /= numpy.sqrt(numpy.sum(a**2.0)) ma = _rotate_to_arbitrary_vector(v, a) ma_inv = _rotate_to_arbitrary_vector(v, a, inv=True) ma = numpy.dot(ma[0], ma_inv[0]) assert numpy.all(numpy.fabs(ma - numpy.eye(3)) < 10.0**tol), ( "Inverse rotation matrix incorrect" ) return None # Test that rotating to vy in particular works as expected def test_rotation_vy(): from galpy.df.streamgapdf import _rotation_vy tol = -10.0 v = numpy.array([[1.0, 0.0, 0.0]]) # Rotate to 90 deg off ma = _rotation_vy(v) assert numpy.fabs(ma[0, 0, 1] + 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 1, 0] - 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 2, 2] - 1.0) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 0, 0]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 0, 2]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 1, 1]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 1, 2]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 2, 0]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) assert numpy.fabs(ma[0, 2, 1]) < 10.0**tol, ( "Rotation matrix to 90 deg off incorrect" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_streamgapdf_impulse.py0000644000175100001660000005405214761352023021131 0ustar00runnerdocker# Tests of the various variations on the impulse approx. from Sanders, Bovy, # & Erkal (2016) import numpy import pytest # Test the Plummer calculation for a perpendicular impact, B&T ex. 8.7 def test_impulse_deltav_plummer_subhalo_perpendicular(): from galpy.df import impulse_deltav_plummer tol = -10.0 kick = impulse_deltav_plummer( numpy.array([[0.0, numpy.pi, 0.0]]), numpy.array([0.0]), 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), 1.5, 4.0, ) # Should be B&T (8.152) assert ( numpy.fabs(kick[0, 0] - 2.0 * 1.5 * 3.0 / numpy.pi * 2.0 / 25.0) < 10.0**tol ), "Perpendicular kick of subhalo perpendicular not as expected" assert ( numpy.fabs(kick[0, 2] + 2.0 * 1.5 * 3.0 / numpy.pi * 2.0 / 25.0) < 10.0**tol ), "Perpendicular kick of subhalo perpendicular not as expected" # Same for along z kick = impulse_deltav_plummer( numpy.array([[0.0, 0.0, numpy.pi]]), numpy.array([0.0]), 3.0, numpy.array([0.0, 0.0, numpy.pi / 2.0]), 1.5, 4.0, ) # Should be B&T (8.152) assert ( numpy.fabs(kick[0, 0] - 2.0 * 1.5 * 3.0 / numpy.pi * 2.0 / 25.0) < 10.0**tol ), "Perpendicular kick of subhalo perpendicular not as expected" assert ( numpy.fabs(kick[0, 1] - 2.0 * 1.5 * 3.0 / numpy.pi * 2.0 / 25.0) < 10.0**tol ), "Perpendicular kick of subhalo perpendicular not as expected" return None # Test the Plummer curved calculation for a perpendicular impact def test_impulse_deltav_plummer_curved_subhalo_perpendicular(): from galpy.df import impulse_deltav_plummer, impulse_deltav_plummer_curvedstream tol = -10.0 kick = impulse_deltav_plummer( numpy.array([[3.4, 0.0, 0.0]]), numpy.array([4.0]), 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), 1.5, 4.0, ) curved_kick = impulse_deltav_plummer_curvedstream( numpy.array([[3.4, 0.0, 0.0]]), numpy.array([[4.0, 0.0, 0.0]]), 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), numpy.array([0.0, 0.0, 0.0]), numpy.array([3.4, 0.0, 0.0]), 1.5, 4.0, ) # Should be equal assert numpy.all(numpy.fabs(kick - curved_kick) < 10.0**tol), ( "curved Plummer kick does not agree with straight kick for straight track" ) # Same for a bunch of positions v = numpy.zeros((100, 3)) v[:, 0] = 3.4 xpos = numpy.random.normal(size=100) kick = impulse_deltav_plummer( v, xpos, 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), 1.5, 4.0 ) xpos = numpy.array([xpos, numpy.zeros(100), numpy.zeros(100)]).T curved_kick = impulse_deltav_plummer_curvedstream( v, xpos, 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), numpy.array([0.0, 0.0, 0.0]), numpy.array([3.4, 0.0, 0.0]), 1.5, 4.0, ) # Should be equal assert numpy.all(numpy.fabs(kick - curved_kick) < 10.0**tol), ( "curved Plummer kick does not agree with straight kick for straight track" ) return None # Test general impulse vs. Plummer def test_impulse_deltav_general(): from galpy.df import impulse_deltav_general, impulse_deltav_plummer from galpy.potential import PlummerPotential tol = -10.0 kick = impulse_deltav_plummer( numpy.array([[3.4, 0.0, 0.0]]), numpy.array([4.0]), 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), 1.5, 4.0, ) pp = PlummerPotential(amp=1.5, b=4.0) general_kick = impulse_deltav_general( numpy.array([[3.4, 0.0, 0.0]]), numpy.array([4.0]), 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), pp, ) assert numpy.all(numpy.fabs(kick - general_kick) < 10.0**tol), ( "general kick calculation does not agree with Plummer calculation for a Plummer potential" ) # Same for a bunch of positions v = numpy.zeros((100, 3)) v[:, 0] = 3.4 xpos = numpy.random.normal(size=100) kick = impulse_deltav_plummer( v, xpos, 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), numpy.pi, numpy.exp(1.0) ) pp = PlummerPotential(amp=numpy.pi, b=numpy.exp(1.0)) general_kick = impulse_deltav_general( v, xpos, 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), pp ) assert numpy.all(numpy.fabs(kick - general_kick) < 10.0**tol), ( "general kick calculation does not agree with Plummer calculation for a Plummer potential" ) return None # Test general impulse vs. Plummer for curved stream def test_impulse_deltav_general_curved(): from galpy.df import ( impulse_deltav_general_curvedstream, impulse_deltav_plummer_curvedstream, ) from galpy.potential import PlummerPotential tol = -10.0 kick = impulse_deltav_plummer_curvedstream( numpy.array([[3.4, 0.0, 0.0]]), numpy.array([[4.0, 0.0, 0.0]]), 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), numpy.array([0.0, 0.0, 0.0]), numpy.array([3.4, 0.0, 0.0]), 1.5, 4.0, ) pp = PlummerPotential(amp=1.5, b=4.0) general_kick = impulse_deltav_general_curvedstream( numpy.array([[3.4, 0.0, 0.0]]), numpy.array([[4.0, 0.0, 0.0]]), 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), numpy.array([0.0, 0.0, 0.0]), numpy.array([3.4, 0.0, 0.0]), pp, ) assert numpy.all(numpy.fabs(kick - general_kick) < 10.0**tol), ( "general kick calculation does not agree with Plummer calculation for a Plummer potential, for curved stream" ) # Same for a bunch of positions v = numpy.zeros((100, 3)) v[:, 0] = 3.4 xpos = numpy.random.normal(size=100) xpos = numpy.array([xpos, numpy.zeros(100), numpy.zeros(100)]).T kick = impulse_deltav_plummer_curvedstream( v, xpos, 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), numpy.array([0.0, 0.0, 0.0]), numpy.array([3.4, 0.0, 0.0]), numpy.pi, numpy.exp(1.0), ) pp = PlummerPotential(amp=numpy.pi, b=numpy.exp(1.0)) general_kick = impulse_deltav_general_curvedstream( v, xpos, 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), numpy.array([0.0, 0.0, 0.0]), numpy.array([3.4, 0.0, 0.0]), pp, ) assert numpy.all(numpy.fabs(kick - general_kick) < 10.0**tol), ( "general kick calculation does not agree with Plummer calculation for a Plummer potential, for curved stream" ) return None # Test general impulse vs. Hernquist def test_impulse_deltav_general_hernquist(): from galpy.df import impulse_deltav_general, impulse_deltav_hernquist from galpy.potential import HernquistPotential GM = 1.5 tol = -10.0 kick = impulse_deltav_hernquist( numpy.array([3.4, 0.0, 0.0]), numpy.array([4.0]), 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), GM, 4.0, ) # Note factor of 2 in definition of GM and amp pp = HernquistPotential(amp=2.0 * GM, a=4.0) general_kick = impulse_deltav_general( numpy.array([3.4, 0.0, 0.0]), numpy.array([4.0]), 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), pp, ) assert numpy.all(numpy.fabs(kick - general_kick) < 10.0**tol), ( "general kick calculation does not agree with Hernquist calculation for a Hernquist potential" ) # Same for a bunch of positions GM = numpy.pi v = numpy.zeros((100, 3)) v[:, 0] = 3.4 xpos = numpy.random.normal(size=100) kick = impulse_deltav_hernquist( v, xpos, 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), GM, numpy.exp(1.0) ) pp = HernquistPotential(amp=2.0 * GM, a=numpy.exp(1.0)) general_kick = impulse_deltav_general( v, xpos, 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), pp ) assert numpy.all(numpy.fabs(kick - general_kick) < 10.0**tol), ( "general kick calculation does not agree with Hernquist calculation for a Hernquist potential" ) return None # Test general impulse vs. Hernquist for curved stream def test_impulse_deltav_general_curved_hernquist(): from galpy.df import ( impulse_deltav_general_curvedstream, impulse_deltav_hernquist_curvedstream, ) from galpy.potential import HernquistPotential GM = 1.5 tol = -10.0 kick = impulse_deltav_hernquist_curvedstream( numpy.array([3.4, 0.0, 0.0]), numpy.array([4.0, 0.0, 0.0]), 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), numpy.array([0.0, 0.0, 0.0]), numpy.array([3.4, 0.0, 0.0]), GM, 4.0, ) # Note factor of 2 in definition of GM and amp pp = HernquistPotential(amp=2.0 * GM, a=4.0) general_kick = impulse_deltav_general_curvedstream( numpy.array([3.4, 0.0, 0.0]), numpy.array([4.0, 0.0, 0.0]), 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), numpy.array([0.0, 0.0, 0.0]), numpy.array([3.4, 0.0, 0.0]), pp, ) assert numpy.all(numpy.fabs(kick - general_kick) < 10.0**tol), ( "general kick calculation does not agree with Hernquist calculation for a Hernquist potential, for curved stream" ) # Same for a bunch of positions GM = numpy.pi v = numpy.zeros((100, 3)) v[:, 0] = 3.4 xpos = numpy.random.normal(size=100) xpos = numpy.array([xpos, numpy.zeros(100), numpy.zeros(100)]).T kick = impulse_deltav_hernquist_curvedstream( v, xpos, 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), numpy.array([0.0, 0.0, 0.0]), numpy.array([3.4, 0.0, 0.0]), GM, numpy.exp(1.0), ) pp = HernquistPotential(amp=2.0 * GM, a=numpy.exp(1.0)) general_kick = impulse_deltav_general_curvedstream( v, xpos, 3.0, numpy.array([0.0, numpy.pi / 2.0, 0.0]), numpy.array([0.0, 0.0, 0.0]), numpy.array([3.4, 0.0, 0.0]), pp, ) assert numpy.all(numpy.fabs(kick - general_kick) < 10.0**tol), ( "general kick calculation does not agree with Hernquist calculation for a Hernquist potential, for curved stream" ) return None def test_hernquistX_negative(): from galpy.df.streamgapdf import HernquistX with pytest.raises(ValueError) as excinfo: HernquistX(-1.0) return None def test_hernquistX_unity(): from galpy.df.streamgapdf import HernquistX assert HernquistX(1.0) == 1.0, ( "Hernquist X function not returning 1 with argument 1" ) return None # Test general impulse vs. full orbit integration for zero force def test_impulse_deltav_general_orbit_zeroforce(): from galpy.df import ( impulse_deltav_general_orbitintegration, impulse_deltav_plummer_curvedstream, ) from galpy.potential import PlummerPotential tol = -6.0 rcurv = 10.0 vp = 220.0 x0 = numpy.array([rcurv, 0.0, 0.0]) v0 = numpy.array([0.0, vp, 0.0]) w = numpy.array([1.0, numpy.pi / 2.0, 0.0]) plummer_kick = impulse_deltav_plummer_curvedstream(v0, x0, 3.0, w, x0, v0, 1.5, 4.0) pp = PlummerPotential(amp=1.5, b=4.0) vang = vp / rcurv angrange = numpy.pi maxt = 5.0 * angrange / vang galpot = constantPotential() orbit_kick = impulse_deltav_general_orbitintegration( v0, x0, 3.0, w, x0, v0, pp, maxt, galpot ) assert numpy.all(numpy.fabs(orbit_kick - plummer_kick) < 10.0**tol), ( "general kick with acceleration calculation does not agree with Plummer calculation for a Plummer potential, for straight" ) # Same for a bunch of positions tol = -5.0 pp = PlummerPotential(amp=numpy.pi, b=numpy.exp(1.0)) theta = numpy.linspace(-numpy.pi / 4.0, numpy.pi / 4.0, 100) xc, yc = rcurv * numpy.cos(theta), rcurv * numpy.sin(theta) Xc = numpy.zeros((100, 3)) Xc[:, 0] = xc Xc[:, 1] = yc vx, vy = -vp * numpy.sin(theta), vp * numpy.cos(theta) V = numpy.zeros((100, 3)) V[:, 0] = vx V[:, 1] = vy plummer_kick = impulse_deltav_plummer_curvedstream( V, Xc, 3.0, w, x0, v0, numpy.pi, numpy.exp(1.0) ) orbit_kick = impulse_deltav_general_orbitintegration( V, Xc, 3.0, w, x0, v0, pp, maxt, galpot ) assert numpy.all(numpy.fabs(orbit_kick - plummer_kick) < 10.0**tol), ( "general kick calculation does not agree with Plummer calculation for a Plummer potential, for curved stream" ) return None # Test general impulse vs. full stream and halo integration for zero force def test_impulse_deltav_general_fullintegration_zeroforce(): from galpy.df import ( impulse_deltav_general_fullplummerintegration, impulse_deltav_plummer_curvedstream, ) tol = -3.0 rcurv = 10.0 vp = 220.0 GM = 1.5 rs = 4.0 x0 = numpy.array([rcurv, 0.0, 0.0]) v0 = numpy.array([0.0, vp, 0.0]) w = numpy.array([1.0, numpy.pi / 4.0 * vp, 0.0]) plummer_kick = impulse_deltav_plummer_curvedstream(v0, x0, 3.0, w, x0, v0, GM, rs) galpot = constantPotential() orbit_kick = impulse_deltav_general_fullplummerintegration( v0, x0, 3.0, w, x0, v0, galpot, GM, rs, tmaxfac=100.0, N=1000 ) nzeroIndx = numpy.fabs(plummer_kick) > 10.0**tol assert numpy.all( numpy.fabs((orbit_kick - plummer_kick) / plummer_kick)[nzeroIndx] < 10.0**tol ), ( "general kick with acceleration calculation does not agree with Plummer calculation for a Plummer potential, for straight" ) assert numpy.all( numpy.fabs(orbit_kick - plummer_kick)[True ^ nzeroIndx] < 10.0**tol ), ( "general kick with acceleration calculation does not agree with Plummer calculation for a Plummer potential, for straight" ) # Same for a bunch of positions tol = -2.5 GM = numpy.pi rs = numpy.exp(1.0) theta = numpy.linspace(-numpy.pi / 4.0, numpy.pi / 4.0, 10) xc, yc = rcurv * numpy.cos(theta), rcurv * numpy.sin(theta) Xc = numpy.zeros((10, 3)) Xc[:, 0] = xc Xc[:, 1] = yc vx, vy = -vp * numpy.sin(theta), vp * numpy.cos(theta) V = numpy.zeros((10, 3)) V[:, 0] = vx V[:, 1] = vy plummer_kick = impulse_deltav_plummer_curvedstream(V, Xc, 3.0, w, x0, v0, GM, rs) orbit_kick = impulse_deltav_general_fullplummerintegration( V, Xc, 3.0, w, x0, v0, galpot, GM, rs, tmaxfac=100.0 ) nzeroIndx = numpy.fabs(plummer_kick) > 10.0**tol assert numpy.all( numpy.fabs((orbit_kick - plummer_kick) / plummer_kick)[nzeroIndx] < 10.0**tol ), ( "full stream+halo integration calculation does not agree with Plummer calculation for a Plummer potential, for curved stream" ) assert numpy.all( numpy.fabs(orbit_kick - plummer_kick)[True ^ nzeroIndx] < 10.0**tol ), ( "full stream+halo integration calculation does not agree with Plummer calculation for a Plummer potential, for curved stream" ) return None # Test general impulse vs. full stream and halo integration for fast encounter def test_impulse_deltav_general_fullintegration_fastencounter(): from galpy.df import ( impulse_deltav_general_fullplummerintegration, impulse_deltav_general_orbitintegration, ) from galpy.potential import LogarithmicHaloPotential, PlummerPotential tol = -2.0 GM = 1.5 rs = 4.0 x0 = numpy.array([1.5, 0.0, 0.0]) v0 = numpy.array([0.0, 1.0, 0.0]) # circular orbit w = numpy.array([0.0, 0.0, 100.0]) # very fast compared to v=1 lp = LogarithmicHaloPotential(normalize=1.0) pp = PlummerPotential(amp=GM, b=rs) orbit_kick = impulse_deltav_general_orbitintegration( v0, x0, 3.0, w, x0, v0, pp, 5.0 * numpy.pi, lp ) full_kick = impulse_deltav_general_fullplummerintegration( v0, x0, 3.0, w, x0, v0, lp, GM, rs, tmaxfac=10.0, N=1000 ) # Kick should be in the X direction assert numpy.fabs((orbit_kick - full_kick) / full_kick)[0, 0] < 10.0**tol, ( "Acceleration kick does not agree with full-orbit-integration kick for fast encounter" ) assert numpy.all(numpy.fabs(orbit_kick - full_kick)[0, 1:] < 10.0**tol), ( "Acceleration kick does not agree with full-orbit-integration kick for fast encounter" ) return None # Test straight, stream impulse vs. Plummer, similar setup as Fig. 1 in # stream paper def test_impulse_deltav_plummerstream(): from galpy.df import impulse_deltav_plummer, impulse_deltav_plummerstream from galpy.util import conversion V0, R0 = 220.0, 8.0 GM = 10.0**-2.0 / conversion.mass_in_1010msol(V0, R0) rs = 0.625 / R0 b = rs stream_phi = numpy.linspace(-numpy.pi / 2.0, numpy.pi / 2.0, 201) stream_r = 10.0 / R0 stream_v = 220.0 / V0 x_gc = stream_r * stream_phi v_gc = numpy.tile([0.000001, stream_v, 0.000001], (201, 1)) w = numpy.array([0.0, 132.0, 176]) / V0 wmag = numpy.sqrt(numpy.sum(w**2.0)) tol = -5.0 # Plummer sphere kick kick = impulse_deltav_plummer(v_gc[101], x_gc[101], -b, w, GM, rs) # Kick from stream with length 0.01 r_s (should be ~Plummer sphere) dt = 0.01 * rs * R0 / wmag / V0 * conversion.freq_in_kmskpc(V0, R0) stream_kick = impulse_deltav_plummerstream( v_gc[101], x_gc[101], -b, w, lambda t: GM / dt, rs, -dt / 2.0, dt / 2.0 ) assert numpy.all(numpy.fabs((kick - stream_kick) / kick) < 10.0**tol), ( "Short stream impulse kick calculation does not agree with Plummer calculation by %g" % (numpy.amax(numpy.fabs((kick - stream_kick) / kick))) ) # Same for a bunch of positions kick = impulse_deltav_plummer(v_gc, x_gc, -b, w, GM, rs) # Kick from stream with length 0.01 r_s (should be ~Plummer sphere) dt = 0.01 * rs * R0 / wmag / V0 * conversion.freq_in_kmskpc(V0, R0) stream_kick = impulse_deltav_plummerstream( v_gc, x_gc, -b, w, lambda t: GM / dt, rs, -dt / 2.0, dt / 2.0 ) assert numpy.all( (numpy.fabs((kick - stream_kick) / kick) < 10.0**tol) * (numpy.fabs(kick) >= 10**-4.0) + (numpy.fabs(kick - stream_kick) < 10**tol) * (numpy.fabs(kick) < 10**-4.0) ), ( f"Short stream impulse kick calculation does not agree with Plummer calculation by rel: {numpy.amax(numpy.fabs((kick - stream_kick) / kick)[numpy.fabs(kick) >= 10**-4.0]):g}, abs: {numpy.amax(numpy.fabs(kick - stream_kick)[numpy.fabs(kick) < 10**-3.0]):g}" ) def test_impulse_deltav_plummerstream_tmaxerror(): from galpy.df import impulse_deltav_plummer, impulse_deltav_plummerstream from galpy.util import conversion V0, R0 = 220.0, 8.0 GM = 10.0**-2.0 / conversion.mass_in_1010msol(V0, R0) rs = 0.625 / R0 b = rs stream_phi = numpy.linspace(-numpy.pi / 2.0, numpy.pi / 2.0, 201) stream_r = 10.0 / R0 stream_v = 220.0 / V0 x_gc = stream_r * stream_phi v_gc = numpy.tile([0.000001, stream_v, 0.000001], (201, 1)) w = numpy.array([0.0, 132.0, 176]) / V0 wmag = numpy.sqrt(numpy.sum(w**2.0)) tol = -5.0 # Same for infinite integration limits kick = impulse_deltav_plummer(v_gc[101], x_gc[101], -b, w, GM, rs) # Kick from stream with length 0.01 r_s (should be ~Plummer sphere) dt = 0.01 * rs * R0 / wmag / V0 * conversion.freq_in_kmskpc(V0, R0) with pytest.raises(ValueError) as excinfo: stream_kick = impulse_deltav_plummerstream( v_gc[101], x_gc[101], -b, w, lambda t: GM / dt, rs ) return None # Test the Plummer curved calculation for a perpendicular stream impact: # short impact should be the same as a Plummer-sphere impact def test_impulse_deltav_plummerstream_curved_subhalo_perpendicular(): from galpy.df import ( impulse_deltav_plummer_curvedstream, impulse_deltav_plummerstream_curvedstream, ) from galpy.potential import LogarithmicHaloPotential from galpy.util import conversion R0, V0 = 8.0, 220.0 lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) tol = -5.0 GM = 10.0**-2.0 / conversion.mass_in_1010msol(V0, R0) rs = 0.625 / R0 dt = 0.01 * rs / (numpy.pi / 4.0) kick = impulse_deltav_plummer_curvedstream( numpy.array([0.5, 0.1, 0.2]), numpy.array([1.2, 0.0, 0.0]), rs, numpy.array([0.1, numpy.pi / 4.0, 0.1]), numpy.array([1.2, 0.0, 0.0]), numpy.array([0.5, 0.1, 0.2]), GM, rs, ) stream_kick = impulse_deltav_plummerstream_curvedstream( numpy.array([[0.5, 0.1, 0.2]]), numpy.array([[1.2, 0.0, 0.0]]), numpy.array([0.0]), rs, numpy.array([0.1, numpy.pi / 4.0, 0.1]), numpy.array([1.2, 0.0, 0.0]), numpy.array([0.5, 0.1, 0.2]), lambda t: GM / dt, rs, lp, -dt / 2.0, dt / 2.0, ) # Should be equal assert numpy.all(numpy.fabs((kick - stream_kick) / kick) < 10.0**tol), ( "Curved, short Plummer-stream kick does not agree with curved Plummer-sphere kick by %g" % (numpy.amax(numpy.fabs((kick - stream_kick) / kick))) ) # Also test with other array shape input for x and v kick = impulse_deltav_plummer_curvedstream( numpy.array([[0.5, 0.1, 0.2]]), numpy.array([[1.2, 0.0, 0.0]]), rs, numpy.array([0.1, numpy.pi / 4.0, 0.1]), numpy.array([1.2, 0.0, 0.0]), numpy.array([0.5, 0.1, 0.2]), GM, rs, ) stream_kick = impulse_deltav_plummerstream_curvedstream( numpy.array([0.5, 0.1, 0.2]), numpy.array([1.2, 0.0, 0.0]), numpy.array([0.0]), rs, numpy.array([0.1, numpy.pi / 4.0, 0.1]), numpy.array([1.2, 0.0, 0.0]), numpy.array([0.5, 0.1, 0.2]), lambda t: GM / dt, rs, lp, -dt / 2.0, dt / 2.0, ) assert numpy.all(numpy.fabs((kick - stream_kick) / kick) < 10.0**tol), ( "Curved, short Plummer-stream kick does not agree with curved Plummer-sphere kick by %g" % (numpy.amax(numpy.fabs((kick - stream_kick) / kick))) ) return None from galpy.potential import Potential class constantPotential(Potential): def __init__(self): Potential.__init__(self, amp=1.0) self.hasC = False return None def _Rforce(self, R, z, phi=0.0, t=0.0): return 0.0 def _zforce(self, R, z, phi=0.0, t=0.0): return 0.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_streamspraydf.py0000644000175100001660000005243314761352023017763 0ustar00runnerdockerimport copy import numpy import pytest from galpy.actionAngle import actionAngleIsochroneApprox from galpy.df import chen24spraydf, fardal15spraydf, streamdf, streamspraydf from galpy.orbit import Orbit from galpy.potential import ( ChandrasekharDynamicalFrictionForce, HernquistPotential, LogarithmicHaloPotential, MovingObjectPotential, MWPotential2014, PlummerPotential, TriaxialNFWPotential, ) from galpy.util import conversion # for unit conversions from galpy.util import coords ################################ Tests against streamdf ###################### def test_streamspraydf_deprecation(): # Check if the deprecating class raises the correct warning lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) ro, vo = 8.0, 220.0 with pytest.warns(DeprecationWarning): spdf = streamspraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ) # Setup both DFs @pytest.fixture(scope="module") def setup_testStreamsprayAgainstStreamdf(): lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) aAI = actionAngleIsochroneApprox(pot=lp, b=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) ro, vo = 8.0, 220.0 # Set up streamdf sigv = 0.365 # km/s sdf_bovy14 = streamdf( sigv / 220.0, progenitor=obs, pot=lp, aA=aAI, leading=True, nTrackChunks=11, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ) # Set up streamspraydf f15spdf_bovy14 = fardal15spraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ) c24spdf_bovy14 = chen24spraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ) return sdf_bovy14, [f15spdf_bovy14, c24spdf_bovy14] def test_sample_bovy14(setup_testStreamsprayAgainstStreamdf): # Load objects that were setup above sdf_bovy14, spdfs_bovy14 = setup_testStreamsprayAgainstStreamdf for spdf_bovy14 in spdfs_bovy14: numpy.random.seed(1) RvR_sdf = sdf_bovy14.sample(n=1000) RvR_spdf = spdf_bovy14.sample(n=1000, integrate=True, return_orbit=False) # Sanity checks # Range in Z indx = (RvR_sdf[3] > 4.0 / 8.0) * (RvR_sdf[3] < 5.0 / 8.0) # mean assert ( numpy.fabs(numpy.mean(RvR_sdf[0][indx]) - numpy.mean(RvR_spdf[0][indx])) < 6e-2 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean)" ) assert ( numpy.fabs(numpy.mean(RvR_sdf[1][indx]) - numpy.mean(RvR_spdf[1][indx])) < 5e-2 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean)" ) assert ( numpy.fabs(numpy.mean(RvR_sdf[2][indx]) - numpy.mean(RvR_spdf[2][indx])) < 5e-2 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean)" ) assert ( numpy.fabs(numpy.mean(RvR_sdf[4][indx]) - numpy.mean(RvR_spdf[4][indx])) < 5e-2 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean)" ) assert ( numpy.fabs(numpy.mean(RvR_sdf[5][indx]) - numpy.mean(RvR_spdf[5][indx])) < 1e-1 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean)" ) # Another range in Z indx = (RvR_sdf[3] > 5.0 / 8.0) * (RvR_sdf[3] < 6.0 / 8.0) # mean assert ( numpy.fabs(numpy.mean(RvR_sdf[0][indx]) - numpy.mean(RvR_spdf[0][indx])) < 1e-1 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean)" ) assert ( numpy.fabs(numpy.mean(RvR_sdf[1][indx]) - numpy.mean(RvR_spdf[1][indx])) < 3e-2 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean)" ) assert ( numpy.fabs(numpy.mean(RvR_sdf[2][indx]) - numpy.mean(RvR_spdf[2][indx])) < 4e-2 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean)" ) assert ( numpy.fabs(numpy.mean(RvR_sdf[4][indx]) - numpy.mean(RvR_spdf[4][indx])) < 3e-2 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean)" ) assert ( numpy.fabs(numpy.mean(RvR_sdf[5][indx]) - numpy.mean(RvR_spdf[5][indx])) < 1e-1 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean)" ) return None def test_bovy14_sampleorbit(setup_testStreamsprayAgainstStreamdf): # Load objects that were setup above sdf_bovy14, spdfs_bovy14 = setup_testStreamsprayAgainstStreamdf for spdf_bovy14 in spdfs_bovy14: numpy.random.seed(1) XvX_sdf = sdf_bovy14.sample(n=1000, xy=True) XvX_spdf = spdf_bovy14.sample( n=1000 ) # returns Orbit, from which we can get anything we want # Sanity checks # Range in Z indx = (XvX_sdf[2] > 4.0 / 8.0) * (XvX_sdf[2] < 5.0 / 8.0) # mean assert ( numpy.fabs(numpy.mean(XvX_sdf[0][indx]) - numpy.mean(XvX_spdf.x()[indx])) < 6e-2 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean, xy)" ) assert ( numpy.fabs(numpy.mean(XvX_sdf[1][indx]) - numpy.mean(XvX_spdf.y()[indx])) < 2e-1 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean, xy)" ) assert ( numpy.fabs(numpy.mean(XvX_sdf[4][indx]) - numpy.mean(XvX_spdf.vy()[indx])) < 3e-2 ), ( "streamdf and streamspraydf do not generate similar samples for the Bovy (2014) stream (mean, xy)" ) return None def test_integrate(setup_testStreamsprayAgainstStreamdf): # Test that sampling at stripping + integrate == sampling at the end # Load objects that were setup above _, spdfs_bovy14 = setup_testStreamsprayAgainstStreamdf for spdf_bovy14 in spdfs_bovy14: # Sample at at stripping numpy.random.seed(4) RvR_noint, dt_noint = spdf_bovy14.sample( n=100, return_orbit=False, returndt=True, integrate=False ) # and integrate for ii in range(len(dt_noint)): to = Orbit(RvR_noint[:, ii]) to.integrate(numpy.linspace(-dt_noint[ii], 0.0, 1001), spdf_bovy14._pot) RvR_noint[:, ii] = [ to.R(0.0), to.vR(0.0), to.vT(0.0), to.z(0.0), to.vz(0.0), to.phi(0.0), ] # Sample today numpy.random.seed(4) RvR, dt = spdf_bovy14.sample( n=100, return_orbit=False, returndt=True, integrate=True ) # Should agree assert numpy.amax(numpy.fabs(dt - dt_noint)) < 1e-10, ( "Times not the same when sampling with and without integrating" ) assert numpy.amax(numpy.fabs(RvR - RvR_noint)) < 1e-7, ( "Phase-space points not the same when sampling with and without integrating" ) return None def test_integrate_rtnonarray(): # Test that sampling at stripping + integrate == sampling at the end # For a potential that doesn't support array inputs nfp = TriaxialNFWPotential(normalize=1.0, b=0.9, c=0.8) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) ro, vo = 8.0, 220.0 for streamspraydf in [fardal15spraydf, chen24spraydf]: # Set up streamspraydf spdf_bovy14 = streamspraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=nfp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ) # Sample at at stripping numpy.random.seed(4) RvR_noint, dt_noint = spdf_bovy14.sample( n=100, return_orbit=False, returndt=True, integrate=False ) # and integrate for ii in range(len(dt_noint)): to = Orbit(RvR_noint[:, ii]) to.integrate(numpy.linspace(-dt_noint[ii], 0.0, 1001), spdf_bovy14._pot) RvR_noint[:, ii] = [ to.R(0.0), to.vR(0.0), to.vT(0.0), to.z(0.0), to.vz(0.0), to.phi(0.0), ] # Sample today numpy.random.seed(4) RvR, dt = spdf_bovy14.sample( n=100, return_orbit=False, returndt=True, integrate=True ) # Should agree assert numpy.amax(numpy.fabs(dt - dt_noint)) < 1e-10, ( "Times not the same when sampling with and without integrating" ) assert numpy.amax(numpy.fabs(RvR - RvR_noint)) < 1e-7, ( "Phase-space points not the same when sampling with and without integrating" ) return None def test_center(): # Test that a stream around a different center is generated # when using center # In this example, we'll generate a stream in the LMC orbiting the MW # LMC and its orbit ro, vo = 8.0, 220.0 o = Orbit( [5.13200034, 1.08033051, 0.2332339, -3.48068653, 0.94950884, -1.54626091] ) # Result from from_name('LMC') tMWPotential2014 = copy.deepcopy(MWPotential2014) tMWPotential2014[2] *= 1.5 cdf = ChandrasekharDynamicalFrictionForce( GMs=10 / conversion.mass_in_1010msol(vo, ro), rhm=5.0 / ro, dens=tMWPotential2014, ) ts = numpy.linspace(0.0, -10.0, 1001) / conversion.time_in_Gyr(vo, ro) o.integrate(ts, tMWPotential2014 + cdf) lmcpot = HernquistPotential( amp=2 * 10 / conversion.mass_in_1010msol(vo, ro), a=5.0 / ro / (1.0 + numpy.sqrt(2.0)), ) # rhm = (1+sqrt(2)) a moving_lmcpot = MovingObjectPotential(o, pot=lmcpot) # Now generate a stream within the LMC, progenitor at 8x kpc on circular orbit of = o(ts[-1]) # LMC at final point, earliest time, for convenience # Following pos in kpc, vel in km/s R_in_lmc = 1.0 prog_phasespace = ( of.x(use_physical=False) + R_in_lmc, of.y(use_physical=False), of.z(use_physical=False), of.vx(use_physical=False), of.vy(use_physical=False) + lmcpot.vcirc(R_in_lmc, use_physical=False), of.vz(use_physical=False), ) prog_pos = coords.rect_to_cyl( prog_phasespace[0], prog_phasespace[1], prog_phasespace[2] ) prog_vel = coords.rect_to_cyl_vec( prog_phasespace[3], prog_phasespace[4], prog_phasespace[5], None, prog_pos[1], None, cyl=True, ) prog = Orbit( [prog_pos[0], prog_vel[0], prog_vel[1], prog_pos[2], prog_vel[2], prog_pos[1]], ro=8.0, vo=220.0, ) # Integrate prog forward prog.integrate(ts[::-1], tMWPotential2014 + moving_lmcpot) for streamspraydf in [fardal15spraydf, chen24spraydf]: # Then set up streamspraydf spdf = streamspraydf( 2e4 / conversion.mass_in_msol(vo, ro), progenitor=prog(0.0), pot=tMWPotential2014 + moving_lmcpot, rtpot=lmcpot, tdisrupt=10.0 / conversion.time_in_Gyr(vo, ro), center=o, centerpot=tMWPotential2014 + cdf, ) # Generate stream numpy.random.seed(1) stream_RvR = spdf.sample(n=300, return_orbit=False, integrate=True) stream_pos = coords.cyl_to_rect(stream_RvR[0], stream_RvR[5], stream_RvR[3]) # Stream should lie on a circle with radius R_in_lmc stream_R_wrt_LMC = numpy.sqrt( (stream_pos[0] - o.x(use_physical=False)) ** 2.0 + (stream_pos[1] - o.y(use_physical=False)) ** 2.0 ) assert numpy.fabs(numpy.mean(stream_R_wrt_LMC) - R_in_lmc) < 0.1, ( "Stream generated in the LMC does not appear to be on a circle within the LMC" ) assert numpy.fabs(numpy.std(stream_R_wrt_LMC)) < 0.15, ( "Stream generated in the LMC does not appear to be on a circle within the LMC" ) return None def test_sample_orbit_rovoetc(): # Test that the sample orbit output has the same ro/vo/etc. as the # input progenitor lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) ro, vo = 9.0, 230.0 zo, solarmotion = 0.03, [-20.0, 30.0, 40.0] obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596], ro=ro, vo=vo, zo=zo, solarmotion=solarmotion, ) for streamspraydf in [fardal15spraydf, chen24spraydf]: # Set up streamspraydf spdf_bovy14 = streamspraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ) sam = spdf_bovy14.sample(n=10) assert obs._roSet is sam._roSet, ( "Sampled streamspraydf orbits do not have the same roSet as the progenitor orbit" ) assert obs._voSet is sam._voSet, ( "Sampled streamspraydf orbits do not have the same voSet as the progenitor orbit" ) assert numpy.fabs(obs._ro - sam._ro) < 1e-10, ( "Sampled streamspraydf orbits do not have the same ro as the progenitor orbit" ) assert numpy.fabs(obs._vo - sam._vo) < 1e-10, ( "Sampled streamspraydf orbits do not have the same vo as the progenitor orbit" ) assert numpy.fabs(obs._zo - sam._zo) < 1e-10, ( "Sampled streamspraydf orbits do not have the same zo as the progenitor orbit" ) assert numpy.all(numpy.fabs(obs._solarmotion - sam._solarmotion) < 1e-10), ( "Sampled streamspraydf orbits do not have the same solarmotion as the progenitor orbit" ) # Another one ro = 9.0 zo, solarmotion = 0.03, [-20.0, 30.0, 40.0] obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596], ro=ro, zo=zo, solarmotion=solarmotion, ) for streamspraydf in [fardal15spraydf, chen24spraydf]: # Set up streamspraydf spdf_bovy14 = streamspraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ) sam = spdf_bovy14.sample(n=10) assert obs._roSet, ( "Test requires that ro be set for the progenitor orbit, but it appears not to have been set" ) assert not obs._voSet, ( "Test requires that vo not be set for the progenitor orbit, but it appears to have been set" ) assert obs._roSet is sam._roSet, ( "Sampled streamspraydf orbits do not have the same roSet as the progenitor orbit" ) assert obs._voSet is sam._voSet, ( "Sampled streamspraydf orbits do not have the same voSet as the progenitor orbit" ) assert numpy.fabs(obs._ro - sam._ro) < 1e-10, ( "Sampled streamspraydf orbits do not have the same ro as the progenitor orbit" ) assert numpy.fabs(obs._vo - sam._vo) < 1e-10, ( "Sampled streamspraydf orbits do not have the same vo as the progenitor orbit" ) assert numpy.fabs(obs._zo - sam._zo) < 1e-10, ( "Sampled streamspraydf orbits do not have the same zo as the progenitor orbit" ) assert numpy.all(numpy.fabs(obs._solarmotion - sam._solarmotion) < 1e-10), ( "Sampled streamspraydf orbits do not have the same solarmotion as the progenitor orbit" ) # And another one vo = 230.0 zo, solarmotion = 0.03, [-20.0, 30.0, 40.0] obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596], vo=vo, zo=zo, solarmotion=solarmotion, ) for streamspraydf in [fardal15spraydf, chen24spraydf]: # Set up streamspraydf spdf_bovy14 = streamspraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ) sam = spdf_bovy14.sample(n=10) assert obs._voSet, ( "Test requires that vo be set for the progenitor orbit, but it appears not to have been set" ) assert not obs._roSet, ( "Test requires that ro not be set for the progenitor orbit, but it appears to have been set" ) assert obs._roSet is sam._roSet, ( "Sampled streamspraydf orbits do not have the same roSet as the progenitor orbit" ) assert obs._voSet is sam._voSet, ( "Sampled streamspraydf orbits do not have the same voSet as the progenitor orbit" ) assert numpy.fabs(obs._ro - sam._ro) < 1e-10, ( "Sampled streamspraydf orbits do not have the same ro as the progenitor orbit" ) assert numpy.fabs(obs._vo - sam._vo) < 1e-10, ( "Sampled streamspraydf orbits do not have the same vo as the progenitor orbit" ) assert numpy.fabs(obs._zo - sam._zo) < 1e-10, ( "Sampled streamspraydf orbits do not have the same zo as the progenitor orbit" ) assert numpy.all(numpy.fabs(obs._solarmotion - sam._solarmotion) < 1e-10), ( "Sampled streamspraydf orbits do not have the same solarmotion as the progenitor orbit" ) return None def test_integrate_with_prog(): # Test integrating orbits with the progenitor's potential lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) ro, vo = 8.0, 220.0 # Without the progenitor's potential spdf = chen24spraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ) numpy.random.seed(4) RvR, dt = spdf.sample(n=100, return_orbit=False, returndt=True, integrate=True) # With the progenitor's potential, but set to zero-mass spdf = chen24spraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), progpot=PlummerPotential(0, 0), ) numpy.random.seed(4) RvR_withprog, dt_withprog = spdf.sample( n=100, return_orbit=False, returndt=True, integrate=True ) # Should agree assert numpy.amax(numpy.fabs(dt - dt_withprog)) < 1e-10, ( "Times not the same when sampling with and without prognitor's potential" ) assert numpy.amax(numpy.fabs(RvR - RvR_withprog)) < 1e-7, ( "Phase-space points not the same when sampling with and without prognitor's potential" ) return None def test_chen24spraydf_default_parameters(): # Test the default parameters of chen24spraydf can be changed lp = LogarithmicHaloPotential(normalize=1.0, q=0.9) obs = Orbit( [1.56148083, 0.35081535, -1.15481504, 0.88719443, -0.47713334, 0.12019596] ) ro, vo = 8.0, 220.0 # Default parameters spdf = chen24spraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), ) numpy.random.seed(4) RvR_default, dt_default = spdf.sample( n=100, return_orbit=False, returndt=True, integrate=True ) # Modified parameters, but only slightly spdf = chen24spraydf( 2 * 10.0**4.0 / conversion.mass_in_msol(vo, ro), progenitor=obs, pot=lp, tdisrupt=4.5 / conversion.time_in_Gyr(vo, ro), mean=numpy.array([1.6, -0.525344, 0, 1, 0.349066, 0]), cov=numpy.array( [ [0.1225, 0, 0, 0, -0.085521, 0], [0, 0.161143, 0, 0, 0, 0], [0, 0, 0.043865, 0, 0, 0], [0, 0, 0, 0, 0, 0], [-0.085521, 0, 0, 0, 0.121847, 0], [0, 0, 0, 0, 0, 0.147435], ] ), ) numpy.random.seed(4) RvR, dt = spdf.sample(n=100, return_orbit=False, returndt=True, integrate=True) # Should agree assert numpy.amax(numpy.fabs(dt_default - dt)) < 1e-10, ( "Times not the same when changing the default parameters" ) assert numpy.amax(numpy.fabs(RvR_default - RvR)) > 1e-7, ( "Phase-space points should not be the same when changing the default parameters" ) assert numpy.amax(numpy.fabs(RvR_default - RvR)) < 1e-2, ( "Phase-space points too different when sampling with and without prognitor's potential" ) return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741018131.0 galpy-1.10.2/tests/test_util.py0000644000175100001660000000670614761352023016056 0ustar00runnerdocker# Test the functions in galpy/util/__init__.py import numpy def test_save_pickles(): import os import pickle import tempfile from galpy.util import save_pickles savethis = numpy.linspace(0.0, 100.0, 1001) savefile, tmp_savefilename = tempfile.mkstemp() try: os.close(savefile) # Easier this way save_pickles(tmp_savefilename, savethis) savefile = open(tmp_savefilename, "rb") restorethis = pickle.load(savefile) savefile.close() assert numpy.all(numpy.fabs(restorethis - savethis) < 10.0**-10.0), ( "save_pickles did not work as expected" ) finally: os.remove(tmp_savefilename) # Also test the handling of KeyboardInterrupt try: save_pickles(tmp_savefilename, savethis, testKeyboardInterrupt=True) except KeyboardInterrupt: pass else: raise AssertionError( "save_pickles with testKeyboardInterrupt=True did not raise KeyboardInterrupt" ) savefile = open(tmp_savefilename, "rb") restorethis = pickle.load(savefile) savefile.close() assert numpy.all(numpy.fabs(restorethis - savethis) < 10.0**-10.0), ( "save_pickles did not work as expected when KeyboardInterrupted" ) if os.path.exists(tmp_savefilename): os.remove(tmp_savefilename) return None def test_logsumexp(): from galpy.util import logsumexp sumthis = numpy.array([[0.0, 1.0]]) sum = numpy.log(numpy.exp(0.0) + numpy.exp(1.0)) assert numpy.all(numpy.fabs(logsumexp(sumthis, axis=0) - sumthis) < 10.0**-10.0), ( "galpy.util.logsumexp did not work as expected" ) assert numpy.fabs(logsumexp(sumthis, axis=1) - sum) < 10.0**-10.0, ( "galpy.util.logsumexp did not work as expected" ) assert numpy.fabs(logsumexp(sumthis, axis=None) - sum) < 10.0**-10.0, ( "galpy.util.logsumexp did not work as expected" ) return None def test_fast_cholesky_invert(): from galpy.util import fast_cholesky_invert matrix = numpy.array([[2.0, 1.0], [1.0, 4.0]]) invmatrix = fast_cholesky_invert(matrix) unit = numpy.dot(invmatrix, matrix) assert numpy.all(numpy.fabs(numpy.diag(unit) - 1.0) < 10.0**-8.0), ( "fast_cholesky_invert did not work as expected" ) assert numpy.fabs(unit[0, 1] - 0.0) < 10.0**-8.0, ( "fast_cholesky_invert did not work as expected" ) assert numpy.fabs(unit[1, 0] - 0.0) < 10.0**-8.0, ( "fast_cholesky_invert did not work as expected" ) # Check the other way around unit = numpy.dot(matrix, invmatrix) assert numpy.all(numpy.fabs(numpy.diag(unit) - 1.0) < 10.0**-8.0), ( "fast_cholesky_invert did not work as expected" ) assert numpy.fabs(unit[0, 1] - 0.0) < 10.0**-8.0, ( "fast_cholesky_invert did not work as expected" ) assert numpy.fabs(unit[1, 0] - 0.0) < 10.0**-8.0, ( "fast_cholesky_invert did not work as expected" ) # Also check determinant invmatrix, logdet = fast_cholesky_invert(matrix, logdet=True) assert numpy.fabs(logdet - numpy.log(7.0)) < 10.0**-8.0, ( "fast_cholesky_invert's determinant did not work as expected" ) return None def test_quadpack(): from galpy.util.quadpack import dblquad int = dblquad(lambda y, x: 4.0 * x * y, 0.0, 1.0, lambda z: 0.0, lambda z: 1.0) assert numpy.fabs(int[0] - 1.0) < int[1], ( "galpy.util.quadpack.dblquad did not work as expected" ) return None