.. filters tutorial

.. _filters_tutorial

Subviews and Filters
====================

Analyzing simulations often boils down to comparing properties of
groups of particles or sections of the simulated volume. Therefore a
generic task in analysis routines is to search for indices of
particles (or cells) corresponding to some interesting property, so
analysis code often looks something like this:

>>> index = numpy.where(sim["group"]==group_id)
>>> (sim["mass"][index]*sim["pos"][index]).mean(axis=0)

In this example, we obtained the center of mass of some group
specified by ``group_id``. Such patterns are fine, but they are ugly to
look at and illegible when written out in long complicated chains.

For this reason, ``pynbody`` has the concept of sub-views of
simulations. Subviews behave exactly like a full simulation, but they
reference only a subset of the data of that full simulation.

This makes writing code which processes individual halos trivial; for
instance

>>> (sim["mass"]*sim["pos"]).mean(axis=0)

will find the centre of mass of whatever you throw at it. So if we had
some way of defining a *sub-view* of the whole simulation that refers
to just the halo that we want, called ``halo``, then we could do
something like

>>> (halo["mass"]*halo["pos"]).mean(axis=0)

The indexing is taken care of under the hood and all the boilerplate
indexing code is gone. In this tutorial, we discuss some of the ways
that subviews and filters can be used in ``pynbody``.

How do I create a simple subview?
---------------------------------

Quite simply, indexing or slicing operations commute with array-fetch
operations, i.e.

>>> subsim = sim[slice] # -> new SimSnap object
>>> sim[slice]["x"] == sim["x"][slice] # -> True
>>> subsim["x"] == sim["x"][slice] # -> True

``slice`` here can literally be a python slice (e.g. ``sim[22:73:2]``,
starting at the 22nd particle, finishing at the 72nd, taking every
second particle). It can also be a list of particle indices.

Or, more interestingly, it can be a ``filter``. More on that below, but first...

Conceptually, everything is a pointer
-------------------------------------

Whenever you create a sub-view of your simulation, you should think of
it as a _pointer_ into, not a copy of, the original data.

Thus changing anything in the arrays of your sub-view makes the
corresponding change in the full data.

>>> subsim['x'][0] = 22

You can verify that the corresponding element of ``sim['x']`` has now changed

Filters
-------

:mod:`pynbody.filt` defines abstract filters which can be used in place of
index lists. For instance,

>>> radius = "1 kpc" # or might be a float if you already know your units
>>> centre = (0,0,0) # Take the origin for now
>>> sphere = sub[pynbody.filt.Sphere(radius, centre)]

``sphere`` is now a standard subsim, as though you'd generated it with
your own index list.

>>> sphere["mass"].sum() # -> total mass in sphere
>>> sphere["pos"] # etc


Under the hood, filters are very simple objects which have a
``__call__`` method taking a simulation as the sole parameter, and
returning a boolean array representing whether particles are included
(``True``) or excluded (``False``) according to the given filter
object. The framework uses this to generate an index list and proceeds
as though you'd passed that index list in.

The benefits that this brings are:

  * Code clarity
  * Code factorization (guaranteed that the 'sphere' filter works,
    rather than having to recode a sphere index-list operation each
    time)
  * Future extensibility (e.g. we can just start having Sphere's
    accept quantities with units, rather than having to reimplement to
    include units in all code which pulls out spheres)
  * Parallelizability (the 'filter' objects can be trivially sent to
    another node, whereas an index list is irrelevant on another node)

Filters can be combined using the following operators, which can of course be chained together:

============  ========================
**Operator**  **Particle included if**
============  ========================
``f1 & f2``     Filters ``f1`` and ``f2`` both include particle
``f1 | f2``     Filter ``f1`` OR ``f2`` includes particle
``~f``          Filter ``f`` does NOT include particle
============  ========================

There are several filter classes currently implemented. Generic filter
classes work like band-pass filters and are conveniently named so. For example,

>>> young = pynbody.filt.LowPass('age', '1 Gyr')
>>> old = pynbody.filt.HighPass('age', '10 Gyr')
>>> intermediate = pynbody.filt.BandPass('age', '3 Gyr', '7 Gyr')

would create three filters that each select particles with different
value ranges for the ``age`` array.

Additionaly, several "geometric" filter classes are available. We
already saw :class:`~pynbody.filt.Sphere` used above, but
:class:`~pynbody.filt.Disc`, :class:`~pynbody.filt.Cuboid`, and two
convenience functions :func:`~pynbody.filt.SolarNeighborhood` and
:func:`~pynbody.filt.Annulus` are also available.
