---
title: Tiling
source: https://infrared.city/docs/sdk/1.0/python/tiling/
---

# Tiling

Tiling infrastructure: grid generation, orchestration, and merging.

## TileService

Generate a grid of tiles covering a GeoJSON polygon.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `polygon` | `dict` | A GeoJSON Polygon object (`{"type": "Polygon", "coordinates": ...}`). Validated and winding-order-normalized on construction. | *required* |
| `logger` | `Logger` | Python logger instance. | *required* |
| `analysis_type` | `[AnalysesName](../analyses/#infrared_sdk.analyses.AnalysesName)` | Reserved for future per-type tile size differentiation. | `None` |
| `max_tiles_override` | `int` | Override the default maximum number of non-empty tiles (100 by default). `None` keeps the default. | `None` |

### polygon  `instance-attribute`

```python
polygon: dict = validate_polygon(polygon)
```

Validated GeoJSON Polygon.

### points  `instance-attribute`

```python
points: List[Point] = [
Point(latitude=pos[1], longitude=pos[0]) for pos in ring[:-1]
]
```

Internal polygon vertices as Point objects (lat/lon named fields).

### logger  `instance-attribute`

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

Logger passed to the service.

### analysis_type  `instance-attribute`

```python
analysis_type: Optional[Union[AnalysesName, str]] = analysis_type
```

Analysis type the grid is generated for, or `None`.

### tile_size  `instance-attribute`

```python
tile_size: TileSize = TileSize(
x=self.config.inference_size_m, y=self.config.inference_size_m
)
```

Tile size in meters, from the tiling config.

### max_non_empty  `instance-attribute`

```python
max_non_empty: Optional[int] = max_tiles_override
```

The caller's override of the maximum non-empty tiles, or `None`.

### config  `instance-attribute`

```python
config: TilingConfig = get_tiling_config(
str(analysis_type) if analysis_type else None
)
```

Tiling configuration for the analysis type.

### generate_tiles_for_polygon

```python
generate_tiles_for_polygon() -> List[List[Tile]]
```

Generate a grid of tiles covering the polygon's bounding box.

Returns:

| Type | Description |
| --- | --- |
| `list[list[Tile]]` | 2-D grid where `tiles[row][col]` is a `Tile`. Row 0 is the southernmost row; column 0 is the westernmost column. Each tile is marked `empty=True` if it does not intersect the polygon (neither centroid inside nor bbox overlap). |

Raises:

| Type | Description |
| --- | --- |
| `PolygonValidationError` | If the number of non-empty tiles exceeds the cap in force. The message names the tile count, the cap, the grid family, the `max_tiles_override` value that would clear it, and what the run would cost at `DEFAULT_TOKENS_PER_JOB` tokens a tile. |

## AreaPreview  `dataclass`

Preview and cost estimate for an area analysis before submission.

`would_bill_jobs`, `estimated_time_s` and `estimated_cost_tokens` are priced from the planned number of jobs, never from `tile_count` alone. Grid analyses submit one job per tile, so `would_bill_jobs == tile_count` there. A facade (`analysis_surfaces`) run can split a tile into several billed sub-batches when its estimated sensor count exceeds the server cap. Passing `payload=` (with `buildings=` if the geometry is not embedded) runs the same offline batch split `run_area` submits, so `would_bill_jobs` reflects the real job count before any HTTP call.

Warnings

Without `payload=`, `would_bill_jobs` falls back to `tile_count`, which is safe when there is no facade geometry to preview yet but an under-estimate for a facade payload.

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `tile_count` | `int` | Number of tiles covering the polygon. |
| `estimated_time_s` | `float` | Estimated run time in seconds. |
| `estimated_cost_tokens` | `int` | Estimated cost in tokens. |
| `would_bill_jobs` | `int or None` | Number of jobs the run would submit and bill. |
| `sensor_count` | `int or None` | Total sensors across the planned batches. `None` for a grid analysis (no facade batching) or when `payload=` was omitted. |

## AreaResult  `dataclass`

Bases: `_PhysicalGridMixin`

