diff --git a/skills/deforestation-risk-screen/SKILL.md b/skills/deforestation-risk-screen/SKILL.md new file mode 100644 index 0000000..92ebbba --- /dev/null +++ b/skills/deforestation-risk-screen/SKILL.md @@ -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=`. 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 diff --git a/skills/land-cover-baseline-and-change/SKILL.md b/skills/land-cover-baseline-and-change/SKILL.md new file mode 100644 index 0000000..bee3989 --- /dev/null +++ b/skills/land-cover-baseline-and-change/SKILL.md @@ -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 == "") + 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) diff --git a/skills/land-use-carbon-flux/SKILL.md b/skills/land-use-carbon-flux/SKILL.md new file mode 100644 index 0000000..fc7fb93 --- /dev/null +++ b/skills/land-use-carbon-flux/SKILL.md @@ -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 diff --git a/skills/priority-biome-overlap/SKILL.md b/skills/priority-biome-overlap/SKILL.md new file mode 100644 index 0000000..b4511ab --- /dev/null +++ b/skills/priority-biome-overlap/SKILL.md @@ -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) diff --git a/skills/screen-portfolio/SKILL.md b/skills/screen-portfolio/SKILL.md new file mode 100644 index 0000000..5a32c55 --- /dev/null +++ b/skills/screen-portfolio/SKILL.md @@ -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)