Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions skills/deforestation-risk-screen/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
name: deforestation-risk-screen
description: Run a Hansen Global Forest Change screen across a portfolio of AOIs and report total forest loss, loss-by-year, and percent of AOI lost per plot. Useful for TNFD Locate, CSRD E4, and supply-chain risk assessment (without the EUDR-specific cutoff).
license: MIT
---

# Deforestation risk screen

Use this skill to assess forest-loss exposure across multiple sites — supplier farms, owned land, planned acquisitions — without the regulatory framing of the EUDR check. Common for TNFD Locate (sensitive locations), CSRD E4 (ecosystem extent change), and general supply-chain risk.

For the EUDR-specific version with the 31 December 2020 cutoff and DDS-shaped output, use [`eudr-due-diligence`](../eudr-due-diligence/SKILL.md) instead.

## Prerequisites

- [`screen-portfolio`](../screen-portfolio/SKILL.md) — to subscribe Hansen Global Forest Change to each plot.
- [`compute-area-by-threshold`](../compute-area-by-threshold/SKILL.md) — to count loss pixels per year.

## Steps

1. **Screen the portfolio with Hansen.** Run [`screen-portfolio`](../screen-portfolio/SKILL.md) with `dataset_id=<hansen-uuid>`. Confirm the dataset id against [docs.cecil.earth/datasets](https://docs.cecil.earth/datasets).

2. **For each plot, count loss pixels.** Hansen's `loss_year` band encodes the year of forest loss (1 = 2001, …, 24 = 2024). For each plot's loaded `ds`:

```python
import numpy as np

loss = ds["loss_year"]
nodata = loss.attrs.get("_FillValue")
if nodata is not None:
loss = loss.where(loss != nodata)

values = np.ravel(loss.values)
valid = values[~np.isnan(values)]
any_loss = valid[valid > 0]

loss_by_year = {
int(y) + 2000: int((any_loss == y).sum()) for y in np.unique(any_loss)
}
total_loss_pct = (any_loss.size / valid.size) * 100.0 if valid.size else 0.0
total_loss_hectares = total_loss_pct / 100.0 * aoi.hectares
```

3. **Build the per-plot record.**

```python
{
"plot_id": plot_id,
"aoi_hectares": aoi.hectares,
"total_loss_hectares": total_loss_hectares,
"total_loss_pct": total_loss_pct,
"loss_by_year": loss_by_year,
"first_loss_year": min(loss_by_year, default=None),
"last_loss_year": max(loss_by_year, default=None),
}
```

4. **Roll up the portfolio.** Sum totals, rank plots by `total_loss_pct`, and surface the worst-affected sites for follow-up.

## Important constraints

- **Hansen scope.** Hansen reports forest loss but does not directly report degradation, replanting, or short-rotation cycles — interpret `loss_by_year` accordingly. Use the `tree_cover` band as the year-2000 baseline if relevant.
- **Equal-area approximation.** Hectare conversion uses `aoi.hectares × pixel-percentage`. For high-latitude or very large AOIs, weight by actual pixel area (via `rioxarray` and the dataset's CRS) instead.
- **Not a replacement for EUDR.** This skill does not produce a Due Diligence Statement. Use [`eudr-due-diligence`](../eudr-due-diligence/SKILL.md) for that.

## References

- [`screen-portfolio`](../screen-portfolio/SKILL.md)
- [`compute-area-by-threshold`](../compute-area-by-threshold/SKILL.md)
- [`eudr-due-diligence`](../eudr-due-diligence/SKILL.md)
- Hansen et al., *High-Resolution Global Maps of 21st-Century Forest Cover Change*, Science 2013
64 changes: 64 additions & 0 deletions skills/land-cover-baseline-and-change/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
name: land-cover-baseline-and-change
description: Establish a baseline-year land-cover composition for each AOI in a portfolio and compute year-over-year deltas per class. Feeds CSRD ESRS E4 (biodiversity & ecosystems) ecosystem-extent disclosure and TNFD's Evaluate phase.
license: MIT
---

# Land-cover baseline and change

Use this skill to characterise what's on a site (forest, cropland, urban, water, …) and how that changes over time. Required input for CSRD E4 ecosystem extent and TNFD's Evaluate phase.

## Prerequisites

- [`screen-portfolio`](../screen-portfolio/SKILL.md) — to subscribe a categorical land-cover dataset to each plot.

## Steps

1. **Subscribe Land Cover 9-Class** (or another categorical land-cover dataset) to the portfolio via [`screen-portfolio`](../screen-portfolio/SKILL.md).

2. **For each plot, count pixels per class per timestep.**

```python
import numpy as np

classes = ds[list(ds.data_vars)[0]] # confirm the variable name from ds.data_vars
nodata = classes.attrs.get("_FillValue")
if nodata is not None:
classes = classes.where(classes != nodata)

results_by_time = {}
times = classes["time"].values if "time" in classes.dims else [None]
for t in times:
slice_da = classes.sel(time=t) if t is not None else classes
values = np.ravel(slice_da.values)
valid = values[~np.isnan(values)].astype(int)
unique, counts = np.unique(valid, return_counts=True)
results_by_time[str(t)] = {int(c): int(n) for c, n in zip(unique, counts)}
```

3. **Pick a baseline year.** Typically the earliest available timestep, or a reporting-framework-specific year (CSRD reporters often use 2020 or a company-specific baseline).

4. **Compute deltas.** For each comparison year, subtract baseline counts class-by-class to get net change in pixels per class. Convert to hectares using `aoi.hectares × class_count / total_valid_count`.

5. **Decode class codes to readable names.** Class codes are dataset-specific. Fetch the variable's `reference_table` to map integer codes to names:

```python
dataset = client.get_dataset(dataset_id)
var = next(v for v in dataset.variables if v.name == "<variable>")
code_to_name = {row["code"]: row["name"] for row in var.reference_table}
```

Never guess class codes; always derive them from `reference_table`.

## Important constraints

- **Pick the right dataset.** Land-cover datasets differ in classes, resolution, and update frequency. Check the catalogue and pick one that matches the user's reporting framework.
- **Consistency over time.** Some land-cover datasets re-classify pixels between releases for reasons unrelated to actual change (updated training data, methodology revisions). When possible, compare years from the same release.
- **Tiny plots.** For plots smaller than a few pixels, class composition is noisy; flag low-confidence cases.

## References

- [`screen-portfolio`](../screen-portfolio/SKILL.md)
- [Cecil SDK reference](https://docs.cecil.earth/sdk)
- CSRD ESRS E4 — Biodiversity and ecosystems
- TNFD LEAP approach (Evaluate phase)
86 changes: 86 additions & 0 deletions skills/land-use-carbon-flux/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
name: land-use-carbon-flux
description: Estimate emissions or removals from land-use change across a portfolio by combining land-cover transitions with a carbon-density dataset. Approximate by design — feeds CSRD ESRS E1 (climate change) Scope 1/3 land-use disclosures.
license: MIT
---

# Land-use carbon flux

Use this skill to estimate the carbon impact of land-use change across a portfolio of sites — for CSRD E1 disclosures, science-based-targets validation, and corporate footprint reporting that includes Scope 3 land-use emissions.

This skill is deliberately an approximation: full IPCC-grade flux accounting requires species-specific carbon stocks, time-since-disturbance models, and disaggregation between living/dead biomass and soil carbon, none of which are fully inferrable from gridded products alone. Treat the output as a screening estimate.

## Prerequisites

- [`screen-portfolio`](../screen-portfolio/SKILL.md) — once per dataset (one run for land cover, another for carbon density).
- [`land-cover-baseline-and-change`](../land-cover-baseline-and-change/SKILL.md) — to obtain per-plot transitions between two timesteps.

## Steps

1. **Pick datasets.**
- Land cover: e.g. Land Cover 9-Class.
- Carbon density: e.g. Planet Forest Carbon Monitoring (`aboveground_live_carbon_density`, in Mg C / ha) — confirm against [docs.cecil.earth/datasets](https://docs.cecil.earth/datasets).

2. **Subscribe both datasets** to the portfolio via [`screen-portfolio`](../screen-portfolio/SKILL.md). You'll have two subscription ids per plot.

3. **Compute the land-cover transition matrix per plot.** For two timesteps `t0` and `t1`, count pixels for every (class@t0 → class@t1) pair:

```python
import numpy as np

lc_ds = client.load_xarray(lc_subscription.id)
lc = lc_ds[list(lc_ds.data_vars)[0]] # confirm against lc_ds.data_vars
t0 = lc.sel(time=baseline_year).values
t1 = lc.sel(time=comparison_year).values
stack = np.stack([t0.ravel(), t1.ravel()], axis=1)
pairs, counts = np.unique(stack, axis=0, return_counts=True)
transitions = {
tuple(p.astype(int)): int(c)
for p, c in zip(pairs, counts)
if not np.isnan(p).any()
}
```

4. **Compute mean carbon density per plot.**

```python
carbon_ds = client.load_xarray(carbon_subscription.id)
acd = carbon_ds["aboveground_live_carbon_density"]
mean_acd = float(acd.mean(dim=["x", "y"]).sel(time=baseline_year)) # Mg C / ha
```

5. **Apply per-transition emission/removal factors.** Document the factors you use — CSRD requires the methodology to be auditable. Conservative defaults for a screening estimate:

```python
FOREST_LIKE = {1, 2, 3} # dataset-specific class codes
CROPLAND = {6}
URBAN = {7}

def factor(c0, c1):
if c0 in FOREST_LIKE and c1 in (CROPLAND | URBAN):
return 1.0 # full above-ground stock loss
return 0.0

per_pixel_ha = aoi.hectares / total_valid_pixels
flux_mg_c = sum(
count * per_pixel_ha * mean_acd * factor(c0, c1)
for (c0, c1), count in transitions.items()
)
```

6. **Build the per-plot record** with `flux_mg_c`, the underlying transition matrix, and a methodology string (factors applied, datasets used, baseline year, sign convention).

## Important constraints

- **Approximate.** The output is a screening estimate, not an inventory. For audit-grade reporting, replace the simple factors with peer-reviewed look-up tables (IPCC Tier 2/3, country-specific) and disaggregate above-/below-ground/soil pools.
- **Mean carbon density.** Step 4 takes the AOI-level mean. For plots with strong internal heterogeneity, weight `mean_acd` per land-cover class instead.
- **Sign convention.** Document whether positive flux is emission or removal — frameworks differ.
- **Not Scope attribution.** This estimates emissions associated with land-use change on the AOI; deciding which Scope they belong to is a corporate-accounting judgement separate from this skill.

## References

- [`screen-portfolio`](../screen-portfolio/SKILL.md)
- [`land-cover-baseline-and-change`](../land-cover-baseline-and-change/SKILL.md)
- [`use-cases/calculate-total-carbon-storage.ipynb`](../../use-cases/calculate-total-carbon-storage.ipynb)
- IPCC 2019 Refinement to the 2006 IPCC Guidelines for National Greenhouse Gas Inventories (AFOLU)
- CSRD ESRS E1 — Climate change
66 changes: 66 additions & 0 deletions skills/priority-biome-overlap/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
name: priority-biome-overlap
description: Determine whether AOIs intersect priority forest types or sensitive biomes by subscribing Global Forest Type / Global Forest Cover (or similar) and reporting per-class area per plot. Supports TNFD Locate (sensitive locations).
license: MIT
---

# Priority biome / forest-type overlap

Use this skill for TNFD Locate when the user needs to check whether their portfolio overlaps with sensitive ecosystems — primary forest, mangroves, peat, etc. Generalises to any categorical raster of biome / ecosystem types.

## Prerequisites

- [`screen-portfolio`](../screen-portfolio/SKILL.md)
- [`compute-area-by-threshold`](../compute-area-by-threshold/SKILL.md) — applied per class with `np.equal`.

## Steps

1. **Subscribe a forest-type or biome dataset** (e.g. Global Forest Type, Global Forest Cover) to the portfolio via [`screen-portfolio`](../screen-portfolio/SKILL.md). Confirm the dataset id and the list of class codes against [docs.cecil.earth/datasets](https://docs.cecil.earth/datasets) and `client.get_dataset(dataset_id).variables`.

2. **Define the priority class set.** Either:
- Per-framework: e.g. TNFD's "high-integrity natural ecosystems".
- Per-user: a list of class codes the user cares about.

3. **For each plot, count pixels per class:**

```python
import numpy as np

classes = ds[list(ds.data_vars)[0]] # confirm against ds.data_vars
values = np.ravel(classes.values)
valid = values[~np.isnan(values)].astype(int)
unique, counts = np.unique(valid, return_counts=True)
composition = {int(c): int(n) for c, n in zip(unique, counts)}

priority_pixels = sum(composition.get(c, 0) for c in priority_class_codes)
priority_pct = (priority_pixels / valid.size) * 100.0 if valid.size else 0.0
priority_hectares = priority_pct / 100.0 * aoi.hectares
```

4. **Build the verdict per plot:**

```python
{
"plot_id": plot_id,
"aoi_hectares": aoi.hectares,
"priority_hectares": priority_hectares,
"priority_pct": priority_pct,
"composition": composition, # full breakdown by code
"intersects_priority": priority_pixels > 0,
}
```

5. **Roll up the portfolio.** Surface plots that intersect priority biomes at all, and rank by priority footprint for follow-up.

## Important constraints

- **Class codes are dataset-specific.** Use `client.get_dataset(...).variables[i].reference_table` to map codes to names — never guess.
- **Priority definitions are framework-specific.** TNFD, IUCN KBA, and country-specific lists each have their own definitions. Document which definition you applied.
- **Overlap ≠ impact.** Spatial overlap is necessary but not sufficient evidence of impact; pair with land-cover change or activity data for the impact assessment.

## References

- [`screen-portfolio`](../screen-portfolio/SKILL.md)
- [`compute-area-by-threshold`](../compute-area-by-threshold/SKILL.md)
- TNFD LEAP approach (Locate phase)
- [Cecil dataset catalogue](https://docs.cecil.earth/datasets)
65 changes: 65 additions & 0 deletions skills/screen-portfolio/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
name: screen-portfolio
description: Subscribe a chosen Cecil dataset to a portfolio of plots in one batch and return a tidy table mapping each input to its AOI id and subscription id. The foundation for any multi-site analysis (TNFD, CSRD, EUDR, supply chain).
license: MIT
---

# Screen a portfolio of AOIs

Use this skill when the user has more than one site/plot/property and wants the same dataset subscribed against all of them. Most regulatory and supply-chain workflows start here.

## Prerequisites

- [`subscribe-and-load`](../subscribe-and-load/SKILL.md) — this skill applies it across many AOIs.

## Steps

1. **Inputs.** A list of plots, each with:
- `plot_id` — a stable identifier (supplier code, asset id, etc.).
- `geometry` — GeoJSON Polygon or Point.

Plus a single `dataset_id` (the dataset to subscribe to all plots).

2. **Look up existing AOIs to keep runs idempotent.** Re-running a screen shouldn't create duplicate AOIs. Use `external_ref=plot_id`.

```python
existing = {a.external_ref: a for a in client.list_aois() if a.external_ref}
```

3. **Create AOIs and subscriptions per plot.**

```python
records = []
for plot in plots:
aoi = existing.get(plot["plot_id"]) or client.create_aoi(
geometry=plot["geometry"],
external_ref=plot["plot_id"],
)
subscription = client.create_subscription(
aoi_id=aoi.id,
dataset_id=dataset_id,
external_ref=plot["plot_id"],
)
records.append({
"plot_id": plot["plot_id"],
"aoi_id": aoi.id,
"aoi_hectares": aoi.hectares,
"subscription_id": subscription.id,
})
```

4. **Wait for staging.** Subscriptions process asynchronously. Poll each one (or all of them in parallel) using the `subscribe-and-load` polling pattern. For large portfolios, fan out with `concurrent.futures.ThreadPoolExecutor` so plots stage in parallel.

5. **Return the result table** as a `pandas.DataFrame`. Downstream skills (`deforestation-risk-screen`, `land-cover-baseline-and-change`, `priority-biome-overlap`, `eudr-due-diligence`) take this table as input.

## Important constraints

- **Dataset constraints.** Some datasets enforce min/max AOI hectares or vertex counts — see `client.get_dataset(dataset_id).constraints`. Validate inputs against these before bulk-creating subscriptions, otherwise individual plots will fail mid-batch.
- **Cost.** Every subscription is potentially billable. Check `client.get_dataset(dataset_id).pricing` before running across a large portfolio.
- **Cleanup.** For exploratory runs, archive aggressively: `client.archive_subscription(s.id)` and `client.archive_aoi(a.id)`.

## References

- [`subscribe-and-load`](../subscribe-and-load/SKILL.md)
- [Cecil SDK reference](https://docs.cecil.earth/sdk)
- [Dataset catalogue](https://docs.cecil.earth/datasets)
Loading