diff --git a/poetry.lock b/poetry.lock index 55ab2da..5ddeb9d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -243,6 +243,15 @@ delayed = ["cloudpickle (>=0.2.2)", "toolz (>=0.8.2)"] diagnostics = ["bokeh (>=1.0.0)"] distributed = ["distributed (>=2.0)"] +[[package]] +category = "main" +description = "A backport of the dataclasses module for Python 3.6" +marker = "python_version >= \"3.6\" and python_version < \"3.7\"" +name = "dataclasses" +optional = false +python-versions = ">=3.6, <3.7" +version = "0.7" + [[package]] category = "dev" description = "Decorators for Humans" @@ -1445,7 +1454,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "6f0bf41c9c01d6b0502bd02c8331219aefc84d4b93601371178721e58089e045" +content-hash = "9abcd6e98074e02245ae6dd9f31b384a6eee1aafefec7425a06fadf2608fdbe2" python-versions = "^3.6" [metadata.files] @@ -1551,6 +1560,10 @@ dask = [ {file = "dask-2.14.0-py3-none-any.whl", hash = "sha256:ddcfc2afa13a359aa707e5b3369127286543967a268061b50ee0cc891793602f"}, {file = "dask-2.14.0.tar.gz", hash = "sha256:3885d0d071fb49707c7311f9762c1863800a6f13fa7a1dc12ec7006d8404320c"}, ] +dataclasses = [ + {file = "dataclasses-0.7-py3-none-any.whl", hash = "sha256:3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836"}, + {file = "dataclasses-0.7.tar.gz", hash = "sha256:494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6"}, +] decorator = [ {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, diff --git a/pyproject.toml b/pyproject.toml index e666137..719a249 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ keywords = ["astronomy", "radio-astronomy", "single-dish"] python = "^3.6" astropy = "^4.0" dask = {version = "^2.12", extras = ["complete"]} +dataclasses = { version = "^0.7", python = "~3.6" } matplotlib = "^3.2" netcdf4 = "^1.5" numpy = "^1.18" diff --git a/sdarray/creator.py b/sdarray/creator.py new file mode 100644 index 0000000..7b571e4 --- /dev/null +++ b/sdarray/creator.py @@ -0,0 +1,242 @@ +"""Array creator module. + +""" +__all__ = ["ArrayDef", "CoordDef", "array_creator"] + + +# standard library +from dataclasses import dataclass +from datetime import datetime +from textwrap import dedent, indent, wrap +from typing import Callable, Hashable, Sequence, Union + + +# dependent packages +import numpy as np +from xarray import DataArray + + +# constants +INDENT = " " * 4 +ELEMENT_TYPES = bool, str, int, float, datetime + + +# type aliases +Array = Union[np.ndarray, Union[ELEMENT_TYPES]] +Dtype = Union[np.dtype, type, str] +Dims = Union[Hashable, Sequence[Hashable]] +Element = Union[ELEMENT_TYPES] +Shape = Sequence[int] + + +# data classes +@dataclass(frozen=True) +class ArrayDef: + """Definition of array.""" + + name: str #: Name of array. + dims: Dims #: Dimensions of array. + short_desc: str #: Short description. + long_desc: str = "" #: Long description (if any). + + @property + def docstring(self) -> str: + """String for function's docstring.""" + return ( + f"{self.name}: " + f"{self.short_desc} " + f"{self.long_desc} " + f"Dims: ``{self.dims}``." + ) + + @property + def docstring_wrapped(self) -> str: + """String for function's docstring (wrapped).""" + return f"\n{INDENT}".join(wrap(self.docstring)) + + +@dataclass(frozen=True) +class CoordDef: + """Definition of coordinate.""" + + name: str #: Name of coordinate. + dims: Dims #: Dimension(s) of coordinate. + type: str #: Type of value(s). + default: Element #: Default value(s). + short_desc: str #: Short description. + long_desc: str = "" #: Long description (if any). + + @property + def docstring(self) -> str: + """String for function's docstring.""" + return ( + f"{self.name}: " + f"{self.short_desc} " + f"{self.long_desc} " + f"Dims: ``{self.dims}``. " + f"Type: ``{self.type}``. " + f"Default: ``{self.default!r}``." + ) + + @property + def docstring_wrapped(self) -> str: + """String for function's docstring (wrapped).""" + return f"\n{INDENT}".join(wrap(self.docstring)) + + +# main functions +def array_creator(cls: type) -> type: + """Decorator which adds array creator functions (static methods) to class. + + Args: + cls: Class which has class attributes (``array_def``, ``coord_defs``). + + Returns: + cls: Class with creator functions (``array``, ``ones``, ``zeros``). + + Raises: + AttributeError: Raised if class does not have + either ``array_def`` or ``coord_defs``. + + """ + if not hasattr(cls, "array_def"): + raise AttributeError("Class must have array_def attribute.") + + if not hasattr(cls, "coord_defs"): + raise AttributeError("Class must have coord_defs attribute.") + + creator = get_creator(cls.array_def, cls.coord_defs) + + cls.__call__ = staticmethod(creator) + cls.ones = staticmethod(get_ones(creator)) + cls.zeros = staticmethod(get_zeros(creator)) + + return cls + + +# helper functions +def get_creator(array_def: ArrayDef, coord_defs: Sequence[CoordDef]) -> Callable: + """Return creator function with given definitions of array and coordinates. + + Args: + array_def: Definition of array. + coord_defs: Sequence of coordinate definitions. + + Returns: + creator: Creator function. + + """ + + def creator(array: Array, **coords: Array) -> DataArray: + """\ + Create DataArray from given array and coordinates. + + Args: + {array} + {coords} + + Returns: + array: DataArray with given coordinates. + + """ + array = DataArray(array, dims=array_def.dims) + + for cd in coord_defs: + coord = coords.get(cd.name, None) + shape = [array.sizes[dim] for dim in cd.dims] + + if coord is None: + coord = np.full(shape, cd.default) + elif isinstance(coord, ELEMENT_TYPES): + coord = np.full(shape, coord) + + array.coords[cd.name] = cd.dims, np.asarray(coord, cd.type) + + return array + + # update docstring + doc_array = indent(array_def.docstring_wrapped, INDENT) + doc_coords = indent("\n".join(cd.docstring_wrapped for cd in coord_defs), INDENT) + creator.__doc__ = dedent(creator.__doc__).format(array=doc_array, coords=doc_coords) + + return creator + + +def get_ones(creator: Callable) -> Callable: + """Return ones function based on given creator function.""" + + def ones(shape: Shape, dtype: Dtype = float, **coords: Array) -> DataArray: + """Create DataArray filled with ones from given shape and coordinates. + + Args: + shape: Shape of array. + dtype: Data type of array. Default: ``float64``. + coords: Coordinates. See creator function for more details. + + Returns: + array: DataArray filled with ones with given coordinates. + + """ + return creator(np.ones(shape, dtype), **coords) + + return ones + + +def get_zeros(creator: Callable) -> Callable: + """Return zeros function based on given creator function.""" + + def zeros(shape: Shape, dtype: Dtype = float, **coords: Array) -> DataArray: + """Create DataArray filled with zeros from given shape and coordinates. + + Args: + shape: Shape of array. + dtype: Data type of array. Default: ``float64``. + coords: Coordinates. See creator function for more details. + + Returns: + array: DataArray filled with zeros with given coordinates. + + """ + return creator(np.zeros(shape, dtype), **coords) + + return zeros + + +def get_empty(creator: Callable) -> Callable: + """Return empty function based on given creator function.""" + + def empty(shape: Shape, dtype: Dtype = float, **coords: Array) -> DataArray: + """Create uninitialized DataArray from given shape and coordinates. + + Args: + shape: Shape of array. + dtype: Data type of array. Default: ``float64``. + coords: Coordinates. See creator function for more details. + + Returns: + array: Uninitialized DataArray with given coordinates. + + """ + return creator(np.empty(shape, dtype), **coords) + + return empty + + +def get_full(creator: Callable) -> Callable: + """Return full function based on given creator function.""" + + def full(shape: Shape, fill_value: Element, **coords: Array,) -> DataArray: + """Create uninitialized DataArray from given shape and coordinates. + + Args: + shape: Shape of array. + dtype: Data type of array. Default: ``float64``. + coords: Coordinates. See creator function for more details. + + Returns: + array: Uninitialized DataArray with given coordinates. + + """ + return creator(np.full(shape, fill_value), **coords) + + return full