Source code for openworld_radio_twin.simulation.building_grid

import numpy as np
from shapely import contains_xy
from shapely.geometry import Polygon

from openworld_radio_twin.environment import EnvironmentData
from openworld_radio_twin.models import BuildingFeature, GridMapping
from openworld_radio_twin.simulation.native_geometry import column_intervals
from openworld_radio_twin.simulation.scene_builder import (
    _compiled_terrain_elevation,
    localize_buildings,
)


def _rasterize_buildings(
    buildings: list[BuildingFeature],
    mapping: GridMapping,
    width: int,
    height: int,
    slice_height_agl_m: float | None,
    environment: EnvironmentData | None = None,
) -> tuple[np.ndarray, np.ndarray]:
    if width <= 0 or height <= 0:
        raise ValueError("Coverage dimensions must be positive")
    localized = localize_buildings(
        buildings,
        mapping.origin_longitude,
        mapping.origin_latitude,
        environment,
    )
    east = mapping.first_sample_east_m + np.arange(width) * mapping.column_step_m
    north = mapping.first_sample_north_m + np.arange(height) * mapping.row_step_m
    mask = np.zeros((height, width), dtype=bool)
    roofs = np.full((height, width), np.nan, dtype=np.float32)

    for building in localized:
        if building.native_vertices:
            rows, columns, lower, upper = column_intervals(
                building.native_vertices, building.native_triangles, east, north
            )
            ground = (
                np.zeros(len(rows))
                if environment is None
                else _compiled_terrain_elevation(environment.terrain, east[columns], north[rows])
            )
            lower = lower + building.terrain_anchor_m - ground
            upper = upper + building.terrain_anchor_m - ground
            included = (
                np.ones(len(rows), dtype=bool)
                if slice_height_agl_m is None
                else (lower <= slice_height_agl_m) & (slice_height_agl_m <= upper)
            )
            rr, cc = rows[included], columns[included]
            mask[rr, cc] = True
            np.fmax.at(roofs, (rr, cc), upper[included])
            continue
        if slice_height_agl_m is not None and not (
            building.minimum_height_m <= slice_height_agl_m <= building.roof_height_m
        ):
            continue
        polygon = Polygon(building.exterior, building.holes)
        if polygon.is_empty or not polygon.is_valid or polygon.area <= 0:
            continue
        min_east, min_north, max_east, max_north = polygon.bounds
        columns = np.flatnonzero((east >= min_east) & (east <= max_east))
        rows = np.flatnonzero((north >= min_north) & (north <= max_north))
        if not columns.size or not rows.size:
            continue
        sample_east, sample_north = np.meshgrid(east[columns], north[rows])
        occupied = contains_xy(polygon, sample_east, sample_north)
        region = np.ix_(rows, columns)
        region_mask = mask[region]
        region_roofs = roofs[region]
        region_mask |= occupied
        region_roofs[occupied] = np.fmax(region_roofs[occupied], building.roof_height_m)
        mask[region] = region_mask
        roofs[region] = region_roofs

    roofs[~mask] = np.nan
    return mask, roofs


[docs] def rasterize_building_heights( buildings: list[BuildingFeature], mapping: GridMapping, width: int, height: int, receiver_height_agl_m: float, environment: EnvironmentData | None = None, ) -> tuple[np.ndarray, np.ndarray]: """Sample building occupancy at one AGL plane on exact grid cell centers.""" return _rasterize_buildings( buildings, mapping, width, height, receiver_height_agl_m, environment, )
[docs] def rasterize_building_footprints( buildings: list[BuildingFeature], mapping: GridMapping, width: int, height: int, ) -> np.ndarray: """Sample the vertical building projection on exact grid cell centers.""" mask, _ = _rasterize_buildings(buildings, mapping, width, height, None) return mask