konigcell.dynamic2d#

konigcell.dynamic2d(positions, mode, values=None, radii=None, pixels=None, resolution=None, xlim=None, ylim=None, executor=<class 'concurrent.futures.thread.ThreadPoolExecutor'>, max_workers=None, verbose=True)[source]#

Pixelize / rasterize a moving particle’s trajectory onto a 2D pixel grid.

This function is very general, so the description below may seem abstract; do check out the examples for specific applications and consult these docs if / as needed.

The particle’s positions are given as a 2D NumPy array, with two columns representing the X and Y locations; each row corresponds to a single recorded particle position. Multiple trajectories can be separated by a row of NaN.

As a particle moves in a straight line between two consecutive positions, it will be approximated as the convex hull of two circles (centred at the given positions, with radii defined by the input radii) - like a rectangle with two circular ends. Each such circular rectangle defined by two consecutive particle positions will be rasterized onto a pixel grid.

The values contained by the returned pixel grid depend on the input mode:

  1. kc.RATIO: each pixel will contain the ratio of its intersected area to the total area of the circular rectangle defined by a particle moving between two adjacent positions. This is useful for e.g. velocity probability distributions.

  2. kc.INTERSECTION: each pixel will contain the intersected area of the circular rectangle; if the entire pixel was covered by the particle movement, it will simply contain the pixel area p_dx * p_dy. This is useful for e.g. residence time distributions.

  3. kc.PARTICLE: each pixel will contain the area spanned by the circular rectangle.

  4. kc.ONE: each intersected pixel will have one added to it.

The areas defined above can be multiplied by a particle-related quantity, e.g. particle velocity or time interval between two consecutive particle locations. They are defined by the input values; if unset all values are considered 1.

The particles may have a radius; if the input radii is given as a single number, all particles will have this radius. Alternatively, the particle may change radius if radii is given as a NumPy vector containing a radius associated with each particle location. If unset (None), the particle is considered point-like / infinitesimally small.

The pixel grid can be defined in two ways:

  1. Set the input pixels to a pre-existing konigcell.Pixels to reuse it, minimising memory allocation.

  2. Set the input resolution so that a new konigcell.Pixels will be created and returned by this function. If unset (None), xlim and ylim will be computed automatically to contain all particle positions.

The rasterization can be done in parallel, as defined by the input executor, which can be a concurrent.futures.Executor subclass; e.g. ThreadPoolExecutor for multi-threaded execution, or mpi4py.futures MPIPoolExecutor for distributing the computation across multiple MPI nodes. It can also be an instance of such class, configured beforehand.

The number of parallel workers (threads, processes or ranks) is set by max_workers; if set to 1, execution is sequential (and produces better error messages). If unset, the os.cpu_count() is used.

If verbose is True, the computation is timed and (hopefully) helpful messages are printed during execution.

Parameters:
positions(N, 2) np.ndarray[ndim=2, dtype=float64]

The 2D particle positions. Separate multiple trajectories with a row of NaN.

modekc.RATIO, kc.INTERSECTION, kc.PARTICLE, kc.ONE

The rasterization mode, see above for full details.

valuesfloat, (N-1,) np.ndarray, optional

The particle values to rasterize, will be multiplied with what the mode returns; if a single float, all values are set to it. Multiple values can be given in a NumPy array for each particle movement, so it needs 1 fewer values than positions (e.g. for particle positions A, B, C, there are only movements AB then BC). If unset (default), it is considered 1.

radiifloat, (N,) np.ndarray, optional

Each particle’s radius; if a single number, all particles are will have this radius. Multiple radii can be given in a NumPy array for each particle position. If None (default), the particle is considered point-like / infinitesimally small.

pixelskc.Pixels, optional

A pre-created pixel grid to use; if unset, a new one will be created - so resolution must be set.

resolution2-tuple, optional

If pixels is unset, a new pixel grid will be created; this resolution contains the number of pixels in the X and Y dimensions, e.g. (512, 512). There must be at least 2 pixels in each dimension.

xlim2-tuple, optional

If pixels is unset, a new pixel grid will be created; you can manually set the physical rectangle spanned by this new grid as [xmin, xmax]. If unset, it is automatically computed to contain all particle positions.

ylim2-tuple, optional

Same as for xlim.

executorconcurrent.futures.Executor subclass or instance

The parallel executor to use, implementing the Executor interface. For distributed computation with MPI, use MPIPoolExecutor from mpi4py. The default is ThreadPoolExecutor, which has the lowest overhead - useful because the main computation is done by C code releasing the GIL.

max_workersint, optional

The maximum number of workers (threads, processes or ranks) to use by the parallel executor; if 1, it is sequential (and produces the clearest error messages should they happen). If unset, the os.cpu_count() is used.

verbosebool or str default True

If True, time the computation and print the state of the execution. If str, show a message before loading bars.

Examples

Compute the occupancy grid of a 2D particle of radius 0.5 moving between 3 positions:

>>> import numpy as np
>>> positions = np.array([[0, 0], [1, 1], [2, 1]])
>>>
>>> import konigcell as kc
>>> pixels = kc.dynamic2d(
>>>     positions,
>>>     kc.INTERSECTION,
>>>     radii = 0.5,
>>>     resolution = (500, 500),
>>>     xlim = [-2, 5],
>>>     ylim = [-2, 5],
>>> )

The kc.INTERSECTION pixellisation mode adds the area shaded by the particle’s movement onto the grid.

For a residence time distribution, compute the time difference between the particle’s timestamps:

>>> times = np.array([0, 2, 3])
>>> dt = np.diff(times)
>>> pixels = kc.dynamic2d(
>>>     positions,
>>>     kc.RATIO,
>>>     values = dt,
>>>     radii = 0.5,
>>>     resolution = (500, 500),
>>> )

The kc.RATIO pixellisation mode splits a given value (in this case the time difference) across a particle’s trajectory proportionally to the shaded area.

If you omit xlim and ylim, they will be computed automatically to include all particle positions.

You can reuse the same pixels grid for multiple pixellisations:

>>> kc.dynamic2d(
>>>     positions,
>>>     kc.INTERSECTION,
>>>     radii = 0.5,
>>>     pixels = pixels,
>>> )

In this case the resolution, xlim and ylim don’t need to be set anymore; they are extracted from the Pixels grid.

You can set the maximum number of workers for the parallel pixellisation; e.g. for single-threaded execution:

>>> pixels = kc.dynamic2d(
>>>     positions,
>>>     kc.ONE,
>>>     radii = 0.5,
>>>     resolution = (500, 500),
>>>     max_workers = 1,
>>> )

The kc.ONE pixellisation flag simply adds 1 to each pixel intersected by the moving particle.

Finally, here is a complete example computing the residence time distribution of a 2D particle moving randomly in a box of length 5:

>>> positions = np.random.random((10_000, 2)) * 5
>>> times = np.linspace(0, 100, 10_000)
>>> dt = np.diff(times)
>>>
>>> rtd = kc.dynamic2d(
>>>     positions,
>>>     kc.RATIO,
>>>     values = dt,
>>>     radii = 0.5,
>>>     resolution = (500, 500),
>>> )