Skip to content

Commit f19a820

Browse files
committed
chore(AdjacencyNeighbourhood): add tests
1 parent 075b4dd commit f19a820

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
from typing import Set
2+
3+
import geopandas as gpd
4+
import pytest
5+
from shapely import geometry
6+
7+
from srai.neighbourhoods import AdjacencyNeighbourhood
8+
from srai.utils.constants import WGS84_CRS
9+
10+
11+
@pytest.fixture # type: ignore
12+
def no_geometry_gdf() -> gpd.GeoDataFrame:
13+
"""Get empty GeoDataFrame."""
14+
return gpd.GeoDataFrame()
15+
16+
17+
@pytest.fixture # type: ignore
18+
def empty_gdf() -> gpd.GeoDataFrame:
19+
"""Get GeoDataFrame with no geometry."""
20+
return gpd.GeoDataFrame(geometry=[])
21+
22+
23+
@pytest.fixture # type: ignore
24+
def squares_regions_fixture() -> gpd.GeoDataFrame:
25+
"""
26+
This GeoDataFrame represents 9 squares on a cartesian plane in a 3 by 3 grid pattern. Squares
27+
are adjacent by edges and by vertexes. Squares are given compass directions. Visually it looks
28+
like this ("C" means "CENTER"):
29+
30+
[["NW", "N", "NE"],
31+
[ "W", "C", "E"],
32+
["SW", "S", "SE"]]
33+
34+
Returns:
35+
GeoDataFrame: A GeoDataFrame containing square regions.
36+
"""
37+
regions = gpd.GeoDataFrame(
38+
geometry=[
39+
geometry.box(minx=0, miny=0, maxx=1, maxy=1),
40+
geometry.box(minx=1, miny=0, maxx=2, maxy=1),
41+
geometry.box(minx=2, miny=0, maxx=3, maxy=1),
42+
geometry.box(minx=0, miny=1, maxx=1, maxy=2),
43+
geometry.box(minx=1, miny=1, maxx=2, maxy=2),
44+
geometry.box(minx=2, miny=1, maxx=3, maxy=2),
45+
geometry.box(minx=0, miny=2, maxx=1, maxy=3),
46+
geometry.box(minx=1, miny=2, maxx=2, maxy=3),
47+
geometry.box(minx=2, miny=2, maxx=3, maxy=3),
48+
],
49+
index=["SW", "S", "SE", "W", "CENTER", "E", "NW", "N", "NE"], # compass directions
50+
crs=WGS84_CRS,
51+
)
52+
return regions
53+
54+
55+
@pytest.fixture # type: ignore
56+
def rounded_regions_fixture() -> gpd.GeoDataFrame:
57+
"""
58+
This GeoDataFrame represents 9 small squares with buffer on a cartesian plane in a 3 by 3 grid
59+
pattern. Regions are adjacent by edges, but not by vertexes. Regions are given compass
60+
directions. Visually it looks like this ("C" means "CENTER"):
61+
62+
[["NW", "N", "NE"],
63+
[ "W", "C", "E"],
64+
["SW", "S", "SE"]]
65+
66+
Returns:
67+
GeoDataFrame: A GeoDataFrame containing rounded regions.
68+
"""
69+
regions = gpd.GeoDataFrame(
70+
geometry=[
71+
geometry.box(minx=0, miny=0, maxx=0.5, maxy=0.5).buffer(0.25),
72+
geometry.box(minx=1, miny=0, maxx=1.5, maxy=0.5).buffer(0.25),
73+
geometry.box(minx=2, miny=0, maxx=2.5, maxy=0.5).buffer(0.25),
74+
geometry.box(minx=0, miny=1, maxx=0.5, maxy=1.5).buffer(0.25),
75+
geometry.box(minx=1, miny=1, maxx=1.5, maxy=1.5).buffer(0.25),
76+
geometry.box(minx=2, miny=1, maxx=2.5, maxy=1.5).buffer(0.25),
77+
geometry.box(minx=0, miny=2, maxx=0.5, maxy=2.5).buffer(0.25),
78+
geometry.box(minx=1, miny=2, maxx=1.5, maxy=2.5).buffer(0.25),
79+
geometry.box(minx=2, miny=2, maxx=2.5, maxy=2.5).buffer(0.25),
80+
],
81+
index=["SW", "S", "SE", "W", "CENTER", "E", "NW", "N", "NE"], # compass directions
82+
crs=WGS84_CRS,
83+
)
84+
return regions
85+
86+
87+
def test_no_geometry_gdf_attribute_error(no_geometry_gdf: gpd.GeoDataFrame) -> None:
88+
"""Test checks if GeoDataFrames without geometry are disallowed."""
89+
with pytest.raises(AttributeError):
90+
AdjacencyNeighbourhood(no_geometry_gdf)
91+
92+
93+
def test_empty_gdf_empty_set(empty_gdf: gpd.GeoDataFrame) -> None:
94+
"""Test checks if empty GeoDataFrames return empty neighbourhoods."""
95+
neighbourhood = AdjacencyNeighbourhood(empty_gdf)
96+
assert neighbourhood.get_neighbours(1) == set()
97+
98+
99+
def test_lazy_loading_empty_set(squares_regions_fixture: gpd.GeoDataFrame) -> None:
100+
"""Test checks if lookup table is empty after init."""
101+
neighbourhood = AdjacencyNeighbourhood(squares_regions_fixture)
102+
assert neighbourhood.lookup == dict()
103+
104+
105+
def test_generate_all_neighbourhoods_rounded_regions(
106+
rounded_regions_fixture: gpd.GeoDataFrame,
107+
) -> None:
108+
"""Test checks `generate_neighbourhoods` function with rounded regions."""
109+
neighbourhood = AdjacencyNeighbourhood(rounded_regions_fixture)
110+
neighbourhood.generate_neighbourhoods()
111+
assert neighbourhood.lookup == {
112+
"SW": {"W", "S"},
113+
"S": {"SW", "CENTER", "SE"},
114+
"SE": {"E", "S"},
115+
"W": {"SW", "CENTER", "NW"},
116+
"CENTER": {"N", "W", "E", "S"},
117+
"E": {"CENTER", "NE", "SE"},
118+
"NW": {"N", "W"},
119+
"N": {"CENTER", "NE", "NW"},
120+
"NE": {"N", "E"},
121+
}
122+
123+
124+
def test_generate_all_neighbourhoods_squares_regions(
125+
squares_regions_fixture: gpd.GeoDataFrame,
126+
) -> None:
127+
"""Test checks `generate_neighbourhoods` function with square regions."""
128+
neighbourhood = AdjacencyNeighbourhood(squares_regions_fixture)
129+
neighbourhood.generate_neighbourhoods()
130+
assert neighbourhood.lookup == {
131+
"SW": {"W", "S", "CENTER"},
132+
"S": {"SW", "W", "CENTER", "SE", "E"},
133+
"SE": {"E", "S", "CENTER"},
134+
"W": {"N", "SW", "S", "CENTER", "NW"},
135+
"CENTER": {"SW", "N", "W", "S", "SE", "E", "NW", "NE"},
136+
"E": {"N", "S", "CENTER", "SE", "NE"},
137+
"NW": {"W", "N", "CENTER"},
138+
"N": {"W", "CENTER", "E", "NW", "NE"},
139+
"NE": {"E", "N", "CENTER"},
140+
}
141+
142+
143+
def test_adjacency_lazy_loading(rounded_regions_fixture: gpd.GeoDataFrame) -> None:
144+
"""Test checks if lookup table is lazily populated."""
145+
neighbourhood = AdjacencyNeighbourhood(rounded_regions_fixture)
146+
neighbours = neighbourhood.get_neighbours("SW")
147+
assert neighbours == {"W", "S"}
148+
assert neighbourhood.lookup == {
149+
"SW": {"W", "S"},
150+
}
151+
152+
153+
@pytest.mark.parametrize( # type: ignore
154+
"index, distance, neighbours_expected",
155+
[
156+
("SW", -2, set()),
157+
("SW", -1, set()),
158+
("SW", 0, set()),
159+
("SW", 1, {"S", "W"}),
160+
("SW", 2, {"CENTER", "SE", "NW"}),
161+
("SW", 3, {"N", "E"}),
162+
("SW", 4, {"NE"}),
163+
("SW", 5, set()),
164+
("CENTER", 1, {"N", "E", "S", "W"}),
165+
("CENTER", 2, {"NW", "NE", "SW", "SE"}),
166+
("CENTER", 3, set()),
167+
("N", 1, {"NW", "NE", "CENTER"}),
168+
("N", 2, {"E", "S", "W"}),
169+
("N", 3, {"SE", "SW"}),
170+
("N", 4, set()),
171+
],
172+
)
173+
def test_adjacency_lazy_loading_at_distance(
174+
index: str,
175+
distance: int,
176+
neighbours_expected: Set[str],
177+
rounded_regions_fixture: gpd.GeoDataFrame,
178+
) -> None:
179+
"""Test checks `get_neighbours_at_distance` function with rounded regions."""
180+
neighbourhood = AdjacencyNeighbourhood(rounded_regions_fixture)
181+
neighbours = neighbourhood.get_neighbours_at_distance(index, distance)
182+
assert neighbours == neighbours_expected
183+
184+
185+
@pytest.mark.parametrize( # type: ignore
186+
"index, distance, neighbours_expected",
187+
[
188+
("SW", -2, set()),
189+
("SW", -1, set()),
190+
("SW", 0, set()),
191+
("SW", 1, {"S", "W"}),
192+
("SW", 2, {"S", "W", "CENTER", "SE", "NW"}),
193+
("SW", 3, {"S", "W", "CENTER", "SE", "NW", "N", "E"}),
194+
("SW", 4, {"S", "W", "CENTER", "SE", "NW", "N", "E", "NE"}),
195+
("SW", 5, {"S", "W", "CENTER", "SE", "NW", "N", "E", "NE"}),
196+
("CENTER", 1, {"N", "E", "S", "W"}),
197+
("CENTER", 2, {"N", "E", "S", "W", "NW", "NE", "SW", "SE"}),
198+
("CENTER", 3, {"N", "E", "S", "W", "NW", "NE", "SW", "SE"}),
199+
("N", 1, {"NW", "NE", "CENTER"}),
200+
("N", 2, {"NW", "NE", "CENTER", "E", "S", "W"}),
201+
("N", 3, {"NW", "NE", "CENTER", "E", "S", "W", "SE", "SW"}),
202+
("N", 4, {"NW", "NE", "CENTER", "E", "S", "W", "SE", "SW"}),
203+
],
204+
)
205+
def test_adjacency_lazy_loading_up_to_distance(
206+
index: str,
207+
distance: int,
208+
neighbours_expected: Set[str],
209+
rounded_regions_fixture: gpd.GeoDataFrame,
210+
) -> None:
211+
"""Test checks `get_neighbours_up_to_distance` function with rounded regions."""
212+
neighbourhood = AdjacencyNeighbourhood(rounded_regions_fixture)
213+
neighbours = neighbourhood.get_neighbours_up_to_distance(index, distance)
214+
assert neighbours == neighbours_expected

0 commit comments

Comments
 (0)