---
title: Ground materials
source: https://infrared.city/docs/sdk/1.0/python/ground_materials/
---

# Ground materials

Ground materials package -- fetch, clean, and deduplicate material layers.

## GroundMaterialsServiceError

Bases: `Exception`

A ground-material operation failed in a way the caller must see.

## LocalGroundMaterialCleaner

Clean ground-material layers in-process.

The `Local` in the name is historical; the name is kept because it is published API.

`clean()` takes a dict and returns one. A bulk caller should use `clean_json`, or `merge_and_clean_json` when the input is still the per-tile documents: both take and return JSON bytes, so no intermediate layer tree is built as Python objects.

`z_step=None` uses the default of 0.05 m.

### clean

```python
clean(
layers: Dict[str, Any],
*,
latitude: float,
longitude: float,
distance: float,
default_layer: str,
z_step: Optional[float] = None,
) -> Dict[str, Any]
```

Clean a layer dict and return the cleaned layer dict.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `layers` | `dict` | `{layer_name: FeatureCollection}`. | *required* |
| `latitude` | `float` | Latitude of the clean circle's centre, in degrees. | *required* |
| `longitude` | `float` | Longitude of the clean circle's centre, in degrees. | *required* |
| `distance` | `float` | Radius of the clean circle, in metres. | *required* |
| `default_layer` | `str` | Name of the default material layer, used for the backdrop. | *required* |
| `z_step` | `float` | Per-layer z spacing in metres (default 0.05). | `None` |

Returns:

| Type | Description |
| --- | --- |
| `dict` | The cleaned `{layer_name: FeatureCollection}`. |

Raises:

| Type | Description |
| --- | --- |
| `[GroundMaterialsServiceError](#infrared_sdk.ground_materials.GroundMaterialsServiceError)` | If `layers` is not a dict or the cleaning fails. |

### clean_json

```python
clean_json(
layers_json: bytes,
*,
latitude: float,
longitude: float,
distance: float,
default_layer: str,
z_step: Optional[float] = None,
) -> bytes
```

Clean a JSON layer document, bytes in and bytes out.

Nothing here builds a Python object tree.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `layers_json` | `bytes` | `{layer: FeatureCollection}` as a JSON document. | *required* |
| `latitude` | `float` | Latitude of the clean circle's centre, in degrees. | *required* |
| `longitude` | `float` | Longitude of the clean circle's centre, in degrees. | *required* |
| `distance` | `float` | Radius of the clean circle, in metres. | *required* |
| `default_layer` | `str` | Name of the default material layer, used for the backdrop. | *required* |
| `z_step` | `float` | Per-layer z spacing in metres (default 0.05). | `None` |

Returns:

| Type | Description |
| --- | --- |
| `bytes` | The cleaned layers as a UTF-8 JSON document. |

Raises:

| Type | Description |
| --- | --- |
| `[GroundMaterialsServiceError](#infrared_sdk.ground_materials.GroundMaterialsServiceError)` | If the cleaning fails. |

### merge_and_clean_json

```python
merge_and_clean_json(
tile_layers_json: bytes,
*,
latitude: float,
longitude: float,
distance: float,
default_layer: str,
z_step: Optional[float] = None,
) -> bytes
```

Merge per-tile layer documents and clean them in one step.

The merged layer tree never becomes Python objects between the two steps.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `tile_layers_json` | `bytes` | A JSON array of per-tile `{layer: FeatureCollection}` documents. | *required* |
| `latitude` | `float` | Latitude of the clean circle's centre, in degrees. | *required* |
| `longitude` | `float` | Longitude of the clean circle's centre, in degrees. | *required* |
| `distance` | `float` | Radius of the clean circle, in metres. | *required* |
| `default_layer` | `str` | Name of the default material layer, used for the backdrop. | *required* |
| `z_step` | `float` | Per-layer z spacing in metres (default 0.05). | `None` |

Returns:

| Type | Description |
| --- | --- |
| `bytes` | The merged and cleaned layers as a JSON document. |

Raises:

| Type | Description |
| --- | --- |
| `[GroundMaterialsServiceError](#infrared_sdk.ground_materials.GroundMaterialsServiceError)` | If the merge or the cleaning fails. |

Notes

An empty merge returns `b"{}"` and is not cleaned: no layers, no default backdrop.

## GroundMaterialsServiceClient

Bases: `ScrubbedSessionState`

Client for ground-material layers.

`api_key`, `base_url`, `gateway_base_url` and `telemetry` are accepted for construction compatibility with the other service clients and are unused: this client sends no request of its own since the utilities routes were removed. The public data reads go through the range reader of each worker.

### logger  `instance-attribute`

```python
logger: Logger = logger
```

Logger the client reports progress to.

### base_url  `instance-attribute`

```python
base_url: str = base_url
```

Base URL accepted for construction compatibility; never contacted.

### close

```python
close() -> None
```

Release the client's resources. Kept for API compatibility.

### get_area

```python
get_area(
polygon: dict,
*,
max_workers: int = 10,
default_material: str = "asphalt",
timeout: int = 60,
total_timeout: int = 600,
on_progress: Optional[Callable[[TileProgress], None]] = None,
max_tiles_override: Optional[int] = None,
z_step: Optional[float] = None,
overture_release: Optional[str] = None,
analysis_type: Optional[str] = None,
acquisition: Any = REMOVED,
cleaner: Any = REMOVED,
) -> AreaGroundMaterials
```

