Skip to content

feat: propagate measurement and time uncertainty into the result band (closes #3, #4)#38

Open
steps-re wants to merge 2 commits into
bellingcat:mainfrom
steps-re:add-uncertainty-propagation
Open

feat: propagate measurement and time uncertainty into the result band (closes #3, #4)#38
steps-re wants to merge 2 commits into
bellingcat:mainfrom
steps-re:add-uncertainty-propagation

Conversation

@steps-re

@steps-re steps-re commented Jul 7, 2026

Copy link
Copy Markdown

Closes #3. Closes #4.

Currently the width of the possible-location band is a fixed constant. This lets users supply the uncertainty on their measurements and on the observation time, so the band reflects the region actually consistent with the observation.

Model

Both uncertainty sources feed one per-cell consistency surface: the distance from a perfect match (relative difference = 0) to the range of relative differences the observation could plausibly take. A value of 0 means the cell is consistent with the observation; that region is what gets highlighted.

  • Measurement uncertainty (Add measurement of lengths error propagation #3): object_height_uncertainty / shadow_length_uncertainty (or sun_altitude_angle_uncertainty) widen the plausible range. The band on a relative difference r is (1 + r) * sqrt((dh/h)^2 + (ds/s)^2) — the first-order propagated error, which scales with the predicted-to-observed ratio (1 + r). This is exact to first order everywhere (not just at the match) and correctly asymmetric: a long shadow tolerates more absolute error than a short one. The radians conversion cancels for the sun-altitude form, so the same (1 + r) scaling applies there.
  • Time uncertainty (Add time uncertainty #4): time_uncertainty sweeps the observation time by ±dt and evaluates the sun position at t ± dt — two extra global computations, following the approach suggested in the issue thread, rather than re-meshing the whole globe over many moments. The relative difference at each cell then spans a range instead of a single value. (The 3-sample min/max bracket is tight exactly where the candidate band is — near a cell's local solar noon the shadow barely moves — and only under-covers on fast-moving low-sun cells that are excluded regardless.)

Why range containment rather than a 1-sigma divide

The obvious approach — normalise the residual by a 1-sigma band — breaks down near the day/night terminator, where the shadow length is astronomically sensitive to time (1/tan(altitude) -> inf). There the band blows up, and dividing by it makes those cells look like strong candidates. Modelling the plausible range and asking whether it contains a perfect match keeps those cells correctly excluded (their predicted shadow is always far from the observed one).

Verified on a worked example: with a 30-minute time uncertainty the consistent region stays centred on the true solution (median sun altitude 63.4° = arctan(10/5)) with 0% of consistent cells near the terminator; and the measurement-only consistent region matches the analytic first-order bounds.

Compatibility & tests

  • Output is unchanged when no uncertainty is supplied (location_uncertainty stays None, same fixed band as before, base surface bit-identical).
  • Adds find / find_sun CLI options and README docs.
  • New tests cover the measurement case (exact first-order propagation), the time case (widens the region but excludes the terminator), and that a larger uncertainty admits at least as many consistent cells. Full suite passes; black clean.

Note

This touches find_shadows/cli.py, so it may need a trivial rebase if #36 or #37 merge first — happy to do that.

steps-re and others added 2 commits July 6, 2026 21:46
Closes bellingcat#3. Closes bellingcat#4.

Previously the width of the possible-location band was a fixed constant.
This lets users supply the uncertainty on their inputs so the band
reflects the region actually consistent with the observation:

- Measurement uncertainty (bellingcat#3): object_height_uncertainty /
  shadow_length_uncertainty (or sun_altitude_angle_uncertainty) widen
  the band by the propagated relative error.
- Time uncertainty (bellingcat#4): time_uncertainty sweeps the observation time by
  +/- its value, evaluating the sun position at t +/- dt (two extra
  global computations, as suggested on the issue) rather than re-meshing
  the globe over many moments.

Both combine into a per-cell "consistency" surface: the distance from a
perfect match to the range of relative differences the observation could
plausibly take. Using range containment (rather than dividing by a
1-sigma band) keeps near-terminator cells, where the shadow is hugely
time-sensitive, correctly excluded instead of spuriously highlighted.

Output is unchanged when no uncertainty is supplied. Adds CLI options and
tests covering the measurement, time, and combined cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ctor

Addresses review feedback: the measurement uncertainty of the relative
difference r is (1 + r) * sqrt((dh/h)^2 + (ds/s)^2), not the constant
sqrt(...) which is correct only at a perfect match (r = 0). Scaling the
band by (1 + r) — the predicted-to-observed ratio — makes it exact to
first order everywhere and correctly asymmetric (a long shadow tolerates
more absolute error than a short one). Verified the consistent region now
matches the analytic first-order bounds. Also fixes a stale comment and
documents that the 3-sample time bracket is tight where the band is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

Add time uncertainty Add measurement of lengths error propagation

1 participant