diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml new file mode 100644 index 0000000..a47119b --- /dev/null +++ b/.github/workflows/pypi-publish.yml @@ -0,0 +1,42 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Publish package to PyPI + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build package + run: python -m build + - name: Publish package + if: startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/python-package.yml b/.github/workflows/pytest.yml similarity index 97% rename from .github/workflows/python-package.yml rename to .github/workflows/pytest.yml index e4f3834..587c2d5 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/pytest.yml @@ -1,7 +1,7 @@ # 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 +name: pytest on: push: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 538b35e..b8dd309 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,9 @@ repos: - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.8.0 hooks: - id: black - language_version: python3.8 + language_version: python3.10 - repo: https://github.com/pycqa/pylint rev: pylint-2.6.0 hooks: diff --git a/Makefile b/Makefile index 0e09b12..3a8440f 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,12 @@ else HAS_CONDA=True endif +ifeq (,$(shell which mamba)) +HAS_MAMBA=False +else +HAS_MAMBA=True +endif + ################################################################################# # COMMANDS # ################################################################################# @@ -30,6 +36,7 @@ lint: ## Format geograph directory using black format: + isort geograph black geograph ## Set up pre-commit hooks @@ -39,20 +46,22 @@ precommit: ## Set up python interpreter environment and install basic dependencies env: -ifeq (True,$(HAS_CONDA)) - @echo ">>> Detected conda, creating conda environment." +ifeq (True, $(HAS_MAMBA)) + @echo ">>> Detected mamba, creating conda environment via mamba." + + # Create the conda environment + mamba env create --prefix=./env -f requirements/environment.yml + @echo ">>> New mamba env created. Activate from project directory with:\nconda activate ./env" +else ifeq (True,$(HAS_CONDA)) + @echo ">>> Detected conda, creating conda environment." + # Create the conda environment conda env create --prefix=./env -f requirements/environment.yml @echo ">>> New conda env created. Activate from project directory with:\nconda activate ./env" else - @echo ">>> No conda detected. Falling back to virtualenv instead. The python verison will be that of your python3 interpreter." - $(PYTHON_INTERPRETER) -m pip install -q virtualenv virtualenvwrapper - @echo ">>> Installing virtualenvwrapper if not already installed.\nMake sure the following lines are in shell startup file\n\ - export WORKON_HOME=$$HOME/.virtualenvs\nexport PROJECT_HOME=$$HOME/Devel\nsource /usr/local/bin/virtualenvwrapper.sh\n" - @bash -c "source `which virtualenvwrapper.sh`;mkvirtualenv $(PROJECT_NAME) --python=$(PYTHON_INTERPRETER)" - @echo ">>> New virtualenv created. Activate with:\nworkon $(PROJECT_NAME)" + @echo ">>> No conda detected. Please install conda or manually install requirements in your preferred python version." endif ################################################################################# diff --git a/README.md b/README.md index 09b4168..09b055e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ ![GeoGraphViewer demo gif](docs/images/viewer_demo.gif) - __Table of contents:__ 1. Description 1. Installation diff --git a/binder/environment.yml b/binder/environment.yml index 5a9f837..49f7699 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -1,8 +1,38 @@ +prefix: ./env channels: - conda-forge - defaults dependencies: - - python=3.8 - - pip + - python=3.10 + - pip==21.2.4 + # linear algebra and general data analysis + - numpy ==1.23.3 # arrays, linear algebra + - pandas ==1.5.0 # tabular data analysis + + # plotting + - folium ==0.12.1.post1 # plotting maps + - ipyleaflet ==0.17.1 # plotting ipywidget maps + + # interactive computing + - tqdm ==4.64.1 # progress bars + + # geospatial analysis requirements + # vector data + - geopandas ==0.11.1 # manipulating geospatial vector data + - shapely ==1.8.4 # working with vector shapes + - rtree ==1.0.0 # efficiently querying polygon data + # raster data + - rasterio ==1.3.2 # opening and loading raster data + - xarray ==2022.6.0 # useful data structures + + # graph requirements + - networkx ==2.8.6 # manipulating graph data + + # For binder + - rioxarray ==0.12.2 + - seaborn ==0.12.0 + - netcdf4 ==1.6.1 + - pip: - - -r requirements.txt + - pylandstats ==2.4.2 + - -e .. diff --git a/binder/requirements.txt b/binder/requirements.txt deleted file mode 100644 index c39f810..0000000 --- a/binder/requirements.txt +++ /dev/null @@ -1,38 +0,0 @@ -# NOTE: Your final requirements for production/publishing the repo go here. - -# local package --e ./.. - -# linear algebra and general data analysis -numpy==1.22.0 # arrays, linear algebra -pandas==1.2.3 # tabular data analysis - -# interactive computing -jupyter==1.0.0 # for opening jupyter notebooks in binder -tqdm==4.59.0 # progress bars - -# plotting -matplotlib==3.3.4 # general python plotting -seaborn==0.11.1 # fancier plotting styles # TODO: Check for removal -folium==0.12.1 # plotting maps -ipyleaflet==0.13.6 # plotting ipywidget maps - -# geospatial analysis requirements -# vector data -geopandas==0.9.0 # manipulating geospatial vector data -shapely==1.7.1 # working with vector shapes -rtree==0.9.7 # efficiently querying polygon data -descartes==1.1.0 # plotting geopandas vector data -# raster data -rasterio==1.1.8 # opening and loading raster data (Note: version >= 1.2 requires unsetting PROJ_LIB environment variable which is set by fiona -xarray==0.17.0 # useful data structures -rioxarray==0.3.1 # adaptation of xarray for raterio. # TODO: Check for removal - -# graph requirements -networkx==2.5 # manipulating graph data - -# comparison to fragmentation metrics in binder -pylandstats==2.2.1 - -# saving and loading netcdf4 files -netcdf4 \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 201fe03..e805ba7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -76,7 +76,7 @@ html_context = { "display_github": True, "github_user": "ai4er-cdt", - "github_repo": "gtc-biodiversity", + "github_repo": "geograph", "github_version": "main/docs/", } diff --git a/docs/index.rst b/docs/index.rst index 92b4f61..91260c0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -57,6 +57,7 @@ Polygon Data Visualisation installation tutorials geograph + maintenance about diff --git a/docs/maintenance.rst b/docs/maintenance.rst new file mode 100644 index 0000000..0cd5ab5 --- /dev/null +++ b/docs/maintenance.rst @@ -0,0 +1,20 @@ +Maintenance +=========== + +Some tips for the maintainers of this project. + +Deploying +--------- + +A reminder for the maintainers on how to deploy. Follow this checklist (inspired by `this checklist `_ and `this packaging tutorial `_): + +1. Update ``HISTORY.rst`` and commit with message like "aux: add changelog for upcoming release 0.1.0" +2. Run + + .. code-block:: console + + bump2version patch # possible: major / minor / patch + +3. Push commits *and tags* (`see here how to do this in vscode `_) +4. Merge pull request into ``main`` branch. +5. Add release on GitHub (using existing tag) \ No newline at end of file diff --git a/geograph/binary_graph_operations.py b/geograph/binary_graph_operations.py index 5c900d1..c73a730 100644 --- a/geograph/binary_graph_operations.py +++ b/geograph/binary_graph_operations.py @@ -1,9 +1,12 @@ """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 diff --git a/geograph/demo/binder_constants.py b/geograph/demo/binder_constants.py index 57ebe60..ceba438 100644 --- a/geograph/demo/binder_constants.py +++ b/geograph/demo/binder_constants.py @@ -1,7 +1,6 @@ """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) diff --git a/geograph/demo/plot_settings.py b/geograph/demo/plot_settings.py index fe48112..b64ff9f 100644 --- a/geograph/demo/plot_settings.py +++ b/geograph/demo/plot_settings.py @@ -42,6 +42,7 @@ import itertools from distutils.spawn import find_executable from typing import Sequence, Tuple + import matplotlib import matplotlib.style import numpy as np diff --git a/geograph/geograph.py b/geograph/geograph.py index 6a08e31..360363f 100644 --- a/geograph/geograph.py +++ b/geograph/geograph.py @@ -4,6 +4,7 @@ See https://networkx.org/documentation/stable/index.html for graph operations. """ from __future__ import annotations + import bz2 import gzip import inspect @@ -13,6 +14,7 @@ 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 @@ -22,6 +24,7 @@ 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 @@ -107,6 +110,16 @@ def __init__( **apply_buffer (bool, optional): Apply shapely buffer function to the polygons after polygonising. This can fix issues with the polygonisation creating invalid geometries. + + Example:: + >>> import os + >>> from geograph.constants import WGS84 + >>> from geograph.tests.create_data_test import TEST_DATA_FOLDER + >>> gg = GeoGraph(os.path.join(TEST_DATA_FOLDER, "adjacent", "full.gpkg"), WGS84, "savepath.gz") + Graph successfully loaded with 15 nodes and 40 edges. + >>> gg.crs == WGS84 + True + """ super().__init__() self.graph = nx.Graph() diff --git a/geograph/geotimeline.py b/geograph/geotimeline.py index 0a74c5c..c52ef79 100644 --- a/geograph/geotimeline.py +++ b/geograph/geotimeline.py @@ -1,11 +1,14 @@ """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 diff --git a/geograph/metrics.py b/geograph/metrics.py index 8871f9b..0b02ff4 100644 --- a/geograph/metrics.py +++ b/geograph/metrics.py @@ -1,7 +1,9 @@ """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 @@ -51,6 +53,15 @@ def __ge__(self, o: object) -> bool: # 1. Landscape level metrics ######################################################################################## def _num_patches(geo_graph: geograph.GeoGraph) -> Metric: + """ + Calculate number of patches. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + + Returns: + Metric: number of patches + """ return Metric( value=len(geo_graph.df), name="num_patches", @@ -62,6 +73,16 @@ def _num_patches(geo_graph: geograph.GeoGraph) -> Metric: def _avg_patch_area(geo_graph: geograph.GeoGraph) -> Metric: + """ + Calculate the average number of patches. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + + Returns: + Metric: average number of patches + """ + total_area = np.sum(geo_graph.df.area.values) num_patches = geo_graph.get_metric("num_patches").value @@ -75,6 +96,15 @@ def _avg_patch_area(geo_graph: geograph.GeoGraph) -> Metric: def _total_area(geo_graph: geograph.GeoGraph) -> Metric: + """ + Calculate total area of a GeoGraph. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + + Returns: + Metric: total area + """ return Metric( value=np.sum(geo_graph.df.area.values), name="total_area", @@ -85,6 +115,15 @@ def _total_area(geo_graph: geograph.GeoGraph) -> Metric: def _patch_density(geo_graph: geograph.GeoGraph) -> Metric: + """ + Calculate patch density of a GeoGraph. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + + Returns: + Metric: patch density + """ return Metric( value=1.0 / geo_graph.get_metric("avg_patch_area").value, name="patch_density", @@ -95,6 +134,15 @@ def _patch_density(geo_graph: geograph.GeoGraph) -> Metric: def _largest_patch_index(geo_graph: geograph.GeoGraph) -> Metric: + """ + Calculate largest patch index of a GeoGraph. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + + Returns: + Metric: largest patch index + """ total_area = np.sum(geo_graph.df.area.values) max_patch_area = max(geo_graph.df.area.values) @@ -115,6 +163,12 @@ def _shannon_diversity_index(geo_graph: geograph.GeoGraph) -> Metric: Further reference: https://pylandstats.readthedocs.io/en/latest/landscape.html https://en.wikipedia.org/wiki/Diversity_index + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + + Returns: + Metric: Shannon Diversity Index """ class_prop_of_landscape = np.array( [ @@ -142,11 +196,18 @@ def _shannon_diversity_index(geo_graph: geograph.GeoGraph) -> Metric: def _simpson_diversity_index(geo_graph: geograph.GeoGraph) -> Metric: """ - Calculate Simpson diversity of a GeoGraph. + Calculate Simpson Diversity Index of a GeoGraph. Reference: umass.edu/landeco/teaching/landscape_ecology/schedule/chapter9_metrics.pdf + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + + Returns: + Metric: Simpson Diversity Index """ + class_prop_of_landscape = np.array( [ geo_graph.get_metric( @@ -188,6 +249,16 @@ def _simpson_diversity_index(geo_graph: geograph.GeoGraph) -> Metric: def _class_total_area( geo_graph: geograph.GeoGraph, class_value: Union[int, str] ) -> Metric: + """ + Calculate the total area of all patches for a given class in a GeoGraph. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + class_value (Union[int, str]): class value + + Returns: + Metric: total area of class patches + """ class_areas = geo_graph.df["geometry"][ geo_graph.df["class_label"] == class_value @@ -205,6 +276,16 @@ def _class_total_area( def _class_avg_patch_area( geo_graph: geograph.GeoGraph, class_value: Union[int, str] ) -> Metric: + """ + Calculate the average area of patches for a given class in a GeoGraph. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + class_value (Union[int, str]): class value + + Returns: + Metric: average area of class patches + """ class_num_patches = geo_graph.get_metric( "num_patches", class_value=class_value @@ -223,6 +304,16 @@ def _class_avg_patch_area( def _class_num_patches( geo_graph: geograph.GeoGraph, class_value: Union[int, str] ) -> Metric: + """ + Calculate the number of patches for a given class in a GeoGraph. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + class_value (Union[int, str]): class value + + Returns: + Metric: number of class patches + """ return Metric( value=np.sum(geo_graph.df["class_label"] == class_value), name=f"num_patches_class={class_value}", @@ -235,6 +326,16 @@ def _class_num_patches( def _class_proportion_of_landscape( geo_graph: geograph.GeoGraph, class_value: Union[int, str] ) -> Metric: + """ + Calculate the proportional abundance of a given class in a GeoGraph. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + class_value (Union[int, str]): class value + + Returns: + Metric: class proportional abundance + """ class_total_area = geo_graph.get_metric("total_area", class_value=class_value).value total_area = geo_graph.get_metric("total_area").value @@ -251,6 +352,16 @@ def _class_proportion_of_landscape( def _class_patch_density( geo_graph: geograph.GeoGraph, class_value: Union[int, str] ) -> Metric: + """ + Calculate the patch density a given class in a GeoGraph. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + class_value (Union[int, str]): class value + + Returns: + Metric: class patch density + """ class_num_patches = geo_graph.get_metric( "num_patches", class_value=class_value @@ -270,10 +381,18 @@ def _class_largest_patch_index( geo_graph: geograph.GeoGraph, class_value: Union[int, str] ) -> Metric: """ - Return proportion of total landscape comprised by largest patch of given class. + Calculate the proportion of total landscape comprised by largest patch of given class. Definition taken from: https://pylandstats.readthedocs.io/en/latest/landscape.html + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + class_value (Union[int, str]): class value + + Returns: + Metric: proportion of total landscape comprised by largest patch + """ total_area = geo_graph.get_metric("total_area").value class_areas = geo_graph.df["geometry"][ @@ -297,6 +416,16 @@ def _class_largest_patch_index( def _class_total_edge( geo_graph: geograph.GeoGraph, class_value: Union[int, str] ) -> Metric: + """ + Calculate the total edgelength of patches a given class in a GeoGraph. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + class_value (Union[int, str]): class value + + Returns: + Metric: total edgelength of class patches + """ # TODO: Implement option for not counting edges. class_perimeters = geo_graph.df["geometry"][ @@ -316,12 +445,19 @@ def _class_edge_density( geo_graph: geograph.GeoGraph, class_value: Union[int, str] ) -> Metric: """ - Return total length of class edges for the given class. + Calculate the edge length per unit area for the given class in a GeoGraph. Note: Currently the boundary also counted towards the edge length. Definition taken from: https://pylandstats.readthedocs.io/en/latest/landscape.html + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + class_value (Union[int, str]): class value + + Returns: + Metric: class edge length per unit area """ # TODO: Implement option for not counting edges. @@ -345,10 +481,17 @@ def _class_shape_index( geo_graph: geograph.GeoGraph, class_value: Union[int, str] ) -> Metric: """ - Return shape index of given class. + Calculate shape index of given class for a GeoGraph. Definition taken from: https://pylandstats.readthedocs.io/en/latest/landscape.html + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + class_value (Union[int, str]): class value + + Returns: + Metric: class shape index """ total_edge = geo_graph.get_metric("total_edge", class_value=class_value).value total_area = geo_graph.get_metric("total_area", class_value=class_value).value @@ -372,11 +515,19 @@ def _class_effective_mesh_size( geo_graph: geograph.GeoGraph, class_value: Union[int, str] ) -> Metric: """ - Return effective mesh size of given class. + Calculate effective mesh size of given class. Definition taken from: https://pylandstats.readthedocs.io/en/latest/landscape.html + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + class_value (Union[int, str]): class value + + Returns: + Metric: class mesh size """ + class_areas = geo_graph.df["geometry"][ geo_graph.df["class_label"] == class_value ].area @@ -416,6 +567,15 @@ def _class_effective_mesh_size( def _num_components(geo_graph: geograph.GeoGraph) -> Metric: + """ + Calculate number of connected components in GeoGraph. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + + Returns: + Metric: number of connected components in the graph + """ return Metric( value=nx.number_connected_components(geo_graph.graph), name="num_components", @@ -426,6 +586,15 @@ def _num_components(geo_graph: geograph.GeoGraph) -> Metric: def _avg_component_area(geo_graph: geograph.GeoGraph) -> Metric: + """ + Calculate average component area in GeoGraph. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + + Returns: + Metric: average component area + """ if not geo_graph.components.has_df: print("Calculating component polygons...") geo_graph.components = geo_graph.get_graph_components(calc_polygons=True) @@ -443,8 +612,14 @@ def _avg_component_isolation(geo_graph: geograph.GeoGraph) -> Metric: """ Calculate the average distance to the next-nearest component. - Warning: very computationally expensive for graphs with more than ~100 + !!! Warning !!!: very computationally expensive for graphs with more than ~100 components. + + Args: + geo_graph (geograph.GeoGraph): GeoGraph + + Returns: + Metric: avverage distance to the next-nearest component """ if not geo_graph.components.has_df or not geo_graph.components.has_distance_edges: print( @@ -504,14 +679,14 @@ def _get_metric( Calculate selected metric for the given GeoGraph. Args: - name (str): Name of the metric to compute + name (str): name of the metric to compute geo_graph (geograph.GeoGraph): GeoGraph object to compute the metric for - class_value (Optional[int], optional): The value of the desired class if a + class_value (Optional[int], optional): value of the desired class if a class level metric is desired. None if a landscape/component level metric is desired. Defaults to None. Returns: - Metric: The desired metric. + Metric: desired metric. """ # Landscape level metrics if class_value is None: diff --git a/geograph/tests/create_data_test.py b/geograph/tests/create_data_test.py index c7fd485..13b7247 100644 --- a/geograph/tests/create_data_test.py +++ b/geograph/tests/create_data_test.py @@ -2,14 +2,18 @@ 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 +TEST_DATA_FOLDER = SRC_PATH / "tests" / "testdata" + def _polygonise_splits( arr: np.ndarray, named_slices: Iterable[Dict[str, Tuple]] @@ -45,7 +49,6 @@ def _polygonise_splits( 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) # 1. Create non-overlapping polygons @@ -60,9 +63,11 @@ def test_create_data() -> None: "upper_left": ((0, 2), (3, 6)), "lower_right": ((2, 4), (0, 3)), } - # Poligonise + + # Polygonise polygons1 = _polygonise_splits(arr1, splits_of_interest) polygons1["full"] = polygonise(arr1, transform=get_array_transform(arr1)) + # Save for save_name, df in polygons1.items(): save_path = TEST_DATA_FOLDER / "adjacent" / f"{save_name}.gpkg" diff --git a/geograph/tests/utils.py b/geograph/tests/utils.py index c0d0b04..f9ab395 100644 --- a/geograph/tests/utils.py +++ b/geograph/tests/utils.py @@ -1,13 +1,14 @@ """Convenience functions for creating and analysing test data for GeoGraph""" from typing import Iterable, Tuple + import affine -import geograph import geopandas as gpd import matplotlib.pyplot as plt import numpy as np import seaborn as sns -from geograph.utils.rasterio_utils import polygonise +import geograph +from geograph.utils.rasterio_utils import polygonise # Mirror the x axis AFFINE_MIRROR_X = affine.Affine(-1, 0, 0, 0, 1, 0) diff --git a/geograph/utils/geopandas_utils.py b/geograph/utils/geopandas_utils.py index 1753c7b..1df5fc2 100644 --- a/geograph/utils/geopandas_utils.py +++ b/geograph/utils/geopandas_utils.py @@ -1,15 +1,16 @@ """Helper functions for operating with geopandas objects.""" from typing import Dict, List + import geopandas as gpd import networkx as nx import tqdm +from shapely.geometry import MultiPolygon + from geograph.utils.polygon_utils import ( connect_with_interior_bulk, connect_with_interior_or_edge_bulk, connect_with_interior_or_edge_or_corner_bulk, ) -from shapely.geometry import MultiPolygon - # For switching identifiction mode in `identify_node` _BULK_SPATIAL_IDENTIFICATION_FUNCTION = { diff --git a/geograph/utils/polygon_utils.py b/geograph/utils/polygon_utils.py index 731d4fa..c96635c 100644 --- a/geograph/utils/polygon_utils.py +++ b/geograph/utils/polygon_utils.py @@ -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****" diff --git a/geograph/utils/rasterio_utils.py b/geograph/utils/rasterio_utils.py index f6212d1..ea82311 100644 --- a/geograph/utils/rasterio_utils.py +++ b/geograph/utils/rasterio_utils.py @@ -1,13 +1,15 @@ """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 import numpy as np from rasterio.crs import CRS from rasterio.features import shapes from rasterio.io import DatasetReader +import geograph.utils.geopandas_utils as gpd_utils + class CoordinateSystemError(Exception): """Basic exception for coordinate system errors.""" diff --git a/geograph/visualisation/control_widgets.py b/geograph/visualisation/control_widgets.py index b1a9e3d..e87ebdf 100644 --- a/geograph/visualisation/control_widgets.py +++ b/geograph/visualisation/control_widgets.py @@ -1,9 +1,12 @@ """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 @@ -58,7 +61,7 @@ def __init__(self, viewer: geoviewer.GeoGraphViewer) -> None: view_tab = [visibility_widget, widget_utils.HRULE, metrics_widget] # Create combined widget, each key corresponds to a tab - combined_widget_dict = dict() + combined_widget_dict = {} combined_widget_dict["View"] = widgets.VBox(view_tab) if self.viewer.small_screen: combined_widget_dict["Metrics"] = metrics_widget diff --git a/geograph/visualisation/folium_utils.py b/geograph/visualisation/folium_utils.py index ae4fc43..83f533c 100644 --- a/geograph/visualisation/folium_utils.py +++ b/geograph/visualisation/folium_utils.py @@ -1,9 +1,12 @@ """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 + +import geograph from geograph.constants import CHERNOBYL_COORDS_WGS84, UTM35N from geograph.visualisation import graph_utils diff --git a/geograph/visualisation/geoviewer.py b/geograph/visualisation/geoviewer.py index 17b6e6e..24a89ae 100644 --- a/geograph/visualisation/geoviewer.py +++ b/geograph/visualisation/geoviewer.py @@ -1,15 +1,18 @@ """This module contains the GeoGraphViewer to visualise GeoGraphs""" from __future__ import annotations + import logging import threading import time from typing import TYPE_CHECKING, List, Optional, Union + import folium -import geograph import ipyleaflet import ipywidgets as widgets import pandas as pd import traitlets + +import geograph from geograph import metrics from geograph.constants import CHERNOBYL_COORDS_WGS84, WGS84 from geograph.visualisation import ( diff --git a/geograph/visualisation/graph_utils.py b/geograph/visualisation/graph_utils.py index 861903d..09231d8 100644 --- a/geograph/visualisation/graph_utils.py +++ b/geograph/visualisation/graph_utils.py @@ -1,9 +1,12 @@ """This module contains utility function for generally plotting graphs.""" from __future__ import annotations + from typing import Tuple -import geograph + import geopandas as gpd import shapely.geometry + +import geograph from geograph.constants import WGS84 diff --git a/geograph/visualisation/widget_utils.py b/geograph/visualisation/widget_utils.py index 1d2f17c..f28aa15 100644 --- a/geograph/visualisation/widget_utils.py +++ b/geograph/visualisation/widget_utils.py @@ -1,9 +1,9 @@ """Module with utils for logging, debugging and styling ipywidgets.""" import logging + import IPython.display import ipywidgets as widgets - # Styling widgets HRULE = widgets.HTML('
') diff --git a/notebooks/3-demo-geographviewer-polesia.ipynb b/notebooks/3-demo-geographviewer-polesia.ipynb index 5ffe3a2..1d89abc 100644 --- a/notebooks/3-demo-geographviewer-polesia.ipynb +++ b/notebooks/3-demo-geographviewer-polesia.ipynb @@ -8,6 +8,32 @@ "# Visualising GeoGraph Interactively" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "ddea6d87", + "metadata": {}, + "outputs": [], + "source": [ + "import pathlib\n", + "import geopandas as gpd\n", + "\n", + "# Loading Polesia data \n", + "data_path = DATA_DIR / \"polesia\" / \"polesia_landcover_sample.gpkg\"\n", + "gdf = gpd.read_file(data_path)\n", + "\n", + "# Looking at south-west corner of data\n", + "# Choosen because are of wilderness as discussed\n", + "minx, miny, maxx, maxy = gdf.total_bounds\n", + "square_len = 25000\n", + "\n", + "gdf = gdf.cx[ minx:minx+square_len , miny:miny+square_len]\n", + "\n", + "print(\"Number of patches in region of interest:\", len(gdf))\n", + "gdf.plot()\n", + "gdf.head(5)" + ] + }, { "cell_type": "markdown", "id": "86ba856b-c101-4414-945e-75a016237f7e", @@ -97,6 +123,32 @@ "## 2. Loading Data" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "fc186ea7", + "metadata": {}, + "outputs": [], + "source": [ + "import pathlib\n", + "import geopandas as gpd\n", + "\n", + "# Loading Polesia data \n", + "data_path = DATA_DIR / \"polesia\" / \"polesia_landcover_sample.gpkg\"\n", + "gdf = gpd.read_file(data_path)\n", + "\n", + "# Looking at south-west corner of data\n", + "# Choosen because are of wilderness as discussed\n", + "minx, miny, maxx, maxy = gdf.total_bounds\n", + "square_len = 25000\n", + "\n", + "gdf = gdf.cx[ minx:minx+square_len , miny:miny+square_len]\n", + "\n", + "print(\"Number of patches in region of interest:\", len(gdf))\n", + "gdf.plot()\n", + "gdf.head(5)" + ] + }, { "cell_type": "markdown", "id": "searching-serial", diff --git a/pytest.ini b/pytest.ini index 1ace2db..33a00b6 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,5 +2,5 @@ [pytest] addopts = --doctest-modules doctest_encoding = latin1 -doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL +doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL NUMBER diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt index 4406113..b557b90 100644 --- a/requirements/dev-requirements.txt +++ b/requirements/dev-requirements.txt @@ -1,75 +1,11 @@ # NOTE: These requirements are used for developing code on the repo. # As a standard they include certain formatters and linters. -# local package --e ../. - # external requirements (mostly linters and formatters) -pylint # pylint linter +pylint==2.6.0 # pylint linter mypy # python type checker -black # automatic formatting provider +black==22.8.0 # automatic formatting provider pre-commit # for git precommit hooks isort # automatic import sorter python-dotenv # environment variable manager -pydocstyle # set pydocstyle - -# linear algebra and general data analysis -numpy # arrays, linear algebra -scipy # linear algebra and numerical mathematics -numba # speeding up array operations -pandas # tabular data analysis - -# plotting -matplotlib # general python plotting -seaborn # fancier plotting styles -descartes # geospatial plotting of shapefiles -folium # plotting maps -ipyleaflet # plotting ipywidget maps - -# interactive computing -jupyterlab # jupyter notebooks -tqdm # progress bars - -# geospatial analysis requirements -rasterio # opening and loading raster data -fiona # manipulating geospatial vector data -geopandas # manipulating geospatial vector data -shapely # working with vector shapes -pycrs # working with coordinate reference systems -geopy # convenient API requests to geocoders -xarray # useful data structures -rioxarray # adaptation of xarray for raterio. -dask[array] # allows to composite multiple satellite images stored in different shards -dask[dataframe] # allows more lazy operation for xarray. -dask[dataframe] # allows more lazy operation for xarray. -dask[distributed] # allows distributed computing -netCDF4 # makes sure that the default driver is netCDF4. -bottleneck # needed for fill forward (xarray.DataArray.ffil) - -# additional -networkx # manipulating graph data -rtree # rtree library - -# gdrive functionality -google-api-python-client -google-auth-httplib2 -google-auth-oauthlib - -# make videos for animating timeseries etc. -imageio -imageio-ffmpeg - -# xgboost -xgboost # gradient boosted regression -sklearn # sklearn -graphviz # can plot the decission tree - -# -pillow -torch==1.8 -torchvision -wandb -pytorch-lightning -git+https://github.com/qubvel/segmentation_models.pytorch -hydra-core -twine # for publishing to PyPI +pydocstyle # set pydocstyle \ No newline at end of file diff --git a/requirements/doc-requirements.txt b/requirements/doc-requirements.txt index f30109f..eb6fae8 100644 --- a/requirements/doc-requirements.txt +++ b/requirements/doc-requirements.txt @@ -1,77 +1,5 @@ -# NOTE: These requirements are used for developing code on the repo. -# As a standard they include certain formatters and linters. - -# local package -# -e ../. - -# external requirements (mostly linters and formatters) -pylint # pylint linter -mypy # python type checker -black # automatic formatting provider -pre-commit # for git precommit hooks -isort # automatic import sorter -python-dotenv # environment variable manager -pydocstyle # set pydocstyle - -# linear algebra and general data analysis -numpy # arrays, linear algebra -scipy # linear algebra and numerical mathematics -numba # speeding up array operations -pandas # tabular data analysis - -# plotting -matplotlib # general python plotting -seaborn # fancier plotting styles -descartes # geospatial plotting of shapefiles -folium # plotting maps -ipyleaflet # plotting ipywidget maps - -# interactive computing -jupyterlab # jupyter notebooks -tqdm # progress bars - -# geospatial analysis requirements -rasterio # opening and loading raster data -fiona # manipulating geospatial vector data -geopandas # manipulating geospatial vector data -shapely # working with vector shapes -pycrs # working with coordinate reference systems -geopy # convenient API requests to geocoders -xarray # useful data structures -rioxarray # adaptation of xarray for raterio. -dask[array] # allows to composite multiple satellite images stored in different shards -dask[dataframe] # allows more lazy operation for xarray. -dask[dataframe] # allows more lazy operation for xarray. -dask[distributed] # allows distributed computing -netCDF4 # makes sure that the default driver is netCDF4. -bottleneck # needed for fill forward (xarray.DataArray.ffil) - -# additional -networkx # manipulating graph data -rtree # rtree library - -# gdrive functionality -google-api-python-client -google-auth-httplib2 -google-auth-oauthlib - -# make videos for animating timeseries etc. -imageio -imageio-ffmpeg - -# xgboost -xgboost # gradient boosted regression -sklearn # sklearn -graphviz # can plot the decission tree - -# -pillow -torch==1.8 -torchvision -wandb -pytorch-lightning -git+https://github.com/qubvel/segmentation_models.pytorch -hydra-core +# NOTE: These requirements are used by readthedocs.io to autogenerate +# the documentation for this repo. #docs sphinx diff --git a/requirements/environment.yml b/requirements/environment.yml index f563457..0c2b0db 100644 --- a/requirements/environment.yml +++ b/requirements/environment.yml @@ -3,7 +3,30 @@ channels: - conda-forge - defaults dependencies: - - python=3.8 + - python=3.10 - pip + # linear algebra and general data analysis + - numpy 1.* # arrays, linear algebra + - pandas 1.5.* # tabular data analysis + + # plotting + - folium 0.12.* # plotting maps + - ipyleaflet 0.* # plotting ipywidget maps + + # interactive computing + - tqdm 4.* # progress bars + + # geospatial analysis requirements + # vector data + - geopandas 0.11.* # manipulating geospatial vector data + - shapely 1.* # working with vector shapes + - rtree 1.* # efficiently querying polygon data + # raster data + - rasterio 1.3.* # opening and loading raster data + - xarray 2022.* # useful data structures + + # graph requirements + - networkx 2.8.* # manipulating graph data + - pip: - - -r dev-requirements.txt + - -e .. diff --git a/requirements/requirements.txt b/requirements/requirements.txt deleted file mode 100644 index c30b1e1..0000000 --- a/requirements/requirements.txt +++ /dev/null @@ -1,27 +0,0 @@ -# NOTE: Your final requirements for production/publishing the repo go here. - -# local package --e . - -# linear algebra and general data analysis -numpy # arrays, linear algebra -pandas # tabular data analysis - -# plotting -folium # plotting maps -ipyleaflet # plotting ipywidget maps - -# interactive computing -tqdm # progress bars - -# geospatial analysis requirements -# vector data -geopandas # manipulating geospatial vector data -shapely # working with vector shapes -rtree # efficiently querying polygon data -# raster data -rasterio==1.1.8 # opening and loading raster data -xarray # useful data structures - -# graph requirements -networkx # manipulating graph data