Skip to content

Commit

Permalink
Merge pull request #81 from ai4er-cdt/feature/unit-tests
Browse files Browse the repository at this point in the history
Feature/unit tests
  • Loading branch information
sdat2 authored Jul 22, 2022
2 parents 759716a + 77de58f commit 27b5ee7
Show file tree
Hide file tree
Showing 44 changed files with 828 additions and 759 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
name: Continuous integration loop

# Triggers the workflow on pull request
# events but only for the master branch
# events but only for the main branch
on:
pull_request:
branches: [master]
branches: [main]

jobs:
build:
Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Python package

on:
push:
branches: [ "main", "feature/unit-tests" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ${{matrix.os}}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.8", "3.9", "3.10"]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest
pip install -e .
- name: Test with pytest
run: |
pytest geograph
14 changes: 8 additions & 6 deletions docs/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import glob


def create_notebook_links():
"""Create links and entries for notebook in sphinx docs.
Expand All @@ -13,7 +14,7 @@ def create_notebook_links():
notebooks = glob.glob("./notebooks/*.ipynb")
notebooks.sort()

file_name_tmp = 'docs/notebooks/{}.nblink'
file_name_tmp = "docs/notebooks/{}.nblink"
file_content_tmp = """
{{
"path": "../../notebooks/{}.ipynb"
Expand All @@ -32,12 +33,13 @@ def create_notebook_links():
nb_name = path.split("/")[-1].split(".")[0]
file_name = file_name_tmp.format(nb_name)
file_content = file_content_tmp.format(nb_name)
with open(file_name,'w') as f:
f.write(file_content)
rst_index += "\n notebooks/{}".format(nb_name)
with open(file_name, "w") as f:
f.write(file_content)
rst_index += "\n notebooks/{}".format(nb_name)

with open("docs/tutorials.rst",'w') as f:
with open("docs/tutorials.rst", "w") as f:
f.write(rst_index)


if __name__ == "__main__":
create_notebook_links()
create_notebook_links()
3 changes: 0 additions & 3 deletions geograph/binary_graph_operations.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
"""Contains tools for binary operations between GeoGraph objects."""
from __future__ import annotations

from typing import TYPE_CHECKING, Dict, List, Tuple

import geopandas as gpd
from shapely.geometry.base import BaseGeometry
from shapely.geometry.polygon import Polygon

import geograph.utils.geopandas_utils as gpd_utils
from geograph.utils.polygon_utils import EMPTY_POLYGON, collapse_empty_polygon

Expand Down
2 changes: 1 addition & 1 deletion geograph/demo/binder_constants.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""This file is for constants relevant to the binder demo."""

from geograph.constants import PROJECT_PATH


# Data directory on GWS
DATA_DIR = PROJECT_PATH / "data"
# Polygon data of Chernobyl Exclusion Zone (CEZ)
Expand Down
9 changes: 6 additions & 3 deletions geograph/demo/plot_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
import itertools
from distutils.spawn import find_executable
from typing import Sequence, Tuple

import matplotlib
import matplotlib.style
import numpy as np
Expand All @@ -57,6 +56,10 @@ def ps_defaults(use_tex: bool = True) -> None:
Args:
use_tex (bool, optional): Whether or not to use latex matplotlib backend.
Defaults to True.
Example::
>>> from geograph.demo.plot_settings import ps_defaults
>>> ps_defaults(use_tex=False)
"""
# matplotlib.use('agg') this used to be required for jasmin
p_general = {
Expand Down Expand Up @@ -142,7 +145,7 @@ def label_subplots(
def get_dim(
width: float = 600,
fraction_of_line_width: float = 1,
ratio: float = (5 ** 0.5 - 1) / 2,
ratio: float = (5**0.5 - 1) / 2,
) -> Tuple[float, float]:
"""Return figure height, width in inches to avoid scaling in latex.
Expand Down Expand Up @@ -180,7 +183,7 @@ def set_dim(
fig: matplotlib.pyplot.figure,
width: float = 600,
fraction_of_line_width: float = 1,
ratio: float = (5 ** 0.5 - 1) / 2,
ratio: float = (5**0.5 - 1) / 2,
) -> None:
"""Set aesthetic figure dimensions to avoid scaling in latex.
Expand Down
15 changes: 6 additions & 9 deletions geograph/geograph.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
See https://networkx.org/documentation/stable/index.html for graph operations.
"""
from __future__ import annotations

import bz2
import gzip
import inspect
Expand All @@ -14,7 +13,6 @@
from copy import deepcopy
from itertools import zip_longest
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Union

import geopandas as gpd
import networkx as nx
import numpy as np
Expand All @@ -24,7 +22,6 @@
import shapely
from shapely.prepared import prep
from tqdm import tqdm

from geograph import binary_graph_operations, metrics
from geograph.metrics import CLASS_METRICS_DICT, Metric
from geograph.utils import rasterio_utils
Expand Down Expand Up @@ -677,12 +674,12 @@ def add_habitat(
# barrier polygon. If this results in multiple polygons,
# then the barrier polygon fully cuts the buffered polygon.
#
# We find the resulting buffered polygon fragment that contains
# the original node polygon, by selecting the polygon that
# contains a representative point sampled from the original
# polygon.
# We then check if that polygon fragment intersects the
# neighbour polygon we're trying to reach.
# We find the resulting buffered polygon fragment that contains
# the original node polygon, by selecting the polygon that
# contains a representative point sampled from the original
# polygon.
# We then check if that polygon fragment intersects the
# neighbour polygon we're trying to reach.
# If it does not, there is no path.
# If it does, there may be a path or there may not - it
# would require complex pathfinding code to discover, but
Expand Down
7 changes: 2 additions & 5 deletions geograph/geotimeline.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
"""Module for analysing multiple GeoGraph objects."""
from __future__ import annotations

import datetime
from bisect import bisect_left
from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union

import numpy as np
import pandas as pd
import xarray as xr

from geograph import GeoGraph
from geograph.binary_graph_operations import NodeMap, identify_graphs

Expand Down Expand Up @@ -121,7 +118,7 @@ def _sort_by_time(self, reverse: bool = False) -> None:
}

def _load_from_sequence(self, graph_list: List[TimedGeoGraph]) -> None:
"""Loads the sorted list of timed geographs into the timeline. """
"""Loads the sorted list of timed geographs into the timeline."""

# Make sure list is sorted in ascending time order (earliest = left)
by_time = lambda item: item.time
Expand Down Expand Up @@ -186,7 +183,7 @@ def node_map_cache(self, time1: TimeStamp, time2: TimeStamp) -> NodeMap:
raise NotCachedError

def _empty_node_map_cache(self) -> None:
""" Empties the node map cache."""
"""Empties the node map cache."""
self._node_map_cache = dict()

def timestack(self, use_cached: bool = True) -> List[NodeMap]:
Expand Down
6 changes: 2 additions & 4 deletions geograph/metrics.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"""Functions for calculating metrics from a GeoGraph."""
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Optional, Union

import networkx as nx
import numpy as np

Expand Down Expand Up @@ -164,7 +162,7 @@ def _simpson_diversity_index(geo_graph: geograph.GeoGraph) -> Metric:
)

return Metric(
value=1 - np.sum(class_prop_of_landscape ** 2),
value=1 - np.sum(class_prop_of_landscape**2),
name="simpson_diversity_index",
description=description,
variant="conventional",
Expand Down Expand Up @@ -391,7 +389,7 @@ def _class_effective_mesh_size(
)

return Metric(
value=np.sum(class_areas ** 2) / total_area,
value=np.sum(class_areas**2) / total_area,
name=f"effective_mesh_size_class={class_value}",
description=description,
variant="conventional",
Expand Down
Empty file added geograph/tests/__init__.py
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
Note: We may want to delete this at some point.
"""
import numpy as np
from typing import Dict, Iterable, Tuple

import geopandas as gpd
import numpy as np
import pytest
from geograph.constants import SRC_PATH
from geograph.tests.utils import get_array_transform, polygonise_sub_array
from geograph.utils.rasterio_utils import polygonise
Expand Down Expand Up @@ -41,7 +41,9 @@ def _polygonise_splits(
return result


if __name__ == "__main__":
@pytest.mark.unit
def test_create_data() -> None:
"""Create the test data."""
print("Creating test data ... ")
TEST_DATA_FOLDER = SRC_PATH / "tests" / "testdata"
TEST_DATA_FOLDER.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -96,3 +98,7 @@ def _polygonise_splits(
polygons_t = polygonise(arr_t, transform=get_array_transform(arr_t))
save_path = TEST_DATA_FOLDER / "timestack" / f"time_{i}.gpkg"
polygons_t.to_file(save_path, driver="GPKG")


if __name__ == "__main__":
test_create_data()
Binary file modified geograph/tests/testdata/adjacent/full.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/adjacent/lower_left.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/adjacent/lower_right.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/adjacent/upper_left.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/adjacent/upper_right.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/overlapping/full.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/overlapping/lower_left.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/overlapping/lower_right.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/overlapping/upper_left.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/overlapping/upper_right.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/timestack/time_0.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/timestack/time_1.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/timestack/time_2.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/timestack/time_3.gpkg
Binary file not shown.
Binary file modified geograph/tests/testdata/timestack/time_4.gpkg
Binary file not shown.
3 changes: 1 addition & 2 deletions geograph/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Convenience functions for creating and analysing test data for GeoGraph"""
from typing import Iterable, Tuple

import affine
import geograph
import geopandas as gpd
Expand All @@ -9,6 +8,7 @@
import seaborn as sns
from geograph.utils.rasterio_utils import polygonise


# Mirror the x axis
AFFINE_MIRROR_X = affine.Affine(-1, 0, 0, 0, 1, 0)
# Mirror the y axis
Expand Down Expand Up @@ -86,7 +86,6 @@ def polygonise_sub_array(
Args:
arr (np.ndarray): The numpy array from which to select the sub-array
x_lims (Tuple[int, int]): The x-limits of the sub-array. Must be >=0 or None.
y_lims (Tuple[int, int]): The y-limits of the sub-array. Must be >=0 or None.
Returns:
Expand Down
2 changes: 1 addition & 1 deletion geograph/utils/geopandas_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Helper functions for operating with geopandas objects."""
from typing import Dict, List

import geopandas as gpd
import networkx as nx
import tqdm
Expand All @@ -11,6 +10,7 @@
)
from shapely.geometry import MultiPolygon


# For switching identifiction mode in `identify_node`
_BULK_SPATIAL_IDENTIFICATION_FUNCTION = {
"corner": connect_with_interior_or_edge_or_corner_bulk,
Expand Down
2 changes: 1 addition & 1 deletion geograph/utils/polygon_utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Helper functions for overlap computations with polygons in shapely."""
from typing import List

from geopandas.array import GeometryArray
from numpy import ndarray
from shapely.geometry.polygon import Polygon


# Note: All DE-9IM patterns below are streamlined to work well with polygons.
# They are not guaranteed to work on lower dimensional objects (points/lines)
CORNER_ONLY_PATTERN = "FF*F0****"
Expand Down
2 changes: 0 additions & 2 deletions geograph/utils/rasterio_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""A collection of utility functions for data loading with rasterio."""

from typing import Iterable, Optional, Tuple, Union

import affine
import geograph.utils.geopandas_utils as gpd_utils
import geopandas as gpd
Expand Down
20 changes: 9 additions & 11 deletions geograph/visualisation/control_widgets.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
"""Module with widgets to control GeoGraphViewer."""

from __future__ import annotations

import logging
from typing import Dict, Optional

import ipywidgets as widgets
import traitlets
from geograph.visualisation import geoviewer, widget_utils
Expand Down Expand Up @@ -807,20 +804,21 @@ def _hover_callback(self, feature, **kwargs): # pylint: disable=unused-argument
"""Callback function on hover on graph polygon patch"""
try:
# self.logger.debug("HoverWidget callback called.")
new_value = widget_utils.create_html_header(
"Current Patch"
).value + """</br>
new_value = (
widget_utils.create_html_header("Current Patch").value
+ """</br>
<b>Class label:</b> {}</br>
<b>Area:</b> {:.2f} ha</br>
<b>Perimeter:</b> {:.2f} km</br>
<b>Shape index:</b> {:.2f}</br>
<b>Fractal dim.:</b> {:.2f}
""".format(
feature["properties"]["class_label"],
feature["properties"]["area"] / 1e4,
feature["properties"]["perimeter"] / 1e3,
feature["properties"]["shape_index"],
feature["properties"]["fractal_dimension"],
feature["properties"]["class_label"],
feature["properties"]["area"] / 1e4,
feature["properties"]["perimeter"] / 1e3,
feature["properties"]["shape_index"],
feature["properties"]["fractal_dimension"],
)
)
if "node_dynamic" in feature["properties"].keys():
new_value += "</br><b>Node dyanmic:</b> {}".format(
Expand Down
3 changes: 0 additions & 3 deletions geograph/visualisation/folium_utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
"""Module with utility functions to plot graphs in folium."""

from __future__ import annotations

from typing import Callable, List, Optional, Tuple

import folium
import geograph
import geopandas as gpd
Expand Down
Loading

0 comments on commit 27b5ee7

Please sign in to comment.