---
title: Facade layout
source: https://infrared.city/docs/sdk/1.0/python/facade_layout/
---

# Facade layout

Rebuild a facade or roof sensor layout offline from the original inputs.

`synthesize_facade_layout` gives the same frames and canonical cell order that the merge of a live area run attaches. With `emit_cell_tris=True` it also gives the per-cell triangles. `surfgrid_version` is pinned by the caller. It is pure: no HTTP and no billing.

`FacadeLayout.to_bytes` and `FacadeLayout.from_bytes` save and reload a layout without the geometry, for the case where the model was edited after the run. The triangles of a full (unclipped) cell are not stored, because they follow from its frame, so the export is far smaller than the triangle geometry an `emit_cell_tris=True` merge carries.

Rebuilding and exporting are separate calls, so a caller that only wants to show a recomputed layout never pays for compression: `synthesize_facade_layout` only rebuilds, and `FacadeLayout.to_bytes` runs the compact export only when it is called. Bulk numeric data stays in numpy buffers throughout.

One job covering every building in `buildings` is the common, untiled case. A caller that tiled its own run either names each job's building ids and `tile_origin` by hand in `jobs`, or passes that run's saved `AreaSchedule` as `schedule=` and lets the SDK derive both from it.

## FacadeLayoutJobSpec  `dataclass`

One job to synthesize.

`tile_origin` reproduces one tile of a tiled run. Pass it together with `synthesize_facade_layout`'s `site_origin`; omit both for an untiled run.

Warnings

A tiled submission projects each tile's geometry around that tile's own reference point, so omitting `tile_origin` for a tiled job re-synthesizes in the site frame instead and drifts every frame by the tile row's `cos(latitude)` term.

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `job_id` | `str` | Identifier of the job, as recorded by the run. |
| `active_ids` | `sequence of str or None` | Ids of the buildings that belong to this job. `None` means every building. |
| `tile_origin` | `tuple of float or None` | `(lon, lat)` of the tile the job covered. `tile_origin_of` computes it from a saved `AreaSchedule`. `None` for an untiled job. |

## FacadeLayoutFrame  `dataclass`

One coplanar surface region, with its cells in canonical order.

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `key` | `str` | Surface id. It matches the id of the same surface in a result's `SurfaceColumns`. |
| `origin` | `ndarray` | `(x, y, z)` of the grid origin. |
| `u_axis` | `ndarray` | Direction of the grid's u axis. |
| `v_axis` | `ndarray` | Direction of the grid's v axis. |
| `grid_size` | `float` | Cell size of the grid. |
| `nu` | `int` | Number of cells along the u axis. |
| `nv` | `int` | Number of cells along the v axis. |
| `cells` | `list of int or None` | One entry per grid cell, indexed `cells[i * nu + j]`: the index of the cell's sensor in the job's flat sensor arrays, or `None` where the grid cell lies outside the surface. |

## FacadeLayoutJob  `dataclass`

One job's rebuilt layout.

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `job_id` | `str` | Identifier of the job. |
| `has_terrain` | `bool` | Whether the job was built with terrain. |
| `sensor_layout_hash` | `str` | Hash of the sensor layout. It equals the server's hash for the same inputs and parameters. |
| `entity_hashes` | `list of str` | Content hashes of the entities the layout was built from. |
| `warnings` | `list of str` | Non-fatal notices raised while building the layout. |
| `culled_below_terrain` | `int` | Number of sensor cells dropped for lying below the terrain. `0` without terrain. |
| `points` | `ndarray` | Flat sensor positions `[x, y, z, ...]`, `float64`. |
| `normals` | `ndarray` | Flat sensor normals `[x, y, z, ...]`, `float64`. |
| `cell_area` | `ndarray or None` | Per-sensor cell coverage fraction, `float32`. `None` when partial cells are disabled. |
| `cell_tris` | `ndarray or None` | Triangle coordinates of all cells, concatenated in sensor order, `float32`. `None` when not built. |
| `cell_tris_offsets` | `ndarray or None` | Offsets into `cell_tris` that split it per sensor, with one terminal offset, `uint32`. `None` when `cell_tris` is. |
| `frames` | `list of FacadeLayoutFrame` | The surface regions of the job. |

## FacadeLayout

A rebuilt or reloaded facade layout.

Create one with `synthesize_facade_layout` or `FacadeLayout.from_bytes`; do not call the constructor directly.

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `jobs` | `list of FacadeLayoutJob` | The rebuilt layout of each job. |

