ExZarr.Indexing (ExZarr v1.1.0)

View Source

Advanced indexing operations for ExZarr arrays.

This module provides comprehensive indexing support including:

  • Basic slicing with step support
  • Negative index handling
  • Fancy indexing (integer arrays)
  • Boolean indexing (masks)
  • Block indexing (chunk-aligned access)

Index Normalization

Indices can be negative (counting from the end) and are automatically normalized to positive values:

normalize_index(-1, 10) # => 9
normalize_index(-5, 10) # => 5
normalize_index(5, 10)  # => 5

Slice Specifications

Slices can include start, stop, and step:

%{start: 0, stop: 10, step: 1}    # Every element from 0 to 9
%{start: 0, stop: 10, step: 2}    # Every other element
%{start: 10, stop: 0, step: -1}   # Reverse order

Summary

Functions

Computes chunk-aligned block boundaries for efficient access.

Extracts indices from a boolean mask.

Normalizes an index to a positive value within bounds.

Normalizes a slice specification to have explicit start, stop, and step values.

Converts a multidimensional slice specification to start/stop tuples for each dimension.

Computes the effective slice range from a normalized slice specification.

Computes the size (number of elements) of a slice.

Validates boolean indexing with a mask.

Validates fancy indexing with integer arrays.

Types

index()

@type index() :: integer()

index_spec()

@type index_spec() :: index() | slice_spec() | [index()] | {:boolean, tuple()}

slice_spec()

@type slice_spec() :: %{start: index() | nil, stop: index() | nil, step: integer()}

Functions

compute_block_indices(start, stop, chunk_size)

@spec compute_block_indices(non_neg_integer(), non_neg_integer(), pos_integer()) :: [
  {non_neg_integer(), non_neg_integer()}
]

Computes chunk-aligned block boundaries for efficient access.

Given a start/stop range and chunk size, computes the blocks that fully or partially overlap with the range. Returns block boundaries that are chunk-aligned.

Examples

iex> ExZarr.Indexing.compute_block_indices(5, 25, 10)
[{0, 10}, {10, 20}, {20, 30}]

iex> ExZarr.Indexing.compute_block_indices(10, 20, 10)
[{10, 20}]

mask_to_indices(mask)

@spec mask_to_indices(tuple()) :: [integer()]

Extracts indices from a boolean mask.

Returns a list of indices where the mask is true.

Examples

iex> mask = {true, false, true, false, true}
iex> ExZarr.Indexing.mask_to_indices(mask)
[0, 2, 4]

normalize_index(index, size)

@spec normalize_index(index(), pos_integer()) ::
  {:ok, non_neg_integer()} | {:error, atom()}

Normalizes an index to a positive value within bounds.

Handles negative indices by counting from the end of the dimension. Validates that the index is within the valid range [0, size).

Examples

iex> ExZarr.Indexing.normalize_index(5, 10)
{:ok, 5}

iex> ExZarr.Indexing.normalize_index(-1, 10)
{:ok, 9}

iex> ExZarr.Indexing.normalize_index(-5, 10)
{:ok, 5}

iex> ExZarr.Indexing.normalize_index(15, 10)
{:error, :index_out_of_bounds}

normalize_slice(slice, size)

@spec normalize_slice(slice_spec(), pos_integer()) ::
  {:ok, slice_spec()} | {:error, term()}

Normalizes a slice specification to have explicit start, stop, and step values.

Fills in defaults for missing values:

  • start defaults to 0 (or size-1 for negative step)
  • stop defaults to size (or -1 for negative step)
  • step defaults to 1

Handles negative indices and validates the slice is within bounds.

Examples

iex> ExZarr.Indexing.normalize_slice(%{start: 0, stop: 10, step: 1}, 20)
{:ok, %{start: 0, stop: 10, step: 1}}

iex> ExZarr.Indexing.normalize_slice(%{start: -5, stop: nil, step: 1}, 20)
{:ok, %{start: 15, stop: 20, step: 1}}

iex> ExZarr.Indexing.normalize_slice(%{start: nil, stop: nil, step: -1}, 20)
{:ok, %{start: 19, stop: -1, step: -1}}

parse_slice_spec(specs, shape)

@spec parse_slice_spec([index_spec()], tuple()) ::
  {:ok, {list(), list()}} | {:error, term()}

Converts a multidimensional slice specification to start/stop tuples for each dimension.

Takes a list of index specifications (integers, slices, or arrays) and converts them to normalized start/stop tuples per dimension.

Examples

iex> ExZarr.Indexing.parse_slice_spec([5, %{start: 0, stop: 10, step: 1}], {100, 100})
{:ok, {[5], [%{start: 0, stop: 10, step: 1}]}}

slice_indices(map)

@spec slice_indices(slice_spec()) :: [integer()]

Computes the effective slice range from a normalized slice specification.

Returns a list of indices that the slice covers, handling step correctly. This is useful for computing the actual size of a slice result.

Examples

iex> ExZarr.Indexing.slice_indices(%{start: 0, stop: 10, step: 1})
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

iex> ExZarr.Indexing.slice_indices(%{start: 0, stop: 10, step: 2})
[0, 2, 4, 6, 8]

iex> ExZarr.Indexing.slice_indices(%{start: 10, stop: 0, step: -2})
[10, 8, 6, 4, 2]

slice_size(map)

@spec slice_size(slice_spec()) :: non_neg_integer()

Computes the size (number of elements) of a slice.

Examples

iex> ExZarr.Indexing.slice_size(%{start: 0, stop: 10, step: 1})
10

iex> ExZarr.Indexing.slice_size(%{start: 0, stop: 10, step: 2})
5

iex> ExZarr.Indexing.slice_size(%{start: 10, stop: 0, step: -1})
10

validate_boolean_mask(mask, dim_size)

@spec validate_boolean_mask(tuple(), pos_integer()) :: :ok | {:error, term()}

Validates boolean indexing with a mask.

Ensures the mask has the same shape as the dimension being indexed.

Examples

iex> mask = {true, false, true, false, true}
iex> ExZarr.Indexing.validate_boolean_mask(mask, 5)
:ok

iex> mask = {true, false, true}
iex> ExZarr.Indexing.validate_boolean_mask(mask, 5)
{:error, :mask_size_mismatch}

validate_fancy_indices(indices, dim_size)

@spec validate_fancy_indices([integer()], pos_integer()) :: :ok | {:error, term()}

Validates fancy indexing with integer arrays.

Ensures all indices in the array are within bounds for the dimension.

Examples

iex> ExZarr.Indexing.validate_fancy_indices([0, 5, 9], 10)
:ok

iex> ExZarr.Indexing.validate_fancy_indices([0, 5, 15], 10)
{:error, :index_out_of_bounds}