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

Implement RoI Interactions with Points #395

Closed
wants to merge 29 commits into from
Closed
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2aa3df5
Create RoI base skeleton
willGraham01 Jan 27, 2025
5ce2a33
Write 1D and 2D ROI classes
willGraham01 Jan 27, 2025
588df0b
Write some basic instantiation tests
willGraham01 Jan 27, 2025
b69cc71
Pre-commit lint
willGraham01 Jan 27, 2025
19798e5
Fix polygon boundary attributes, return our wrappers instead
willGraham01 Jan 27, 2025
33031e8
Write methods for automating broadcasts across space dimension of xar…
willGraham01 Jan 28, 2025
7169beb
Fix mypy typing
willGraham01 Jan 28, 2025
8f3d5e8
Ignore mypy typehint bugs (it can't cope with additional kwargs being…
willGraham01 Jan 28, 2025
b637037
Fix classmethods not being extendable
willGraham01 Jan 29, 2025
2efe064
Preserve wrapped function docstrings
willGraham01 Jan 29, 2025
3743860
Will is learning markdown != rst
willGraham01 Jan 29, 2025
c0e4a14
Use xr.apply_ufunc
willGraham01 Jan 30, 2025
3eff3d3
Tidy some docstring explanations
willGraham01 Jan 30, 2025
066fafe
Will still doesn't know rst
willGraham01 Jan 30, 2025
fa28685
Allow make_broadcastable to preserve old function call behaviour
willGraham01 Jan 30, 2025
85e9d99
Merge branch 'wgraham-broadcasting-decorator' into wgraham-roi-is-inside
willGraham01 Jan 31, 2025
47148dc
Write in_inside method for determining if points are included in regi…
willGraham01 Jan 29, 2025
8da0835
SonarQ analysis recommendations
willGraham01 Jan 29, 2025
021abba
More informative name
willGraham01 Jan 29, 2025
62bee38
Missed an unused variable
willGraham01 Jan 29, 2025
28494ac
Update test that used default values
willGraham01 Jan 29, 2025
4bcb925
Docstring linting
willGraham01 Jan 31, 2025
ec460ad
Write in_inside method for determining if points are included in regi…
willGraham01 Jan 29, 2025
a715ef0
nearest point to template, but want to preserve original methods
willGraham01 Jan 30, 2025
a023fe8
Write test for nearest_point_to
willGraham01 Jan 30, 2025
4bda7ef
Write distance_to method
willGraham01 Jan 30, 2025
b773ce1
Add tests for distance_to method
willGraham01 Jan 30, 2025
a41e468
Write vector_to method
willGraham01 Jan 30, 2025
114cffe
Remove file that reappeared during rebase
willGraham01 Jan 31, 2025
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
Prev Previous commit
Next Next commit
Write test for nearest_point_to
willGraham01 committed Jan 31, 2025
commit a023fe8f47ab25b927aafbe1a792400b84cfe212
12 changes: 9 additions & 3 deletions movement/roi/base.py
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
from numpy.typing import ArrayLike

LineLike: TypeAlias = shapely.LinearRing | shapely.LineString
PointLike: TypeAlias = tuple[float, float]
PointLike: TypeAlias = tuple[float, ...] | list[float]
PointLikeList: TypeAlias = Sequence[PointLike]
RegionLike: TypeAlias = shapely.Polygon
SupportedGeometry: TypeAlias = LineLike | RegionLike
@@ -204,7 +204,9 @@ def points_are_inside(
)
return point_is_inside

@broadcastable_method(only_broadcastable_along="space")
@broadcastable_method(
only_broadcastable_along="space", new_dimension_name="nearest point"
)
def nearest_point_to(
self, /, position: ArrayLike, boundary: bool = False
) -> np.ndarray:
@@ -233,7 +235,11 @@ def nearest_point_to(
region, ``position`` itself will be returned. To find the nearest point
to ``position`` on the boundary of a region, pass the ``boundary`
argument as ``True`` to this method. Take care though - the boundary of
a line is considered to be its endpoints.
a line is considered to be just its endpoints.
See Also
--------
shapely.shortest_line : Underlying used to compute the nearest point.
"""
from_where = self.region.boundary if boundary else self.region
222 changes: 219 additions & 3 deletions tests/test_unit/test_roi/test_nearest_point_to.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,225 @@
from typing import Any

import numpy as np
import pytest
import xarray as xr

from movement.roi.base import BaseRegionOfInterest
from movement.roi.line import LineOfInterest


@pytest.fixture
def points_of_interest() -> dict[str, np.ndarray]:
return xr.DataArray(
np.array(
[
[-0.5, 0.50],
[0.00, 0.50],
[0.40, 0.45],
[2.00, 1.00],
[0.40, 0.75],
[0.95, 0.90],
[0.80, 0.76],
]
),
dims=["time", "space"],
coords={"space": ["x", "y"]},
)


@pytest.fixture()
def unit_line_in_x() -> LineOfInterest:
return LineOfInterest([[0.0, 0.0], [1.0, 0.0]])


@pytest.mark.parametrize(
["region", "other_fn_args", "expected_output"],
[
pytest.param(
"unit_square",
{"boundary": True},
np.array(
[
[0.00, 0.50],
[0.00, 0.50],
[0.00, 0.45],
[1.00, 1.00],
[0.40, 1.00],
[1.00, 0.90],
[1.00, 0.76],
]
),
id="Unit square, boundary only",
),
pytest.param(
"unit_square",
{},
np.array(
[
[0.00, 0.50],
[0.00, 0.50],
[0.40, 0.45],
[1.00, 1.00],
[0.40, 0.75],
[0.95, 0.90],
[0.80, 0.76],
]
),
id="Unit square, whole region",
),
pytest.param(
"unit_square_with_hole",
{"boundary": True},
np.array(
[
[0.00, 0.50],
[0.00, 0.50],
[0.25, 0.45],
[1.00, 1.00],
[0.40, 0.75],
[1.00, 0.90],
[0.75, 0.75],
]
),
id="Unit square w/ hole, boundary only",
),
pytest.param(
"unit_square_with_hole",
{},
np.array(
[
[0.00, 0.50],
[0.00, 0.50],
[0.25, 0.45],
[1.00, 1.00],
[0.40, 0.75],
[0.95, 0.90],
[0.80, 0.76],
]
),
id="Unit square w/ hole, whole region",
),
pytest.param(
"unit_line_in_x",
{},
np.array(
[
[0.00, 0.00],
[0.00, 0.00],
[0.40, 0.00],
[1.00, 0.00],
[0.40, 0.00],
[0.95, 0.00],
[0.80, 0.00],
]
),
id="Line, whole region",
),
pytest.param(
"unit_line_in_x",
{"boundary": True},
np.array(
[
[0.00, 0.00],
[0.00, 0.00],
[0.00, 0.00],
[1.00, 0.00],
[0.00, 0.00],
[1.00, 0.00],
[1.00, 0.00],
]
),
id="Line, boundary only",
),
],
)
def test_nearest_point_to(
region: BaseRegionOfInterest,
points_of_interest: xr.DataArray,
other_fn_args: dict[str, Any],
expected_output: xr.DataArray,
request,
) -> None:
if isinstance(region, str):
region = request.getfixturevalue(region)
if isinstance(points_of_interest, str):
points_of_interest = request.getfixturevalue(points_of_interest)
if isinstance(expected_output, str):
expected_output = request.get(expected_output)
elif isinstance(expected_output, np.ndarray):
expected_output = xr.DataArray(
expected_output,
dims=["time", "nearest point"],
)

points_of_interest = points_of_interest
nearest_points = region.nearest_point_to(
points_of_interest, **other_fn_args
)

xr.testing.assert_allclose(nearest_points, expected_output)


@pytest.mark.parametrize(
["region", "position", "fn_kwargs", "possible_nearest_points"],
[
pytest.param(
"unit_square",
[0.5, 0.5],
{"boundary": True},
[
np.array([0.0, 0.5]),
np.array([0.5, 0.0]),
np.array([1.0, 0.5]),
np.array([0.5, 1.0]),
],
id="Centre of the unit square",
),
pytest.param(
"unit_line_in_x",
[0.5, 0.0],
{"boundary": True},
[
np.array([0.0, 0.0]),
np.array([1.0, 0.0]),
],
id="Boundary of a line",
),
],
)
def test_tie_breaks(
region: BaseRegionOfInterest,
position: np.ndarray,
fn_kwargs: dict[str, Any],
possible_nearest_points: list[np.ndarray],
request,
) -> None:
"""Check behaviour when points are tied for nearest.
This can only occur when we have a Polygonal region, or a multi-line 1D
region. In this case, there may be multiple points in the region of
interest that are tied for closest. ``shapely`` does not actually document
how it breaks ties here, but we can at least check that it identifies one
of the possible correct points.
"""
if isinstance(region, str):
region = request.getfixturevalue(region)
if not isinstance(position, np.ndarray | xr.DataArray):
position = np.array(position)

nearest_point_found = region.nearest_point_to(position, **fn_kwargs)

def test_nearest_point_to(unit_square: BaseRegionOfInterest) -> None:
nearest = unit_square.nearest_point_to(np.array([-1.0, 0.0]))
sq_dist_to_nearest_pt = np.sum((nearest_point_found - position) ** 2)

pass
n_matches = 0
for possibility in possible_nearest_points:
# All possibilities should be approximately the same distance away
# from the position
assert np.isclose(
np.sum((possibility - position) ** 2), sq_dist_to_nearest_pt
)
# We should match at least one possibility,
# track to see if we do.
if np.isclose(nearest_point_found, possibility).all():
n_matches += 1
assert n_matches == 1