Source code for pynbody.util.indexing_tricks

"""Tricks for manipulating indexes and slices into arrays for internal use by pynbody."""

from __future__ import annotations

import numpy as np


def _gcf(a, b):
    """Return the greatest common factor of a and b"""
    while b > 0:
        a, b = b, a % b
    return a


def _lcm(a, b):
    """Return the least common multiple of a and b"""
    return (a * b) // _gcf(a, b)


[docs] def intersect_slices(s1, s2, array_length=None): """Given two python slices s1 and s2, return a slice that picks out all members of s1 and s2. That is, if d is an array, then ``d[intersect_slices(s1,s2,len(d))]`` returns all elements of ``d`` that are in both ``d[s1]`` and ``d[s2]``. Note that it may not be possible to do this without information on the length of the array referred to, hence all slices with end-relative indexes are first converted into begin-relative indexes. The slice returned may be specific to the length specified. If the slices are mutually exclusive, a zero-length slice is returned. Parameters ---------- s1 : slice The first slice s2 : slice The second slice array_length : int, optional The length of the array to which the slices refer. If not specified, the slices must have positive start and stop. Returns ------- slice A slice that picks out all elements of s1 and s2, as described above. """ assert array_length is not None or \ (s1.start >= 0 and s2.start >= 0 and s1.stop >= 0 and s2.start >= 0) s1_start = s1.start s2_start = s2.start s1_stop = s1.stop s2_stop = s2.stop s1_step = s1.step s2_step = s2.step if s1_step is None: s1_step = 1 if s2_step is None: s2_step = 1 assert s1_step > 0 and s2_step > 0 if s1_start < 0: s1_start = array_length + s1_start if s1_start < 0: return slice(0, 0) if s2_start < 0: s2_start = array_length + s2_start if s2_start < 0: return slice(0, 0) if s1_stop < 0: s1_stop = array_length + s1_stop if s1_stop < 0: return slice(0, 0) if s2_stop < 0: s2_stop = array_length + s2_stop if s2_stop < 0: return slice(0, 0) step = _lcm(s1_step, s2_step) start = max(s1_start, s2_start) stop = min(s1_stop, s2_stop) if stop <= start: return slice(0, 0) s1_offset = start - s1_start s2_offset = start - s2_start s1_offset_x = int(s1_offset) s2_offset_x = int(s2_offset) if s1_step == s2_step and s1_offset % s1_step != s2_offset % s1_step: # slices are mutually exclusive return slice(0, 0) # There is surely a more efficient way to do the following, but # it eludes me for the moment while s1_offset % s1_step != 0 or s2_offset % s2_step != 0: start += 1 s1_offset += 1 s2_offset += 1 if s1_offset % s1_step == s1_offset_x % s1_step and s2_offset % s2_step == s2_offset_x % s2_step: # slices are mutually exclusive return slice(0, 0) if step == 1: step = None return slice(start, stop, step)
[docs] def relative_slice(s_relative_to, s): """Return a slice s_prime with the property that ar[s_relative_to][s_prime] == ar[s]. Clearly this will not be possible for arbitrarily chosen s_relative_to and s, but it should be possible for any ``s = intersect_slices(s_relative_to, s_any)``, which is the use case envisioned here. If impossible, a ValueError is raised. This code does not work with end-relative (i.e. negative) start or stop positions. """ assert (s_relative_to.start >= 0 and s.start >= 0 and s.stop >= 0) if s.start == s.stop: return slice(0, 0, None) s_relative_to_step = s_relative_to.step if s_relative_to.step is not None else 1 s_step = s.step if s.step is not None else 1 if (s.start - s_relative_to.start) % s_relative_to_step != 0: raise ValueError("Incompatible slices") if s_step % s_relative_to_step != 0: raise ValueError("Incompatible slices") start = (s.start - s_relative_to.start) // s_relative_to_step step = s_step // s_relative_to_step stop = start + \ (s_relative_to_step - 1 + s.stop - s.start) // s_relative_to_step if step == 1: step = None return slice(start, stop, step)
[docs] def chained_slice(s1, s2): """Return a slice s3 with the property that ar[s1][s2] == ar[s3]""" assert (s1.start >= 0 and s2.start >= 0 and s1.stop >= 0 and s2.stop >= 0) s1_start = s1.start or 0 s2_start = s2.start or 0 s1_step = s1.step or 1 s2_step = s2.step or 1 start = s1_start + s2_start * s1_step step = s1_step * s2_step if s1.stop is None and s2.stop is None: stop = None elif s1.stop is None: stop = start + step * (s2.stop - s2_start) // s2_step elif s2.stop is None: stop = s1.stop else: stop_s2 = start + step * (s2.stop - s2_start) // s2_step stop_s1 = s1.stop stop = stop_s2 if stop_s2 < stop_s1 else stop_s1 return slice(start, stop, step)
[docs] def index_before_slice(s, index): """Return an index array new_index such that ``ar[s][new_index] == ar[index]``. Arguments --------- s : slice The slice to apply to the array index : array-like The index array to apply to the sliced array Returns ------- new_index : array-like The index array that will pick out the same elements of the sliced array as index does of the original array """ start = s.start or 0 step = s.step or 1 assert start >= 0 assert step >= 0 assert s.stop is None or s.stop >= 0 new_index = start + index * step if s.stop is not None: new_index = new_index[np.where(new_index < s.stop)] return new_index
[docs] def concatenate_indexing(i1, i2): """Given either a numpy array or slice for both i1 and i2, return an object such that ar[i3] == ar[i1][i2]. As a convenience, if i2 is None, i1 is returned. Parameters ---------- i1 : array-like or slice The first indexing or slicing operation to apply i2 : array-like or slice The second indexing or slicing operation to apply Returns ------- i3 : array-like or slice The combined indexing or slicing operation """ if isinstance(i1, tuple) and len(i1) == 1: i1 = i1[0] if isinstance(i2, tuple) and len(i2) == 1: i2 = i2[0] if i2 is None: return i1 if isinstance(i1, slice) and isinstance(i2, slice): return chained_slice(i1, i2) elif isinstance(i1, slice) and isinstance(i2, (np.ndarray, list)): return index_before_slice(i1, i2) elif isinstance(i1, (np.ndarray, list)) and isinstance(i2, (slice, np.ndarray)): return np.asarray(i1)[i2] else: raise TypeError("Don't know how to chain these index types")
[docs] def indexing_length(sl_or_ar): """Given either an array or slice, return ``len(ar[sl_or_ar])`` This assumes that the slice does not overrun the array, e.g. if an array ``ar`` is shorter than the stop index of the slice ``sl`` then this routine will return an inaccurate result for ``len(ar[sl])``. Parameters ---------- sl_or_ar : slice or array-like The slice or array-like object to consider Returns ------- int The length of the array after slicing """ if isinstance(sl_or_ar, slice): step = sl_or_ar.step or 1 diff = (sl_or_ar.stop - sl_or_ar.start) return diff // step + (diff % step > 0) else: return len(sl_or_ar)