Orbit and phase-space position objects in more detail

Introduction

The astropy.units subpackage is excellent for working with numbers and associated units, however, dynamical quantities often contain many quantities with mixed units. An example is a position in phase-space, which may contain some quantities with length units and some quantities with velocity units. The PhaseSpacePosition and Orbit subclasses are designed to work with these structures – click these shortcuts to jump to a section below:

Some imports needed for the code below:

>>> import astropy.units as u
>>> import numpy as np
>>> import gala.potential as gp
>>> import gala.dynamics as gd
>>> from gala.units import galactic
>>> np.random.seed(42)

Phase-space positions

It is often useful to represent full phase-space positions quantities jointly. For example, if you need to transform the velocities to a new coordinate representation or frame, the positions often enter into the transformations. The PhaseSpacePosition subclasses provide an interface for handling these numbers. At present, only the CartesianPhaseSpacePosition is fully implemented.

To create a CartesianPhaseSpacePosition object, pass in a cartesian position and velocity to the initializer:

>>> gd.CartesianPhaseSpacePosition(pos=[4.,8.,15.]*u.kpc,
...                                vel=[-150.,50.,15.]*u.km/u.s)
<CartesianPhaseSpacePosition N=3, shape=(1,)>

Of course, this works with arrays of positions and velocities as well:

>>> x = np.random.uniform(-10,10,size=(3,128))
>>> v = np.random.uniform(-200,200,size=(3,128))
>>> gd.CartesianPhaseSpacePosition(pos=x*u.kpc,
...                                vel=v*u.km/u.s)
<CartesianPhaseSpacePosition N=3, shape=(128,)>

This works for arbitrary numbers of dimensions, e.g., we define a position:

>>> w = gd.CartesianPhaseSpacePosition(pos=[4.,8.]*u.kpc,
...                                    vel=[-150,45.]*u.km/u.s)
>>> w
<CartesianPhaseSpacePosition N=2, shape=(1,)>

We can check the dimensionality using the ndim attribute:

>>> w.ndim
2

For objects with ndim=3, we can also easily transform the full phase-space vector to new representations or coordinate frames. These transformations use the astropy.coordinates framework and the velocity transforms from gala.coordinates.

>>> from astropy.coordinates import CylindricalRepresentation
>>> x = np.random.uniform(-10,10,size=(3,128))
>>> v = np.random.uniform(-200,200,size=(3,128))
>>> w = gd.CartesianPhaseSpacePosition(pos=x*u.kpc,
...                                    vel=v*u.km/u.s)
>>> cyl_pos, cyl_vel = w.represent_as(CylindricalRepresentation)

The represent_as method returns two objects that contain the position in the new representation, and the velocity in the new representation. The position is returned as a BaseRepresentation subclass. The velocity is presently returned as a single Quantity object with the velocity components represented in velocity units (not in angular velocity units!) but this will change in the future when velocity support is added to Astropy:

