Comparing Buffer Dissolve Strategies for Capacity Aggregation

The scenario: two screening runs over the same substation set report 1,840 MW and 620 MW of available capacity in the same corridor. Both used the same buffers, the same headroom figures and the same geometry library. They differ only in how overlapping zones were dissolved, and that choice is worth a factor of three. This page compares the four strategies that actually get used, and it extends grid capacity buffer analysis.

Root-cause analysis

Dissolution is where a spatial question becomes an electrical claim, and the strategies disagree because they answer subtly different questions.

  1. Union with a sum answers “how much headroom exists among the assets serving this area” — and because overlapping assets usually share an upstream constraint, the sum promises capacity the network cannot deliver.
  2. Union with a minimum answers “how much can any single point in this area be sure of” — which is conservative and correct for siting, and pessimistic where two genuinely independent assets overlap.
  3. No dissolve at all answers “what does each asset offer” and pushes the reconciliation onto whoever reads the map, which in practice means the maximum is read off and the shared constraint is forgotten.
  4. Allocation — Voronoi or capacity-weighted — answers “which asset would actually serve this point”, which is the closest analogue to how an interconnection is assigned and the hardest to defend without a network model.
The same three zones under four dissolve strategies Four small plan views of the same three overlapping circular capacity zones labelled 120, 80 and 45 megawatts. The first, no dissolve, keeps the three circles overlapping and is annotated as leaving the reconciliation to the reader. The second, union with a sum, merges them into one outline reporting 245 megawatts and is marked as manufacturing capacity. The third, union with a minimum, merges them into the same outline reporting 45 megawatts and is marked as the defensible screening choice. The fourth, allocation, cuts the merged outline into three cells at the midlines between assets, each carrying its own asset capacity. Three zones · 120, 80 and 45 MW · four ways to combine them no dissolve 120 80 45 reader takes the maximum union + sum 245 MW 245 MW — manufactured union + min 45 MW 45 MW — defensible allocate 120 80 45 per-asset service areas Identical geometry, three different numbers: the strategy is the answer, not an implementation detail — and a figure that travels without its strategy name cannot be compared with anyone else’s. Allocation is the only strategy that changes the outlines as well as the numbers.

Pre-flight validation

Before choosing, measure how much overlap exists. A corridor with negligible overlap makes the choice irrelevant; a dense one makes it the dominant assumption in the whole screen.

python
import geopandas as gpd


def overlap_profile(zones: gpd.GeoDataFrame, *, capacity_field: str = "available_capacity_mw") -> dict:
    """How much of the buffered area is covered by more than one asset."""
    union_area = zones.geometry.union_all().area
    sum_area = float(zones.geometry.area.sum())
    overlap_area = sum_area - union_area

    # Depth: how many zones cover the typical overlapping point.
    inter = gpd.overlay(zones, zones, how="intersection", keep_geom_type=True)
    inter = inter[inter[f"{capacity_field}_1"] != inter[f"{capacity_field}_2"]]

    return {
        "zones": len(zones),
        "union_area_km2": union_area / 1e6,
        "summed_area_km2": sum_area / 1e6,
        "overlap_share": overlap_area / sum_area if sum_area else 0.0,
        "overlapping_pairs": len(inter) // 2,
        "sum_capacity_mw": float(zones[capacity_field].sum()),
        "min_capacity_mw": float(zones[capacity_field].min()),
    }

An overlap share below about five percent means any strategy will do; above twenty percent the strategy is the answer, and it belongs in the report rather than in the code.

Fix implementation

python
import geopandas as gpd
from shapely.ops import unary_union