### layout_key  `property`

```python
layout_key: str
```

Key that identifies this layout.

It covers the geometry, the synthesis parameters, the surfgrid version and the core version, so two layouts with the same key are the same layout. Store it with each result you may want to show again later.

### surfgrid_version  `property`

```python
surfgrid_version: int
```

Surfgrid version the layout was built at.

### core_version  `property`

```python
core_version: str
```

Version of the bundled geometry core that built the layout.

### synth_params  `property`

```python
synth_params: Dict[str, Any]
```

Synthesis parameters the layout was built with.

### to_bytes

```python
to_bytes() -> bytes
```

Export this layout in a compact form.

The export leaves out the triangles of full cells and is zlib-compressed. Reload it with `FacadeLayout.from_bytes`. It is computed once and cached.

Returns:

| Type | Description |
| --- | --- |
| `bytes` | The exported layout. |

### from_bytes  `classmethod`

```python
from_bytes(
data: bytes, schedule: Optional["AreaSchedule"] = None
) -> "FacadeLayout"
```

Load a layout exported by `FacadeLayout.to_bytes`.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `data` | `bytes` | The bytes returned by `to_bytes`. | *required* |
| `schedule` | `[AreaSchedule](../tiling/#infrared_sdk.tiling.AreaSchedule)` | The saved tiled run's `AreaSchedule`, when the exported layout came from `synthesize_facade_layout` called with `schedule=`. Each job's frames are exported in that job's own tile frame, so without the schedule a reloaded tiled layout stays in tile frame and `attach_values` refuses every tiled surface. With it, the frames are moved into the site frame. Omit for an untiled layout. | `None` |

Returns:

| Type | Description |
| --- | --- |
| `[FacadeLayout](#infrared_sdk.facade_layout.FacadeLayout)` | The reloaded layout. |

Raises:

| Type | Description |
| --- | --- |
| `ValueError` | If `schedule` has no usable polygon origin or does not contain a job of the layout. |

## tile_origin_of

```python
tile_origin_of(schedule: 'AreaSchedule', job_id: str) -> Tuple[float, float]
```

Return the `(lon, lat)` of a job's tile south-west corner.

This is the same computation `run_area` used to anchor that tile when it submitted it.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `schedule` | `[AreaSchedule](../tiling/#infrared_sdk.tiling.AreaSchedule)` | The saved schedule of a tiled run. | *required* |
| `job_id` | `str` | A job id from `schedule.jobs`. | *required* |

Returns:

| Type | Description |
| --- | --- |
| `tuple of float` | `(lon, lat)` of the tile's south-west corner. |

Raises:

| Type | Description |
| --- | --- |
| `ValueError` | If `job_id` is not one of the values of `schedule.jobs`, or if `schedule.polygon` has no usable origin. |

## attach_values

```python
attach_values(
layout: "FacadeLayout", columns: Any, *, expected_layout_key: str
) -> Any
```

Verify a layout against a live or reloaded `SurfaceColumns`.

The check refuses a mismatch. It first compares `layout.layout_key` with `expected_layout_key`, which covers the geometry, the synthesis parameters, the surfgrid version and the core version, so a rebuild whose geometry drifted gets a different key. This catches even a change too small to move a frame's origin, axes or grid, such as a small hole inside a cell that is still present.

Two further checks guard against a `columns` mismatch the key alone cannot catch, such as the wrong surface entirely. They join by surface id, never by row, so the row order of `columns` does not have to match the job and frame order of `layout`. For every frame the layout names, this checks that:

1. `columns` has that surface;
2. its origin, axes and grid dimensions agree with the layout's, within float rounding;
3. its cell layout -- which grid cells actually have a sensor, in canonical row-major order -- matches exactly. `columns.has_value` is False for a grid cell with no sensor, one entry per cell in the order `frame.cells` uses, so presence is compared exactly.

Store `layout_key` with every result so you can pass it back here later.

Warnings

Never read `expected_layout_key` off `layout` itself: that checks nothing, because a layout always agrees with its own key.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `layout` | `[FacadeLayout](#infrared_sdk.facade_layout.FacadeLayout)` | The rebuilt or reloaded layout. | *required* |
| `columns` | `SurfaceColumns` | The surface columns of the result to verify against. | *required* |
| `expected_layout_key` | `str` | The `layout_key` stored alongside the result when it first ran. | *required* |

Returns:

