Source code for openworld_radio_twin.rt

"""Sionna-compatible loader extended with geographic scene compilation."""

from __future__ import annotations

import json
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path
from typing import Any, Literal
from weakref import WeakKeyDictionary

from openworld_radio_twin.compiler import (
    BuildingMode,
    CellSize,
    TerrainMode,
    _add_derived_assets,
    _geometry_request,
    _run_sync,
    _scene_lock,
    cached_scene_directory,
    compile_cached_scene,
    resolve_scene_directory,
)
from openworld_radio_twin.geodesy import LocalFrame
from openworld_radio_twin.scene_format import SCENE_PROVENANCE_PATH, SCENE_XML_PATH
from openworld_radio_twin.simulation.scene_builder import (
    MaterialProfileName,
    load_scene_assets,
    measurement_surface_path,
    transmitter_scene_position,
)
from openworld_radio_twin.simulation.scene_builder import (
    terrain_elevation as _terrain_elevation,
)

Device = Literal["auto", "cuda", "cpu"]
_SCENE_DIRECTORIES: WeakKeyDictionary[Any, Path] = WeakKeyDictionary()

__all__ = [
    "Device",
    "load_scene",
    "load_scene_async",
    "measurement_surface",
    "position",
    "scene_frame",
    "scene_path",
    "terrain_elevation",
]


def _version_tuple(value: str) -> tuple[int, int, int]:
    numbers = []
    for part in value.split(".")[:3]:
        digits = "".join(character for character in part if character.isdigit())
        numbers.append(int(digits or 0))
    return tuple((*numbers, 0, 0)[:3])


def _sionna_runtime(device: Device):
    if device not in {"auto", "cuda", "cpu"}:
        raise ValueError("device must be 'auto', 'cuda', or 'cpu'")
    try:
        import mitsuba as mi
    except ImportError as exc:
        raise RuntimeError(
            "Sionna RT is not installed; install 'openworld-radio-twin[rt]'"
        ) from exc

    current = mi.variant()
    requested = (
        None if device == "auto" else f"{'cuda' if device == 'cuda' else 'llvm'}_ad_mono_polarized"
    )
    if current is None:
        try:
            if requested is None:
                mi.set_variant("cuda_ad_mono_polarized", "llvm_ad_mono_polarized")
            else:
                mi.set_variant(requested)
        except (ImportError, RuntimeError) as exc:
            raise RuntimeError(
                f"Could not initialize the requested Sionna {device} runtime"
            ) from exc
        current = mi.variant()
    elif requested is not None and current != requested:
        raise RuntimeError(
            f"Mitsuba variant is already '{current}' on this thread; device='{device}' requires "
            f"'{requested}'. Select the device before importing sionna.rt, or use a separate "
            "process."
        )
    if current not in {"cuda_ad_mono_polarized", "llvm_ad_mono_polarized"}:
        raise RuntimeError(f"Mitsuba variant '{current}' is not compatible with Sionna RT")

    import sionna.rt as sionna_rt

    installed = getattr(sionna_rt, "__version__", None)
    if not installed:
        try:
            installed = version("sionna-rt")
        except PackageNotFoundError as exc:
            raise RuntimeError(
                "Sionna RT is not installed; install 'openworld-radio-twin[rt]'"
            ) from exc
    if not isinstance(installed, str) or not installed.strip():
        raise RuntimeError("The installed Sionna RT package has no readable version metadata")
    if not (2, 0, 1) <= _version_tuple(installed) < (2, 1, 0):
        raise RuntimeError(f"Sionna RT {installed} is unsupported; install version >=2.0.1,<2.1")

    return sionna_rt


def _register(scene: Any, directory: Path | None) -> None:
    if directory is None:
        return
    try:
        _SCENE_DIRECTORIES[scene] = directory
    except TypeError as exc:
        raise RuntimeError("The installed Sionna Scene type cannot be weakly referenced") from exc


def _local_scene_source(filename: str | Path | None) -> tuple[str | None, Path | None]:
    if filename is None:
        return None, None
    candidate = Path(filename).expanduser()
    if candidate.is_dir():
        xml_path = candidate / SCENE_XML_PATH
    else:
        xml_path = candidate
    resolved = xml_path.resolve()
    directory = resolved.parent if (resolved.parent / SCENE_PROVENANCE_PATH).is_file() else None
    return str(resolved), directory


def _load_native_scene(
    filename: str | Path | None,
    merge_shapes: bool,
    merge_shapes_exclude_regex: str | None,
    remove_duplicate_vertices: bool,
    device: Device,
    sionna_options: dict[str, object],
):
    sionna_rt = _sionna_runtime(device)
    source, directory = _local_scene_source(filename)
    scene = sionna_rt.load_scene(
        source,
        merge_shapes,
        merge_shapes_exclude_regex,
        remove_duplicate_vertices,
        **sionna_options,
    )
    _register(scene, directory)
    return scene


