###########################################################################################
# Copyright ironArray SL 2021.
#
# All rights reserved.
#
# This software is the confidential and proprietary information of ironArray SL
# ("Confidential Information"). You shall not disclose such Confidential Information
# and shall use it only in accordance with the terms of the license agreement.
###########################################################################################
import numpy as np
import iarray as ia
from iarray import iarray_ext as ext
from .utils import IllegalArgumentError, zarr_to_iarray_dtypes
from dataclasses import dataclass
from typing import (
Any,
Optional,
Protocol,
Sequence,
Tuple,
TypeVar,
Union,
)
from .dtypes import (
_all_dtypes,
_boolean_dtypes,
_integer_dtypes,
_integer_or_boolean_dtypes,
_floating_dtypes,
_numeric_dtypes,
_dtype_categories,
)
_T_co = TypeVar("_T_co", covariant=True)
class NestedSequence(Protocol[_T_co]):
def __getitem__(self, key: int, /):
...
def __len__(self, /):
...
SupportsBufferProtocol = Any
@dataclass
class DTShape:
"""Shape and data type dataclass.
Parameters
----------
shape: list, tuple
The shape of the array.
dtype: (np.float64, np.float32, np.int64, np.int32, np.int16, np.int8, np.uint64, np.uint32, np.uint16,
np.uint8, np.bool_)
The data type of the elements in the array. The default is np.float64.
"""
shape: Sequence
dtype: (
np.float64,
np.float32,
np.int64,
np.int32,
np.int16,
np.int8,
np.uint64,
np.uint32,
np.uint16,
np.uint8,
np.bool_,
) = np.float64
def __post_init__(self):
if self.shape is None:
raise ValueError("shape must be non-empty")
def empty(
shape: Union[int, Tuple[int, ...]],
*,
device: Optional[ia.Device] = None,
cfg: ia.Config = None,
**kwargs,
) -> ia.IArray:
"""Return an uninitialized array.
An empty array has no data and needs to be filled via a write iterator.
Parameters
----------
shape : int, tuple
The shape of the array to be created.
device: Device
The device on which to place the created array. The only supported value is `"cpu"`.
cfg : :class:`Config`
The configuration to use.
If None (default), global defaults are used.
kwargs : dict
A dictionary for setting some or all of the fields in the :class:`Config`
dataclass that should override the current configuration.
Returns
-------
:ref:`IArray`
The new array.
"""
if device not in ["cpu", None]:
raise ValueError(f"Unsupported device {device!r}")
if cfg is None:
cfg = ia.get_config_defaults()
with ia.config(shape=shape, cfg=cfg, **kwargs) as cfg:
dtshape = ia.DTShape(shape, cfg.dtype)
return ext.uninit(cfg, dtshape)
def empty_like(
iarr: ia.IArray, /, *, device: Optional[ia.Device] = None, cfg: ia.Config = None, **kwargs
) -> ia.IArray:
"""Returns an uninitialized array with the same shape as an input array :paramref:`iarr`.
Parameters
----------
iarr: :ref:`IArray`
device: Device
The device on which to place the created array. The only supported value is `"cpu"`.
cfg: :class:`Config`
The configuration for running the expression.
If None (default), global defaults are used.
kwargs: dict
A dictionary for setting some or all of the fields in the :class:`Config`
dataclass that should override the current configuration.
Returns
-------
:ref:`IArray`
The new array.
"""
return empty(iarr.shape, device=device, cfg=cfg, **kwargs)
def arange(
start: Union[int, float],
/,
stop: Optional[Union[int, float]] = None,
step: Union[int, float] = 1,
*,
shape: Sequence = None,
device: Optional[ia.Device] = None,
cfg: ia.Config = None,
**kwargs,
) -> ia.IArray:
"""Return evenly spaced values within a given interval.
`shape`, `device`, `cfg` and `kwargs` are the same than for :func:`empty`.
`start`, `stop`, `step` are the same as in `np.arange `_.
Returns
-------
:ref:`IArray`
The new array.
See Also
--------
empty : Create an empty array.
"""
if device not in ["cpu", None]:
raise ValueError(f"Unsupported device {device!r}")
if cfg is None:
cfg = ia.get_config_defaults()
with ia.config(cfg=cfg, **kwargs) as cfg:
if (
cfg.np_dtype is not None
and type(start) in [np.datetime64, np.timedelta64]
and start.dtype.str[1:] != cfg.np_dtype[1:]
):
raise ValueError("`start` has to be the same type as `cfg.np_dtype`")
if (
cfg.np_dtype is not None
and type(stop) in [np.datetime64, np.timedelta64]
and stop.dtype.str[1:] != cfg.np_dtype[1:]
):
raise ValueError("`stop` has to be the same type as `cfg.np_dtype`")
if step == 0:
raise ValueError("`step` cannot be 0")
if stop is None:
if cfg.np_dtype is not None:
# For datetimes
stop = np.array(start, dtype=cfg.dtype)
else:
stop = start
start = np.array(0, dtype=cfg.dtype)
elif cfg.np_dtype is not None:
# For datetimes
stop = np.array(stop, dtype=cfg.dtype)
start = np.array(start, dtype=cfg.dtype)
if (stop - start <= 0 and step > 0) or (stop - start >= 0 and step < 0):
# Return 0 length array
shape = (0,)
return empty(shape, cfg=cfg, **kwargs)
if shape is None:
shape = [np.ceil((stop - start) / step)]
elif np.prod(shape) > (stop - start) / step:
raise ValueError("shape must agree with `start`, `stop` and `step`")
with ia.config(shape=shape, cfg=cfg, **kwargs) as cfg:
# Another config context for chunks, blocks defaults
slice_ = slice(start, stop, step)
dtshape = ia.DTShape(shape, cfg.dtype)
return ext.arange(cfg, slice_, dtshape)
def asarray(
obj: Union[ia.IArray, bool, int, float, NestedSequence, SupportsBufferProtocol],
/,
*,
device: Optional[ia.Device] = None,
copy: Optional[bool] = None,
cfg: ia.Config = None,
**kwargs,
) -> ia.IArray:
"""
Convert the input to an :ref:`IArray`.
Parameters
----------
obj: :ref:`IArray`, Python scalar, (possibly nested) sequence of Python scalars, or object supporting the Python buffer protocol
The input to convert into an :ref:`IArray`.
device: Device
The device on which to place the created array. The only supported value is `"cpu"`.
copy: bool
Whether to copy the buffer data in case of an :ref:`IArray` instance.
cfg: :class:`ia.Config`
The configuration to use.
If None (default), global defaults are used.
kwargs: dict
A dictionary for setting some or all of the fields in the :class:`Config`
dataclass that should override the current configuration.
Returns
-------
out: :ref:`IArray`
An array containing the data of :paramref:`obj`.
"""
if device not in ["cpu", None]:
raise ValueError(f"Unsupported device {device!r}")
if cfg is None:
cfg = ia.get_config_defaults()
if isinstance(obj, ia.IArray):
with ia.config(cfg=cfg, **kwargs) as cfg:
if copy is None:
if np.dtype(cfg.dtype).itemsize < np.dtype(obj.dtype).itemsize:
copy = True
else:
copy = False
if copy:
return obj.copy(cfg=cfg)
else:
if (
cfg.urlpath is not None
or cfg.contiguous not in [None, obj.cfg.contiguous]
or cfg.chunks not in [None, obj.chunks]
or cfg.blocks not in [None, obj.blocks]
):
raise ValueError("Cannot change array config when avoiding the copy")
if cfg.dtype == obj.dtype:
return obj[...]
else:
return ia.astype(obj, cfg.dtype)
else:
copy = True if copy is None else copy
if not copy:
raise ValueError("Cannot avoid copy for non IArray instances")
with ia.config(cfg=cfg, **kwargs) as cfg:
dtype = cfg.dtype if cfg.np_dtype is None else cfg.np_dtype
arr = np.asarray(obj, dtype=dtype)
res = ia.empty(arr.shape)
if arr.ndim == 0:
res[()] = arr[()]
else:
res[...] = arr[...]
del arr
return res
def linspace(
start: Union[int, float],
stop: Union[int, float],
/,
num: int,
*,
shape: Sequence = None,
device: Optional[ia.Device] = None,
endpoint: bool = True,
cfg: ia.Config = None,
**kwargs,
) -> ia.IArray:
"""Return evenly spaced numbers over a specified interval. If :paramref:`endpoint` is False,
the numbers will be generated over the half-open interval `[start, stop)`.
`shape`, `device`, `cfg` and `kwargs` are the same than for :func:`empty`.
`start`, `stop` are the same as in `np.linspace `_.
Returns
-------
:ref:`IArray`
The new array.
See Also
--------
empty : Create an empty array.
"""
if device not in ["cpu", None]:
raise ValueError(f"Unsupported device {device!r}")
if shape is None:
shape = [num]
elif np.prod(shape) != num:
raise ValueError("`shape` must agree with `num`")
if not endpoint and len(shape) > 1:
raise ValueError("`endpoint` can only be False with 1-dim arrays")
if cfg is None:
cfg = ia.get_config_defaults()
with ia.config(shape=shape, cfg=cfg, **kwargs) as cfg:
if not endpoint:
shape = [num + 1]
dtshape = ia.DTShape(shape, cfg.dtype)
a = ext.linspace(cfg, start, stop, dtshape)
if not endpoint:
a.resize([num])
return a
def zeros(
shape: Union[int, Tuple[int, ...]],
*,
device: Optional[ia.Device] = None,
cfg: ia.Config = None,
**kwargs,
) -> ia.IArray:
"""Return a new array of given shape and type, filled with zeros.
`shape`, `device`, `cfg` and `kwargs` are the same than for :func:`empty`.
Returns
-------
:ref:`IArray`
The new array.
See Also
--------
empty : Create an empty array.
ones : Create an array filled with ones.
"""
if device not in ["cpu", None]:
raise ValueError(f"Unsupported device {device!r}")
if cfg is None:
cfg = ia.get_config_defaults()
with ia.config(shape=shape, cfg=cfg, **kwargs) as cfg:
if cfg.dtype not in _all_dtypes:
raise TypeError("dtype is not supported")
dtshape = ia.DTShape(shape, cfg.dtype)
return ext.zeros(cfg, dtshape)
def zeros_like(
iarr: ia.IArray, /, *, device: Optional[ia.Device] = None, cfg: ia.Config = None, **kwargs
) -> ia.IArray:
"""Return a new array of same shape as :paramref:`iarr`, filled with zeros.
`shape`, `cfg` and `kwargs` are the same than for :func:`empty`.
Returns
-------
:ref:`IArray`
The new array.
See Also
--------
empty : Create an empty array.
ones : Create an array filled with ones.
"""
return zeros(iarr.shape, device=device, cfg=cfg, **kwargs)
def concatenate(shape: Sequence, data: list, cfg: ia.Config = None, **kwargs) -> ia.IArray:
"""Concatenate a list of one-chunk arrays into a specified shape.
`cfg` and `kwargs` are the same as for :func:`empty`.
Parameters
----------
shape: Sequence
The shape of the concatenated array.
data: list
A list with the arrays (with one chunk) to concatenate
Returns
-------
:ref:`IArray`
The concatenated array.
"""
if cfg is None:
cfg = ia.get_config_defaults()
with ia.config(
shape=shape, cfg=cfg, chunks=data[0].chunks, blocks=data[0].blocks, **kwargs
) as cfg:
dtshape = ia.DTShape(shape, data[0].dtype)
return ext.concatenate(cfg, data, dtshape)
def from_cframe(
cframe: [bytes, bytearray], copy: bool = False, cfg: ia.Config = None, **kwargs
) -> ia.IArray:
"""Create an array from a cframe in bytes.
`cfg` and `kwargs` are the same as for :func:`empty`.
Parameters
----------
cframe: bytes
The cframe in bytes.
copy: bool
If `copy` is True, a copy is made.
Returns
-------
:ref:`IArray`
The new array.
"""
if not cfg:
cfg = ia.get_config_defaults()
with ia.config(cfg=cfg, **kwargs) as cfg:
return ext.from_cframe(cfg, cframe, copy)
def ones(
shape: Union[int, Tuple[int, ...]],
*,
device: Optional[ia.Device] = None,
cfg: ia.Config = None,
**kwargs,
) -> ia.IArray:
"""Return a new array of given shape and type, filled with ones.
`shape`, `cfg` and `kwargs` are the same than for :func:`empty`.
Returns
-------
:ref:`IArray`
The new array.
See Also
--------
empty : Create an empty array.
zeros : Create an array filled with zeros.
"""
if device not in ["cpu", None]:
raise ValueError(f"Unsupported device {device!r}")
if cfg is None:
cfg = ia.get_config_defaults()
with ia.config(shape=shape, cfg=cfg, **kwargs) as cfg:
if cfg.dtype not in _all_dtypes:
raise TypeError("dtype is not supported")
dtshape = ia.DTShape(shape, cfg.dtype)
return ext.ones(cfg, dtshape)
def ones_like(
iarr: ia.IArray, /, *, device: Optional[ia.Device] = None, cfg: ia.Config = None, **kwargs
) -> ia.IArray:
"""Return a new array with the same shape as an input array :paramref:`iarr`, filled with ones.
Parameters
----------
iarr: :ref:`IArray`
device: ia.Device
The device on which to place the created array. The only supported value is `"cpu"`.
cfg: :class:`Config`
The configuration for running the expression.
If None (default), global defaults are used.
kwargs: dict
A dictionary for setting some or all of the fields in the :class:`Config`
dataclass that should override the current configuration.
Returns
-------
:ref:`IArray`
The new array.
"""
return ones(iarr.shape, device=device, cfg=cfg, **kwargs)
def full(
shape: Union[int, Tuple[int, ...]],
fill_value: Union[bool, int, float],
*,
device: Optional[ia.Device] = None,
cfg: ia.Config = None,
**kwargs,
) -> ia.IArray:
"""Return a new array of given shape and type, filled with :paramref:`fill_value`.
`shape`, `device`, `cfg` and `kwargs` are the same than for :func:`empty`.
Returns
-------
:ref:`IArray`
The new array.
See Also
--------
empty : Create an empty array.
zeros : Create an array filled with zeros.
"""
if device not in ["cpu", None]:
raise ValueError(f"Unsupported device {device!r}")
if cfg is None:
cfg = ia.get_config_defaults()
with ia.config(shape=shape, cfg=cfg, **kwargs) as cfg:
if cfg.dtype not in _all_dtypes:
raise TypeError("dtype is not supported")
dtshape = ia.DTShape(shape, cfg.dtype)
if (
cfg.np_dtype is not None
and type(fill_value) in [np.datetime64, np.timedelta64]
and fill_value.dtype.str[1:] != cfg.np_dtype[1:]
):
raise ValueError("`fill_value` has to be the same type as `cfg.np_dtype`")
fill_value = np.array(fill_value, dtype=cfg.dtype)
return ext.full(cfg, fill_value, dtshape)
def full_like(
iarr: ia.IArray,
/,
fill_value: Union[bool, int, float],
*,
device: Optional[ia.Device] = None,
cfg: ia.Config = None,
**kwargs,
) -> ia.IArray:
"""Return a new array with the same shape as an input array :paramref:`iarr`, filled with :paramref:`fill_value`.
`fill_value`, `device`, `cfg` and `kwargs` are the same than for :func:`empty`.
Returns
-------
:ref:`IArray`
The new array.
"""
return full(iarr.shape, fill_value=fill_value, device=device, cfg=cfg, **kwargs)
def zarr_proxy(zarr_urlpath, cfg: ia.Config = None, **kwargs) -> ia.IArray:
"""Return a read-only Zarr proxy array.
`cfg` and `kwargs` are the same than for :func:`empty` except by `nthreads`, which is
always set to 1 (multi-threading is not yet supported).
The data type and chunks must not differ from the original Zarr array.
A Zarr proxy is a regular IArray array but with a special attribute called `zproxy_urlpath`. This
attribute is protected when `attrs.clear()` is used; but can still be deleted with `del attrs["zproxy_urlpath"]`,
`attrs.popitem()` or `attrs.pop("zproxy_urlpath")`.
This IArray has an additional attribute called `proxy_attrs` which contains the Zarr attributes. The user can
get and set these attributes.
Parameters
----------
zarr_urlpath : str
The path to the Zarr array.
If it is stored in the cloud, the path must begin with ``s3://``.
Returns
-------
:ref:`IArray`
The zarr proxy array.
Notes
-----
As a proxy, this array does not contain the data from the original array, it only reads it when needed.
But if a :func:`save` is done, a copy of all the data will be made and assigned to a new
and usual on disk :ref:`IArray`. To create a persistent proxy on-disk,
you can specificy the :paramref:`urlpath` during :func:`zarr_proxy` execution time.
"""
z = ext._zarray_from_proxy(zarr_urlpath)
# Create iarray
dtype = zarr_to_iarray_dtypes[str(z.dtype)]
if cfg is None:
cfg = ia.get_config_defaults()
if kwargs != {}:
if "dtype" in kwargs:
if kwargs.pop("dtype") != dtype:
raise AttributeError("dtype cannot differ from the original array")
if "chunks" in kwargs:
if tuple(kwargs.pop("chunks")) != z.chunks:
raise AttributeError("chunks cannot differ from the original array")
if "blocks" in kwargs:
blocks = tuple(kwargs.pop("blocks"))
else:
blocks = z.chunks
if "nthreads" in kwargs:
if kwargs.pop("nthreads") != 1:
raise IllegalArgumentError("Cannot use parallelism when interacting with Zarr")
with ia.config(
cfg=cfg, dtype=dtype, chunks=z.chunks, blocks=blocks, nthreads=1, **kwargs
) as cfg:
a = empty(shape=z.shape, cfg=cfg)
# Set special attr to identify zarr_proxy
a.attrs["zproxy_urlpath"] = zarr_urlpath
# Create reference to zarr.attrs
a.zarr_attrs = z.attrs
# Assign postfilter
ext.set_zproxy_postfilter(a)
return a