| Type | Description |
| --- | --- |
| `SurfaceColumns` | `columns`, unchanged. This function only verifies. |

Raises:

| Type | Description |
| --- | --- |
| `ValueError` | If the layout key differs, if frame keys are not unique across the layout, or if a surface of the layout is missing from `columns` or differs in geometry or cell layout. |

## synthesize_facade_layout

```python
synthesize_facade_layout(
buildings: Mapping[str, Mapping[str, Any]],
*,
analysis_surfaces: str,
surface_grid_size: float,
ground: Optional[Mapping[str, Mapping[str, Any]]] = None,
terrain_alignment: Optional[str] = None,
surface_offset: float = 0.1,
partial_cells: bool = True,
min_coverage: float = 0.05,
mesh_cleaning: str = "auto",
max_sensors_per_job: int = 262144,
emit_cell_tris: bool = False,
surfgrid_version: Optional[int] = None,
site_origin: Optional[Tuple[float, float]] = None,
jobs: Optional[Sequence[FacadeLayoutJobSpec]] = None,
schedule: Optional["AreaSchedule"] = None,
) -> FacadeLayout
```

Rebuild a facade or roof layout offline from the original inputs.

This only rebuilds the layout; call `FacadeLayout.to_bytes` on the result to export it.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `buildings` | `mapping` | The buildings of the run, keyed by building id, as submitted. | *required* |
| `analysis_surfaces` | `str` | Which surfaces to grid: `"facades"`, `"roofs"` or `"all"`. | *required* |
| `surface_grid_size` | `float` | Grid cell size, as used by the run. | *required* |
| `ground` | `mapping` | Terrain geometry of the run, if it had any. | `None` |
| `terrain_alignment` | `str` | Terrain alignment of the run (`"to-ground"` or `"as-is"`), if it had one. | `None` |
| `surface_offset` | `float` | Offset of the sensors from the surface, in metres. This is the default a live run uses when the field is absent; set it only to rebuild a layout that used a different, explicit value. | `0.1` |
| `partial_cells` | `bool` | If true, boundary cells are clipped to the surface and kept when enough of them lies inside. If false, a cell is kept when its centre lies inside the surface. | `True` |
| `min_coverage` | `float` | Minimum in-surface area fraction for a boundary cell to be kept. Used when `partial_cells` is true. | `0.05` |
| `mesh_cleaning` | `str` | `"auto"` cleans each building before gridding; `"off"` uses the meshes as drawn. | `"auto"` |
| `max_sensors_per_job` | `int` | Per-building sensor cap. | `262144` |
| `emit_cell_tris` | `bool` | Whether the layout holds the clipped triangles of every cell (`FacadeLayoutJob.cell_tris`). Leave it off for render buffers (`columns.render_buffers()`) and for the normal case: the outline gives the exact border and no per-cell geometry is needed. Turn it on only to draw the old per-cell mesh or to export the cell geometry. It makes the layout about 3 times larger. The sensors and the hashes do not change. | `False` |
| `surfgrid_version` | `int` | Surfgrid version to rebuild at. Omit to use the version of the bundled geometry core; the version used is always available as `FacadeLayout.surfgrid_version`. A version that cannot be built is refused, never mapped to the nearest one. | `None` |
| `site_origin` | `tuple of float` | `(lon, lat)` frame in which the coordinates of `buildings` and `ground` are expressed. Pass it together with a job's `tile_origin` to reproduce a tiled job exactly; omit both for an untiled run. | `None` |
| `jobs` | `sequence of FacadeLayoutJobSpec` | Reproduces a saved run's job split. Omit for one job over every building. | `None` |
| `schedule` | `[AreaSchedule](../tiling/#infrared_sdk.tiling.AreaSchedule)` | The `AreaSchedule` returned by a tiled `run_area` call. When given, `site_origin` and `jobs` are derived from it: the polygon's own origin, and one `FacadeLayoutJobSpec` per scheduled job with its `tile_origin` computed the way `run_area` computed it. Cannot be combined with `site_origin` or `jobs`. | `None` |

Returns:

| Type | Description |
| --- | --- |
| `[FacadeLayout](#infrared_sdk.facade_layout.FacadeLayout)` | The rebuilt layout. |

Raises:

| Type | Description |
| --- | --- |
| `ValueError` | If `schedule` is combined with `site_origin` or `jobs`, or if the inputs are refused (for example an unbuildable `surfgrid_version`). |