Read, deduplicate, and clean ground materials for a polygon area.

The site is read once: as a single rectangle up to 4 km2, and above that as a grid of roughly 2 x 2 km read chunks, at most two in flight. Nothing is composed per simulation tile.

There is no partial answer. A chunk that fails raises `TiledRunError`, whose `failed_tiles` names the failed chunks and whose `__cause__` is the first underlying error.

The returned `AreaGroundMaterials` can be reused across multiple `run_area()` calls (tile assignment happens at submission).

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `polygon` | `dict` | GeoJSON Polygon. | *required* |
| `max_workers` | `int` | Read chunks in flight. A CEILING of 2 applies, so this can only ask for FEWER: `1` reads the chunks one at a time. Raising it does nothing — the memory bound of the site read is "at most two chunks resident". | `10` |
| `default_material` | `str` | Default material layer name for the clean step (default "asphalt"). | `'asphalt'` |
| `timeout` | `int` | Per-REQUEST budget in seconds (default 60), trimmed to what is left of `total_timeout`. | `60` |
| `total_timeout` | `int` | Wall-clock deadline over the whole site read, in seconds (default 600). Checked between chunks and before each read. | `600` |
| `on_progress` | `callable` | Progress callback `(TileProgress) -> None`, once per read CHUNK. `tile_id` is the chunk id and `total_count` the chunk count. | `None` |
| `max_tiles_override` | `int` | Override the default maximum number of non-empty tiles. | `None` |
| `z_step` | `float` | Per-layer z spacing in metres (default 0.05). | `None` |
| `overture_release` | `str` | Pin the Overture collections to one immutable release. The resolved release is reported on the result. | `None` |
| `analysis_type` | `str` | The analysis this acquisition is for. It decides the read margin (363 m for `wind-speed` / `pedestrian-wind-comfort` and 544 m for every other analysis) and the tile grid the read rectangles are centred on. Omit it and the read uses the widest margin, valid for every analysis. | `None` |
| `acquisition` | `object` | Removed. Passing a value raises `TypeError`. | `REMOVED` |
| `cleaner` | `object` | Removed. Passing a value raises `TypeError`. | `REMOVED` |

Returns:

| Type | Description |
| --- | --- |
| `[AreaGroundMaterials](#infrared_sdk.ground_materials.AreaGroundMaterials)` | Cleaned material layers for the whole polygon. `failed_tiles` is empty: a read that could not cover the polygon raises instead of answering. |

Raises:

| Type | Description |
| --- | --- |
| `PolygonValidationError` | If `polygon` is not a valid GeoJSON Polygon, if the grid it needs exceeds the tile ceiling, or if `max_tiles_override` is not a non-negative integer. |
| `TiledRunError` | If a read chunk fails or `total_timeout` expires. |
| `GeodataDependencyError` | If the optional `[geodata]` extra is not installed. |
| `[GroundMaterialsServiceError](#infrared_sdk.ground_materials.GroundMaterialsServiceError)` | If the merged and cleaned layers cannot be produced. |
| `TypeError` | If the removed `acquisition` or `cleaner` argument is passed. |

## AreaGroundMaterials  `dataclass`

Result of ground-material fetch for a polygon area.

### layers  `instance-attribute`

```python
layers: Dict[str, dict]
```

Material layers keyed by name (e.g. 'vegetation', 'water', 'asphalt'), each a GeoJSON FeatureCollection.

### polygon  `instance-attribute`

```python
polygon: dict
```

The input GeoJSON polygon.

### total_features  `instance-attribute`

```python
total_features: int
```

Total features across all layers after dedup.

### execution_time  `instance-attribute`

```python
execution_time: float
```

Wall-clock seconds for the full pipeline.

### overture_release  `class-attribute` `instance-attribute`

```python
overture_release: Optional[str] = None
```

Overture Maps release the layers were read from. The public index pointer moves daily, so the same polygon can otherwise return different ground layers on two days with no signal to the caller; pass the value back as `overture_release=` to pin the run. Several releases are joined with ", " on the rare run that straddles a refresh.

### failed_tiles  `class-attribute` `instance-attribute`

```python
failed_tiles: List[Dict[str, Any]] = field(default_factory=list)
```

Tiles that failed every retry, as `{"tile_id", "row", "col", "error"}`, the same shape `AreaVegetation.failed_tiles` carries. A partial read is otherwise invisible: the layers simply cover less ground than the polygon, which reads as "there is nothing there". Empty means every tile was read.

### read_margin_m  `class-attribute` `instance-attribute`

```python
read_margin_m: Optional[float] = None
```

Half extent, in metres, of the read rectangle every tile was fetched with: 363 m for the wind analyses, 544 m otherwise. `run_area` refuses a run whose analysis needs more than this.

### analysis_type  `class-attribute` `instance-attribute`

```python
analysis_type: Optional[str] = None
```

The analysis type the read margin was taken from. `None` on layers built by hand, which make no claim and are never refused.
