Wire carbon analytics into API and add impact report endpoint#124
Wire carbon analytics into API and add impact report endpoint#124UTKARSH698 wants to merge 4 commits into
Conversation
Deforestation runs can now surface carbon loss estimates. Adds an
`enable_carbon` flag to POST /api/predict that, for deforestation
analyses, embeds a `carbon_estimation` block (carbon_tonnes,
hectares_lost, co2_equivalent) computed from the inference pixel count
via the existing analytics.carbon module.
Adds GET /api/reports/{run_id} returning a structured impact report
(hectares_lost, carbon_tonnes, co2_equivalent, confidence_interval,
region_bbox). The report recomputes from the stored pixel count so it
works whether or not carbon was requested at predict time.
Both paths degrade gracefully if the analytics module is unavailable.
Adds tests validating the carbon math against hand-computed IPCC
values and covering both new endpoints.
Closes Climate-Vision#14
|
Thanks @UTKARSH698 this is solid work. The feature flag and try/except fallbacks are the right call, and I really like that you tested the carbon math against numbers you worked out by hand. That's the part most people skip. Three things to fix before I merge:
Small stuff, optional: np.ones(pixels) builds a whole array just to add it up fine at test sizes, but a real tile could be tens of millions of pixels. A from_pixel_count() helper would avoid that. And you're already computing biomass_tonnes in the dataclass, so you may as well return it. Happy to take the frontend follow-up separately as you suggested that split was the right instinct. |
- /api/reports now reuses the forest_type and region chosen at predict time (persisted in the carbon_estimation payload) instead of always assuming tropical_moist, so predict and report agree for a given run. - Label the confidence interval as tCO2e so it can't be misread as a band around carbon_tonnes (off by the 44/12 factor). - Reject an unknown forest_type with 422 instead of silently falling back to a generic biomass density and returning wrong numbers with 200. - Add tests for report/predict consistency, the CI unit, and the 422.
- Add CarbonEstimator.estimate_from_pixel_count so the impact report no longer allocates an np.ones(pixels) array just to sum it back; a real tile can be tens of millions of pixels. estimate_from_mask now delegates to it, so the two paths stay identical. - Surface the already-computed biomass_tonnes in the report response.
|
Thanks @UTKARSH698 — this is solid work. The feature flag and try/except fallbacks are the right call, and I really like that you tested the carbon math against numbers you worked out by hand. That's the part most people skip. Three things to fix before I merge: 1. The two endpoints can disagree with each other. In /api/predict you let the caller pass forest_type and region, and you use them. But in /api/reports/{run_id} you create CarbonEstimator() with no arguments, so it always assumes tropical_moist. That means if someone runs a mangrove analysis, the predict response and the report will give two different carbon numbers for the same run — and there's no way to tell which is right. The fix: save forest_type and region into the result payload when you save carbon_estimation, then read them back in the report. 2. Label what the confidence interval measures. It's a range around CO2-equivalent, but the response shows carbon_tonnes and co2_equivalent right next to an unlabelled lowerupper. Someone will assume it applies to carbon_tonnes and be off by a factor of 3.67. Either rename it co2_equivalent_ci or add "unit": "tCO2e". 3. A typo in forest_type should be an error, not a silent guess. Right now AGB_DENSITY.get(forest_type, 200.0) quietly falls back to a default. So "tropical-moist" with a hyphen returns wrong numbers and a 200 OK. Check the value against the known keys and return a 422 if it isn't one. Small stuff, optional: np.ones(pixels) builds a whole array just to add it up — fine at test sizes, but a real tile could be tens of millions of pixels. A from_pixel_count() helper would avoid that. And you're already computing biomass_tonnes in the dataclass, so you may as well return it. Happy to take the frontend follow-up separately as you suggested — that split was the right instinct. |
|
Thanks for the thorough review, @Goldokpa — all three are addressed, plus both optional items. Pushed in 1. Endpoint disagreement (forest_type/region) — 2. Unlabelled confidence interval — the interval now carries 3. Silent forest_type fallback — an unknown Optional, both done:
One thing I left as-is: |
|
Excellent turnaround, @UTKARSH698 this addresses everything and a few things I didn't ask for. Persisting forest_type/region and having the report read them back is exactly the right fix. I especially appreciate two details you added on your own, the fallback for runs saved before the change, so existing reports don't break, and the mangrove test asserting the report differs from the tropical_moist default. That second one is testing that the fix actually changed behavior rather than just that the code runs most people don't think to do that. |
An unmatched region (wrong case, trailing space) previously fell back to a 1.0 adjustment factor and returned a confident but wrong figure. Since these numbers are meant to be cited, validate region against the known keys and fail loudly at both /api/predict and /api/reports, mirroring the existing forest_type guard.
|
@Goldokpa the region 422 guard is in (commit 8a6df3f). It now rejects any |
|
Reviewed the diff —all three fixes verified, including the fallback path when enable_carbon was false at predict time, which is the easiest place to get this subtly wrong and you handled it cleanly. I also checked that validating against AGB_DENSITY alone is sufficient: it and ROOT_SHOOT_RATIO have identical keys, so nothing can pass validation and still hit a silent ratio default. Good. Yes to the region guard same 422 treatment. Once that's in I'll merge. One note: carbon_estimation in the /api/predict response now includes forest_type and region alongside the figures. That's a public response-shape change and I'm happy with it, but please call it out in the PR description so it lands in the changelog correctly. |
|
@Goldokpa done — added an API change — response shape section to the PR description calling out the additive |
Summary
Closes #14. Wires the existing
analytics.carbonmodule into the API so deforestation runs surface carbon loss estimates, and adds a structured impact report endpoint.POST /api/predict— new optionalenable_carbonflag (plus optionalforest_type/region). When set on adeforestationrun, the responseresultgains acarbon_estimationblock:carbon_tonnes,hectares_lost,co2_equivalent, derived frominference.non_forest_pixelsviaestimate_carbon_loss().GET /api/reports/{run_id}— returns a complete impact report:hectares_lost,carbon_tonnes,co2_equivalent,confidence_interval(Monte Carlo band viaCarbonEstimator.estimate_from_pixel_count, which avoids allocating a multi-million-element mask array just to sum it), andregion_bbox. It recomputes from the stored pixel count, so a report is available even ifenable_carbonwas not set at predict time.API change — response shape (changelog note)
carbon_estimationin thePOST /api/predictresponse now includes two additional fields alongside the figures:forest_type— forest type used for the estimate (defaults totropical_moist)region— regional adjustment key used (defaults todefault)These are persisted with the run so
GET /api/reports/{run_id}reproduces the exact same numbers rather than re-deriving under a hard-coded default. Additive only — no fields removed or renamed; consumers readingcarbon_tonnes/hectares_lost/co2_equivalentare unaffected.Related validation added during review: unknown
forest_typeorregionnow returns 422 with the valid keys, andconfidence_intervalcarries"unit": "tCO2e".Acceptance criteria
carbon_estimationfield (behind the flag)/api/reports/{run_id}returns a complete impact reportTesting
New
tests/test_carbon_api.py(10 tests, all passing):Follow-up (out of scope here)
The issue's final checkbox — wiring the report into the frontend dashboard — is intentionally left for a separate PR to keep this change backend-focused and reviewable. Happy to take that on as a follow-up.