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.
- 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.
- 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.
- 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.
- 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.
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.
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
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.
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_allrather 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
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
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.
Related
- Grid Capacity Buffer Analysis — the parent workflow and its minimum-not-sum rule
- Modeling Thermal Headroom for Interconnection Screening — where the per-asset capacity figures come from
- Calculating 5 km Proximity Buffers Around Substations in Shapely — building the zones this page dissolves
- Modeling Substation Connectivity Graphs with NetworkX — the network model an allocation strategy needs