Calculating Buildable Area After Setback and Habitat Exclusions
The scenario: a land team receives a buildable-area figure of 2,478 hectares, a layout engineer fits turbines into it, and 190 hectares of that area turn out to be unusable because a crane cannot be set up within 40 metres of the exclusion edge. The geometry was right and the number was wrong, because buildable area for a report and buildable area for a machine are different quantities. This page computes both, and it sits under environmental constraint and exclusion screening.
Root-cause analysis
Three distinct quantities get called “buildable area”, and conflating them is the whole problem.
- Gross remainder. The study area minus the union of exclusions. This is what a screening report means, and it is the largest of the three.
- Effective area after working room. The gross remainder eroded inward by the construction offset, because a machine needs room to operate and a rotor tip needs clearance. On a study area with a long, irregular exclusion boundary this can be five to ten percent smaller.
- Placeable area. What is left once minimum-dimension constraints apply: a 12-metre-wide sliver between two wetlands is in both quantities above and holds nothing at all.
The second and third are not refinements of the first — they answer a different question, and a report that gives one number without saying which is inviting the disagreement above.
Pre-flight validation
The check worth running first is dimensional rather than areal: measure the negative buffer that extinguishes each remainder piece, which is a direct measure of how narrow it is.
import geopandas as gpd
def characterise_remainder(remainder: gpd.GeoSeries, *, probe_m: float = 40.0) -> gpd.GeoDataFrame:
"""For each piece, its area and whether it survives an inward buffer of probe_m."""
parts = remainder.explode(index_parts=False).reset_index(drop=True)
eroded = parts.buffer(-probe_m)
return gpd.GeoDataFrame(
{
"piece_ha": parts.area / 10_000.0,
"eroded_ha": eroded.area / 10_000.0,
"survives_probe": ~eroded.is_empty,
"min_width_lt_2x_probe": eroded.is_empty,
},
geometry=parts,
crs=remainder.crs,
)
A piece that vanishes under a 40-metre inward buffer is narrower than 80 metres somewhere along its length, which for a wind layout means it holds no turbine and for a solar layout means it holds one row at most.
Fix implementation
import geopandas as gpd
EQUAL_AREA_EPSG = 5070
def buildable_area(
study: gpd.GeoDataFrame,
setbacks: dict[str, gpd.GeoDataFrame],
habitat: gpd.GeoDataFrame,
*,
working_epsg: int,
setback_m: dict[str, float],
construction_offset_m: float = 40.0,
min_piece_ha: float = 2.0,
) -> dict:
"""Return the three buildable quantities with a per-source accounting."""
study_p = study.to_crs(working_epsg)
study_geom = study_p.union_all()
parts, per_source = [], {}
for name, gdf in setbacks.items():
layer = gdf.to_crs(working_epsg).clip(study_geom)
if layer.empty:
per_source[name] = 0.0
continue
buffered = layer.geometry.buffer(setback_m[name]).union_all()
per_source[name] = _ha(buffered.intersection(study_geom), working_epsg)
parts.append(buffered)
hab = habitat.to_crs(working_epsg).clip(study_geom)
if not hab.empty:
hab_geom = hab.union_all()
per_source["habitat"] = _ha(hab_geom, working_epsg)
parts.append(hab_geom)
excluded = gpd.GeoSeries(parts, crs=study_p.crs).union_all() if parts else None
gross = study_geom.difference(excluded) if excluded is not None else study_geom
effective = gross.buffer(-construction_offset_m).buffer(construction_offset_m)
pieces = gpd.GeoSeries([effective], crs=study_p.crs).explode(index_parts=False)
placeable = pieces[pieces.area / 10_000.0 >= min_piece_ha].union_all()
return {
"gross_remainder_ha": _ha(gross, working_epsg),
"effective_ha": _ha(effective, working_epsg),
"placeable_ha": _ha(placeable, working_epsg) if placeable else 0.0,
"per_source_ha": per_source,
"geometry": {"gross": gross, "effective": effective, "placeable": placeable},
}
def _ha(geom, working_epsg: int) -> float:
return float(
gpd.GeoSeries([geom], crs=working_epsg).to_crs(EQUAL_AREA_EPSG).area.iloc[0]
) / 10_000.0
The buffer(-offset).buffer(+offset) pair is a morphological opening: it removes anything narrower
than twice the offset and restores the shape of everything that survives. That is exactly the
“working room” definition, and it is far more robust than trying to detect narrow regions directly.
Fallback routing and performance tuning
- Do the opening once on the union, not per parcel. A negative buffer is expensive relative to a difference, and the result on the union is the same as the union of the results only when the parcels are disjoint — which for a study area they are.
- Pick
min_piece_hafrom the technology, not from taste. A 2-hectare floor is roughly one wind turbine with its pad and access; a solar project can use far smaller pieces, and a 2-hectare floor would discard real capacity. - Simplify before the opening, not after. A negative buffer on a geometry with 40,000 vertices is slow; simplifying to a metre first costs nothing in a construction-offset context and speeds the operation by an order of magnitude.
- Watch for buffer artefacts on self-touching rings. A negative buffer on an invalid geometry can return an empty result rather than raising, which silently reports zero buildable area.
Downstream validation
def assert_buildable(result: dict, gross_study_ha: float) -> None:
"""CI gate: the three quantities must be ordered and bounded."""
g, e, p = result["gross_remainder_ha"], result["effective_ha"], result["placeable_ha"]
assert g <= gross_study_ha * 1.0001, "remainder exceeds the study area — wrong frame"
assert e <= g * 1.0001, "effective area exceeds gross remainder — opening applied backwards"
assert p <= e * 1.0001, "placeable exceeds effective — the piece filter grew the geometry"
assert g > 0 or sum(result["per_source_ha"].values()) >= gross_study_ha * 0.999, (
"zero buildable area with under-full exclusions — check the union clipping"
)
Frequently asked questions
Which of the three numbers should go in the report?
All three, labelled. The gross remainder answers “how much land is unconstrained”, the effective area answers “how much can be worked”, and the placeable area answers “how much can hold the technology”. A single figure invites the reader to assume whichever definition suits them, and the gap between the first and the third is routinely ten percent or more.
Is a morphological opening the same as an inward buffer?
No — an inward buffer alone shrinks everything, including the pieces that were wide enough. The opening restores the survivors to their original shape and keeps only the removal of the narrow parts, which is what “working room” actually means. Reporting the eroded area rather than the opened area under-states buildable land by the width of the offset around the entire perimeter.
How should the construction offset be chosen?
From the equipment and the interface, not from a round number. A crane pad needs its own radius, a rotor tip needs clearance from a property line, and an access road needs a corridor. In practice the binding offset is usually the largest of those, and recording which one bound is more useful than the figure itself.
Does habitat get a setback of its own?
Often, and it varies by species and season rather than by geometry. Several state programmes specify a buffer around a nest or a lek rather than around the mapped habitat polygon, which means the buffer belongs on the point feature and not on the polygon. Encoding the rule with its input — as in regulatory boundary mapping — keeps that distinction visible.
Should the placeable-area filter run before or after the opening?
After. The opening removes the narrow parts of pieces, which can split one large piece into several smaller ones — and a piece that falls below the area floor only after being split is exactly the piece the filter exists to remove. Running the filter first hides those splits and keeps land the layout cannot use.
What happens to the discarded pieces?
They should be kept and reported, not dropped. A study area with nineteen sub-hectare fragments totalling 124 hectares tells a land team something useful: the constraints are fragmenting the site rather than merely shrinking it, which changes access-road cost and sometimes the technology choice. Publishing the count and the total is one extra row in the accounting.
How does this figure relate to the interconnection screen?
It bounds it. A site whose placeable area supports 18 turbines cannot use a 200 megawatt interconnection position, and a site with abundant placeable area and no nearby capacity is equally stuck. The two screens are independent and both binding, which is why a portfolio ranking should carry the placeable hectares and the available headroom as separate columns rather than as a single blended score.
Does the offset change when the technology changes?
Substantially. A fixed-tilt solar block needs a few metres of working room and a wind turbine needs tens, so the same site produces materially different effective areas for the two technologies. That is a feature rather than a nuisance: it is the honest reason a fragmented site can suit solar and not wind, and reporting the offset alongside the figure is what makes the comparison legible.
What if the study area has no exclusions at all?
Report that explicitly rather than skipping the stage. A run that finds nothing to remove should still produce the accounting with zeros, the three buildable quantities equal to the study area minus only the construction offset, and the layer list that was checked. A missing accounting is indistinguishable from a stage that never ran, and “we checked and found nothing” is a different statement from silence — particularly in a submission where a reviewer is looking for evidence that the check happened.
Related
- Environmental Constraint & Exclusion Screening — the parent workflow
- Building Multi-Layer Exclusion Masks with GeoPandas Overlay — producing the union this page subtracts
- Wind Farm Layout & Wake Modeling — the consumer that needs the placeable figure rather than the gross one
- Clipping Solar Parcels to County Setback Boundaries in GeoPandas — the statutory setback geometry