diff --git a/sdarray/__init__.py b/sdarray/__init__.py index 0772879..1fdace9 100644 --- a/sdarray/__init__.py +++ b/sdarray/__init__.py @@ -3,5 +3,8 @@ # submodules -from . import coords -from . import dims +from . import array + + +# aliases +from .array import Array diff --git a/sdarray/array/__init__.py b/sdarray/array/__init__.py new file mode 100644 index 0000000..a65ec25 --- /dev/null +++ b/sdarray/array/__init__.py @@ -0,0 +1,9 @@ +# submodules +from . import array +from . import coords +from . import dims +from . import utils + + +# aliases +from .array import Array diff --git a/sdarray/array/array.py b/sdarray/array/array.py new file mode 100644 index 0000000..11c54fb --- /dev/null +++ b/sdarray/array/array.py @@ -0,0 +1,85 @@ +# standard library +from dataclasses import dataclass +from typing import Any, Tuple + + +# dependencies +from xarray_dataclasses import AsDataArray, Coordof, Data + + +# submodules +from . import coords +from . import dims +from .utils import KeywordOnly + + +# dataclasses +@dataclass +class Array(AsDataArray, KeywordOnly): + """Common single-dish data structure.""" + + data: Data[Tuple[dims.time, dims.chan], Any] + """Two-dimensional array data.""" + + mask: Coordof[coords.Mask] = False + """Mask of array data.""" + + sigma: Coordof[coords.Sigma] = 0.0 + """Uncertainty of array data.""" + + weight: Coordof[coords.Weight] = 1.0 + """Weight of array data.""" + + time: Coordof[dims.Time] = "2000-01-01" + """Start time in UTC.""" + + obs: Coordof[coords.Obs] = "0" + """Observation label.""" + + scan: Coordof[coords.Scan] = "0" + """Scan label.""" + + mode: Coordof[coords.Mode] = "0" + """Mode label.""" + + exposure: Coordof[coords.Exposure] = 0.0 + """Exposure time.""" + + interval: Coordof[coords.Interval] = 0.0 + """Interval time.""" + + longitude: Coordof[coords.Longitude] = 0.0 + """Sky longitude.""" + + latitude: Coordof[coords.Latitude] = 0.0 + """Sky latitude.""" + + chan: Coordof[dims.Chan] = 0 + """Channel number.""" + + beam: Coordof[coords.Beam] = "0" + """Beam label.""" + + spw: Coordof[coords.SpW] = "0" + """Spectral window label.""" + + pol: Coordof[coords.Pol] = "0" + """Polarization label.""" + + lon_offset: Coordof[coords.LonOffset] = 0.0 + """Offset from sky longitude.""" + + lat_offset: Coordof[coords.LatOffset] = 0.0 + """Offset from sky latitude.""" + + center_freq: Coordof[coords.CenterFreq] = 0.0 + """Center frequency.""" + + ref_freq: Coordof[coords.RefFreq] = 0.0 + """Reference frequency.""" + + resolution: Coordof[coords.Resolution] = 0.0 + """Spectral resolution.""" + + width: Coordof[coords.Width] = 0.0 + """Channel width.""" diff --git a/sdarray/coords.py b/sdarray/array/coords.py similarity index 68% rename from sdarray/coords.py rename to sdarray/array/coords.py index 4fbfe26..a818f3e 100644 --- a/sdarray/coords.py +++ b/sdarray/array/coords.py @@ -1,25 +1,3 @@ -__all__ = [ - "Scan", - "Mode", - "Exposure", - "Interval", - "Longitude", - "Latitude", - "Beam", - "SpW", - "Pol", - "CenterFreq", - "RefFreq", - "Resolution", - "Width", - "LonOffset", - "LatOffset", - "Mask", - "Sigma", - "Weight", -] - - # standard library from dataclasses import dataclass from typing import Tuple @@ -31,15 +9,52 @@ # submodules -from .dims import time, chan +from . import dims + + +# dataclasses (time-chan data) +@dataclass +class Mask: + """Mask of array data.""" + + data: Data[Tuple[dims.time, dims.chan], np.bool_] + long_name: Attr[str] = "Mask for array data" + short_name: Attr[str] = "mask" + + +@dataclass +class Sigma: + """Uncertainty of array data.""" + + data: Data[Tuple[dims.time, dims.chan], np.float64] + long_name: Attr[str] = "Uncertainty of array data" + short_name: Attr[str] = "sigma" + + +@dataclass +class Weight: + """Weight of array data.""" + + data: Data[Tuple[dims.time, dims.chan], np.float64] + long_name: Attr[str] = "Weight for array data" + short_name: Attr[str] = "weight" # dataclasses (time labels) +@dataclass +class Obs: + """Observation label.""" + + data: Data[dims.time, np.str_] + long_name: Attr[str] = "Observation label" + short_name: Attr[str] = "obs" + + @dataclass class Scan: """Scan label.""" - data: Data[time, np.str_] + data: Data[dims.time, np.str_] long_name: Attr[str] = "Scan label" short_name: Attr[str] = "scan" @@ -48,17 +63,17 @@ class Scan: class Mode: """Mode label.""" - data: Data[time, np.str_] + data: Data[dims.time, np.str_] long_name: Attr[str] = "Mode label" short_name: Attr[str] = "mode" -# dataclasses (time observables) +# dataclasses (time data) @dataclass class Exposure: """Exposure time.""" - data: Data[time, np.float64] + data: Data[dims.time, np.float64] long_name: Attr[str] = "Exposure time" short_name: Attr[str] = "exposure" units: Attr[str] = "s" @@ -68,7 +83,7 @@ class Exposure: class Interval: """Interval time.""" - data: Data[time, np.float64] + data: Data[dims.time, np.float64] long_name: Attr[str] = "Interval time" short_name: Attr[str] = "interval" units: Attr[str] = "s" @@ -78,7 +93,7 @@ class Interval: class Longitude: """Sky longitude.""" - data: Data[time, np.float64] + data: Data[dims.time, np.float64] long_name: Attr[str] = "Sky longitude" "" short_name: Attr[str] = "longitude" units: Attr[str] = "deg" @@ -88,7 +103,7 @@ class Longitude: class Latitude: """Sky latitude.""" - data: Data[time, np.float64] + data: Data[dims.time, np.float64] long_name: Attr[str] = "Sky latitude" short_name: Attr[str] = "latitude" units: Attr[str] = "deg" @@ -99,7 +114,7 @@ class Latitude: class Beam: """Beam label.""" - data: Data[chan, np.str_] + data: Data[dims.chan, np.str_] long_name: Attr[str] = "Beam label" short_name: Attr[str] = "beam" @@ -108,7 +123,7 @@ class Beam: class SpW: """Spectral window label.""" - data: Data[chan, np.str_] + data: Data[dims.chan, np.str_] long_name: Attr[str] = "Spectral window label" short_name: Attr[str] = "spw" @@ -117,17 +132,37 @@ class SpW: class Pol: """Polarization label.""" - data: Data[chan, np.str_] + data: Data[dims.chan, np.str_] long_name: Attr[str] = "Polarization label" short_name: Attr[str] = "pol" -# dataclasses (chan observables) +# dataclasses (chan data) +@dataclass +class LonOffset: + """Offset from sky longitude.""" + + data: Data[dims.chan, np.float64] + long_name: Attr[str] = "Offset from sky longitude" + short_name: Attr[str] = "lon_offset" + units: Attr[str] = "deg" + + +@dataclass +class LatOffset: + """Offset from sky latitude.""" + + data: Data[dims.chan, np.float64] + long_name: Attr[str] = "Offset from sky latitude" + short_name: Attr[str] = "lat_offset" + units: Attr[str] = "deg" + + @dataclass class CenterFreq: """Center frequency.""" - data: Data[chan, np.float64] + data: Data[dims.chan, np.float64] long_name: Attr[str] = "Center frequency" short_name: Attr[str] = "center_freq" units: Attr[str] = "Hz" @@ -137,7 +172,7 @@ class CenterFreq: class RefFreq: """Reference frequency.""" - data: Data[chan, np.float64] + data: Data[dims.chan, np.float64] long_name: Attr[str] = "Reference frequency" short_name: Attr[str] = "ref_freq" units: Attr[str] = "Hz" @@ -147,7 +182,7 @@ class RefFreq: class Resolution: """Spectral resolution.""" - data: Data[chan, np.float64] + data: Data[dims.chan, np.float64] long_name: Attr[str] = "Spectral resolution" short_name: Attr[str] = "resolution" units: Attr[str] = "Hz" @@ -157,55 +192,7 @@ class Resolution: class Width: """Channel width.""" - data: Data[chan, np.float64] + data: Data[dims.chan, np.float64] long_name: Attr[str] = "Channel width" short_name: Attr[str] = "width" units: Attr[str] = "Hz" - - -@dataclass -class LonOffset: - """Offset from sky longitude.""" - - data: Data[chan, np.float64] - long_name: Attr[str] = "Offset from sky longitude" - short_name: Attr[str] = "lon_offset" - units: Attr[str] = "deg" - - -@dataclass -class LatOffset: - """Offset from sky latitude.""" - - data: Data[chan, np.float64] - long_name: Attr[str] = "Offset from sky latitude" - short_name: Attr[str] = "lat_offset" - units: Attr[str] = "deg" - - -# dataclasses (time-chan observables) -@dataclass -class Mask: - """Mask for an array.""" - - data: Data[Tuple[time, chan], np.bool_] - long_name: Attr[str] = "Mask for an array" - short_name: Attr[str] = "mask" - - -@dataclass -class Sigma: - """Uncertainty of an array.""" - - data: Data[Tuple[time, chan], np.float64] - long_name: Attr[str] = "Uncertainty of an array" - short_name: Attr[str] = "sigma" - - -@dataclass -class Weight: - """Weight for an array.""" - - data: Data[Tuple[time, chan], np.float64] - long_name: Attr[str] = "Weight for an array" - short_name: Attr[str] = "weight" diff --git a/sdarray/dims.py b/sdarray/array/dims.py similarity index 71% rename from sdarray/dims.py rename to sdarray/array/dims.py index 3737f26..da11eff 100644 --- a/sdarray/dims.py +++ b/sdarray/array/dims.py @@ -1,6 +1,3 @@ -__all__ = ["Time", "Chan", "time", "chan"] - - # standard library from dataclasses import dataclass @@ -11,12 +8,12 @@ from xarray_dataclasses import Attr, Data -# constants +# type hints time = Literal["time"] -"""Type hint for the time axis.""" +"""Type hint of the time dimension.""" chan = Literal["chan"] -"""Type hint for the channel axis.""" +"""Type hint of the channel dimension.""" # dataclasses @@ -31,8 +28,8 @@ class Time: @dataclass class Chan: - """Channel number.""" + """Generic channel.""" data: Data[chan, np.int64] - long_name: Attr[str] = "Channel number" + long_name: Attr[str] = "Generic channel" short_name: Attr[str] = "chan" diff --git a/sdarray/array/utils.py b/sdarray/array/utils.py new file mode 100644 index 0000000..84ca5b3 --- /dev/null +++ b/sdarray/array/utils.py @@ -0,0 +1,32 @@ +# standard library +from typing import Any, Type, TypeVar + + +# dependencies +from astropy.units import Quantity + + +# type hints +T = TypeVar("T") + + +# utility classes +class KeywordOnly: + """Limit the second and subsequent ``__init__`` args to be keyword-only.""" + + def __new__(cls: Type[T], *args: Any, **kwargs: Any) -> T: + if len(args) > 1: + raise TypeError("The second and subsequent args are keyword-only.") + + return super().__new__(cls) + + +class UnitConversion: + """Convert data to the defined units if they are given with units.""" + + def __post_init__(self) -> None: + if not (hasattr(self, "data") and hasattr(self, "units")): + return + + if isinstance(self.data, Quantity): + self.data = self.data.to(self.units).value # type: ignore diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_coords.py b/tests/array/test_coords.py similarity index 79% rename from tests/test_coords.py rename to tests/array/test_coords.py index aa329e6..dcd20fd 100644 --- a/tests/test_coords.py +++ b/tests/array/test_coords.py @@ -10,9 +10,19 @@ # test functions +def test_obs() -> None: + data = np.array(["0"], np.str_) + array = asdataarray(sd.array.coords.Obs(data)) + + assert array.data == data + assert array.dims == ("time",) + assert array.dtype.type == data.dtype.type + assert array.attrs[SHORT_NAME] == "obs" + + def test_scan() -> None: data = np.array(["0"], np.str_) - array = asdataarray(sd.coords.Scan(data)) + array = asdataarray(sd.array.coords.Scan(data)) assert array.data == data assert array.dims == ("time",) @@ -22,7 +32,7 @@ def test_scan() -> None: def test_mode() -> None: data = np.array(["0"], np.str_) - array = asdataarray(sd.coords.Mode(data)) + array = asdataarray(sd.array.coords.Mode(data)) assert array.data == data assert array.dims == ("time",) @@ -32,7 +42,7 @@ def test_mode() -> None: def test_exposure() -> None: data = np.array([0.0], np.float64) - array = asdataarray(sd.coords.Exposure(data)) + array = asdataarray(sd.array.coords.Exposure(data)) assert array.data == data assert array.dims == ("time",) @@ -43,7 +53,7 @@ def test_exposure() -> None: def test_interval() -> None: data = np.array([0.0], np.float64) - array = asdataarray(sd.coords.Interval(data)) + array = asdataarray(sd.array.coords.Interval(data)) assert array.data == data assert array.dims == ("time",) @@ -54,7 +64,7 @@ def test_interval() -> None: def test_longitude() -> None: data = np.array([0.0], np.float64) - array = asdataarray(sd.coords.Longitude(data)) + array = asdataarray(sd.array.coords.Longitude(data)) assert array.data == data assert array.dims == ("time",) @@ -65,7 +75,7 @@ def test_longitude() -> None: def test_latitude() -> None: data = np.array([0.0], np.float64) - array = asdataarray(sd.coords.Latitude(data)) + array = asdataarray(sd.array.coords.Latitude(data)) assert array.data == data assert array.dims == ("time",) @@ -76,7 +86,7 @@ def test_latitude() -> None: def test_beam() -> None: data = np.array(["0"], np.str_) - array = asdataarray(sd.coords.Beam(data)) + array = asdataarray(sd.array.coords.Beam(data)) assert array.data == data assert array.dims == ("chan",) @@ -86,7 +96,7 @@ def test_beam() -> None: def test_spw() -> None: data = np.array(["0"], np.str_) - array = asdataarray(sd.coords.SpW(data)) + array = asdataarray(sd.array.coords.SpW(data)) assert array.data == data assert array.dims == ("chan",) @@ -96,7 +106,7 @@ def test_spw() -> None: def test_pol() -> None: data = np.array(["0"], np.str_) - array = asdataarray(sd.coords.Pol(data)) + array = asdataarray(sd.array.coords.Pol(data)) assert array.data == data assert array.dims == ("chan",) @@ -106,7 +116,7 @@ def test_pol() -> None: def test_center_freq() -> None: data = np.array([0.0], np.float64) - array = asdataarray(sd.coords.CenterFreq(data)) + array = asdataarray(sd.array.coords.CenterFreq(data)) assert array.data == data assert array.dims == ("chan",) @@ -117,7 +127,7 @@ def test_center_freq() -> None: def test_ref_freq() -> None: data = np.array([0.0], np.float64) - array = asdataarray(sd.coords.RefFreq(data)) + array = asdataarray(sd.array.coords.RefFreq(data)) assert array.data == data assert array.dims == ("chan",) @@ -128,7 +138,7 @@ def test_ref_freq() -> None: def test_resolution() -> None: data = np.array([0.0], np.float64) - array = asdataarray(sd.coords.Resolution(data)) + array = asdataarray(sd.array.coords.Resolution(data)) assert array.data == data assert array.dims == ("chan",) @@ -139,7 +149,7 @@ def test_resolution() -> None: def test_width() -> None: data = np.array([0.0], np.float64) - array = asdataarray(sd.coords.Width(data)) + array = asdataarray(sd.array.coords.Width(data)) assert array.data == data assert array.dims == ("chan",) @@ -150,7 +160,7 @@ def test_width() -> None: def test_lon_offset() -> None: data = np.array([0.0], np.float64) - array = asdataarray(sd.coords.LonOffset(data)) + array = asdataarray(sd.array.coords.LonOffset(data)) assert array.data == data assert array.dims == ("chan",) @@ -161,7 +171,7 @@ def test_lon_offset() -> None: def test_lat_offset() -> None: data = np.array([0.0], np.float64) - array = asdataarray(sd.coords.LatOffset(data)) + array = asdataarray(sd.array.coords.LatOffset(data)) assert array.data == data assert array.dims == ("chan",) @@ -172,7 +182,7 @@ def test_lat_offset() -> None: def test_mask() -> None: data = np.array([[False]], np.bool_) - array = asdataarray(sd.coords.Mask(data)) + array = asdataarray(sd.array.coords.Mask(data)) assert array.data == data assert array.dims == ("time", "chan") @@ -182,7 +192,7 @@ def test_mask() -> None: def test_sigma() -> None: data = np.array([[0.0]], np.float64) - array = asdataarray(sd.coords.Sigma(data)) + array = asdataarray(sd.array.coords.Sigma(data)) assert array.data == data assert array.dims == ("time", "chan") @@ -192,7 +202,7 @@ def test_sigma() -> None: def test_weight() -> None: data = np.array([[1.0]], np.float64) - array = asdataarray(sd.coords.Weight(data)) + array = asdataarray(sd.array.coords.Weight(data)) assert array.data == data assert array.dims == ("time", "chan") diff --git a/tests/test_dims.py b/tests/array/test_dims.py similarity index 85% rename from tests/test_dims.py rename to tests/array/test_dims.py index a38e088..fd55485 100644 --- a/tests/test_dims.py +++ b/tests/array/test_dims.py @@ -11,7 +11,7 @@ # test functions def test_time() -> None: data = np.array(["2000-01-01"], np.datetime64) - array = asdataarray(sd.dims.Time(data)) + array = asdataarray(sd.array.dims.Time(data)) assert array.data == data assert array.dims == ("time",) @@ -21,7 +21,7 @@ def test_time() -> None: def test_chan() -> None: data = np.array([0], np.int64) - array = asdataarray(sd.dims.Chan(data)) + array = asdataarray(sd.array.dims.Chan(data)) assert array.data == data assert array.dims == ("chan",)