rle-python 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rle/core/__init__.py +77 -0
- rle/core/_entrypoints.py +51 -0
- rle/core/aoo.py +815 -0
- rle/core/aoo_grid.py +54 -0
- rle/core/cli.py +49 -0
- rle/core/ecosystem_codes.py +108 -0
- rle/core/ecosystems.py +690 -0
- rle/core/eoo.py +243 -0
- rle/core/registry.py +62 -0
- rle/core/rle.py +71 -0
- rle/core/viz.py +183 -0
- rle_python-0.2.0.dist-info/METADATA +96 -0
- rle_python-0.2.0.dist-info/RECORD +16 -0
- rle_python-0.2.0.dist-info/WHEEL +4 -0
- rle_python-0.2.0.dist-info/entry_points.txt +5 -0
- rle_python-0.2.0.dist-info/licenses/LICENSE +201 -0
rle/core/eoo.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Extent of Occurrence (EOO) computation for RLE assessments.
|
|
2
|
+
|
|
3
|
+
Provides the ``EOO`` class hierarchy and the ``make_eoo()`` helper for
|
|
4
|
+
computing EOO from a local ``Ecosystems`` instance. Earth Engine EOO support
|
|
5
|
+
lives in the optional ``rle-python-gee`` package.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
|
|
10
|
+
from rle.core.ecosystems import (
|
|
11
|
+
Ecosystems,
|
|
12
|
+
EcosystemKind,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EOONotComputedError(Exception):
|
|
17
|
+
"""Raised when accessing EOO results before compute() has been called."""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
super().__init__(
|
|
21
|
+
"EOO has not been computed yet. "
|
|
22
|
+
"Call .compute() to run the computation."
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EOO(ABC):
|
|
27
|
+
"""Base class for Extent of Occurrence computations.
|
|
28
|
+
|
|
29
|
+
Subclasses implement ``_compute()`` to calculate the convex hull
|
|
30
|
+
geometry and area.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, ecosystems: Ecosystems):
|
|
34
|
+
self._ecosystems = ecosystems
|
|
35
|
+
self._computed = False
|
|
36
|
+
self._geometry = None
|
|
37
|
+
self._area_km2: float | None = None
|
|
38
|
+
self._crs = None
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def _compute(self) -> None:
|
|
42
|
+
"""Run the EOO computation.
|
|
43
|
+
|
|
44
|
+
Must store results so that ``_load_geometry()`` and
|
|
45
|
+
``_load_area_km2()`` can retrieve them.
|
|
46
|
+
"""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def _load_geometry(self):
|
|
51
|
+
"""Return the computed convex hull geometry."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def _load_area_km2(self) -> float:
|
|
56
|
+
"""Return the EOO area in km²."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
def compute(self) -> "EOO":
|
|
60
|
+
"""Run the EOO computation. Returns self for method chaining."""
|
|
61
|
+
self._compute()
|
|
62
|
+
self._computed = True
|
|
63
|
+
self._geometry = None
|
|
64
|
+
self._area_km2 = None
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def geometry(self):
|
|
69
|
+
"""The convex hull geometry. Raises EOONotComputedError if not computed."""
|
|
70
|
+
if not self._computed:
|
|
71
|
+
raise EOONotComputedError()
|
|
72
|
+
if self._geometry is None:
|
|
73
|
+
self._geometry = self._load_geometry()
|
|
74
|
+
return self._geometry
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def area_km2(self) -> float:
|
|
78
|
+
"""EOO area in km²."""
|
|
79
|
+
if not self._computed:
|
|
80
|
+
raise EOONotComputedError()
|
|
81
|
+
if self._area_km2 is None:
|
|
82
|
+
self._area_km2 = self._load_area_km2()
|
|
83
|
+
return self._area_km2
|
|
84
|
+
|
|
85
|
+
# -- export / visualization ----------------------------------------------
|
|
86
|
+
|
|
87
|
+
def to_geodataframe(self):
|
|
88
|
+
"""Return the convex hull as a single-row GeoDataFrame."""
|
|
89
|
+
import geopandas as gpd
|
|
90
|
+
|
|
91
|
+
return gpd.GeoDataFrame(
|
|
92
|
+
{"area_km2": [self.area_km2]},
|
|
93
|
+
geometry=[self.geometry],
|
|
94
|
+
crs=self._crs,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def to_gdf_for_viz(self, *, get_fill_color=None, get_line_color=None, **_):
|
|
98
|
+
"""Return (gdf, style_dict) for static-image fallback rendering."""
|
|
99
|
+
if get_fill_color is None:
|
|
100
|
+
get_fill_color = [255, 0, 0, 40]
|
|
101
|
+
if get_line_color is None:
|
|
102
|
+
get_line_color = [255, 0, 0, 255]
|
|
103
|
+
return self.to_geodataframe(), {"fill": get_fill_color, "edge": get_line_color}
|
|
104
|
+
|
|
105
|
+
def to_layer(self, *, get_fill_color=None, get_line_color=None):
|
|
106
|
+
"""Return a lonboard PolygonLayer of the EOO convex hull."""
|
|
107
|
+
gdf = self.to_geodataframe()
|
|
108
|
+
if gdf.geometry.is_empty.all():
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
from lonboard import PolygonLayer
|
|
113
|
+
except ImportError:
|
|
114
|
+
raise ImportError(
|
|
115
|
+
"lonboard is required for visualization. "
|
|
116
|
+
"Install it with: pip install rle-python[viz]"
|
|
117
|
+
) from None
|
|
118
|
+
|
|
119
|
+
if get_fill_color is None:
|
|
120
|
+
get_fill_color = [255, 0, 0, 40]
|
|
121
|
+
if get_line_color is None:
|
|
122
|
+
get_line_color = [255, 0, 0, 255]
|
|
123
|
+
|
|
124
|
+
return [PolygonLayer.from_geopandas(
|
|
125
|
+
gdf,
|
|
126
|
+
get_fill_color=get_fill_color,
|
|
127
|
+
get_line_color=get_line_color,
|
|
128
|
+
line_width_min_pixels=2,
|
|
129
|
+
)]
|
|
130
|
+
|
|
131
|
+
def to_map(self, **kwargs):
|
|
132
|
+
"""Return a lonboard Map showing the EOO convex hull."""
|
|
133
|
+
try:
|
|
134
|
+
from lonboard import Map
|
|
135
|
+
except ImportError:
|
|
136
|
+
raise ImportError(
|
|
137
|
+
"lonboard is required for visualization. "
|
|
138
|
+
"Install it with: pip install rle-python[viz]"
|
|
139
|
+
) from None
|
|
140
|
+
|
|
141
|
+
return Map(layers=self.to_layer(), **kwargs)
|
|
142
|
+
|
|
143
|
+
# -- display -------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def __repr__(self) -> str:
|
|
146
|
+
if not self._computed:
|
|
147
|
+
return f"{type(self).__name__}(not computed)"
|
|
148
|
+
return (
|
|
149
|
+
f"{type(self).__name__}("
|
|
150
|
+
f"area_km2={self.area_km2:,.0f})"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _repr_html_(self) -> str:
|
|
154
|
+
if not self._computed:
|
|
155
|
+
return (
|
|
156
|
+
f"<b>{type(self).__name__}</b><br>"
|
|
157
|
+
f"<i>Not computed — call .compute() to run</i>"
|
|
158
|
+
)
|
|
159
|
+
return (
|
|
160
|
+
f"<b>{type(self).__name__}</b><br>"
|
|
161
|
+
f"EOO: {self.area_km2:,.0f} km²"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class EOOVectorLocal(EOO):
|
|
166
|
+
"""EOO from a local vector dataset (GeoDataFrame, GeoJSON, GeoParquet)."""
|
|
167
|
+
|
|
168
|
+
def _compute(self) -> None:
|
|
169
|
+
import geopandas as gpd
|
|
170
|
+
|
|
171
|
+
gdf = self._ecosystems.to_geodataframe()
|
|
172
|
+
|
|
173
|
+
self._crs = gdf.crs or "EPSG:4326"
|
|
174
|
+
|
|
175
|
+
# Union all geometries, then compute convex hull
|
|
176
|
+
union = gdf.geometry.union_all()
|
|
177
|
+
hull = union.convex_hull
|
|
178
|
+
|
|
179
|
+
# Degenerate inputs (single point, collinear points, empty) produce
|
|
180
|
+
# Point/LineString/GeometryCollection which lonboard cannot render.
|
|
181
|
+
geom_type = hull.geom_type if hull is not None and not hull.is_empty else None
|
|
182
|
+
if geom_type not in ("Polygon", "MultiPolygon"):
|
|
183
|
+
from shapely import Polygon
|
|
184
|
+
if hull is None or hull.is_empty:
|
|
185
|
+
hull = Polygon()
|
|
186
|
+
else:
|
|
187
|
+
hull = hull.buffer(0.0001)
|
|
188
|
+
|
|
189
|
+
self._computed_geometry = hull
|
|
190
|
+
|
|
191
|
+
# Compute area in km² using equal-area projection (ESRI:54034)
|
|
192
|
+
hull_gdf = gpd.GeoDataFrame(geometry=[hull], crs=self._crs)
|
|
193
|
+
hull_ea = hull_gdf.to_crs("ESRI:54034")
|
|
194
|
+
self._computed_area = hull_ea.geometry.iloc[0].area / 1e6
|
|
195
|
+
|
|
196
|
+
def _load_geometry(self):
|
|
197
|
+
return self._computed_geometry
|
|
198
|
+
|
|
199
|
+
def _load_area_km2(self) -> float:
|
|
200
|
+
return self._computed_area
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# Factory
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def make_eoo(data, **kwargs) -> EOO:
|
|
209
|
+
"""Create an EOO from an ``Ecosystems`` instance.
|
|
210
|
+
|
|
211
|
+
Call ``.compute()`` to run the computation before accessing results.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
data: An ``Ecosystems`` instance. For local vector data this returns
|
|
215
|
+
an :class:`EOOVectorLocal`. Earth Engine ecosystems require the
|
|
216
|
+
``rle-python-gee`` package; construct the EE EOO backend
|
|
217
|
+
explicitly from ``rle.gee``.
|
|
218
|
+
**kwargs: Additional arguments passed to the backend constructor.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
An EOO instance. Call .compute() to run the computation.
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
>>> from rle.core import Ecosystems
|
|
225
|
+
>>> eco = Ecosystems.from_file("ecosystems.geojson", ecosystem_column="ECO_NAME")
|
|
226
|
+
>>> eoo = make_eoo(eco).compute()
|
|
227
|
+
>>> print(eoo.area_km2)
|
|
228
|
+
"""
|
|
229
|
+
if isinstance(data, Ecosystems):
|
|
230
|
+
kind = data.kind
|
|
231
|
+
if kind == EcosystemKind.VECTOR_LOCAL:
|
|
232
|
+
return EOOVectorLocal(data, **kwargs)
|
|
233
|
+
if kind in (EcosystemKind.EE_FEATURE_COLLECTION, EcosystemKind.EE_IMAGE):
|
|
234
|
+
raise ValueError(
|
|
235
|
+
f"EOO for {kind.value} requires the 'rle-python-gee' package. "
|
|
236
|
+
f"Install it and construct the Earth Engine EOO backend from rle.gee."
|
|
237
|
+
)
|
|
238
|
+
raise ValueError(f"EOO not yet supported for {kind.value}")
|
|
239
|
+
|
|
240
|
+
raise TypeError(
|
|
241
|
+
"make_eoo expects an Ecosystems instance. Construct one first, e.g. "
|
|
242
|
+
"Ecosystems.from_file(path, ecosystem_column=...) or a backend from rle.gee."
|
|
243
|
+
)
|
rle/core/registry.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Backend discovery for the ``rle`` namespace.
|
|
2
|
+
|
|
3
|
+
Backend distributions (e.g. ``rle-python-gee``) advertise their data-access
|
|
4
|
+
backends through an ``rle.backends`` entry point. Each entry point is a
|
|
5
|
+
callable returning one or more :class:`BackendInfo` records.
|
|
6
|
+
|
|
7
|
+
This registry is for *discovery and introspection* only (e.g. the ``rle
|
|
8
|
+
backends`` CLI command). Backends are constructed explicitly by importing
|
|
9
|
+
their classes — there is no auto-dispatch here.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from importlib.metadata import entry_points
|
|
16
|
+
from typing import Callable, Optional
|
|
17
|
+
|
|
18
|
+
_GROUP = "rle.backends"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class BackendInfo:
|
|
23
|
+
"""Describes a single data-access backend.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
name: Stable identifier, e.g. ``"gee-feature-collection"``.
|
|
27
|
+
cls: The backend class (an ``Ecosystems``/``AOOGrid``/``EOO`` subclass).
|
|
28
|
+
capability: One of ``"ecosystems"``, ``"aoo"``, ``"eoo"``.
|
|
29
|
+
distribution: The distribution that provides it, e.g. ``"rle-python-gee"``.
|
|
30
|
+
can_handle: Optional predicate for a future URI/data dispatcher.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
cls: type
|
|
35
|
+
capability: str
|
|
36
|
+
distribution: str = ""
|
|
37
|
+
can_handle: Optional[Callable[[object], bool]] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def iter_backends() -> list[BackendInfo]:
|
|
41
|
+
"""Return every backend advertised by installed ``rle.backends`` entry points.
|
|
42
|
+
|
|
43
|
+
Entry points whose callable fails to load or run are skipped silently so a
|
|
44
|
+
broken optional backend never breaks discovery of the others.
|
|
45
|
+
"""
|
|
46
|
+
infos: list[BackendInfo] = []
|
|
47
|
+
for ep in entry_points(group=_GROUP):
|
|
48
|
+
try:
|
|
49
|
+
factory = ep.load()
|
|
50
|
+
result = factory()
|
|
51
|
+
except Exception:
|
|
52
|
+
continue
|
|
53
|
+
if isinstance(result, (list, tuple)):
|
|
54
|
+
infos.extend(r for r in result if isinstance(r, BackendInfo))
|
|
55
|
+
elif isinstance(result, BackendInfo):
|
|
56
|
+
infos.append(result)
|
|
57
|
+
return infos
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def list_backends() -> list[str]:
|
|
61
|
+
"""Return the names of all installed backends, sorted."""
|
|
62
|
+
return sorted(b.name for b in iter_backends())
|
rle/core/rle.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# https://iucnrle.org/rle-categ-and-criteria
|
|
2
|
+
rle_criteria = {
|
|
3
|
+
"A": {
|
|
4
|
+
"description": "Reduction in geographic distribution",
|
|
5
|
+
"threshold": 0.5
|
|
6
|
+
},
|
|
7
|
+
"B": {
|
|
8
|
+
"description": "Restricted geographic distribution",
|
|
9
|
+
"threshold": 0.5
|
|
10
|
+
},
|
|
11
|
+
"C": {
|
|
12
|
+
"description": "Environmental degradation",
|
|
13
|
+
"threshold": 0.5
|
|
14
|
+
},
|
|
15
|
+
"D": {
|
|
16
|
+
"description": "Disruption of biotic processes and interactions",
|
|
17
|
+
"threshold": 0.5
|
|
18
|
+
},
|
|
19
|
+
"E": {
|
|
20
|
+
"description": "Quantitative analysis that estimates the probability of ecosystem collapse",
|
|
21
|
+
"threshold": 0.5
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Source: https://iucnrle.org/rle-categ-and-criteria
|
|
26
|
+
rle_categories = [
|
|
27
|
+
{
|
|
28
|
+
"name": "Collapsed",
|
|
29
|
+
"abbreviation": "CO",
|
|
30
|
+
"threatened": True,
|
|
31
|
+
"background_color": "black",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "Critically Endangered",
|
|
35
|
+
"abbreviation": "CR",
|
|
36
|
+
"threatened": True,
|
|
37
|
+
"background_color": "red",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "Endangered",
|
|
41
|
+
"abbreviation": "EN",
|
|
42
|
+
"threatened": True,
|
|
43
|
+
"background_color": "orange",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "Vulnerable",
|
|
47
|
+
"abbreviation": "VU",
|
|
48
|
+
"threatened": True,
|
|
49
|
+
"background_color": "yellow",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "Near Threatened",
|
|
53
|
+
"abbreviation": "NT",
|
|
54
|
+
"background_color": "green",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "Least Concern",
|
|
58
|
+
"abbreviation": "LC",
|
|
59
|
+
"background_color": "darkgreen",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "Data Deficient",
|
|
63
|
+
"abbreviation": "DD",
|
|
64
|
+
"background_color": "lightgray",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "Not Evaluated",
|
|
68
|
+
"abbreviation": "NE",
|
|
69
|
+
"background_color": "white",
|
|
70
|
+
}
|
|
71
|
+
]
|
rle/core/viz.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Visualization helpers that gracefully fall back to static images.
|
|
2
|
+
|
|
3
|
+
The main entry point is :func:`smart_map`, which attempts to build an
|
|
4
|
+
interactive :class:`lonboard.Map`. If any layer construction raises a
|
|
5
|
+
``max_features`` / "too many" :class:`ValueError`, it falls back to rendering
|
|
6
|
+
the same data as a static matplotlib PNG so that documents (e.g. Quarto
|
|
7
|
+
notebooks) still render instead of failing.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Iterable, Sequence
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_FALLBACK_MARKERS = ("max_features", "too many")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_fallback_error(exc: BaseException) -> bool:
|
|
20
|
+
msg = str(exc)
|
|
21
|
+
return any(marker in msg for marker in _FALLBACK_MARKERS)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _normalize_specs(specs: Iterable[Any]) -> list[tuple[str, Any, dict | None]]:
|
|
25
|
+
"""Normalize heterogeneous spec entries to a uniform internal shape.
|
|
26
|
+
|
|
27
|
+
Each returned entry is one of:
|
|
28
|
+
- ("source", datasource, kwargs_dict) — has .to_layer() / .to_gdf_for_viz()
|
|
29
|
+
- ("layer", prebuilt_lonboard_layer, None)
|
|
30
|
+
"""
|
|
31
|
+
normalized: list[tuple[str, Any, dict | None]] = []
|
|
32
|
+
for entry in specs:
|
|
33
|
+
if isinstance(entry, tuple) and len(entry) == 2 and isinstance(entry[1], dict):
|
|
34
|
+
normalized.append(("source", entry[0], entry[1]))
|
|
35
|
+
elif hasattr(entry, "to_layer"):
|
|
36
|
+
normalized.append(("source", entry, {}))
|
|
37
|
+
else:
|
|
38
|
+
normalized.append(("layer", entry, None))
|
|
39
|
+
return normalized
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def smart_map(
|
|
43
|
+
specs: Sequence[Any],
|
|
44
|
+
*,
|
|
45
|
+
view_state: dict | None = None,
|
|
46
|
+
title: str | None = None,
|
|
47
|
+
figsize: tuple[float, float] = (8, 8),
|
|
48
|
+
):
|
|
49
|
+
"""Build an interactive ``lonboard.Map`` or fall back to a static PNG.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
specs: Iterable of layer specs. Each entry may be:
|
|
53
|
+
- a datasource with ``.to_layer()`` (uses default styling)
|
|
54
|
+
- a ``(datasource, kwargs)`` tuple forwarded to ``.to_layer(**kwargs)``
|
|
55
|
+
- a pre-built lonboard layer (passed through as-is)
|
|
56
|
+
view_state: Optional initial view state forwarded to ``lonboard.Map``.
|
|
57
|
+
title: Optional title rendered on the static fallback image.
|
|
58
|
+
figsize: Matplotlib figsize for the static fallback.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
``lonboard.Map`` on success, or ``IPython.display.Image`` (PNG) on
|
|
62
|
+
fallback. Both render inline in Jupyter / Quarto.
|
|
63
|
+
"""
|
|
64
|
+
normalized = _normalize_specs(specs)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
layers: list = []
|
|
68
|
+
for kind, obj, kwargs in normalized:
|
|
69
|
+
if kind == "source":
|
|
70
|
+
layers.extend(obj.to_layer(**(kwargs or {})))
|
|
71
|
+
else:
|
|
72
|
+
layers.append(obj)
|
|
73
|
+
except ValueError as e:
|
|
74
|
+
if not _is_fallback_error(e):
|
|
75
|
+
raise
|
|
76
|
+
return _static_fallback(normalized, title=title, figsize=figsize)
|
|
77
|
+
|
|
78
|
+
from lonboard import Map
|
|
79
|
+
|
|
80
|
+
map_kwargs = {}
|
|
81
|
+
if view_state is not None:
|
|
82
|
+
map_kwargs["view_state"] = view_state
|
|
83
|
+
return Map(layers=layers, **map_kwargs)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _static_fallback(
|
|
87
|
+
normalized: list[tuple[str, Any, dict | None]],
|
|
88
|
+
*,
|
|
89
|
+
title: str | None,
|
|
90
|
+
figsize: tuple[float, float],
|
|
91
|
+
):
|
|
92
|
+
import io
|
|
93
|
+
|
|
94
|
+
import matplotlib.pyplot as plt
|
|
95
|
+
from IPython.display import Image
|
|
96
|
+
|
|
97
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
98
|
+
|
|
99
|
+
skipped = 0
|
|
100
|
+
for kind, obj, kwargs in normalized:
|
|
101
|
+
if kind != "source":
|
|
102
|
+
skipped += 1
|
|
103
|
+
continue
|
|
104
|
+
if not hasattr(obj, "to_gdf_for_viz"):
|
|
105
|
+
skipped += 1
|
|
106
|
+
continue
|
|
107
|
+
gdf, style = obj.to_gdf_for_viz(**(kwargs or {}))
|
|
108
|
+
if gdf is None or len(gdf) == 0:
|
|
109
|
+
continue
|
|
110
|
+
_plot_gdf(ax, gdf, style)
|
|
111
|
+
|
|
112
|
+
ax.set_aspect("equal")
|
|
113
|
+
ax.set_xlabel("Longitude")
|
|
114
|
+
ax.set_ylabel("Latitude")
|
|
115
|
+
if title:
|
|
116
|
+
ax.set_title(title)
|
|
117
|
+
if skipped:
|
|
118
|
+
ax.text(
|
|
119
|
+
0.99, 0.01,
|
|
120
|
+
f"{skipped} pre-built layer(s) omitted in static view",
|
|
121
|
+
transform=ax.transAxes, ha="right", va="bottom",
|
|
122
|
+
fontsize=8, color="gray",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
buf = io.BytesIO()
|
|
126
|
+
fig.tight_layout()
|
|
127
|
+
fig.savefig(buf, format="png", dpi=120, bbox_inches="tight")
|
|
128
|
+
plt.close(fig)
|
|
129
|
+
return Image(data=buf.getvalue(), format="png")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _plot_gdf(ax, gdf, style):
|
|
133
|
+
"""Plot a GeoDataFrame onto ``ax`` using lonboard-style color kwargs."""
|
|
134
|
+
from pyproj import CRS
|
|
135
|
+
|
|
136
|
+
if gdf.crs is not None and CRS.from_user_input(gdf.crs).to_epsg() != 4326:
|
|
137
|
+
gdf = gdf.to_crs("EPSG:4326")
|
|
138
|
+
|
|
139
|
+
fill = _lonboard_color_to_mpl(style.get("fill"), len(gdf))
|
|
140
|
+
edge = _lonboard_color_to_mpl(style.get("edge"), len(gdf))
|
|
141
|
+
|
|
142
|
+
plot_kwargs: dict[str, Any] = {"ax": ax, "linewidth": 0.5}
|
|
143
|
+
if fill is not None:
|
|
144
|
+
plot_kwargs["color"] = fill
|
|
145
|
+
if edge is not None:
|
|
146
|
+
plot_kwargs["edgecolor"] = edge
|
|
147
|
+
gdf.plot(**plot_kwargs)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _lonboard_color_to_mpl(color, n_rows: int):
|
|
151
|
+
"""Convert lonboard-style RGB(A) colors to matplotlib-friendly floats.
|
|
152
|
+
|
|
153
|
+
Accepts:
|
|
154
|
+
- None (returned as-is)
|
|
155
|
+
- list/tuple of 3 or 4 ints (0-255) → single matplotlib RGBA tuple
|
|
156
|
+
- ndarray shape (N, 3 or 4) uint8/int → array of RGBA tuples (0-1 floats)
|
|
157
|
+
"""
|
|
158
|
+
if color is None:
|
|
159
|
+
return None
|
|
160
|
+
arr = np.asarray(color)
|
|
161
|
+
if arr.ndim == 1:
|
|
162
|
+
rgba = _rgba_from_bytes(arr)
|
|
163
|
+
return rgba
|
|
164
|
+
if arr.ndim == 2:
|
|
165
|
+
if arr.shape[0] != n_rows:
|
|
166
|
+
return None
|
|
167
|
+
if np.issubdtype(arr.dtype, np.integer) or arr.max() > 1.0:
|
|
168
|
+
arr = arr.astype(float) / 255.0
|
|
169
|
+
if arr.shape[1] == 3:
|
|
170
|
+
alpha = np.ones((arr.shape[0], 1))
|
|
171
|
+
arr = np.concatenate([arr, alpha], axis=1)
|
|
172
|
+
return arr
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _rgba_from_bytes(arr: np.ndarray) -> tuple[float, float, float, float]:
|
|
177
|
+
vals = [float(v) for v in arr.tolist()]
|
|
178
|
+
if len(vals) == 3:
|
|
179
|
+
vals.append(255.0)
|
|
180
|
+
r, g, b, a = vals[:4]
|
|
181
|
+
if max(vals) > 1.0:
|
|
182
|
+
r, g, b, a = r / 255.0, g / 255.0, b / 255.0, a / 255.0
|
|
183
|
+
return (r, g, b, a)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rle-python
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Core tools for IUCN Red List of Ecosystems analysis with local and cloud data access
|
|
5
|
+
Project-URL: Homepage, https://rle-assessment.github.io/rle-python/
|
|
6
|
+
Project-URL: Documentation, https://rle-assessment.github.io/rle-python/
|
|
7
|
+
Project-URL: Repository, https://github.com/RLE-Assessment/rle-python
|
|
8
|
+
Project-URL: Issues, https://github.com/RLE-Assessment/rle-python/issues
|
|
9
|
+
Author-email: Tyler Erickson <tyler@vorgeo.com>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: AOO,EOO,IUCN,RLE,Red List of Ecosystems,biodiversity,ecology,geospatial
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
20
|
+
Requires-Python: <3.13,>=3.11
|
|
21
|
+
Requires-Dist: fsspec
|
|
22
|
+
Requires-Dist: geopandas>=1.1.3
|
|
23
|
+
Requires-Dist: numpy>=1.24.0
|
|
24
|
+
Requires-Dist: pandas
|
|
25
|
+
Requires-Dist: pyarrow
|
|
26
|
+
Requires-Dist: pyproj
|
|
27
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
28
|
+
Requires-Dist: rasterio>=1.3.0
|
|
29
|
+
Requires-Dist: rasterstats
|
|
30
|
+
Requires-Dist: rioxarray
|
|
31
|
+
Requires-Dist: shapely>=2
|
|
32
|
+
Requires-Dist: typer>=0.20.0
|
|
33
|
+
Provides-Extra: aws
|
|
34
|
+
Requires-Dist: s3fs; extra == 'aws'
|
|
35
|
+
Provides-Extra: gcs
|
|
36
|
+
Requires-Dist: gcsfs; extra == 'gcs'
|
|
37
|
+
Provides-Extra: viz
|
|
38
|
+
Requires-Dist: lonboard; extra == 'viz'
|
|
39
|
+
Requires-Dist: matplotlib>=3.9.0; extra == 'viz'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# rle-python
|
|
43
|
+
|
|
44
|
+
Core tools for **IUCN Red List of Ecosystems (RLE)** analysis.
|
|
45
|
+
|
|
46
|
+
`rle-python` provides the RLE data model and assessment business logic
|
|
47
|
+
(Extent of Occurrence, Area of Occupancy grids, ecosystem code assignment,
|
|
48
|
+
criteria/categories) together with **local and cloud-file data access** — it
|
|
49
|
+
has no Earth Engine dependency.
|
|
50
|
+
|
|
51
|
+
Everything imports under the shared `rle` namespace:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from rle.core import Ecosystems, make_aoo_grid, make_eoo
|
|
55
|
+
|
|
56
|
+
eco = Ecosystems.from_file("ecosystems.geojson", ecosystem_column="ECO_NAME")
|
|
57
|
+
aoo = make_aoo_grid(eco).compute()
|
|
58
|
+
print(aoo.cell_count, aoo.aoo_km2)
|
|
59
|
+
|
|
60
|
+
eoo = make_eoo(eco).compute()
|
|
61
|
+
print(eoo.area_km2)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install rle-python # local files
|
|
68
|
+
pip install rle-python[gcs] # + gs:// GeoParquet (gcsfs)
|
|
69
|
+
pip install rle-python[aws] # + s3:// GeoParquet (s3fs)
|
|
70
|
+
pip install rle-python[viz] # + interactive maps (lonboard) / static fallback
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Optional backends
|
|
74
|
+
|
|
75
|
+
Additional data sources are provided by separate distributions that install
|
|
76
|
+
into the same `rle` namespace and register themselves for discovery:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install rle-python-gee # Google Earth Engine backends -> rle.gee
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from rle.gee import GeeEcosystems
|
|
84
|
+
eco = GeeEcosystems("projects/my-project/assets/ecosystems")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
List the backends available in your environment:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
rle backends
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Layout
|
|
94
|
+
|
|
95
|
+
`rle-python` ships the `rle.core` package under a PEP 420 namespace (`rle/`),
|
|
96
|
+
so multiple `rle-*` distributions can coexist without conflict.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
rle/core/__init__.py,sha256=UMRyfBaTAaZMLzlHE-k8WRS6gqPtRCPCLA6HAL3rmFY,1843
|
|
2
|
+
rle/core/_entrypoints.py,sha256=L9if3ON2aRz7xRlRLIoGA6mXlQ6GAMXQ1zaqFl7myT8,1333
|
|
3
|
+
rle/core/aoo.py,sha256=mR7sCWNG0RlpM42BXUCr0vXcwfPyBd_VHqgeEed-WJU,29730
|
|
4
|
+
rle/core/aoo_grid.py,sha256=F5WzEbZ5RcYcjjbaGOeMz8EdlQ5B6tRwjhHw8DV6_Hc,1934
|
|
5
|
+
rle/core/cli.py,sha256=2zTjmTPlEgTphIBiAf8tn2MSk9K1SuZVrGZVbxzfEt0,1250
|
|
6
|
+
rle/core/ecosystem_codes.py,sha256=Ybb5cNqW0kt8ljt1ayfTwMfgiRRL_LBp_LE-CGcnMdw,3840
|
|
7
|
+
rle/core/ecosystems.py,sha256=ynzi_tELB7194UoVkrpmVvuQ_xBZjpxim897Ligd9-U,26867
|
|
8
|
+
rle/core/eoo.py,sha256=2p4lRvyezY6EZDDYVBX7kI9dAjGDxRjr0GwnZHh-Pj8,8030
|
|
9
|
+
rle/core/registry.py,sha256=7KprFt9vg0tkgvafTJD6NtD6BtWoei3cSG0bmJIz-_I,2106
|
|
10
|
+
rle/core/rle.py,sha256=-3PosA-gqqVIcVlE2YHbbGHXVnJjQfWOzhF-Ah2zBCI,1731
|
|
11
|
+
rle/core/viz.py,sha256=iH7kfVjjaD__ywU2r5HfT7PlNWSS0mE1fxe7jncLwHE,5889
|
|
12
|
+
rle_python-0.2.0.dist-info/METADATA,sha256=nI5QKjkr1fA6-S2izNzhTReDEhlB3BKoQpn2KZKeaGw,3111
|
|
13
|
+
rle_python-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
14
|
+
rle_python-0.2.0.dist-info/entry_points.txt,sha256=mIPm-a2k-v17XI-X7ZZ91Ml5UkT0_O2tZyEv-sP2Ex4,95
|
|
15
|
+
rle_python-0.2.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
16
|
+
rle_python-0.2.0.dist-info/RECORD,,
|