def _geographic_mode(
    filename: str | Path | None,
    latitude: float | None,
    longitude: float | None,
    radius_m: float | None,
) -> bool:
    supplied = (latitude is not None, longitude is not None, radius_m is not None)
    if any(supplied) and not all(supplied):
        raise ValueError("latitude, longitude, and radius_m must be provided together")
    if all(supplied) and filename is not None:
        raise ValueError("filename and geographic coordinates are mutually exclusive")
    return all(supplied)


[docs] def load_scene( filename: str | Path | None = None, merge_shapes: bool = True, merge_shapes_exclude_regex: str | None = None, remove_duplicate_vertices: bool = False, *, latitude: float | None = None, longitude: float | None = None, radius_m: float | None = None, cache_dir: str | Path | None = None, buildings: BuildingMode = "auto", building_source: str = "auto", terrain: TerrainMode = "elevation", material_profile: MaterialProfileName = "itu", terrain_resolution_m: float = 5.0, device: Device = "auto", **sionna_options: object, ): """Load a native Sionna scene from a file, an empty scene, or WGS84 coordinates.""" if _geographic_mode(filename, latitude, longitude, radius_m): assert latitude is not None and longitude is not None and radius_m is not None filename = _run_sync( compile_cached_scene( latitude=latitude, longitude=longitude, radius_m=radius_m, cache_dir=cache_dir, buildings=buildings, building_source=building_source, terrain=terrain, material_profile=material_profile, terrain_resolution_m=terrain_resolution_m, ) ) return _load_native_scene( filename, merge_shapes, merge_shapes_exclude_regex, remove_duplicate_vertices, device, sionna_options, )
[docs] async def load_scene_async( *, latitude: float, longitude: float, radius_m: float, cache_dir: str | Path | None = None, buildings: BuildingMode = "auto", building_source: str = "auto", terrain: TerrainMode = "elevation", material_profile: MaterialProfileName = "itu", terrain_resolution_m: float = 5.0, device: Device = "auto", merge_shapes: bool = True, merge_shapes_exclude_regex: str | None = None, remove_duplicate_vertices: bool = False, **sionna_options: object, ): """Compile asynchronously, then load the native scene on the caller's event-loop thread.""" filename = await compile_cached_scene( latitude=latitude, longitude=longitude, radius_m=radius_m, cache_dir=cache_dir, buildings=buildings, building_source=building_source, terrain=terrain, material_profile=material_profile, terrain_resolution_m=terrain_resolution_m, ) return _load_native_scene( filename, merge_shapes, merge_shapes_exclude_regex, remove_duplicate_vertices, device, sionna_options, )
[docs] def scene_path(scene: Any) -> Path: """Return the canonical directory registered for a native Sionna scene.""" try: return _SCENE_DIRECTORIES[scene] except (KeyError, TypeError) as exc: raise ValueError( "Scene has no OWRT metadata; load it with openworld_radio_twin.rt.load_scene " "or pass a scene directory to the geographic helper" ) from exc
def _directory(scene_or_path: Any) -> Path: if isinstance(scene_or_path, (str, Path)): return resolve_scene_directory(scene_or_path) return scene_path(scene_or_path)
[docs] def scene_frame(scene_or_path: Any) -> LocalFrame: """Restore the scene's public WGS84-to-ENU transform.""" directory = _directory(scene_or_path) provenance = json.loads((directory / SCENE_PROVENANCE_PATH).read_text(encoding="utf-8")) origin = provenance["coordinate_contract"]["origin"] return LocalFrame.at(float(origin["longitude"]), float(origin["latitude"]))
[docs] def terrain_elevation(scene_or_path: Any, east_m: float, north_m: float) -> float: """Sample local terrain elevation in metres at an ENU position.""" return _terrain_elevation(load_scene_assets(_directory(scene_or_path)), east_m, north_m)
[docs] def position( scene_or_path: Any, *, latitude: float, longitude: float, height_agl: float, ) -> list[float]: """Return the absolute ENU position expected by Sionna's radio devices.""" return list( transmitter_scene_position( load_scene_assets(_directory(scene_or_path)), longitude, latitude, height_agl, ) )
[docs] def measurement_surface( scene_or_path: Any, *, cell_size: CellSize = (10.0, 10.0), height: float = 1.5, ): """Return a native Mitsuba mesh following terrain at a constant AGL height.""" directory = _directory(scene_or_path) with _scene_lock(directory): assets = load_scene_assets(directory) _add_derived_assets(directory, assets, height, cell_size, None) path = measurement_surface_path(assets, cell_size, height) return _sionna_runtime("auto").load_mesh(str(path), flip_normals=False)
def cache_path( *, latitude: float, longitude: float, radius_m: float, cache_dir: str | Path | None = None, buildings: BuildingMode = "auto", building_source: str = "auto", terrain: TerrainMode = "elevation", material_profile: MaterialProfileName = "itu", terrain_resolution_m: float = 5.0, ) -> Path: """Resolve a geographic scene's deterministic cache directory without compiling it.""" request = _geometry_request( latitude, longitude, radius_m, buildings, terrain, material_profile, terrain_resolution_m, building_source, ) return cached_scene_directory(request, cache_dir)