Geo / OSM¶
The geo subpackage is the bridge between the GIS world and the
generator stack. It ingests building outlines from OpenStreetMap
(via the Overpass API, ODbL) — and in the future from rasterised
maps via image recognition — projects them into a local
right-handed ENU frame in metres, and feeds them into
TnNetworkGenerator
through the OsmBuildingPlacement
member of the PlacementSpec
discriminated union.
Mathematical / physical context¶
A foundation electrode according to DIN 18014 is a closed
conductor inside the building's Streifenfundament. Within the
quasi-static regime (\(f \le 1\,\mathrm{kHz}\)), the relevant field
problem is the same Laplace equation that drives the rest of
groundfield (see Generators for the full
PDE statement); the role of the geo layer is purely topological:
which buildings exist, where are they, how are their foundation
rings oriented?
Three mechanisms are sensitive to the geometry, not only to the count of houses:
- Galvanic dissipation per building. A foundation electrode's spreading admittance scales with the conductor length actually buried — for a closed perimeter ring that is the polygon's perimeter. The oriented minimum bounding rectangle (OMBR, see below) conserves the perimeter to within a few percent on the residential / commercial shapes we care about, so the OMBR-based reduction captures the dominant contribution to the cluster impedance.
- Inductive coupling to PEN and measurement leads. The orientation of the foundation ring matters for the mutual inductance to a parallel PEN trunk along the street. Below 1 kHz this is in the Carson / Sommerfeld regime (ADRs 0005 / 0006); the orientation enters the Neumann integral via the direction cosine on the wire elements.
- Spatial layout of the houses. Real Reihenhauszeilen do not sit on a Manhattan grid. Cluster impedance of the PEN backbone is not separable from layout — the Manhattan grid used as a reference case in ADR-0009 is the upper-bound symmetric idealisation; the OSM-driven layout is the realism case.
The OMBR projection is the Phase-A reduction defined in ADR-0011. It maps an arbitrary building outline to a rectangle by
implemented via the rotating-calipers algorithm in
shapely.geometry.Polygon.minimum_rotated_rectangle. For an
L-shape this contains the polygon (so the perimeter approximation
is an upper bound) and aligns with the dominant edge; for a
clean rectangle it is the rectangle itself. Phase B (deferred)
replaces the rectangle by a polygonal Strip chain along
polygon.buffer(-inset).exterior — this is required only when
non-rectangular foundation interiors become electrically relevant
(typically Gewerbehallen with complex outlines).
Validity envelope¶
- Frequency — \(f \le 1\,\mathrm{kHz}\) (same as the rest of the generator stack).
- Projection — azimuthal equidistant (
+proj=aeqd) centred on a user-supplied origin. Residual distortion \(< 10^{-5}\) within ~5 km of the origin, well below soil-resistivity uncertainty and OSM polygon quantisation. Single tangent plane, no UTM-zone bookkeeping. - Origin — never inferred from data. Two runs over the same area at different dates produce identical metric coordinates regardless of which features the underlying query returns. This is the reproducibility guarantee any Monte-Carlo study relies on.
- Stochasticity — the only geometric stochastic axis
preserved across the OMBR override is
FoundationElectrodeSpec.presence_prob. Bernoulli per building, fully reproducible under a seeded RNG. All geometric distributions (size_m,size_xy_m,orientation_deg) are overridden by the polygon and therefore deterministic per footprint. The non-geometric fields that govern the foundation's electrical environment — in particular the concrete-shell parametersconcrete_rho_ohm_m(moisture state) andconcrete_thickness_m(typical 30–200 mm) from ADR-0012 — continue to accept :class:AnyDistribution, so an OSM-driven Monte-Carlo run still varies independently across the moisture classes per realisation.
Installation¶
The geo layer is gated behind an optional dependency group named
geo (see pyproject.toml):
The optional dependencies are :mod:requests (HTTP to the
Overpass endpoint), :mod:pyproj (the projection), and
:mod:shapely (the OMBR and polygon hygiene). The
BuildingFootprint
data class itself has zero optional dependencies and is
importable on a core install — only the active functions
(querying Overpass, building a Projector,
computing the OMBR) need the extra. The
:class:ImportError raised on first use carries the install
hint verbatim.
Subpackage layout¶
src/groundfield/geo/
├── footprint.py # BuildingFootprint Pydantic model
├── projection.py # Projector (WGS84 -> local ENU)
├── osm.py # Overpass-API client + on-disk cache
└── placement.py # OsmBuildingPlacement (PlacementSpec member)
API reference¶
BuildingFootprint ¶
Bases: BaseModel
A single building's footprint, projected to the local ENU frame.
Attributes:
| Name | Type | Description |
|---|---|---|
polygon_xy_m |
Ring
|
Vertices of the exterior ring in metres, CCW. May be passed open or closed; on validation the ring is normalised to CCW and stored open (no duplicate final vertex). |
holes_xy_m |
list[Ring]
|
Interior rings (holes) in metres, CW. Each hole is normalised on validation. Empty list means no holes (the typical residential case). |
levels |
Optional[float]
|
|
building_use |
Optional[str]
|
Value of the |
osm_id |
Optional[int]
|
Original OSM way / relation id. Useful for tracing back to the source feature for debugging. |
osm_tags |
dict[str, str]
|
Untouched OSM key/value tag dictionary. Lets consumers
extract additional information (e.g. |
Notes
The instance is immutable (frozen=True) so that a
:class:generators.placement.OsmBuildingPlacement can rely on
the polygon not changing while the generator iterates over
sites.
area_m2 ¶
Signed-area-based polygon area in m² (holes subtracted).
Returns:
| Type | Description |
|---|---|
float
|
|
axis_aligned_bounding_rectangle ¶
Axis-aligned bounding rectangle of the exterior ring.
Returns:
| Type | Description |
|---|---|
tuple
|
|
Notes
Pure-Python implementation; no :mod:shapely dependency.
Used as a fall-back when the optional geo extra is not
installed; for a fully rotated foundation electrode use
:meth:oriented_bounding_rectangle instead.
centroid_xy_m ¶
Geometric centroid of the exterior ring in metres.
Notes
Holes are ignored — for the residential / commercial building shapes we care about, the moment shift caused by a hole is at most a few centimetres and irrelevant to the soil-spreading calculation. Implements
.. math::
C_x = \frac{1}{6A}\sum_{i=0}^{n-1} (x_i + x_{i+1})(x_i y_{i+1} - x_{i+1} y_i),
analogously for :math:C_y.
oriented_bounding_rectangle ¶
Oriented minimum bounding rectangle (OMBR) of the exterior ring.
Returns:
| Type | Description |
|---|---|
tuple
|
|
Notes
Uses Shapely's Polygon.minimum_rotated_rectangle,
which implements the rotating-calipers algorithm. Requires
the optional geo extra; raises :class:ImportError
with the install hint otherwise.
Physical interpretation
The OMBR is the canonical projection of an arbitrary building outline onto a Streifenfundament (DIN 18014): the foundation ring follows the OMBR's perimeter, so its side lengths fix the conductor length that contributes to the galvanic spreading admittance, and the orientation fixes the angle relative to the PEN trunk for the inductive-coupling calculation. For a typical residential L-shape the OMBR captures the dominant edge alignment within a few degrees; full polygonal Strip chains are deferred to Phase B (ADR-0011).
Projector ¶
WGS84 (longitude, latitude) <-> local ENU (x_m, y_m) projection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lat0_deg
|
float
|
Latitude and longitude of the projection origin in decimal
degrees, WGS84. The origin is the point that maps to
|
required |
lon0_deg
|
float
|
Latitude and longitude of the projection origin in decimal
degrees, WGS84. The origin is the point that maps to
|
required |
ellps
|
str
|
Ellipsoid name accepted by :mod: |
'WGS84'
|
Notes
The projection is azimuthal equidistant
(+proj=aeqd) centred on (lat0, lon0) with the WGS84
ellipsoid. For distances :math:r \lesssim 5 \, \mathrm{km}
around the origin the residual distortion is below
:math:10^{-5} (a few centimetres on a kilometre baseline),
which is two orders of magnitude below typical OSM polygon
quantisation. Conformality is not preserved exactly — for
our use case (placing electrodes and measuring distances
between them) equal distance to the origin is the relevant
invariant, not angle preservation.
The projection is not thread-safe: each :class:Projector
holds two :class:pyproj.Transformer instances internally;
instantiate one per worker if you parallelise.
ring_to_xy_m ¶
Vectorised forward projection of a list of (lat, lon).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lat_lon
|
list[tuple[float, float]]
|
List of WGS84 |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[float, float]]
|
Same length, each tuple in local ENU metres. |
to_lat_lon ¶
Inverse of :meth:to_xy_m: local ENU -> WGS84.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_m
|
float
|
Local ENU coordinates in metres. |
required |
y_m
|
float
|
Local ENU coordinates in metres. |
required |
Returns:
| Type | Description |
|---|---|
tuple[float, float]
|
|
to_xy_m ¶
Project a single point WGS84 -> local ENU.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lat_deg
|
float
|
WGS84 coordinates of the point in decimal degrees. |
required |
lon_deg
|
float
|
WGS84 coordinates of the point in decimal degrees. |
required |
Returns:
| Type | Description |
|---|---|
tuple[float, float]
|
|
build_query ¶
build_query(
lat0_deg: float,
lon0_deg: float,
radius_m: float,
*,
timeout_s: int = DEFAULT_TIMEOUT_S
) -> str
Construct the Overpass-QL query string for a buildings-in-radius lookup.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lat0_deg
|
float
|
Centre of the lookup in WGS84 decimal degrees. |
required |
lon0_deg
|
float
|
Centre of the lookup in WGS84 decimal degrees. |
required |
radius_m
|
float
|
Search radius in metres. Must be positive. |
required |
timeout_s
|
int
|
Query timeout passed to Overpass via |
DEFAULT_TIMEOUT_S
|
Returns:
| Type | Description |
|---|---|
str
|
A deterministic Overpass-QL query. The string is byte-identical across runs given the same inputs, so its SHA-256 hash is a stable cache key. |
query_buildings ¶
query_buildings(
lat0_deg: float,
lon0_deg: float,
radius_m: float,
*,
cache_dir: Optional[Path] = None,
endpoint: str = DEFAULT_ENDPOINT,
timeout_s: int = DEFAULT_TIMEOUT_S,
force_refresh: bool = False,
max_retries: int = 1,
_post: Any = None
) -> dict[str, Any]
Fetch (or load from cache) the raw Overpass response.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lat0_deg
|
float
|
Centre of the buildings-in-radius lookup, WGS84 degrees. |
required |
lon0_deg
|
float
|
Centre of the buildings-in-radius lookup, WGS84 degrees. |
required |
radius_m
|
float
|
Search radius in metres. |
required |
cache_dir
|
Optional[Path]
|
Directory in which the response is cached. Defaults to
:func: |
None
|
endpoint
|
str
|
Overpass endpoint URL. Defaults to the canonical mirror. |
DEFAULT_ENDPOINT
|
timeout_s
|
int
|
Per-query timeout passed to Overpass. |
DEFAULT_TIMEOUT_S
|
force_refresh
|
bool
|
If |
False
|
max_retries
|
int
|
Number of retry attempts on |
1
|
_post
|
Any
|
Internal hook for tests: a callable with the same signature
as :func: |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The decoded Overpass JSON payload. The shape is
|
Notes
The cache filename is <sha256(query)>.json; ergo two distinct
queries that happen to round-trip to the same string share a
cache file, and two semantically identical queries with
different float formatting do not — see :func:build_query
for the fixed formatting convention that guarantees stability.
parse_overpass_payload ¶
parse_overpass_payload(
payload: dict[str, Any],
projector: Projector,
*,
min_area_m2: float = 0.0
) -> list[BuildingFootprint]
Convert an Overpass JSON payload into projected
:class:BuildingFootprint instances.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
dict[str, Any]
|
Overpass JSON as returned by :func: |
required |
projector
|
Projector
|
Projection from WGS84 to the local ENU frame. The same projector should be reused across all consumers of one payload to keep the metric coordinates consistent. |
required |
min_area_m2
|
float
|
Skip features whose footprint area is strictly below this
threshold. |
0.0
|
Returns:
| Type | Description |
|---|---|
list[BuildingFootprint]
|
Order matches the Overpass element order, which Overpass keeps deterministic for the same query. |
Notes
Multipolygon relations (type=multipolygon) are handled by
walking the relation's members and treating each outer
way as an independent footprint with the inner ways of the
same relation attached as holes. Relations without an
outer member are skipped.
query_and_project ¶
query_and_project(
lat0_deg: float,
lon0_deg: float,
radius_m: float,
*,
projector: Optional[Projector] = None,
cache_dir: Optional[Path] = None,
endpoint: str = DEFAULT_ENDPOINT,
timeout_s: int = DEFAULT_TIMEOUT_S,
min_area_m2: float = 0.0,
force_refresh: bool = False,
max_retries: int = 1
) -> tuple[list[BuildingFootprint], Projector]
Run :func:query_buildings and :func:parse_overpass_payload
end-to-end.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lat0_deg
|
float
|
Buildings-in-radius parameters. |
required |
lon0_deg
|
float
|
Buildings-in-radius parameters. |
required |
radius_m
|
float
|
Buildings-in-radius parameters. |
required |
projector
|
Optional[Projector]
|
If |
None
|
cache_dir
|
Optional[Path]
|
Forwarded to :func: |
None
|
endpoint
|
Optional[Path]
|
Forwarded to :func: |
None
|
timeout_s
|
Optional[Path]
|
Forwarded to :func: |
None
|
force_refresh
|
Optional[Path]
|
Forwarded to :func: |
None
|
min_area_m2
|
float
|
Skip features below this footprint area. Defaults to
|
0.0
|
max_retries
|
int
|
Number of retry attempts on |
1
|
Returns:
| Type | Description |
|---|---|
tuple[list[BuildingFootprint], Projector]
|
Projected footprints and the projector used. The projector is returned so the caller can re-use it for further coordinate transforms (e.g. plotting a substation marker at a known lat/lon). |
OverpassError ¶
Bases: RuntimeError
Raised on a non-success Overpass response.
Attributes:
| Name | Type | Description |
|---|---|---|
status_code |
HTTP status code if available, else |
|
body |
First 1 kB of the response body for diagnostic purposes. |
OsmBuildingPlacement ¶
Bases: BaseModel
Placement driven by a list of pre-projected building footprints.
The class implements the same generate(n, rng) -> list[(x, y)]
interface as :class:generators.placement.ManhattanGridPlacement
and :class:generators.placement.ExplicitPlacement, so any
consumer that accepts a :class:PlacementSpec will accept this
placement too (once it has been added to the discriminated union;
see Task 3 in ADR-0011's implementation plan).
Attributes:
| Name | Type | Description |
|---|---|---|
footprints |
list[BuildingFootprint]
|
Pre-projected building polygons in the local ENU frame. Order is preserved; the k-th footprint corresponds to the k-th building site. |
min_area_m2 |
float
|
Skip footprints whose area is strictly below this threshold
on :meth: |
selection |
Literal['first_n', 'all']
|
How to choose footprints when the generator requests fewer than are available:
|
Notes
The class is intentionally a plain Pydantic BaseModel with
a kind discriminator field; it does not inherit from a
common PlacementSpec ABC because the existing union uses
structural typing via the kind literal. Adding this class
to generators.placement.PlacementSpec is a one-line union
update tracked as a follow-up task.
n_footprints
property
¶
Number of footprints surviving the min_area_m2 filter.
Identical to len(placement) and provided as an explicit
attribute so consumers can pre-flight an i against
:meth:footprint_at without invoking len() on the model
instance.
footprint_at ¶
Return the footprint associated with site i.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
i
|
int
|
Site index, matching the order returned by
:meth: |
required |
strict
|
bool
|
If If |
False
|
Returns:
| Type | Description |
|---|---|
BuildingFootprint or None
|
The footprint at index |
Raises:
| Type | Description |
|---|---|
IndexError
|
If |
generate ¶
Return n site positions as polygon centroids.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Number of sites the generator wants to populate. |
required |
rng
|
'np.random.Generator'
|
Unused. Accepted for interface parity with the other
:class: |
required |
Returns:
| Type | Description |
|---|---|
list[tuple[float, float]]
|
Polygon centroids in metres. Length is |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Worked example¶
A full end-to-end demo lives in
the OSM-pipeline example in the docs.
It synthesises six rotated rectangles (so the notebook needs no
internet), feeds them into TnNetworkGenerator, compares the
resulting cluster impedance against a Manhattan-grid reference
with the same building count, and finishes with an optional
live Overpass query (cached locally, gracefully skipped when
the geo extra is absent or no network is available).