>>> cyl_pos 
<CylindricalRepresentation (rho, phi, z) in (kpc, rad, kpc)
    [(2.64929392, 1.5595981, 5.27411405),
...etc.
>>> cyl_vel 
<Quantity [[-185.61668456, 160.10813427, -75.14559842, 138.36905651,
             -60.93410629,  95.60242757,  41.89615149, 128.34632582,
...etc.

There is also support for transforming the cartesian positions and velocities (assumed to be in a Galactocentric frame) to any of the other coordinate frames. The transformation returns two objects: an initialized coordinate frame for the position, and a tuple of Quantity objects for the velocity. Here, velocities are represented in angular velocities for the velocities conjugate to angle variables. For example, in the below transformation to Galactic coordinates, the returned velocity object is a tuple with proper motions and radial velocity, \((\mu_l, \mu_b, v_r)\):

>>> from astropy.coordinates import Galactic
>>> gal_c, gal_v = w.to_frame(Galactic)
>>> gal_c 
<Galactic Coordinate: (l, b, distance) in (deg, deg, kpc)
    [(17.67673481, 31.15412806, 10.19473889),
...etc.
>>> gal_v[0].unit, gal_v[1].unit, gal_v[2].unit
(Unit("mas / yr"), Unit("mas / yr"), Unit("km / s"))

We can easily plot projections of the positions using the plot method:

>>> fig = w.plot()

(Source code, png, hires.png, pdf)

../_images/orbits-in-detail-1.png

This is a thin wrapper around the three_panel function and any keyword arguments are passed through to that function:

>>> fig = w.plot(marker='o', s=40, alpha=0.5)

(Source code, png, hires.png, pdf)

../_images/orbits-in-detail-2.png

Phase-space position API

Functions

combine(args) Combine the input PhaseSpacePosition objects into a single object.

Classes

CartesianPhaseSpacePosition(pos, vel) Represents phase-space positions in Cartesian coordinates, e.g., positions and conjugate momenta (velocities).

Class Inheritance Diagram

Inheritance diagram of gala.dynamics.core.CartesianPhaseSpacePosition

Orbits

The Orbit subclasses all inherit the functionality described above from PhaseSpacePosition, but similarly, at present only the CartesianOrbit is fully implemented. There are some differences between the methods and some functionality that is particular to the orbit classes.

A CartesianOrbit is initialized much like the CartesianPhaseSpacePosition. Whereas an input position with shape ``(2,128)` to a CartesianPhaseSpacePosition represents 128, 2D positions, for an orbit it would represent a single orbit’s positions at 128 timesteps:

>>> t = np.linspace(0,10,128)
>>> pos,vel = np.zeros((2,128)),np.zeros((2,128))
>>> pos[0] = np.cos(t)
>>> pos[1] = np.sin(t)
>>> vel[0] = -np.sin(t)
>>> vel[1] = np.cos(t)
>>> orbit = gd.CartesianOrbit(pos=pos*u.kpc, vel=vel*u.km/u.s)
>>> orbit
<CartesianOrbit N=2, shape=(128,)>

To create a single object that contains multiple orbits, the input position object should have 3 axes. The last axis (axis=2) contains each orbit. So, an input position with shape (2,128,16) would represent 16, 2D orbits with 128 timesteps:

>>> t = np.linspace(0,10,128)
>>> pos,vel = np.zeros((2,128,16)),np.zeros((2,128,16))
>>> Omega = np.random.uniform(size=16)
>>> pos[0] = np.cos(Omega[np.newaxis]*t[:,np.newaxis])
>>> pos[1] = np.sin(Omega[np.newaxis]*t[:,np.newaxis])
>>> vel[0] = -np.sin(Omega[np.newaxis]*t[:,np.newaxis])
>>> vel[1] = np.cos(Omega[np.newaxis]*t[:,np.newaxis])
>>> orbit = gd.CartesianOrbit(pos=pos*u.kpc, vel=vel*u.km/u.s)
>>> orbit
<CartesianOrbit N=2, shape=(128, 16)>

To make full use of the orbit functionality, you must also pass in an array with the time values and an instance of a PotentialBase subclass that represents the potential that the orbit was integrated in:

>>> pot = gp.PlummerPotential(m=1E10, b=1., units=galactic)
>>> orbit = gd.CartesianOrbit(pos=pos*u.kpc, vel=vel*u.km/u.s,
...                           t=t*u.Myr, potential=pot)

(note, in this case pos and vel were not generated from integrating an orbit in the potential pot!) Orbit objects are returned by the integrate_orbit method of potential objects that already have the time and potential set:

>>> pot = gp.PlummerPotential(m=1E10, b=1., units=galactic)
>>> w0 = gd.CartesianPhaseSpacePosition(pos=[10.,0,0]*u.kpc,
...                                     vel=[0.,75,0]*u.km/u.s)
>>> orbit = pot.integrate_orbit(w0, dt=1., n_steps=500)
>>> orbit
<CartesianOrbit N=3, shape=(501,)>
>>> orbit.t 
<Quantity [   0.,   1.,   2.,   3.,   4.,   5.,   6.,   7.,   8.,   9.,
             10.,  11.,  12.,  13.,  14.,  15.,  16.,  17.,  18.,  19.,
...etc.
>>> orbit.potential
<PlummerPotential: m=1.00e+10, b=1.00 (kpc,Myr,solMass,rad)>

From an Orbit object, we can quickly compute quantities like the angular momentum, and estimates for the pericenter, apocenter, eccentricity of the orbit. Estimates for the latter few get better with smaller timesteps:

>>> orbit = pot.integrate_orbit(w0, dt=0.1, n_steps=100000)
>>> np.mean(orbit.angular_momentum(), axis=1) 
<Quantity [ 0.        , 0.        , 0.76703412] kpc2 / Myr>
>>> orbit.eccentricity() 
<Quantity 0.3191563009914265>
>>> orbit.pericenter() 
<Quantity 10.00000005952518 kpc>
>>> orbit.apocenter() 
<Quantity 19.375317870528118 kpc>

Just like above, we can quickly visualize an orbit using the plot method:

>>> fig = orbit.plot()

(Source code, png, hires.png, pdf)

../_images/orbits-in-detail-3.png

This is a thin wrapper around the plot_orbits function and any keyword arguments are passed through to that function:

>>> fig = orbit.plot(linewidth=4., alpha=0.5, color='r')
>>> fig.axes[0].set_xlim(-1.5,1.5) 
>>> fig.axes[0].set_ylim(-1.5,1.5) 

(Source code, png, hires.png, pdf)

../_images/orbits-in-detail-4.png

Orbit API

Functions

combine(args[, along_time_axis]) Combine the input Orbit objects into a single object.

Classes

CartesianOrbit(pos, vel[, t, potential]) Represents an orbit in Cartesian coordinates – positions and velocities (conjugate momenta) at different times.

Class Inheritance Diagram

Inheritance diagram of gala.dynamics.orbit.CartesianOrbit