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/aoo_grid.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Generate AOO grid cells for local (non-Earth Engine) backends."""
|
|
2
|
+
|
|
3
|
+
import geopandas as gpd
|
|
4
|
+
import numpy as np
|
|
5
|
+
from pyproj import Transformer
|
|
6
|
+
from shapely.geometry import box
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ESRI:54034 World Cylindrical Equal Area — the IUCN-approved equal-area
|
|
10
|
+
# projection for RLE AOO grids. The Earth Engine backend (rle.gee.ee_rle.
|
|
11
|
+
# get_aoo_grid_projection) builds the equivalent ee.Projection from WKT.
|
|
12
|
+
AOO_CRS = "ESRI:54034"
|
|
13
|
+
AOO_CELL_SIZE = 10_000 # 10 km in meters
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def generate_aoo_grid(bounds_4326: tuple[float, float, float, float]) -> gpd.GeoDataFrame:
|
|
17
|
+
"""Generate a GeoDataFrame of AOO grid cells covering the given bounds.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
bounds_4326: (minx, miny, maxx, maxy) in EPSG:4326.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
GeoDataFrame with grid cell polygons in EPSG:4326.
|
|
24
|
+
"""
|
|
25
|
+
minx, miny, maxx, maxy = bounds_4326
|
|
26
|
+
|
|
27
|
+
# Transform bounds to equal-area projection
|
|
28
|
+
transformer = Transformer.from_crs("EPSG:4326", AOO_CRS, always_xy=True)
|
|
29
|
+
ea_minx, ea_miny = transformer.transform(minx, miny)
|
|
30
|
+
ea_maxx, ea_maxy = transformer.transform(maxx, maxy)
|
|
31
|
+
|
|
32
|
+
# Snap to grid origin (0, 0) so cells are globally consistent
|
|
33
|
+
ea_minx = np.floor(ea_minx / AOO_CELL_SIZE) * AOO_CELL_SIZE
|
|
34
|
+
ea_miny = np.floor(ea_miny / AOO_CELL_SIZE) * AOO_CELL_SIZE
|
|
35
|
+
ea_maxx = np.ceil(ea_maxx / AOO_CELL_SIZE) * AOO_CELL_SIZE
|
|
36
|
+
ea_maxy = np.ceil(ea_maxy / AOO_CELL_SIZE) * AOO_CELL_SIZE
|
|
37
|
+
|
|
38
|
+
xs = np.arange(ea_minx, ea_maxx, AOO_CELL_SIZE)
|
|
39
|
+
ys = np.arange(ea_miny, ea_maxy, AOO_CELL_SIZE)
|
|
40
|
+
|
|
41
|
+
grid_cols = []
|
|
42
|
+
grid_rows = []
|
|
43
|
+
cells = []
|
|
44
|
+
for x in xs:
|
|
45
|
+
for y in ys:
|
|
46
|
+
grid_cols.append(int(x / AOO_CELL_SIZE))
|
|
47
|
+
grid_rows.append(int(y / AOO_CELL_SIZE))
|
|
48
|
+
cells.append(box(x, y, x + AOO_CELL_SIZE, y + AOO_CELL_SIZE))
|
|
49
|
+
|
|
50
|
+
grid = gpd.GeoDataFrame(
|
|
51
|
+
{"grid_col": grid_cols, "grid_row": grid_rows, "geometry": cells},
|
|
52
|
+
crs=AOO_CRS,
|
|
53
|
+
)
|
|
54
|
+
return grid.to_crs("EPSG:4326")
|
rle/core/cli.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Command-line interface for rle-python core."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from rle.core import __version__
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(
|
|
9
|
+
name="rle",
|
|
10
|
+
help="IUCN Red List of Ecosystems tools (core)",
|
|
11
|
+
add_completion=False,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def backends():
|
|
17
|
+
"""List installed data-access backends (core + any plugins)."""
|
|
18
|
+
from rle.core.registry import iter_backends
|
|
19
|
+
|
|
20
|
+
infos = iter_backends()
|
|
21
|
+
if not infos:
|
|
22
|
+
print("No backends registered.")
|
|
23
|
+
return
|
|
24
|
+
width = max(len(b.name) for b in infos)
|
|
25
|
+
for b in sorted(infos, key=lambda x: (x.capability, x.name)):
|
|
26
|
+
dist = f" [{b.distribution}]" if b.distribution else ""
|
|
27
|
+
print(f" {b.name:<{width}} {b.capability}{dist}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.callback(invoke_without_command=True)
|
|
31
|
+
def main(
|
|
32
|
+
ctx: typer.Context,
|
|
33
|
+
version: Annotated[
|
|
34
|
+
bool,
|
|
35
|
+
typer.Option("--version", "-v", help="Show version and exit"),
|
|
36
|
+
] = False,
|
|
37
|
+
):
|
|
38
|
+
"""Main entry point for the rle CLI."""
|
|
39
|
+
if version:
|
|
40
|
+
print(f"rle-python version {__version__}")
|
|
41
|
+
raise typer.Exit()
|
|
42
|
+
|
|
43
|
+
if ctx.invoked_subcommand is None:
|
|
44
|
+
print("Hello from rle-python!")
|
|
45
|
+
print("\nUse --help to see available commands")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
app()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Assign hierarchical ecosystem codes to a vector dataset.
|
|
2
|
+
|
|
3
|
+
For each functional group code, the distinct ecosystem names are sorted
|
|
4
|
+
alphabetically and assigned a counter starting at 1. The counter is
|
|
5
|
+
appended to the functional group code to form the ecosystem code. For
|
|
6
|
+
example, two ecosystems in functional group ``T1.1`` become ``T1.1.1``
|
|
7
|
+
and ``T1.1.2``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
import geopandas as gpd
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
# Shapefile DBF character fields are limited to 10 characters; longer
|
|
16
|
+
# values are silently truncated by downstream tools that export to .shp,
|
|
17
|
+
# which can collapse distinct ecosystems into the same reported code.
|
|
18
|
+
SHAPEFILE_VALUE_LIMIT = 10
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def assign_ecosystem_codes(
|
|
22
|
+
gdf: gpd.GeoDataFrame,
|
|
23
|
+
*,
|
|
24
|
+
fg_code_col: str,
|
|
25
|
+
eco_name_col: str,
|
|
26
|
+
eco_code_col: str,
|
|
27
|
+
) -> gpd.GeoDataFrame:
|
|
28
|
+
"""Return a copy of ``gdf`` with a new column of hierarchical ecosystem codes.
|
|
29
|
+
|
|
30
|
+
Within each functional group, distinct ecosystem names are sorted
|
|
31
|
+
alphabetically before numbering, so the result is deterministic
|
|
32
|
+
regardless of input row order.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
gdf: Input GeoDataFrame.
|
|
36
|
+
fg_code_col: Existing column holding the functional group code
|
|
37
|
+
(e.g. values like ``"T1.1"``).
|
|
38
|
+
eco_name_col: Existing column holding the ecosystem name.
|
|
39
|
+
eco_code_col: Name of the new column to create and populate.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A copy of ``gdf`` with ``eco_code_col`` added. Rows whose
|
|
43
|
+
functional group code or ecosystem name is null receive a null
|
|
44
|
+
ecosystem code.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValueError: If ``fg_code_col`` or ``eco_name_col`` is missing
|
|
48
|
+
from ``gdf``, or if ``eco_code_col`` already exists.
|
|
49
|
+
|
|
50
|
+
Warns:
|
|
51
|
+
UserWarning: If any generated ecosystem code exceeds
|
|
52
|
+
:data:`SHAPEFILE_VALUE_LIMIT` characters. Such codes will be
|
|
53
|
+
silently truncated if later written to a shapefile field,
|
|
54
|
+
which can collapse distinct ecosystems when reporting
|
|
55
|
+
statistics per ecosystem.
|
|
56
|
+
"""
|
|
57
|
+
# --- Validate columns ---
|
|
58
|
+
for col in (fg_code_col, eco_name_col):
|
|
59
|
+
if col not in gdf.columns:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"column {col!r} not found in input. "
|
|
62
|
+
f"Available columns: {list(gdf.columns)}"
|
|
63
|
+
)
|
|
64
|
+
if eco_code_col in gdf.columns:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"column {eco_code_col!r} already exists in input. "
|
|
67
|
+
f"Refusing to overwrite."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# --- Build (fg, eco_name) -> code mapping ---
|
|
71
|
+
# Distinct (fg, eco_name) pairs, skipping any with nulls, sorted so
|
|
72
|
+
# that within each functional group, ecosystem names are alphabetical.
|
|
73
|
+
pairs = (
|
|
74
|
+
gdf[[fg_code_col, eco_name_col]]
|
|
75
|
+
.dropna()
|
|
76
|
+
.drop_duplicates()
|
|
77
|
+
.sort_values([fg_code_col, eco_name_col])
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
code_map: dict[tuple, str] = {}
|
|
81
|
+
for fg, group in pairs.groupby(fg_code_col, sort=True):
|
|
82
|
+
for counter, eco_name in enumerate(group[eco_name_col].tolist(), start=1):
|
|
83
|
+
code_map[(fg, eco_name)] = f"{fg}.{counter}"
|
|
84
|
+
|
|
85
|
+
# --- Apply mapping to every row ---
|
|
86
|
+
def lookup(row):
|
|
87
|
+
fg = row[fg_code_col]
|
|
88
|
+
eco = row[eco_name_col]
|
|
89
|
+
if pd.isna(fg) or pd.isna(eco):
|
|
90
|
+
return None
|
|
91
|
+
return code_map.get((fg, eco))
|
|
92
|
+
|
|
93
|
+
result = gdf.copy()
|
|
94
|
+
result[eco_code_col] = result.apply(lookup, axis=1)
|
|
95
|
+
|
|
96
|
+
# --- Warn about values that exceed the shapefile character limit ---
|
|
97
|
+
long_codes = sorted(
|
|
98
|
+
{c for c in code_map.values() if len(c) > SHAPEFILE_VALUE_LIMIT}
|
|
99
|
+
)
|
|
100
|
+
if long_codes:
|
|
101
|
+
warnings.warn(
|
|
102
|
+
f"{len(long_codes)} ecosystem code(s) exceed "
|
|
103
|
+
f"{SHAPEFILE_VALUE_LIMIT} characters and will be truncated if "
|
|
104
|
+
f"later written to a shapefile field: {long_codes}",
|
|
105
|
+
stacklevel=2,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return result
|