Source code for eradiate.scenes.surface._central_patch

from __future__ import annotations

import warnings

import attrs
import mitsuba as mi
import numpy as np
import pint
import pinttr

import eradiate

from ._core import Surface
from ..bsdfs import BSDF, BlackBSDF, LambertianBSDF, bsdf_factory
from ..core import Ref, SceneTraversal, traverse
from ..shapes import RectangleShape, shape_factory
from ...attrs import documented, parse_docs
from ...exceptions import OverriddenValueWarning, TraversalError
from ...units import unit_context_config as ucc


def _edges_converter(value):
    # Basic unit conversion and array reshaping
    length_units = ucc.get("length")
    value = np.reshape(
        pinttr.util.ensure_units(value, default_units=length_units).m_as(length_units),
        (-1,),
    )

    # Broadcast if relevant
    if len(value) == 1:
        value = np.full((2,), value[0])

    return value * length_units


[docs] @parse_docs @attrs.define(eq=False, slots=False) class CentralPatchSurface(Surface): """ Central patch surface [``central_patch``]. This surface consists of a rectangular patch, described by its `field` parameter, with a composite reflection model composed of a background uniform component, and a central patch. This class creates a square surface to which two BSDFs will be attached. The two constituent surfaces ``central_patch`` and ``background_surface`` define the properties of the two sections of this surface. The size of the central surface is controlled by setting the ``width`` parameter of the ``central_patch`` surface, while the ``width`` of the ``background_surface`` must be set to ``AUTO`` and the total width of the surface is set by the ``width`` of the main surface object. Note that the ``width`` of a surface defaults to ``AUTO``, which means, omitting the parameter in the ``background_surface`` will yield the correct behaviour. If the ``central_patch`` width is set to ``AUTO`` as well it defaults to one third of the overall surface size, unless a contextual constraint (*e.g.* to match the size of an atmosphere or canopy) is applied. """ shape: RectangleShape | None = documented( attrs.field( default=None, converter=attrs.converters.optional(shape_factory.convert), validator=attrs.validators.optional( attrs.validators.instance_of(RectangleShape) ), ), doc="Shape describing the surface. This parameter may be left unset " "for situations in which setting its value is delegated to another " "component (*e.g.* an :class:`.Experiment` instance owning the " "surface object); however, if it is still unset upon kernel dictionary " "generation, the call to :meth:`.kernel_dict` will raise.", type=".RectangleShape or None", init_type=".RectangleShape or dict, optional", default="None", ) @shape.validator def _shape_validator(self, attribute, value): if value is not None: # Means it's a Shape if value.bsdf is not None: warnings.warn( f"while validating '{attribute.name}': " f"'{attribute.name}.bsdf' should be set to None; it will " "be overridden upon kernel dictionary generation", OverriddenValueWarning, ) bsdf: BSDF = documented( attrs.field( factory=LambertianBSDF, converter=bsdf_factory.convert, validator=attrs.validators.instance_of(BSDF), ), doc="The reflection model attached to the surface.", type=".BSDF", init_type=".BSDF or dict, optional", default=":class:`LambertianBSDF() <.LambertianBSDF>`", ) patch_edges: pint.Quantity | None = documented( pinttr.field( default=None, converter=attrs.converters.optional(_edges_converter), units=ucc.deferred("length"), ), doc="Length of the central patch's edges. If unset, the central patch " "edges will be 1/3 of the surface's edges. " "Unit-enabled field (default: ``ucc['length']``).", type="quantity or None", init_type="quantity or array-like, optional", ) patch_bsdf: BSDF = documented( attrs.field( factory=BlackBSDF, converter=bsdf_factory.convert, validator=attrs.validators.instance_of(BSDF), ), doc="The reflection model attached to the central patch.", type=".BSDF", init_type=".BSDF or dict, optional", default=":class:`BlackBSDF() <.BlackBSDF>`", ) def _texture_scale(self): """ Compute patch texture scaling factor based on configuration. """ # Note: The texture file has a central patch covering 1/3 of its # surface, hence the 1/3 factor. return ( [1.0, 1.0] if self.patch_edges is None else (self.shape.edges / (3.0 * self.patch_edges)).m_as("dimensionless") )
[docs] def update(self) -> None: # Inherit docstring # Fix BSDF IDs self.bsdf.id = self._background_bsdf_id self.patch_bsdf.id = self._patch_bsdf_id # Force BSDF referencing if the shape is defined if self.shape is not None: if isinstance(self.shape.bsdf, BSDF): warnings.warn("Set BSDF will be overridden by surface BSDF settings.") self.shape.bsdf = Ref(id=self._bsdf_id)
@property def _shape_id(self): """ Mitsuba shape object identifier. """ return f"{self.id}_shape" @property def _bsdf_id(self): """ Mitsuba BSDF object identifier (blend). """ return f"{self.id}_bsdf" @property def _background_bsdf_id(self): """ Mitsuba BSDF object identifier (background). """ return f"{self.id}_background_bsdf" @property def _patch_bsdf_id(self): """ Mitsuba BSDF object identifier (patch). """ return f"{self.id}_patch_bsdf" @property def _template_bsdfs(self) -> dict: objects = { "bsdf_0": traverse(self.bsdf)[0].data, "bsdf_1": traverse(self.patch_bsdf)[0].data, } scale = self._texture_scale() to_uv = mi.ScalarTransform4f.scale( [scale[0], scale[1], 1] ) @ mi.ScalarTransform4f.translate( [-0.5 + (0.5 / scale[0]), -0.5 + (0.5 / scale[1]), 0.0] ) result = {f"{self._bsdf_id}.type": "blendbsdf"} for obj_key, obj_params in objects.items(): for key, param in obj_params.items(): result[f"{self._bsdf_id}.{obj_key}.{key}"] = param weight_dict = { "type": "bitmap", "filename": str( eradiate.data.data_store.fetch( "textures/central_patch_surface_mask.bmp" ) ), "filter_type": "nearest", "to_uv": to_uv, "wrap_mode": "clamp", } for key, param in weight_dict.items(): result[f"{self._bsdf_id}.weight.{key}"] = param return result @property def _template_shapes(self) -> dict: kdict_template = traverse(self.shape)[0].data result = {} for key, param in kdict_template.items(): result[f"{self._shape_id}.{key}"] = param return result @property def _params_bsdfs(self) -> dict: objects = { "bsdf_0": traverse(self.bsdf)[1].data, "bsdf_1": traverse(self.patch_bsdf)[1].data, } result = {} for obj_key, obj_params in objects.items(): for key, param in obj_params.items(): result[f"{self._bsdf_id}.{obj_key}.{key}"] = param return result @property def _params_shapes(self) -> dict: return {}
[docs] def traverse(self, callback: SceneTraversal) -> None: # Inherit docstring if self.shape is None: raise TraversalError( "A 'CentralPatchSurface' cannot be traversed if its 'shape' field " "is unset." ) super().traverse(callback)