Skip to content

Commit

Permalink
Merge pull request #64 from wri/temp-filter-for-distalerts
Browse files Browse the repository at this point in the history
Add temporal filters for distalert
  • Loading branch information
yellowcap authored Dec 19, 2024
2 parents aca5bb4 + 3e564eb commit 6f725c8
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 27 deletions.
1 change: 1 addition & 0 deletions tests/test_context_layer_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def test_context_layer_tool_cereal():
)
assert result == "ESA/WorldCereal/2021/MODELS/v100"


def test_context_layer_tool_null():
result = context_layer_tool.invoke(
input={"question": "Provide disturbances for Aveiro Portugal"}
Expand Down
5 changes: 3 additions & 2 deletions tests/test_dist_agent.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@

from zeno.agents.distalert.agent import graph
from zeno.agents.maingraph.utils.state import GraphState


def test_distalert_agent():
initial_state = GraphState(
question="Provide data about disturbance alerts in Aveiro summarized by landcover"
question="Provide data about disturbance alerts in Aveiro summarized by natural lands in 2023"
)
for namespace, chunk in graph.stream(
initial_state, stream_mode="updates", subgraphs=True
Expand All @@ -17,4 +16,6 @@ def test_distalert_agent():
if not messages:
continue
msg = messages[0]
if msg.name == "dist-alerts-tool":
assert "Natural forests" in msg.content
print(msg)
19 changes: 17 additions & 2 deletions tests/test_dist_alerts.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import datetime

from zeno.tools.contextlayer.layers import layer_choices
from zeno.tools.distalert.dist_alerts_tool import dist_alerts_tool

Expand All @@ -6,17 +8,30 @@ def test_dist_alert_tool():

features = ["2323"]
result = dist_alerts_tool.invoke(
input={"features": features, "landcover": layer_choices[1]["dataset"], "threshold": 8}
input={
"features": features,
"landcover": layer_choices[1]["dataset"],
"threshold": 8,
"min_date": datetime.date(2021, 8, 12),
"max_date": datetime.date(2024, 8, 12),
}
)

assert len(result) == 1
assert "AGO.1.3.4_1" in result


def test_dist_alert_tool_no_landcover():

features = ["2323"]
result = dist_alerts_tool.invoke(
input={"features": features, "landcover": None, "threshold": 5}
input={
"features": features,
"landcover": None,
"threshold": 5,
"min_date": datetime.date(2021, 8, 12),
"max_date": datetime.date(2024, 8, 12),
}
)

assert len(result) == 1
Expand Down
26 changes: 24 additions & 2 deletions zeno/tools/contextlayer/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

DatasetNames = Literal[
"",
"WRI/SBTN/naturalLands/v1",
"WRI/SBTN/naturalLands/v1/2020",
"ESA/WorldCover/v200",
"GOOGLE/DYNAMICWORLD/V1",
"JAXA/ALOS/PALSAR/YEARLY/FNF4",
Expand All @@ -14,12 +14,34 @@
layer_choices = [
{
"name": "SBTN Natural Lands Map v1",
"dataset": "WRI/SBTN/naturalLands/v1",
"dataset": "WRI/SBTN/naturalLands/v1/2020",
"description": "The SBTN Natural Lands Map v1 is a 2020 baseline map of natural and non-natural land covers intended for use by companies setting science-based targets for nature, specifically the SBTN Land target #1: no conversion of natural ecosystems. 'Natural' and 'non-natural' definitions were adapted from the Accountability Framework initiative's definition of a natural ecosystem as 'one that substantially resembles - in terms of species composition, structure, and ecological function - what would be found in a given area in the absence of major human impacts' and can include managed ecosystems as well as degraded ecosystems that are expected to regenerate either naturally or through management (AFi 2024). The SBTN Natural Lands Map operationalizes this definition by using proxies based on available data that align with AFi guidance to the extent possible. This map was made by compiling existing global and regional data.You can find the full technical note explaining the methodology linked on the Natural Lands GitHub. This work was a collaboration between Land & Carbon Lab at the World Resources Institute, World Wildlife Fund US, Systemiq, and SBTN.",
"resolution": 30,
"year": 2020,
"band": "classification",
"type": "Image",
"class_table": {
2: {"color": "#246E24", "name": "Natural forests"},
3: {"color": "#B9B91E", "name": "Natural short vegetation"},
4: {"color": "#6BAED6", "name": "Natural water"},
5: {"color": "#06A285", "name": "Mangroves"},
6: {"color": "#FEFECC", "name": "Bare"},
7: {"color": "#ACD1E8", "name": "Snow"},
8: {"color": "#589558", "name": "Wet natural forests"},
9: {"color": "#093D09", "name": "Natural peat forests"},
10: {"color": "#DBDB7B", "name": "Wet natural short vegetation"},
11: {"color": "#99991A", "name": "Natural peat short vegetation"},
12: {"color": "#D3D3D3", "name": "Crop"},
13: {"color": "#D3D3D3", "name": "Built"},
14: {"color": "#D3D3D3", "name": "Non-natural tree cover"},
15: {"color": "#D3D3D3", "name": "Non-natural short vegetation"},
16: {"color": "#D3D3D3", "name": "Non-natural water"},
17: {"color": "#D3D3D3", "name": "Wet non-natural tree cover"},
18: {"color": "#D3D3D3", "name": "Non-natural peat tree cover"},
19: {"color": "#D3D3D3", "name": "Wet non-natural short vegetation"},
20: {"color": "#D3D3D3", "name": "Non-natural peat short vegetation"},
21: {"color": "#D3D3D3", "name": "Non-natural bare"},
}
},
{
"name": "ESA WorldCover",
Expand Down
101 changes: 80 additions & 21 deletions zeno/tools/distalert/dist_alerts_tool.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
from typing import List, Literal, Optional, Union

import ee
Expand All @@ -18,6 +19,9 @@

gadm = fiona.open("data/gadm_410_small.gpkg")

DIST_ALERT_REF_DATE = datetime.date(2020, 12, 31)
DIST_ALERT_SCALE = 30


class DistAlertsInput(BaseModel):
"""Input schema for dist tool"""
Expand All @@ -31,7 +35,14 @@ class DistAlertsInput(BaseModel):
threshold: Optional[Literal[1, 2, 3, 4, 5, 6, 7, 8]] = Field(
default=5, description="Threshold for disturbance alert scale"
)

min_date: Optional[datetime.date] = Field(
default=None,
description="Cutoff date for alerts. Alerts before that date will be excluded.",
)
max_date: Optional[datetime.date] = Field(
default=None,
description="Cutoff date for alerts. Alerts after that date will be excluded.",
)

def print_meta(
layer: Union[ee.image.Image, ee.imagecollection.ImageCollection]
Expand Down Expand Up @@ -72,6 +83,8 @@ def dist_alerts_tool(
features: List[str],
landcover: Optional[str] = None,
threshold: Optional[Literal[1, 2, 3, 4, 5, 6, 7, 8]] = 5,
min_date: Optional[datetime.date] = None,
max_date: Optional[datetime.date] = None,
) -> dict:
"""
Dist alerts tool
Expand All @@ -88,56 +101,102 @@ def dist_alerts_tool(
[ee.Feature(gadm[int(id)].__geo_interface__) for id in features]
)

combo = distalerts.gte(threshold)
today = datetime.date.today()
date_mask = None
if min_date and min_date > DIST_ALERT_REF_DATE and min_date < today:
days_passed = (today - min_date).days
days_since_start = (today - DIST_ALERT_REF_DATE).days
cutoff = days_since_start - days_passed
date_mask = (
ee.ImageCollection("projects/glad/HLSDIST/current/VEG-DIST-DATE")
.mosaic()
.gte(cutoff)
.selfMask()
)

if max_date and max_date > DIST_ALERT_REF_DATE and max_date < today:
days_passed = (today - max_date).days
days_since_start = (today - DIST_ALERT_REF_DATE).days
cutoff = days_since_start - days_passed
date_mask_max = (
ee.ImageCollection("projects/glad/HLSDIST/current/VEG-DIST-DATE")
.mosaic()
.lte(cutoff)
.selfMask()
)
if date_mask:
date_mask = date_mask.And(date_mask_max)
else:
date_mask = date_mask_max

if landcover:
choice = [dat for dat in layer_choices if dat["dataset"] == landcover][0]
if choice["type"] == "ImageCollection":
landcover_layer = ee.ImageCollection(landcover) # .mosaic()
landcover_layer = ee.ImageCollection(landcover)
else:
landcover_layer = ee.Image(landcover)

class_table = get_class_table(choice["band"], landcover_layer)
if "class_table" in choice:
class_table = choice["class_table"]
else:
class_table = get_class_table(choice["band"], landcover_layer)

if choice["type"] == "ImageCollection":
landcover_layer = landcover_layer.mosaic()

landcover_layer = landcover_layer.select(choice["band"])

combo = combo.addBands(landcover_layer)
zone_stats = combo.reduceRegions(
zone_stats_img = (
distalerts.pixelArea()
.divide(10000)
.addBands(landcover_layer)
.updateMask(distalerts.gte(threshold))
)
if date_mask:
zone_stats_img = zone_stats_img.updateMask(
zone_stats_img.selfMask().And(date_mask)
)

zone_stats = zone_stats_img.reduceRegions(
collection=gee_features,
reducer=ee.Reducer.count().group(groupField=1, groupName=choice["band"]),
reducer=ee.Reducer.sum().group(groupField=1, groupName=choice["band"]),
scale=choice["resolution"],
).getInfo()

zone_stats_result = {}
for feat in zone_stats["features"]:
zone_stats_result[feat["properties"]["gadmid"]] = {
class_table[dat[choice["band"]]]["name"]: dat["count"]
class_table[dat[choice["band"]]]["name"]: dat["sum"]
for dat in feat["properties"]["groups"]
}
vectorize = landcover_layer.updateMask(distalerts.gte(threshold).selfMask())
vectorize = landcover_layer.updateMask(distalerts.gte(threshold))
else:
zone_stats = (
distalerts.gte(threshold)
.selfMask()
.reduceRegions(
collection=gee_features,
reducer=ee.Reducer.count(),
scale=30,
)
.getInfo()
zone_stats_img = (
distalerts.pixelArea().divide(10000).updateMask(distalerts.gte(threshold))
)
if date_mask:
zone_stats_img = zone_stats_img.updateMask(
zone_stats_img.selfMask().And(date_mask)
)

zone_stats = zone_stats_img.reduceRegions(
collection=gee_features,
reducer=ee.Reducer.sum(),
scale=DIST_ALERT_SCALE,
).getInfo()

zone_stats_result = {
feat["properties"]["gadmid"]: {"disturbances": feat["properties"]["count"]}
feat["properties"]["gadmid"]: {"disturbances": feat["properties"]["sum"]}
for feat in zone_stats["features"]
}
vectorize = distalerts.gte(threshold).selfMask()
vectorize = (
distalerts.gte(threshold).updateMask(distalerts.gte(threshold)).selfMask()
)

# Vectorize the masked classification
vectors = vectorize.reduceToVectors(
geometryType="polygon",
scale=30,
scale=DIST_ALERT_SCALE,
maxPixels=1e8,
geometry=gee_features,
eightConnected=True,
Expand Down

0 comments on commit 6f725c8

Please sign in to comment.