Source code for abtem.core.ensemble

from __future__ import annotations

import itertools
import warnings
from abc import abstractmethod
from itertools import accumulate
from typing import Any, Callable, Generator, Optional, Union

import dask.array as da
import numpy as np

from abtem.core.axes import AxesMetadataList, AxisMetadata
from abtem.core.chunks import Chunks, ValidatedChunks, chunk_ranges, validate_chunks
from abtem.core.utils import interleave, itemset


def _wrap_with_array(x: Any, ndims: int | None = None) -> np.ndarray:
    if ndims is None:
        ndims = len(x.ensemble_shape)

    wrapped = np.zeros((1,) * ndims, dtype=object)
    itemset(wrapped, 0, x)
    return wrapped


[docs] def unpack_blockwise_args(args) -> tuple: unpacked = tuple(arg.item() if hasattr(arg, "item") else arg for arg in args) return unpacked
[docs] class Ensemble: @property def ensemble_shape(self) -> tuple[int, ...]: """Shape of the ensemble axes.""" return () @property def base_shape(self) -> tuple[int, ...]: """Shape of the base axes.""" return () @property def shape(self) -> tuple[int, ...]: """Shape of the ensemble.""" return self.ensemble_shape + self.base_shape @property def base_axes_metadata(self) -> list[AxisMetadata]: """List of AxisMetadata of the base axes.""" return [] @property def ensemble_axes_metadata(self) -> list[AxisMetadata]: """List of AxisMetadata of the ensemble axes.""" return [] @property def axes_metadata(self) -> AxesMetadataList: """List of AxisMetadata.""" return AxesMetadataList( self.ensemble_axes_metadata + self.base_axes_metadata, self.shape ) @property @abstractmethod def _default_ensemble_chunks(self) -> Chunks: pass def _validate_ensemble_chunks( self, chunks: Optional[Chunks] = None, limit: Union[str, int] = "auto" ) -> ValidatedChunks: if chunks is None: chunks = self._default_ensemble_chunks chunks = validate_chunks(self.ensemble_shape, chunks, max_elements=limit) return chunks @abstractmethod def _partition_args( self, chunks: Optional[Chunks] = None, lazy: bool = True ) -> tuple: pass @abstractmethod def _from_partitioned_args(self) -> Callable[..., np.ndarray]: pass
[docs] def ensemble_blocks(self, chunks: Optional[Chunks] = None) -> da.core.Array: """ Split the ensemble into an array of smaller ensembles. Parameters ---------- chunks : iterable of tuples Block sizes along each dimension. """ chunks = self._validate_ensemble_chunks(chunks) args = self._partition_args(chunks, lazy=True) arg_dims = tuple(len(arg.shape) for arg in args) arg_starts = accumulate((0,) + arg_dims[:-1]) arg_ends = accumulate(arg_dims) arg_ind = tuple( tuple(range(start, end)) for start, end in zip(arg_starts, arg_ends) ) out_ind = tuple(range(sum(arg_dims))) adjust_chunks = {i: axes_chunks for i, axes_chunks in enumerate(chunks)} func = self._from_partitioned_args() with warnings.catch_warnings(): warnings.filterwarnings("ignore", message="Increasing number of chunks") blocks = da.blockwise( func, out_ind, *interleave(args, arg_ind), adjust_chunks=adjust_chunks, concatenate=True, meta=np.array((), dtype=object), ) return blocks
[docs] def generate_blocks( self, chunks: Chunks = 1 ) -> Generator[tuple[tuple[int, ...], tuple[slice, ...], np.ndarray], None, None]: """ Generate chunks of the ensemble. Parameters ---------- chunks : iterable of tuples Block sizes along each dimension. """ chunks = self._validate_ensemble_chunks(chunks) blocks = self._partition_args(chunks=chunks, lazy=False) shape = sum((block.shape for block in blocks), ()) start_stops = chunk_ranges(chunks) # print(start_stops) # print(shape) assert tuple(len(cr) for cr in start_stops) == shape for indices, start_stop in zip( np.ndindex(shape), itertools.product(*start_stops) ): block_indices: tuple[tuple[int, ...], ...] = () j = 0 for block in blocks: n = len(block.shape) block_indices += (tuple(indices[index] for index in range(j, j + n)),) j += n args = tuple(block[i] for i, block in zip(block_indices, blocks)) slics = tuple(slice(start, stop) for start, stop in start_stop) yield indices, slics, self._from_partitioned_args()(*args)
# print(blocks) # for block in blocks: # if len(block.shape) > 1: # print(block) # raise NotImplementedError # axis_indices = tuple( # tuple(range(block.shape[0])) if len(block.shape) else () for block in blocks # ) # if not any(len(indices) for indices in axis_indices): # yield (), (), self._from_partitioned_args()(*blocks) # print(len(tuple(itertools.product(*chunk_ranges(chunks))))) # for block_indices, start_stop in zip( # itertools.product(*axis_indices), # itertools.product(*chunk_ranges(chunks)), # ): # block = tuple(block[i] for i, block in zip(block_indices, blocks)) # slics = tuple(slice(start, stop) for start, stop in start_stop) # print(slics) # yield block_indices, slics, self._from_partitioned_args()(*block)
[docs] class EmptyEnsemble(Ensemble): @property def _default_ensemble_chunks(self) -> Chunks: return () @property def ensemble_axes_metadata(self) -> list[AxisMetadata]: return [] def _partition_args( self, chunks: Optional[Chunks] = None, lazy: bool = True ) -> tuple: return () def _from_partitioned_args(self) -> type: return self.__class__ @property def ensemble_shape(self) -> tuple[int, ...]: return ()
[docs] def concatenate_array_blocks(blocks: np.ndarray) -> np.ndarray: for i in range(len(blocks.shape)): new_blocks = np.empty(blocks.shape[:-1], dtype=object) for indices in np.ndindex(blocks.shape): concat_index = len(indices) - 1 indices = indices[:-1] new_blocks[indices] = np.concatenate(blocks[indices], axis=concat_index) blocks = new_blocks return blocks.item()