Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

methods for size and range of coords #356

Merged
merged 4 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions dascore/core/coordmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
MaybeArray = TypeVar("MaybeArray", ArrayLike, np.ndarray, None)


def _validate_select_coords(coord, coord_name):
def _validate_select_coords(coord, coord_name: str):
"""Ensure multi-dims are not used."""
if not len(coord.shape) == 1:
msg = (
Expand All @@ -92,7 +92,7 @@ def _validate_select_coords(coord, coord_name):
raise CoordError(msg)


def _indirect_coord_updates(cm, dim_name, coord_name, reduction, new_coords):
def _indirect_coord_updates(cm, dim_name, coord_name: str, reduction, new_coords):
"""
Applies trim to coordinates.

Expand Down Expand Up @@ -677,7 +677,7 @@ def shape(self):

@property
def size(self):
"""Return the shape of the dimensions."""
"""Return the size of the patch data matrix."""
return np.prod(self.shape)

def validate_data(self, data):
Expand Down Expand Up @@ -929,22 +929,30 @@ def get_coord(self, coord_name: str) -> BaseCoord:
raise CoordError(msg)
return self.coord_map[coord_name]

def min(self, coord_name):
def min(self, coord_name: str):
"""Return the minimum value of a coordinate."""
return self.get_coord(coord_name).min()

def max(self, coord_name):
def max(self, coord_name: str):
"""Return the maximum value of a coordinate."""
return self.get_coord(coord_name).max()

def step(self, coord_name):
def step(self, coord_name: str):
"""Return the coordinate step."""
return self.get_coord(coord_name).step

def get_array(self, coord_name) -> np.ndarray:
def get_array(self, coord_name: str) -> np.ndarray:
"""Return the coordinate values as a numpy array."""
return np.array(self.get_coord(coord_name))

def coord_size(self, coord_name: str) -> int:
"""Return the coordinate size."""
return self.get_coord(coord_name).size

def coord_range(self, coord_name: str):
"""Return a scaler value for the coordinate (e.g., number of seconds)."""
return self.get_coord(coord_name).coord_range()


def get_coord_manager(
coords: Mapping[str, BaseCoord | np.ndarray] | CoordManager | None = None,
Expand Down
11 changes: 11 additions & 0 deletions dascore/core/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ def shape(self) -> tuple[int, ...]:
"""Return the shape of the coordinate data."""
return self.data.shape

@property
def size(self) -> int:
"""Return the size of the coordinate data."""
return np.prod(self.shape)

@property
def evenly_sampled(self) -> tuple[int, ...]:
"""Returns True if the coord is evenly sampled."""
Expand All @@ -349,6 +354,12 @@ def simplify_units(self) -> Self:
_, unit = get_factor_and_unit(self.units, simplify=True)
return self.convert_units(unit)

def coord_range(self):
"""Return a scaler value for the coordinate (e.g., number of seconds)."""
if not self.evenly_sampled:
raise CoordError("coord_range has to be called on an evenly sampled data.")
return self.max() - self.min() + self.step

@abc.abstractmethod
def sort(self, reverse=False) -> tuple[BaseCoord, slice | ArrayLike]:
"""Sort the contents of the coord. Return new coord and slice for sorting."""
Expand Down
15 changes: 13 additions & 2 deletions dascore/core/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from dascore.core.coords import BaseCoord
from dascore.utils.display import array_to_text, attrs_to_text, get_dascore_text
from dascore.utils.models import ArrayLike
from dascore.utils.time import to_float
from dascore.viz import VizPatchNameSpace


Expand Down Expand Up @@ -160,7 +161,7 @@ def coords(self) -> CoordManager:

@property
def data(self) -> ArrayLike:
"""Return the dimensions contained in patch."""
"""Return the data contained in patch."""
return self._data

@property
Expand All @@ -170,9 +171,19 @@ def shape(self) -> tuple[int, ...]:

@property
def size(self) -> tuple[int, ...]:
"""Return the shape of the data array."""
"""Return the size of the data array."""
return self.coords.size

@property
def seconds(self) -> float:
"""Return number of seconds in the time coordinate."""
return to_float(self.coords.coord_range("time"))

@property
def channel_count(self) -> int:
"""Return number of channels in the distance coordinate."""
return self.coords.coord_size("distance")

# --- basic patch functionality.

update = dascore.proc.update
Expand Down
19 changes: 16 additions & 3 deletions docs/tutorial/patch.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ A single file can be loaded like this:
import dascore as dc
from dascore.utils.downloader import fetch

# first we download an example data file. You can replace
# this with the path to your file.
# here we download an example data file. You can replace
# the next line with the path to your file.
path = fetch("terra15_das_1_trimmed.hdf5")

# then we get the first patch in the spool
Expand Down Expand Up @@ -180,7 +180,7 @@ Specific data formats may also add attributes (e.g. "gauge_length", "pulse_width

## String representation

DASCore Patches have as useful string representation:
DASCore Patches have a useful string representation:

```{python}
import dascore as dc
Expand All @@ -189,6 +189,19 @@ patch = dc.get_example_patch()
print(patch)
```

## Shortcuts

DASCore Patches offer useful shortcuts for quickly accessing information:

```{python}
import dascore as dc

patch = dc.get_example_patch()
print(patch.seconds) # to get the number of seconds in the patch.
print(patch.channel_count) # to get the number of channels in the patch.
```


# Selecting (trimming)

Patches are trimmed using the [`select`](`dascore.Patch.select`) method. Most commonly, `select` takes the coordinate name and a tuple of (lower_limit, upper_limit) as the values. Either limit can be `...` indicating an open interval.
Expand Down
19 changes: 16 additions & 3 deletions tests/test_core/test_coordmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,19 +256,19 @@ def test_size(self, coord_manager):
assert isinstance(coord_manager.size, int | np.int_)

def test_min(self, basic_coord_manager):
"""Ensure we can git min value."""
"""Ensure we can get min value."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, thanks for fixing the typos along the way ;)

expected = np.min(basic_coord_manager.time.data).astype(np.int64)
got = basic_coord_manager.min("time").astype(np.int64)
assert np.isclose(got, expected)

def test_max(self, basic_coord_manager):
"""Ensure we can git max value."""
"""Ensure we can get max value."""
expected = np.max(basic_coord_manager.time.data).astype(np.int64)
got = basic_coord_manager.max("time").astype(np.int64)
assert np.isclose(got, expected)

def test_step(self, basic_coord_manager):
"""Ensure we can git min value."""
"""Ensure we can get min value."""
expected = basic_coord_manager.time.step
assert basic_coord_manager.step("time") == expected

Expand Down Expand Up @@ -297,6 +297,19 @@ def test_iterate(self, basic_coord_manager):
expected = basic_coord_manager.get_coord(dim)
assert all_close(coord, expected)

def test_coord_size(self, random_patch):
"""Ensure we can get size of the coordinate."""
expected = len(random_patch.coords["time"])
assert random_patch.coords.coord_size("time") == expected

def test_coord_range(self, random_patch):
"""Ensure we can get a scaler value for the coordinate."""
coord_array = random_patch.coords["time"]
expected = (
np.max(coord_array) - np.min(coord_array) + random_patch.attrs["time_step"]
)
assert random_patch.coords.coord_range("time") == expected


class TestCoordManagerInputs:
"""Tests for coordinates management."""
Expand Down
10 changes: 9 additions & 1 deletion tests/test_core/test_coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def evenly_sampled_time_delta_coord():
@pytest.fixture(scope="session")
@register_func(COORDS)
def monotonic_float_coord():
"""Create coordinates which are evenly sampled."""
"""Create coordinates which are not evenly sampled."""
ar = np.cumsum(np.abs(np.random.rand(100)))
return get_coord(data=ar)

Expand Down Expand Up @@ -242,6 +242,7 @@ def test_snap(self, coord):
out = coord.snap()
assert isinstance(out, BaseCoord)
assert out.shape == coord.shape
assert out.size == coord.size
# sort order should stay the same
if coord.reverse_sorted:
assert out.reverse_sorted
Expand Down Expand Up @@ -328,6 +329,12 @@ def test_both_values_and_data_raises(self):
with pytest.raises(CoordError, match=msg):
get_coord(data=data, values=data)

def test_coord_range(self, monotonic_float_coord):
"""Ensure that coord_range raises an error for not evenly sampled patches."""
msg = "has to be called on an evenly sampled"
with pytest.raises(CoordError, match=msg):
monotonic_float_coord.coord_range()


class TestCoordSummary:
"""tests for converting to and from summary coords."""
Expand Down Expand Up @@ -859,6 +866,7 @@ def test_arrange_equiv(self):
ar = np.arange(start, stop, step)
coord = get_coord(start=start, stop=stop, step=step)
assert coord.shape == ar.shape
assert coord.size == ar.size

def test_unchanged_len(self):
"""Ensure an array converted to Coord range has same len. See #229."""
Expand Down
16 changes: 16 additions & 0 deletions tests/test_core/test_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,22 @@ def test_coord_time_narrow_select(self, multi_dim_coords_patch):
new_coords = new.coords.coord_map
assert isinstance(new_coords["time"], CoordRange)

def test_seconds(self, random_patch_with_lat):
"""Ensure we can get number of seconds in the patch."""
sampling_interval = random_patch_with_lat.attrs["time_step"] / np.timedelta64(
1, "s"
)
expected = (
random_patch_with_lat.attrs["time_max"]
- random_patch_with_lat.attrs["time_min"]
) / np.timedelta64(1, "s") + sampling_interval
assert random_patch_with_lat.seconds == expected

def test_channel_count(self, random_patch_with_lat):
"""Ensure we can get number of channels in the patch."""
expected = len(random_patch_with_lat.coords["distance"])
assert random_patch_with_lat.channel_count == expected


class TestApplyOperator:
"""Tests for applying various ufunc-type operators."""
Expand Down
Loading