def dissolve_capacity_zones(
    zones: gpd.GeoDataFrame,
    *,
    strategy: str = "union_min",
    capacity_field: str = "available_capacity_mw",
    id_field: str = "substation_id",
) -> gpd.GeoDataFrame:
    """Dissolve overlapping capacity zones under an explicit, named strategy."""
    if strategy == "none":
        out = zones.copy()
        out["strategy"] = "none"
        out["contributors"] = out[id_field].apply(lambda v: [v])
        return out

    if strategy in ("union_min", "union_sum"):
        merged = unary_union(zones.geometry.values)
        parts = list(merged.geoms) if merged.geom_type == "MultiPolygon" else [merged]
        rows = []
        for part in parts:
            contributing = zones[zones.geometry.intersects(part)]
            capacity = (
                contributing[capacity_field].min()
                if strategy == "union_min"
                else contributing[capacity_field].sum()
            )
            rows.append({
                "geometry": part,
                capacity_field: float(capacity),
                "contributors": list(contributing[id_field]),
                "binding_asset": contributing.loc[contributing[capacity_field].idxmin(), id_field],
                "strategy": strategy,
            })
        return gpd.GeoDataFrame(rows, crs=zones.crs)

    if strategy == "allocate":
        # Every point is served by its nearest asset; zones are cut at the midlines.
        allocated = gpd.overlay(
            zones, zones, how="union", keep_geom_type=True
        ).dissolve(by=id_field, aggfunc="first").reset_index()
        allocated["strategy"] = "allocate"
        allocated["contributors"] = allocated[id_field].apply(lambda v: [v])
        return allocated

    raise ValueError(f"unknown dissolve strategy {strategy!r}")

Carrying contributors and binding_asset on every dissolved polygon is what makes the result reviewable: a zone that reports 45 MW should be able to say which asset held it down.

One corridor, four totals A bar chart of the total capacity each dissolve strategy reports for the same corridor of 34 substations: union with a sum at 1,840 megawatts, allocation at 1,180, union with a minimum at 620, and no dissolve reporting a maximum single zone of 210. Each bar is annotated with the number of output polygons — one, 34, nine and 34 respectively — and with whether the figure can be promised to a developer. A note gives the overlap share of the corridor as 31 percent, which is what makes the spread so large. 34 substations, 31% overlap — four totals union + sum 1 840 MW 1 polygon · upper bound only allocate 1 180 MW 34 service cells · needs a network model union + min 620 MW 9 contiguous areas · promisable no dissolve (max zone) 210 MW 34 zones · per-asset only At 31% overlap the strategy dominates every other assumption in the screen. Below about 5% overlap it makes almost no difference — which is why the overlap share belongs in the pre-flight report.

Fallback routing and performance tuning

  • Dissolve once, at the end. Buffering, dissolving and re-buffering compounds vertex counts; build every buffer first, then dissolve in a single unary_union.
  • Simplify before the union, never after. A one-metre simplification on a buffer of several kilometres is invisible and can halve the union cost; simplifying the dissolved result moves the published boundary.
  • Use union_all rather than a pairwise loop. The cascaded implementation is substantially faster on hundreds of zones and produces the same geometry.
  • Watch the part count. A dissolve that returns hundreds of parts usually means the buffers are too small for the asset spacing, which is a modelling signal rather than a performance one.
  • Keep the undissolved zones. They are the evidence for the dissolved figure, and regenerating them costs another full buffer pass.

Downstream validation

python
def assert_dissolve_conservative(dissolved, original, *, capacity_field="available_capacity_mw") -> None:
    """A dissolve may not manufacture capacity, and must name what bound each zone."""
    assert dissolved[capacity_field].sum() <= original[capacity_field].sum() + 1e-6, (
        "dissolved capacity exceeds the sum of the inputs — a strategy that adds headroom"
    )
    assert dissolved.geometry.is_valid.all(), "invalid geometry produced by the dissolve"
    assert dissolved["contributors"].map(len).min() >= 1, "a dissolved zone with no contributors"
    if "binding_asset" in dissolved:
        assert dissolved["binding_asset"].notna().all(), "a zone with no binding asset recorded"
    union_before = original.geometry.union_all().area
    union_after = dissolved.geometry.union_all().area
    assert abs(union_after - union_before) / union_before < 1e-6, (
        "the dissolved footprint differs from the input footprint — geometry was lost or grown"
    )

Choosing between them in practice

