.. _derived:

Automatically Derived Arrays
============================

``Pynbody`` includes a system which automatically derives one array from
others. The idea is that this system

(1) permits analysis code to assume that certain arrays exist --
whether or not they exist in the file;

(2) allows arrays to be kept up-to-date when they depend on other
arrays


Built-in derived arrays
-----------------------

The quantities listed under :mod:`~pynbody.derived` are calculated for
all simulation types. If you want, for example, to calculate the
radius of particles away from the origin, you can just access the ``r`` array and
pynbody will calculate it:

.. ipython::

   In [3]: import pynbody

   In [4]: s = pynbody.load('testdata/gasoline_ahf/g15784.lr.01024.gz')

   In [5]: s['r']

Note that you cannot directly alter a derived array:

.. ipython:: :okexcept:

   In [5]: s['r'][0] = 0

But the array updates itself when one of its dependencies changes:

.. ipython::

   In [5]: s['x']-=1

   In [5]: s['r']

The recalculation only takes place when required -- in the example
above the ``r`` array is stored (just like a normal array) until the ``pos`` array is updated,
at which point it is deleted. When you ask for the ``r`` array again,
it is recalculated.

This is why you're not allowed to change values in a derived array --
your changes could be overwritten, leading to confusing bugs. However,
if you want to make a derived array back into a normal array you can
do so.

.. ipython::

   In [5]: s['r'].derived = False

   In [5]: s['r'][0] = 0

   In [5]: s['r']


At this point you've taken full responsibility for the
array, so if you update its dependencies the framework *won't* help
you out any longer:

.. ipython::

   In [5]: s['x']+=2

   In [5]: s['r'] # NOT updated because we took control by setting derived = False


To get the framework back on your side, you can delete the modified
array:

.. ipython::

   In [5]: del s['r']

   In [5]: s['r']

Here we've deleted then re-derived the ``r`` array, so it's now accurate (and will start
auto-updating again).

Derived functions for specific formats
--------------------------------------

Some derived arrays are specific to certain simulation formats. For example, ramses simulations
need to derive masses for their gas cells and as such :func:`~pynbody.ramses.mass` is registered
as a derived array specifically for the :class:`~pynbody.snapshot.ramses.RamsesSnap` class.


Defining your own deriving functions
------------------------------------

You can easily define your own derived arrays. The easiest way to do
this is using the decorator ``pynbody.derived_array``.

Here's an example of a derived array that calculates the specific
kinetic energy of a particle:

.. ipython::

   In [5]: @pynbody.derived_array
      ...: def specific_ke(sim):
      ...:     return 0.5 * (sim['vel']**2).sum(axis=1)

   In [7]: s['specific_ke']

When your function is called, the framework monitors any arrays it
retrieves from the simulation. It automatically marks the accessed
arrays as dependencies for your function. So, if the velocities now
get changed, your derived array will be recalculated:


.. ipython::

   In [7]: s['vel']*=2

   In [7]: s['specific_ke']

To create a derived array associated with a specific subclass, use the
:meth:`~pynbody.snapshot.simsnap.SimSnap.derived_array` method of that subclass,
e.g.

.. ipython::

   In [5]: @pynbody.snapshot.tipsy.TipsySnap.derived_array
      ...: def half_mass(sim):
      ...:     return 0.5 * sim['mass']

   In [7]: s['half_mass'] # this is a TipsySnap, so this will work

   In [8]: another_snap = pynbody.new(dm=100) # NOT a TipsySnap!

   In [9]: another_snap['half_mass']

The derived array will only be available for the class it was defined for, so the
final command will raise an error.

.. versionchanged:: 2.0
    The method name has been changed from ``derived_quantity`` to ``derived_array``.
    The old name is still available but will be removed in a future version.

.. note::
    ``pynbody.derived_array`` is an alias for
    :func:`pynbody.snapshot.simsnap.SimSnap.derived_array`.

    Derived arrays apply to all subclasses of the class on which they are defined.

.. _stable_derived:

Stable derived arrays
----------------------

Occasionally, you may want to define a derived array that is not automatically recalulated
when its underlying dependencies change. An example from within the framework is the
smoothing length (``smooth``). This is expensive to calculate, and changes to the underlying position
coordinates -- while in theory capable of changing the smoothing length -- are far more commonly
associated with translations or rotations in the course of a normal analysis. In this case, it
would be wasteful to recalculate the smoothing length every time the position coordinates change.

To define a stable derived array, use :meth:`~pynbody.snapshot.simsnap.SimSnap.stable_derived_array`
in place of :meth:`~pynbody.snapshot.simsnap.SimSnap.derived_array`. The array will be derived for
the first time, but will not automatically update:

.. ipython::

   In [5]: @pynbody.snapshot.simsnap.SimSnap.stable_derived_array
      ...: def stable_x_copy(sim):
      ...:     return sim['x']

   In [7]: s['stable_x_copy']

   In [8]: s['x']+=1

   In [9]: s['x']

   In [9]: s['stable_x_copy']
