Source code for eradiate.scenes.biosphere._canopy_loader

from __future__ import annotations

import json
import typing as t
from pathlib import Path

import numpy as np

SCENARIO_FILE_NAME = "scenario.json"


def _update_material(
    elem: dict,
    canopy_name: str,
    spectral_data: dict[str, t.Any | dict[str, t.Any]] | None,
) -> dict:
    """
    Update the material of an element with spectral data if available.

    Parameters
    ----------
    elem : dict
        The element to update.
    canopy_name : str
        The name of the canopy or ground.
    spectral_data : dict[str, t.Any | dict[str, t.Any]] | None
        The spectral data to apply to the element.

    Returns
    -------
    dict
        The updated element.
    """
    elem_copy = elem.copy()
    if (
        spectral_data is not None
        and canopy_name in spectral_data
        and (canopy_name == "ground" or elem["id"] in spectral_data[canopy_name])
    ):
        assert isinstance(spectral_data, dict)
        try:
            del elem_copy["reflectance"]
        except KeyError:
            pass
        try:
            del elem_copy["transmittance"]
        except KeyError:
            pass
        if canopy_name == "ground":
            return {**elem_copy, **spectral_data[canopy_name]}
        else:
            assert isinstance(spectral_data[canopy_name], dict)
            return {**elem_copy, **spectral_data[canopy_name][elem["id"]]}
    else:
        return elem


def _parse_rpv_surface(
    surface: dict[str, t.Any], spectral_data: dict[str, t.Any]
) -> dict[str, t.Any]:
    """
    Convert surface data to RPV representation.

    Parameters
    ----------
    surface : dict
        Surface data to convert.
    spectral_data : dict
        Spectral data to apply to the surface.

    Returns
    -------
    dict
        RPV representation of the surface data.
    """
    return {
        **surface,
        **_update_material(
            {"rho_0": surface.get("rho_0", {})},
            "ground",
            spectral_data,
        ),
        **_update_material(
            {"g": surface.get("g", {})},
            "ground",
            spectral_data,
        ),
        **_update_material(
            {"k": surface.get("k", {})},
            "ground",
            spectral_data,
        ),
    }


def _parse_lambertian_surface(
    surface: dict[str, t.Any], spectral_data: dict[str, t.Any]
) -> dict[str, t.Any]:
    """
    Convert surface data to reflectance representation.

    Parameters
    ----------
    surface : dict
        Surface data to convert.
    spectral_data : dict
        Spectral data to apply to the surface.

    Returns
    -------
    dict
        Reflectance representation of the surface data.
    """
    return {
        **surface,
        **_update_material(
            {"reflectance": surface.get("reflectance", {})},
            "ground",
            spectral_data,
        ),
        **(
            _update_material(
                {"transmittance": surface.get("transmittance", {})},
                "ground",
                spectral_data,
            )
            if "transmittance" in surface
            else {}
        ),
    }


def apply_transformation(transf: np.ndarray, center: np.ndarray) -> np.ndarray:
    """
    Apply transformation matrix to origin and translate the instance positions to the center of the scenario.

    Parameters
    ----------
    transf : np.ndarray
        Array of transformation values.
    center : np.ndarray
        Array representing the center of the scenario.

    Returns
    -------
    np.ndarray
        Transformed positions with center adjustment.
    """
    origin = np.array([0.0, 0.0, 0.0, 1.0]).T

    # Apply the transformation matrix to origin and adjust the center
    return (transf @ origin)[:3].T - center


[docs] def load_scenario( scenario_folder: Path, padding: int, spectral_data: dict[str, t.Any | dict[str, t.Any]] | None = None, ) -> dict: """ Parse JSON file of scenario from a given path. Apply transformation to the data by converting units of wavelengths and compute instance positions. Parameters ---------- scenario_folder : path-like Path of the folder containing scenario JSON file. padding : int Padding to apply to the scenario. spectral_data : dict[str, t.Any or dict[str, t.Any]] or None Spectral data to apply to the scenario, defaults to None (keep original). Example: .. code:: python spectral_data = { "ground": { "reflectance": ground_reflectance, }, "object_name": { "subobject_name": { # Spectral data for the subobject, such as "reflectance": reflectance, "transmittance": transmittance, }, } } Each spectral data specified replaces the original data completely, so it is necessary to specify all the data for the object. Returns ------- dict Returns a dictionary parsed from JSON with transformations applied. """ # Load "scenario.json" as dictionary object scenario = json.loads((scenario_folder / SCENARIO_FILE_NAME).read_text()) # Update dictionary elements with transformations surface = scenario["surface"] size = scenario["canopy"]["size"] center_2d = np.array([size[0], size[1], 0.0]) / 2 return { **scenario, "surface": ( _parse_rpv_surface(surface, spectral_data) if surface["type"] == "rpv" else _parse_lambertian_surface(surface, spectral_data) ), "canopy": { **scenario["canopy"], "instanced_canopy_elements": [ { **elem, "instance_positions": [ apply_transformation(transf, center_2d) for transf in elem["instance_positions"] ], "canopy_element": { **elem["canopy_element"], "mesh_tree_elements": [ _update_material( { **tree, "mesh_filename": ( scenario_folder / tree["mesh_filename"] ), }, canopy_name=elem["canopy_element"]["id"], spectral_data=spectral_data, ) for tree in elem["canopy_element"]["mesh_tree_elements"] ], }, } for elem in scenario["canopy"]["instanced_canopy_elements"] ], "padding": padding, }, }