Four strategy-independent assertions on a dissolved capacity layer A four-row table pairing an assertion with the failure it catches. Dissolved capacity less than or equal to the input sum catches a strategy that manufactures headroom. The dissolved union area equalling the input union area catches geometry lost or grown during the dissolve. Every zone having at least one contributor catches an orphaned polygon produced by a geometry error. Every zone recording a binding asset catches a result that cannot be explained to a reviewer. These hold whichever strategy was chosen dissolved MW <= sum(input MW) a strategy that manufactures headroom union area unchanged geometry lost or grown by the dissolve len(contributors) >= 1 an orphaned polygon from a geometry error binding_asset recorded a number that cannot be explained The strategy name belongs on every row of the output; these four assertions are what make it checkable.

The four strategies are not interchangeable, and the decision follows from what the number will be used for.

Screening a portfolio wants union_min. It cannot over-promise, it produces one figure per contiguous area, and the binding asset is exactly the constraint a developer needs to know about. Its pessimism in the rare case of two genuinely independent assets is the right direction to be wrong in.

Marketing a service territory — showing where capacity broadly exists — can use union_sum provided the figure is labelled as an upper bound and never as available headroom. The distinction sounds pedantic and is the whole difference between a map and a commitment.

Assigning a specific project to a specific asset wants allocate, because that is the question: which substation would this project actually connect to. It needs a network model to be defensible, and without one it is a nearest-neighbour heuristic wearing an electrical costume.

Diagnostics want none. When a number looks wrong, the undissolved zones with their individual capacities are what shows whether the problem is the buffer radii, the headroom figures or the dissolve.

Publishing the strategy name alongside the number is not optional. Two figures that differ by a factor of three are not comparable, and nothing else in the output distinguishes them.

Frequently asked questions

Is the minimum ever too conservative to be useful?

Occasionally, and the honest response is to report both the minimum and the count of contributors rather than to switch strategies. A zone with one contributor at 45 MW and a zone with six contributors whose minimum is 45 MW are very different situations, and the contributor count carries that difference without changing the headline number.

What if two overlapping assets are genuinely independent?

Then the minimum understates, and the fix is a network model rather than a different dissolve. In practice, independence is rare enough at the distances these buffers cover that assuming it is a worse error than assuming shared constraint. Where a planner confirms independence, the pair can be excluded from the dissolve and carried as separate zones with a note.

Does the choice affect the geometry or only the attributes?

Union strategies produce identical geometry and different attributes; allocation produces different geometry, because it cuts the zones at the midlines between assets. That is worth knowing when comparing two maps: identical outlines with different numbers means a reconciliation difference, and different outlines mean a different strategy entirely.

How should the strategy be recorded?

As a column on every output row and a field in the run record, not as a note in a report. A dissolved layer that travels without its strategy is unusable by anyone who did not produce it, and the column costs nothing.

Can capacity be allocated proportionally instead of by minimum?

It can, and it is the least defensible of the options. Proportional allocation implies a sharing rule that the interconnection process does not follow — queue position, not proximity or size, decides who gets the headroom. It produces a smooth, plausible map and a number no planner will confirm.

What about capacity that is available only seasonally?

Dissolve per season and publish the binding season alongside the figure. A zone that offers 87 MW in winter and 12 in summer is genuinely a 12 MW zone for a project that must deliver at the summer peak, and collapsing the two into an annual average hides exactly the constraint that binds.

How do I compare two screens that used different strategies?

Re-run one of them. There is no conversion factor between a summed figure and a minimum figure — the ratio depends entirely on how much overlap the corridor has and how the capacities are distributed among the overlapping assets. Because the undissolved zones are cheap to keep, the practical answer is to store them and re-dissolve under the other strategy, which takes seconds and produces a genuinely comparable pair.

Does the strategy change how many polygons the layer holds?

Substantially, and it is a useful smoke test. A union strategy collapses a corridor to a handful of contiguous areas, while allocation returns one cell per asset and no dissolve returns one zone per asset. A layer whose polygon count matches the asset count was not unioned, whatever the metadata says.