Coordinate transformations
==========================

A :class:`~swiftgalaxy.reader.SWIFTGalaxy` enforces a consistent coordinate system for all particle types and coordinate and velocity arrays (accelerations are not supported, yet). Any relevant particle arrays already in memory are modified when a new transformation is applied, and any relevant particle arrays loaded later will be automatically transformed to the current frame.

The current centre and velocity centre of the coordinate system (in simulation coordinates) are available as properties of a :class:`~swiftgalaxy.reader.SWIFTGalaxy`. The current rotation (about the origin of the simulation coordinates) is also available:

.. code-block:: python

    sg = SWIFTGalaxy(...)
    sg.centre
    sg.velocity_centre
    sg.rotation

It may be that along with the basic coordinates and velocities arrays, you have additional arrays that you wish to live in the same coordinate system. We could imagine, for instance, a property called ``wind_velocity`` attached to star particles and containing a vector giving the velocity of a (directional) stellar wind in simulation box coordinates. We can use the ``transforms_like_velocities`` to specify that this property should have any coordinate transformations that would be relevant to the ``velocities`` of particles applied to these as well:

.. code-block:: python

    SWIFTGalaxy(
        ...,
	transforms_like_coordinates=set(),
	transforms_like_velocities={'wind_velocity'},
    )

The equivalent for coordinate-like arrays is the ``transforms_like_coordinates`` argument. The ``coordinates`` and ``velocities`` arrays are always assumed to transform as coordinates and velocities, respectively. Note that it is not necessary that a name provided in one of these arguments be present for all particle types.

By default, :class:`~swiftgalaxy.reader.SWIFTGalaxy` assumes that particle coordinates are stored in a dataset (one for each particle type) called ``coordinates``, and velocities in datasets called ``velocities``. If your data use non-standard naming of the coordinate and velocity arrays, you can provide alternative names (replace the defaults in the example snippet below with your custom names). Note that the datasets named in these arguments are implicitly added to the sets ``transforms_like_coordinates`` and ``transforms_like_velocities``, respectively.

.. code-block:: python

    SWIFTGalaxy(
        ...,
	coordinates_dataset_name='coordinates',
	velocities_dataset_name='velocities',
    )


Recentering
-----------

Automatic recentering
^^^^^^^^^^^^^^^^^^^^^

By default, a :class:`~swiftgalaxy.reader.SWIFTGalaxy` is recentered on the centre of the galaxy (as defined by the halo catalogue), and its velocity is shifted to follow the bulk velocity of the galaxy (again as defined by the halo catalogue). This behaviour can be disabled by setting ``SWIFTGalaxy(..., auto_recentre=False)``. Note that this recentering only applies to particles -- no coordinate transformations of any kind are every applied to entries in the halo catalogues, so for instance querying the position of the galaxy in the halo catalogue will always give this in the simulation box frame.

Some halo catalogues define more than one centre and/or bulk velocity. The one used is the one returned by :attr:`swiftgalaxy.halo_catalogues._HaloCatalogue.centre` (for the centre) and :attr:`swiftgalaxy.halo_catalogues._HaloCatalogue.velocity_centre` (for the bulk velocity). The centre to be used is configurable if the halo catalogue defines multiple centres. In the case of a :class:`~swiftgalaxy.halo_catalogues.SOAP` halo catalogue, for example, the centre and bulk velocity used can be manipulated by the ``centre_type`` and ``velocity_centre_type`` arguments. By default :class:`~swiftgalaxy.halo_catalogues.SOAP` uses the ``input_halos.halo_centre`` for the position and ``bound_subhalo.centre_of_mass_velocity`` for the bulk velocity. The centre in position could be changed to also use the centre of mass like this:

.. code-block:: python

    SWIFTGalaxy(
        ...,
	SOAP(
	    ...,
	    centre_type='bound_subhalo.centre_of_mass',
	),
    )

Manual recentering
^^^^^^^^^^^^^^^^^^

You may always choose a new coordinate centre or bulk velocity by providing the new centre (or bulk velocity) *in the current coordinate frame* to the appropriate function:

+ :meth:`~swiftgalaxy.reader.SWIFTGalaxy.recentre`
+ :meth:`~swiftgalaxy.reader.SWIFTGalaxy.recentre_velocity`

Recall that :mod:`swiftgalaxy` is unit-aware, so the centres must come with units -- these can be any compatible unit; conversions are handled internally. For example, for a Milky Way-like galaxy already centred on the galactic centre and rotated to lie in the :math:`x-y` plane, switching to a heliocentric frame could be achieved with something like:

.. code-block:: python

    import unyt as u
    sg = SWIFTGalaxy(...)
    ...  # presumably need to perform a rotation to align the plane
    sg.recentre((8, 0, 0) * u.kpc)
    sg.recentre_velocity((220, 0, 0) * u.km * u.s**-1)

Translations
------------

Very similarly to manually recentering the coordinate or velocity frame, functions are provided to apply a translation to the particle coordinate or velocity arrays. Note that velocity translations are referred to as *boosts*:

+ :meth:`~swiftgalaxy.reader.SWIFTGalaxy.translate`
+ :meth:`~swiftgalaxy.reader.SWIFTGalaxy.boost`

The only difference is that these are more convenient when you know the vector to translate by, instead of the vector pointing to the new centre. Keep in mind that the translation vector is interpreted *in the current frame of reference*.

Rotations
---------

Rotations of the coordinate frame apply to both particle coordinates and velocities and are therefore applied to datasets specified by both ``transforms_like_coordinates`` and ``transforms_like_velocities`` (see above). As with all coordinate transformations, a rotation is always interpreted *in the current frame of reference*.

Flexible encoding of rotations (e.g. from a rotation matrix, or Euler angles, or quaternions, etc.) are enabled via the :class:`~scipy.spatial.transform.Rotation` class. For example, if the rotation matrix is known it can be provided as (here with an identity rotation):

.. code-block:: python

    from scipy.spatial.transform import Rotation
    sg = SWIFTGalaxy(...)
    sg.rotate(
        Rotation.from_matrix(
            [[1, 0, 0],
             [0, 1, 0],
             [0, 0, 1]]
        )
    )

For the full list of encodings supported by :class:`~scipy.spatial.transform.Rotation`, see its documentation.

Box wrapping
------------

For a periodic simulation box, the spatial coordinates will automatically be wrapped as necessary to ensure that their absolute values remain less than half a box length. If for any reason you wish to force a box wrapping operation simply call :meth:`~swiftgalaxy.reader.SWIFTGalaxy.wrap_box`.

Copying a coordinate system
---------------------------

A second :class:`~swiftgalaxy.reader.SWIFTGalaxy` can be created using the (current) coordinate system of an existing instance:

.. code-block:: python

    sg1 = SWIFTGalaxy(...)
    sg1.rotate(...)
    sg1.translate(...)
    sg2 = SWIFTGalaxy(..., auto_recentre=False, coordinate_frame_from=sg1)

This is only possible at creation time, and ``auto_recentre`` must be set to ``False``. Also note that this does not lock the coordinate systems together. For instance, if one galaxy is subsequently rotated, the second will not rotate with it.
