Skip to content

Wire carbon analytics into API and add impact report endpoint#124

Open
UTKARSH698 wants to merge 4 commits into
Climate-Vision:mainfrom
UTKARSH698:feat/carbon-impact-report
Open

Wire carbon analytics into API and add impact report endpoint#124
UTKARSH698 wants to merge 4 commits into
Climate-Vision:mainfrom
UTKARSH698:feat/carbon-impact-report

Conversation

@UTKARSH698

@UTKARSH698 UTKARSH698 commented Jul 20, 2026

Copy link
Copy Markdown

Summary

Closes #14. Wires the existing analytics.carbon module into the API so deforestation runs surface carbon loss estimates, and adds a structured impact report endpoint.

  • POST /api/predict — new optional enable_carbon flag (plus optional forest_type / region). When set on a deforestation run, the response result gains a carbon_estimation block: carbon_tonnes, hectares_lost, co2_equivalent, derived from inference.non_forest_pixels via estimate_carbon_loss().
  • GET /api/reports/{run_id} — returns a complete impact report: hectares_lost, carbon_tonnes, co2_equivalent, confidence_interval (Monte Carlo band via CarbonEstimator.estimate_from_pixel_count, which avoids allocating a multi-million-element mask array just to sum it), and region_bbox. It recomputes from the stored pixel count, so a report is available even if enable_carbon was not set at predict time.
  • Both paths degrade gracefully (feature-flagged, wrapped in try/except): if the analytics module is unavailable or the payload has no pixel count, predict simply omits the block and the report returns a clear 4xx/503 rather than erroring the run.

API change — response shape (changelog note)

carbon_estimation in the POST /api/predict response now includes two additional fields alongside the figures:

  • forest_type — forest type used for the estimate (defaults to tropical_moist)
  • region — regional adjustment key used (defaults to default)

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 reading carbon_tonnes / hectares_lost / co2_equivalent are unaffected.

Related validation added during review: unknown forest_type or region now returns 422 with the valid keys, and confidence_interval carries "unit": "tCO2e".

Acceptance criteria

  • Deforestation API response includes carbon_estimation field (behind the flag)
  • /api/reports/{run_id} returns a complete impact report
  • Feature flag gracefully degrades if the carbon module is unavailable
  • Test validates carbon math against known values

Testing

New tests/test_carbon_api.py (10 tests, all passing):

  • Carbon math against hand-computed IPCC values (10,000 px @ 10 m tropical_moist → 100 ha, 17,484 t C, 64,108 t CO2e)
  • Predict omits carbon without the flag / includes correct figures with it
  • Report endpoint returns all required fields with the CO2 point estimate inside the confidence band
  • 404 for an unknown run
tests/test_api.py tests/test_carbon_api.py .... 11 passed

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.

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
@UTKARSH698
UTKARSH698 requested a review from Goldokpa as a code owner July 20, 2026 08:43
@Goldokpa

Copy link
Copy Markdown
Member

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 lower/upper. 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.

- /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.
@Goldokpa

Copy link
Copy Markdown
Member

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.

@UTKARSH698

Copy link
Copy Markdown
Author

Thanks for the thorough review, @Goldokpa — all three are addressed, plus both optional items. Pushed in 62f2581 and b648bfa.

1. Endpoint disagreement (forest_type/region)/api/predict now persists the chosen forest_type and region into the saved carbon_estimation payload, and /api/reports/{run_id} reads them back and constructs CarbonEstimator(forest_type=..., region=...) instead of the bare default. Predict and report now return identical figures for the same run. Runs saved before carbon was requested fall back to the estimator defaults, so existing reports keep working. Covered by a new mangrove test asserting the report matches predict and differs from the tropical_moist default.

2. Unlabelled confidence interval — the interval now carries "unit": "tCO2e", so it can't be mistaken for a band around carbon_tonnes.

3. Silent forest_type fallback — an unknown forest_type (e.g. tropical-moist with a hyphen) now returns 422 with the list of valid values, rather than a 200 backed by a generic density. Validated at predict time so it fails fast.

Optional, both done:

  • Added CarbonEstimator.estimate_from_pixel_count(); the report uses it instead of np.ones(pixels), so no multi-million-element array is allocated just to sum it. estimate_from_mask delegates to it, and a test asserts the two paths are identical.
  • The report now surfaces the already-computed biomass_tonnes.

One thing I left as-is: region still falls back silently to a 1.0 factor for an unknown value, but since both endpoints now read the same persisted region, they stay consistent — happy to add the same 422 guard for region if you'd prefer it symmetric with forest_type. Frontend follow-up will come as a separate PR as discussed. Thanks again!

@Goldokpa

Copy link
Copy Markdown
Member

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.
On your region question: yes, please add the same 422 guard.
You're right that both endpoints stay consistent now, but consistency isn't the risk silence is. "Amazon" capitalized, or "amazon " with a trailing space, quietly applies a 1.0 factor and returns a confident looking number that's wrong by whatever the real regional adjustment would have been. Given that these figures are meant to be cited, I'd rather it fail loudly. Same shape as the forest_type guard, so it should be quick.
Once that's in I'll do a full pass on the diff and get this merged. Thanks for the fast, careful work on this.

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.
@UTKARSH698

Copy link
Copy Markdown
Author

@Goldokpa the region 422 guard is in (commit 8a6df3f). It now rejects any region outside REGIONAL_FACTORS with a 422, symmetric with the forest_type guard, so an unmatched region can no longer silently fall back to a 1.0 factor. Added test_predict_rejects_unknown_region covering it. Full carbon suite is green (10/10). Ready for your merge pass whenever you get a chance thanks!

@Goldokpa

Copy link
Copy Markdown
Member

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.

@UTKARSH698

Copy link
Copy Markdown
Author

@Goldokpa done — added an API change — response shape section to the PR description calling out the additive forest_type/region fields on the /api/predict carbon_estimation block, so it lands in the changelog. That was the last open item alongside the region 422 guard (8a6df3f). All yours for the merge pass whenever you have a moment — thanks again for the careful review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Good First Issue] Wire carbon analytics into API response and add impact report endpoint

2 participants