Public result from an area analysis.

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `merged_grid` | `Any` | The merged grid, a READ-ONLY numpy array in the wire dtype of the run: `float16` (solar radiation, sky view factor, thermal comfort statistics, wind), `float32` (direct sun hours, daylight availability, and the class codes of a categorical result), `int16` (UTCI/TCI: stored value = physical value x `value_divisor`), or `float64` (a run that mixes dtypes). NaN is no value for the float dtypes; `valid` tells it for `int16`. Use `physical_grid()` for physical values: **an `int16` UTCI grid read without the divisor is 10 times too large.** |
| `value_divisor` | `int` | `int16` grids only: physical value = stored value / `value_divisor` (10 for UTCI/TCI). 1 for the float dtypes. |
| `valid` | `ndarray or None` | `int16` grids only: the validity bitmap (`uint8`, one bit per cell in row-major order, least significant bit first). `None` for the float dtypes. |
| `polygon` | `dict` | The source GeoJSON polygon. |
| `analysis_type` | `str` | Which analysis type was run. |
| `grid_shape` | `tuple[int, int]` | (num_rows, num_cols) of the merged grid. |
| `bounds` | `tuple[float, float, float, float] \| None` | Geographic extent of `merged_grid` as `(min_lng, min_lat, max_lng, max_lat)`. When the input polygon is not an integer multiple of `step_m`, the actual grid extent is padded north and east to the next step boundary; this field reports that true extent so visualisers can place the bitmap without a SW-anchored squash. `None` when no grid was produced (e.g. empty schedule). |
| `failed_jobs` | `list[str]` | Job IDs where the job status was Failed. |
| `skipped_jobs` | `list[str]` | Job IDs where download failed (succeeded job but network error), or jobs that were still pending/running when merge was called. |
| `total_jobs` | `int` | Total number of jobs. |
| `succeeded_jobs` | `int` | Number of jobs that succeeded. |
| `min_legend, max_legend` | `float or None` | The exact value range measured over `merged_grid`, not a fold of the per-tile backend estimates. `None` for a categorical result, an empty grid, or when the grid holds no finite value. |
| `legend` | `list[str] or None` | The class labels of a categorical result (wind-comfort classes), sorted and indexed by the class codes in `merged_grid`. `None` for a numeric result. |
| `failed_tiles` | `list[TileFailure]` | Per-tile failure records classified by `TileFailurePhase` (submit / compute / download / skipped). Same tile_id never appears twice; first-seen phase wins (submit > compute > download > skipped). Empty when every tile produced usable output. |

### to_dict

```python
to_dict() -> dict
```

Serialise to a plain dict.

`merged_grid` holds the PHYSICAL values as a nested list (`None` for no value), as before 1.0.0: a dict reader needs no divisor. `value_dtype` and `value_divisor` record the wire dtype, so `from_dict()` gives back the same dtype and the same values.

Returns:

| Type | Description |
| --- | --- |
| `dict` | A JSON-compatible dict that `from_dict()` turns back into an equivalent result. |

### from_dict  `classmethod`

```python
from_dict(data: dict) -> AreaResult
```

Reconstruct from a dict produced by `to_dict()`.

Unknown keys are silently ignored for forward compatibility.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `data` | `dict` | A dict produced by `to_dict()`. | *required* |

Returns:

| Type | Description |
| --- | --- |
| `[AreaResult](#infrared_sdk.tiling.AreaResult)` | The reconstructed result. `merged_grid` has the stored `value_dtype` (`float64` for a dict without one). |

Raises:

| Type | Description |
| --- | --- |
| `KeyError` | If a required key (`polygon`, `analysis_type` or `grid_shape`) is missing. |

## AreaSchedule  `dataclass`

Schedule of submitted jobs for an area analysis.

