Skip to content

Commit

Permalink
Resolve merge conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
jterry64 committed Jun 6, 2024
2 parents 3ba9456 + 8aa2910 commit 028c467
Show file tree
Hide file tree
Showing 14 changed files with 1,181 additions and 59 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python application

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

permissions:
contents: read

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
python setup.py install
- name: Test with pytest
run: |
cd tests/
pytest layers.py metrics.py
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,22 @@
The Cities Indicator Framework (CIF) is a set of Python tools to make it easier to calculate zonal statistics for cities by providing a standardized set of data layers for inputs and a common framework for using those layers to calculate indicators.

## Quick start
If all you want to do is use the CIF, the quickest way to get started is to use our [WRI Cities Indicator Framework Colab Notebook](https://colab.research.google.com/drive/1PV1H-godxJ6h42p74Ij9sdFh3T0RN-7j#scrollTo=eM14UgpmpZL-)
* If all you want to do is use the CIF, the quickest way to get started is to use our [WRI Cities Indicator Framework Colab Notebook](https://colab.research.google.com/drive/1PV1H-godxJ6h42p74Ij9sdFh3T0RN-7j#scrollTo=eM14UgpmpZL-)

## Installation
* `pip install git+https://github.com/wri/cities-cif/releases/latest` gives you the latest stable release.
* `pip install git+https://github.com/wri/cities-cif` gives you the main branch with is not stable.

## PR Review
0. Prerequisites
1. Git
* On Windows I recommend WSL https://learn.microsoft.com/en-us/windows/wsl/tutorials/wsl-git
3. https://cli.github.com/
* On MacOS I recommend the Homebrew option
* If you don't have an ssh key, it will install one for you
4. Conda (or Mamba) to install dependencies
* If you have Homebrew `brew install --cask miniconda`


## Dependencies
### Conda
Expand Down
3 changes: 3 additions & 0 deletions city_metrix/layers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@
from .world_pop import WorldPop
from .built_up_height import BuiltUpHeight
from .average_net_building_height import AverageNetBuildingHeight
from .open_buildings import OpenBuildings
from .tree_canopy_hight import TreeCanopyHeight
from .alos_dsm import AlosDSM
21 changes: 21 additions & 0 deletions city_metrix/layers/alos_dsm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import ee
import xee
import xarray as xr

from .layer import Layer, get_image_collection


class AlosDSM(Layer):
def __init__(self, **kwargs):
super().__init__(**kwargs)

def get_data(self, bbox):
dataset = ee.ImageCollection("JAXA/ALOS/AW3D30/V3_2")
alos_dsm = ee.ImageCollection(dataset
.filterBounds(ee.Geometry.BBox(*bbox))
.select('DSM')
.mean()
)
data = get_image_collection(alos_dsm, bbox, 30, "ALOS DSM").DSM

return data
22 changes: 3 additions & 19 deletions city_metrix/layers/average_net_building_height.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,21 @@
import xee
import ee

from .layer import Layer, get_utm_zone_epsg
from .layer import Layer, get_utm_zone_epsg, get_image_collection


class AverageNetBuildingHeight(Layer):
def __init__(self, **kwargs):
super().__init__(**kwargs)

def get_data(self, bbox):
crs = get_utm_zone_epsg(bbox)
# https://ghsl.jrc.ec.europa.eu/ghs_buH2023.php
# ANBH is the average height of the built surfaces, USE THIS
# AGBH is the amount of built cubic meters per surface unit in the cell
# US - ee.ImageCollection("projects/wri-datalab/GHSL/GHS-BUILT-H-ANBH_R2023A")
# GLOBE - ee.Image("projects/wri-datalab/GHSL/GHS-BUILT-H-ANBH_GLOBE_R2023A")

anbh = ee.Image("projects/wri-datalab/GHSL/GHS-BUILT-H-ANBH_GLOBE_R2023A")

ds = xr.open_dataset(
ee.ImageCollection(anbh),
engine='ee',
scale=100,
crs=crs,
geometry=ee.Geometry.Rectangle(*bbox),
chunks={'X': 512, 'Y': 512}
)

with ProgressBar():
print("Extracting ANBH layer:")
data = ds.b1.compute()
anbh = ee.Image("projects/wri-datalab/GHSL/GHS-BUILT-H-ANBH_GLOBE_R2023A")
data = get_image_collection(ee.ImageCollection(anbh), bbox, 100, "average net building height").b1

# get in rioxarray format
data = data.squeeze("time").transpose("Y", "X").rename({'X': 'x', 'Y': 'y'})

return data
23 changes: 16 additions & 7 deletions city_metrix/layers/esa_world_cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,26 @@ class EsaWorldCover(Layer):
STAC_COLLECTION_ID = "urn:eop:VITO:ESA_WorldCover_10m_2020_AWS_V1"
STAC_ASSET_ID = "ESA_WORLDCOVER_10M_MAP"

def __init__(self, land_cover_class=None, **kwargs):
def __init__(self, land_cover_class=None, year=2020, **kwargs):
super().__init__(**kwargs)
self.land_cover_class = land_cover_class
self.year = year

def get_data(self, bbox):
data = get_image_collection(
ee.ImageCollection("ESA/WorldCover/v100"),
bbox,
10,
"ESA world cover"
).Map
if self.year == 2020:
data = get_image_collection(
ee.ImageCollection("ESA/WorldCover/v100"),
bbox,
10,
"ESA world cover"
).Map
elif self.year == 2021:
data = get_image_collection(
ee.ImageCollection("ESA/WorldCover/v200"),
bbox,
10,
"ESA world cover"
).Map

if self.land_cover_class:
data = data.where(data == self.land_cover_class.value)
Expand Down
44 changes: 44 additions & 0 deletions city_metrix/layers/open_buildings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import ee
import geemap
import pandas as pd
import geopandas as gpd
from shapely.geometry import Polygon, MultiPolygon

from .layer import Layer


class OpenBuildings(Layer):
def __init__(self, country='USA', **kwargs):
super().__init__(**kwargs)
self.country = country

def get_data(self, bbox):
dataset = ee.FeatureCollection(f"projects/sat-io/open-datasets/VIDA_COMBINED/{self.country}")
open_buildings = dataset.filterBounds(ee.Geometry.BBox(*bbox))
openbuilds = geemap.ee_to_gdf(open_buildings).reset_index()

# filter out geom_type GeometryCollection
gc_openbuilds = openbuilds[openbuilds.geom_type == 'GeometryCollection']
if len(gc_openbuilds) > 0:
# select Polygons and Multipolygons from GeometryCollection
gc_openbuilds['geometries'] = gc_openbuilds.apply(lambda x: [g for g in x.geometry.geoms], axis=1)
gc_openbuilds_polygon = []
# iterate over each row in gc_openbuilds
for index, row in gc_openbuilds.iterrows():
for geom in row['geometries']:
# Check if the geometry is a Polygon or MultiPolygon
if isinstance(geom, Polygon) or isinstance(geom, MultiPolygon):
# Create a new row with the same attributes as the original row, but with the Polygon geometry
new_row = row.drop(['geometry', 'geometries'])
new_row['geometry'] = geom
gc_openbuilds_polygon.append(new_row)
if len(gc_openbuilds_polygon) > 0:
# convert list to geodataframe
gc_openbuilds_polygon = gpd.GeoDataFrame(gc_openbuilds_polygon, geometry='geometry')
# replace GeometryCollection with Polygon, merge back to openbuilds
openbuilds = openbuilds[openbuilds.geom_type != 'GeometryCollection']
openbuilds = pd.concat([openbuilds, gc_openbuilds_polygon], ignore_index=True).reset_index()
else:
openbuilds = openbuilds[openbuilds.geom_type != 'GeometryCollection'].reset_index()

return openbuilds
6 changes: 3 additions & 3 deletions city_metrix/layers/open_street_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
class OpenStreetMapClass(Enum):
OPEN_SPACE = {'leisure': ['park', 'nature_reserve', 'common', 'playground', 'pitch', 'track'],
'boundary': ['protected_area', 'national_park']}
OPEN_SPACE_HEAT = {'leisure': ['park', 'nature_reserve', 'common', 'playground', 'pitch', 'track', 'garden', 'golf_course', 'dog_park', 'recreation_ground', 'disc_golf_course'],
OPEN_SPACE_HEAT = {'leisure': ['park', 'nature_reserve', 'common', 'playground', 'pitch', 'garden', 'golf_course', 'dog_park', 'recreation_ground', 'disc_golf_course'],
'boundary': ['protected_area', 'national_park', 'forest_compartment', 'forest']}
WATER = {'water': True,
'natural': ['water'],
Expand All @@ -27,11 +27,11 @@ def __init__(self, osm_class=None, **kwargs):
self.osm_class = osm_class

def get_data(self, bbox):
north, south, east, west = bbox[3], bbox[1], bbox[0], bbox[2]
north, south, east, west = bbox[3], bbox[1], bbox[0], bbox[2]
# Set the OSMnx configuration to disable caching
ox.settings.use_cache = False
try:
osm_feature = ox.features_from_bbox(north, south, east, west, self.osm_class.value)
osm_feature = ox.features_from_bbox(bbox=(north, south, east, west), tags=self.osm_class.value)
# When no feature in bbox, return an empty gdf
except ox._errors.InsufficientResponseError as e:
osm_feature = gpd.GeoDataFrame(pd.DataFrame(columns=['osmid', 'geometry']+list(self.osm_class.value.keys())), geometry='geometry')
Expand Down
29 changes: 29 additions & 0 deletions city_metrix/layers/tree_canopy_hight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from .layer import Layer, get_utm_zone_epsg, get_image_collection

from dask.diagnostics import ProgressBar
import xarray as xr
import xee
import ee


class TreeCanopyHeight(Layer):

name = "tree_canopy_hight"

NO_DATA_VALUE = 0

def __init__(self, **kwargs):
super().__init__(**kwargs)

def get_data(self, bbox):
canopy_ht = ee.ImageCollection("projects/meta-forest-monitoring-okw37/assets/CanopyHeight")
# aggregate time series into a single image
canopy_ht = canopy_ht.reduce(ee.Reducer.mean()).rename("cover_code")




data = get_image_collection(ee.ImageCollection(canopy_ht), bbox, 1, "tree canopy hight")

return data.cover_code

20 changes: 14 additions & 6 deletions city_metrix/layers/urban_land_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,20 @@ def __init__(self, band='lulc', **kwargs):

def get_data(self, bbox):
dataset = ee.ImageCollection("projects/wri-datalab/cities/urban_land_use/V1")
ulu = ee.ImageCollection(dataset
.filterBounds(ee.Geometry.BBox(*bbox))
.select(self.band)
.reduce(ee.Reducer.firstNonNull())
.rename('lulc')
)
# ImageCollection didn't cover the globe
if dataset.filterBounds(ee.Geometry.BBox(*bbox)).size().getInfo() == 0:
ulu = ee.ImageCollection(ee.Image.constant(0)
.clip(ee.Geometry.BBox(*bbox))
.rename('lulc')
)
else:
ulu = ee.ImageCollection(dataset
.filterBounds(ee.Geometry.BBox(*bbox))
.select(self.band)
.reduce(ee.Reducer.firstNonNull())
.rename('lulc')
)

data = get_image_collection(ulu, bbox, 5, "urban land use").lulc

return data
10 changes: 10 additions & 0 deletions docs/developer.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ Hopefully we already have the layers you need in `city_metrix/layers/` and you c

4. Import the new layer class to [city_metrix/layers/\_\_init\_\_.py](../city_metrix/layers/__init__.py)

5. Add a test to [tests/layers.py](../tests/layers.py) to ensure the new layer is working as expected.

6. Add a section to the get layers.ipynb notebook to demonstrate how to use the new layer.

7. Create a PR to merge the new layer into the main branch with these in the PR description:
- Link to Jira ticket (if any)
- A brief description of the new layer
- A link to the Airtable record for the new layer
- Explain how to test the new layer


## Adding an Indicator
Once you have all the data layers you need as inputs, here is the process to create an indicator using them.
Expand Down
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies:
- matplotlib=3.8.2
- jupyterlab=4.0.10
- s3fs=2024.5.0
- geemap=0.32.0
- pip=23.3.1
- pip:
- cartoframes==1.2.5
Loading

0 comments on commit 028c467

Please sign in to comment.