Produced by `run_area()` and consumed by `check_area_state()`, `merge_area_jobs()`, and `run_area_and_wait()`. Use `to_dict()` and `from_dict()` to persist a schedule and resume it later.

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `jobs` | `dict[str, str]` | Mapping of tile_id to job_id. |
| `polygon` | `dict` | The source GeoJSON polygon. |
| `analysis_type` | `str` | Which analysis type was run. |
| `config_hash` | `str` | SHA-256 hash of the payload's config-relevant fields. |
| `tile_positions` | `dict[str, tuple[int, int]]` | Mapping of tile_id to (row, col) grid position. |
| `grid_shape` | `tuple[int, int]` | (num_rows, num_cols) of the tile grid. |
| `webhook_url` | `str or None` | Webhook URL used for this submission. |
| `webhook_events` | `tuple[str, ...] or None` | Webhook events subscribed to. |
| `failed_submissions` | `tuple[str, ...]` | Tile IDs where job submission failed in a way that is SAFE to retry -- the server either never saw the request or answered with a definitive rejection. |
| `uncertain_submissions` | `tuple[str, ...]` | Tile IDs whose submission outcome is UNKNOWN: the request may already have been accepted and queued server-side (a lost/timed-out response, or a 2xx reply whose body didn't parse into a job). Deliberately a SEPARATE bucket from `failed_submissions`, not a variant of it: `run_area(..., retry_from=schedule)` builds its resubmission set from `failed_submissions` alone, so an uncertain tile is never automatically resubmitted -- the SDK cannot tell whether that would create a second billed job, so it does not decide for the caller. Resolving one requires a deliberate step outside this field (check job history for the tile, then start a fresh, non-retry submission if nothing exists). It is carried forward unchanged across a `retry_from` run and can never appear in a retry's own result. |
| `config` | `TilingConfig or None` | The tiling configuration (tile size, step and context margins) the tiles were built with. |
| `submission_abort_status` | `int or None` | HTTP status that fail-fasted the submission batch, or `None` when submission ran to completion. Currently only 402 (insufficient credits): the condition is account-global, so the SDK stops submitting remaining tiles after the first one — those tiles land in `failed_submissions` without an HTTP call. Consumers should treat `402` as "don't retry, top up credits"; plain `failed_submissions` entries with status `None` keep their usual transient-retry semantics. |
| `surface_fields` | `bool` | `True` when the submitted payload carried facade or bring-your-own-sensor fields (`analysis_surfaces` / `sensor_points`); the merge uses it to choose the surface merge. |
| `buildings_hash` | `str or None` | Fingerprint of the buildings/geometries map that produced this schedule. Geometry drives sub-tile batch boundaries but is not part of `config_hash`, so the retry guard compares this separately. `None` on schedules built before the field existed. |
| `terrain_context_margin_m` | `float or None` | The terrain reach these tiles were sliced to, in metres — the FLOORED value actually used (`max(config.context_margin_m, requested)`), not the value passed to `run_area`. So a default solar run reads back `128.0`, not `0.0`. Guarded on retry and on `merge`. `None` on schedules built before the field existed. |
| `max_sensors_per_job` | `float or None` | The facade per-job cap that planned this schedule. Its unit depends on `batching_policy_version`: on an exact v2 schedule it is the caller's cap in RETAINED sensors, `None` for the default target; on a legacy v1 schedule it is the resolved estimate-policy cap. A missing saved policy version means legacy v1 for compatibility. Guarded on retry and on `merge` when present. |
| `batching_policy_version` | `int or None` | Facade batching policy that planned the batches: 2 is the exact retained-sensor policy (it requires complete `batch_membership` and `batch_sensor_counts`); `None` or 1 is the legacy estimate policy. |
| `batch_count_contract_version` | `int or None` | Version of the sensor-counting rules the batch sensor counts were computed under. A retry of a schedule counted under an older version is refused by name. |
| `batch_membership` | `dict[str, tuple[str, ...]] or None` | Building IDs in each facade batch, keyed by batch key (`{tile_id}#batch{i}`). `None` on a grid schedule. |
| `batch_sensor_counts` | `dict[str, int] or None` | Retained sensor count of each facade batch, keyed by batch key. `None` on a grid schedule. |
| `transport` | `str or None` | Transport the jobs were submitted with (`"json"` or `"binary"`). `None` on schedules built before the field existed. |
| `wire_version` | `int or None` | Binary wire version of the submission (`1` for the binary transport), `None` for a JSON submission. |
| `binary_acknowledgements` | `dict[str, dict] or None` | The server's acknowledgement of each binary submission, keyed by schedule key (tile ID or batch key). `None` for a JSON submission. |
| `weather_identity` | `str or None` | The weather these tiles submitted — the columns, the payload latitude and longitude and the window — as a digest, or `None` for an analysis that reads no weather array. `config_hash` covers none of that, so it alone tells two runs of one window against two climates apart. Guarded on retry and `merge`; never sent. |
| `schedule_contract_version` | `int or None` | Version of this client-side record; 4 is the run identity described under `weather_identity`. Versions 2 and 3 held an EPW FILE identity and are refused by name on a weather-bearing resume. `None` predates the field. |
| `site_identity` | `str or None` | Digest of the raw site inputs (buildings with their per-building overrides, trees, ground materials, context and terrain geometry). Guarded on retry and `merge`; never sent. `None` on schedules built before the field existed. |
| `run_id` | `str or None` | Random ID made once per schedule and kept across every retry of it. Client-side only; never sent. |
| `attempts` | `dict[str, int] or None` | Attempt number of the current job or submission of each schedule key; a missing key is attempt 1. Client-side only; never sent. |

### merge

```python
merge(other: AreaSchedule) -> AreaSchedule
```

Merge another schedule into this one (e.g. after retry).

Validates that both schedules refer to the same polygon (via fingerprint), analysis_type, and config_hash. Overlapping tile_ids in *other* override this schedule's jobs. Failures that are now covered by *other* are removed.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `other` | `[AreaSchedule](#infrared_sdk.tiling.AreaSchedule)` | The retry schedule to merge in. | *required* |

Returns:

| Type | Description |
| --- | --- |
| `[AreaSchedule](#infrared_sdk.tiling.AreaSchedule)` | A new merged schedule. |

Raises:

| Type | Description |
| --- | --- |
| `ValueError` | If polygon, analysis_type, or config_hash do not match. |

### to_dict

```python
to_dict() -> dict
```

Serialise to a plain dict for persistence.

Tuple fields are converted to lists for JSON compatibility.

Returns:

| Type | Description |
| --- | --- |
| `dict` | A JSON-compatible dict that `from_dict()` turns back into an equal schedule. |

### from_dict  `classmethod`

```python
from_dict(data: dict) -> AreaSchedule
```

Reconstruct from a dict produced by `to_dict()`.

Unknown keys are silently ignored for forward compatibility.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `data` | `dict` | A dict produced by `to_dict()`. | *required* |

Returns:

| Type | Description |
| --- | --- |
| `[AreaSchedule](#infrared_sdk.tiling.AreaSchedule)` | The reconstructed schedule. |

Raises:

| Type | Description |
| --- | --- |
| `KeyError` | If a required key (`jobs`, `polygon`, `analysis_type`, `config_hash` or `grid_shape`) is missing. |
| `ValueError` | If the saved batch records are inconsistent. |

## AreaState  `dataclass`

Snapshot of area-level job status.

Produced by `check_area_state()` and consumed by `run_area_and_wait()` callbacks.

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `status` | `str` | Derived status: `"empty"` (no jobs), `"pending"`, `"running"`, `"completed"`, `"failed"`, or `"partial"`. |
| `job_states` | `dict[str, JobStatus]` | Mapping of job_id to its current status. |
| `succeeded` | `int` | Count of succeeded jobs. |
| `failed` | `int` | Count of failed jobs. |
| `running` | `int` | Count of running jobs. |
| `pending` | `int` | Count of pending jobs. |
| `total` | `int` | Total number of jobs. |
| `is_complete` | `bool` | True when all jobs are in a terminal state. Also True when the schedule has no job but has uncertain tiles: nothing is in flight and an uncertain tile is never resubmitted, so waiting cannot change the state. The merge then reports each uncertain tile. |
| `uncertain` | `int` | Count of tiles whose submission outcome is unknown: the SDK has no job id to poll for them. The merge reports each as a `TileFailure`; resolve them through job history. |

### from_job_states  `classmethod`

```python
from_job_states(
job_states: Dict[str, JobStatus], *, uncertain: int = 0
) -> AreaState
```

Compute an AreaState from a mapping of job_id to JobStatus.

Status priority: 1. If no jobs -> `"empty"` (distinct from `"completed"`; submission may have failed wholesale or been short-circuited) 2. If all jobs pending -> `"pending"` 3. If any non-terminal job exists (pending or running) -> `"running"` 4. If all terminal and all succeeded -> `"completed"`, or `"partial"` when a submission is uncertain 5. If all terminal and all failed -> `"failed"` 6. If all terminal with mixed results -> `"partial"`

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `job_states` | `dict[str, JobStatus]` | Mapping of job_id to its current status. | *required* |
| `uncertain` | `int` | Number of tiles whose submission outcome is unknown. Default 0. | `0` |

Returns:

| Type | Description |
| --- | --- |
| `[AreaState](#infrared_sdk.tiling.AreaState)` | The derived snapshot. |

## AreaTimeoutError

Bases: `Exception`

Raised when `run_area_and_wait` exceeds its area_timeout.

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `area_state` | `[AreaState](#infrared_sdk.tiling.AreaState)` | The state snapshot at the time of timeout. |
| `message` | `str` | Human-readable timeout message. |

## TileGrid  `dataclass`

Immutable wrapper around a 2-D grid of tiles.

Produced by tile generation for a polygon and consumed by downstream composable methods (`run_area`, `get_by_tiles`).

Tile IDs are deterministic UUIDs derived from the polygon geometry and grid position — the same polygon always produces the same IDs.

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `tiles` | `list[list[Tile]]` | 2-D grid where `tiles[row][col]` is a `Tile`. Row 0 is the southernmost row; column 0 is the westernmost column. |
| `num_rows` | `int` | Number of rows in the grid. |
| `num_cols` | `int` | Number of columns in the grid. |
| `polygon` | `dict` | The source GeoJSON polygon used to generate this grid. |
| `config` | `TilingConfig or None` | The tiling configuration (tile size, step and context margins) the grid was generated with. |

### non_empty_tiles  `property`

```python
non_empty_tiles: List[Tuple[int, int, Tile]]
```

Return non-empty tiles with their grid positions.

Returns:

| Type | Description |
| --- | --- |
| `list[tuple[int, int, Tile]]` | Each element is `(row, col, tile)` for tiles where `tile.empty` is `False`. |

## TiledResult  `dataclass`

Bases: `_PhysicalGridMixin`

Aggregated result from a tiled analysis run.

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `merged_grid` | `Any` | Merged, blended, clipped grid: a READ-ONLY numpy array in the wire dtype of the run (`float16`, `float32`, `int16` or `float64`; see `AreaResult.merged_grid`). Use `physical_grid()` for physical values. Typed as Any to avoid import-time numpy dependency. |
| `value_divisor` | `int` | `int16` grids only: physical value = stored value / `value_divisor`. 1 for the float dtypes. |
| `valid` | `ndarray or None` | `int16` grids only: the validity bitmap (`uint8`, one bit per cell, least significant bit first). `None` for the float dtypes, where NaN is no value. |
| `polygon` | `dict` | Original input GeoJSON polygon. |
| `tile_count` | `int` | Number of tiles that were executed. |
| `grid_shape` | `tuple[int, int]` | (rows, cols) of the merged grid. |
| `failed_tiles` | `list[TileFailure]` | Tiles that failed after all retries. |
| `skipped_tiles` | `list[str]` | Tile IDs that were skipped. |
| `execution_time` | `float` | Total wall-clock seconds for the tiled run. |
| `per_tile_results` | `list[[TileResult](#infrared_sdk.tiling.TileResult)]` | Raw per-tile results. |
| `analysis_type` | `str` | Which analysis type was run. |
| `min_legend, max_legend` | `float or None` | The exact value range measured over `merged_grid`, not a fold of the per-tile backend estimates. `None` for a categorical result, an empty grid, or when the grid holds no finite value. |
| `legend` | `list[str] or None` | The class labels of a categorical result (wind-comfort classes), sorted and indexed by the class codes in `merged_grid`. `None` for a numeric result. |

## TileResult  `dataclass`

Raw result from a single analysis tile.

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `tile_id` | `str` | Unique identifier of the tile. |
| `row` | `int` | Row index in the tile grid. |
| `col` | `int` | Column index in the tile grid. |
| `data` | `dict or None` | The raw result payload for the tile, or `None` when the tile produced no result. |
