Source code for eradiate.experiments._dem

from __future__ import annotations

import typing as t
import warnings

import attrs

from ._core import EarthObservationExperiment
from ._helpers import measure_inside_atmosphere, surface_converter
from ..attrs import documented, parse_docs
from ..scenes.atmosphere import (
    Atmosphere,
    HeterogeneousAtmosphere,
    HomogeneousAtmosphere,
    MolecularAtmosphere,
    atmosphere_factory,
)
from ..scenes.bsdfs import LambertianBSDF
from ..scenes.core import SceneElement
from ..scenes.geometry import (
    PlaneParallelGeometry,
    SceneGeometry,
    SphericalShellGeometry,
)
from ..scenes.integrators import Integrator, VolPathIntegrator, integrator_factory
from ..scenes.measure import DistantMeasure, Measure, TargetPoint
from ..scenes.surface import BasicSurface, DEMSurface
from ..units import to_quantity


[docs] @parse_docs @attrs.define class DEMExperiment(EarthObservationExperiment): """ Simulate radiation in a scene with a digital elevation model (DEM) under a 1D atmosphere. Warnings -------- * Although technically supported, DEMs extending below 0 elevation may be a tricky case because atmospheric profile behaviour below sea level is undefined. This will be addressed in a future release. Notes ----- * When using distant measures, setting a target is highly recommended. This experiment will issue a warning during configuration if it detects that a distant measure is used with no or an inappropriate target. If a distant measure is used and no target is set, it defaults to [0, 0, 0]. * This experiment supports arbitrary measure positioning, except for :class:`.MultiRadiancemeterMeasure`, for which subsensor origins are required to be either all inside or all outside of the atmosphere. If an unsuitable configuration is detected, a :class:`ValueError` will be raised during initialization. * Even without an atmosphere, this experiment requries using a volumetric path tracing integrator. """ geometry: SceneGeometry = documented( attrs.field( default="plane_parallel", converter=SceneGeometry.convert, validator=attrs.validators.instance_of( (PlaneParallelGeometry, SphericalShellGeometry) ), ), doc="Problem geometry.", type=".SceneGeometry", init_type='{"plane_parallel", "spherical_shell"} or dict or ' ".PlaneParallelGeometry or .SphericalShellGeometry", default='"plane_parallel"', ) atmosphere: Atmosphere | None = documented( attrs.field( factory=HomogeneousAtmosphere, converter=attrs.converters.optional(atmosphere_factory.convert), validator=attrs.validators.optional( attrs.validators.instance_of(Atmosphere) ), ), doc="Atmosphere specification. If set to ``None``, no atmosphere will " "be added. " "This parameter can be specified as a dictionary which will be " "interpreted by :data:`.atmosphere_factory`.", type=".Atmosphere or None", init_type=".Atmosphere or dict or None", default=":class:`HomogeneousAtmosphere() <.HomogeneousAtmosphere>`", ) surface: BasicSurface | DEMSurface | None = documented( attrs.field( factory=lambda: BasicSurface(bsdf=LambertianBSDF()), converter=attrs.converters.optional(surface_converter), validator=attrs.validators.optional( attrs.validators.instance_of((BasicSurface, DEMSurface)) ), ), doc="Surface specification. If set to ``None``, no surface will be " "added. This parameter can be specified as a dictionary which will be " "interpreted by :data:`.surface_factory` and :data:`.bsdf_factory`.", type=".Surface or None", init_type=".BasicSurface or .DEMSurface or .BSDF or dict, optional", default=":class:`BasicSurface(bsdf=LambertianBSDF()) <.BasicSurface>`", ) _integrator: Integrator = documented( attrs.field( factory=VolPathIntegrator, converter=integrator_factory.convert, validator=attrs.validators.instance_of(VolPathIntegrator), ), doc="Monte Carlo integration algorithm specification. " "This parameter can be specified as a dictionary which will be " "interpreted by :data:`.integrator_factory`. The DEMExperiment requires" "the use of a .VolPathIntegrator.", type=".VolPathIntegrator", init_type=".VolPathIntegrator or dict", default=":class:`VolPathIntegrator() <.VolPathIntegrator>`", ) def __attrs_post_init__(self): self._normalize_spectral() self._normalize_atmosphere() self._normalize_measures() def _check_geometry_comply_with_molecular_atmosphere(self, atmosphere): """ Check that the experiment geometry is compatible with the molecular atmosphere' vertical extent. Parameters ---------- atmosphere : MolecularAtmosphere The molecular atmosphere to check. Raises ------ ValueError If the geometry vertical extent exceeds the atmosphere vertical extent. """ z = to_quantity(atmosphere.thermoprops.z) thermoprops_lower = z[0] thermoprops_upper = z[-1] suggested_solution = ( "Try to set the experiment geometry so that it does not go beyond " "the vertical extent of the molecular atmosphere." ) if self.geometry.zgrid.levels[0] < thermoprops_lower: raise ValueError( "Attribtues 'geometry' and 'atmosphere' are incompatible: " f"'geometry.zgrid' lower bound ({self.geometry.zgrid.levels[0]}) " f"exceeds lower bound of 'atmosphere.thermoprops' " f"({thermoprops_lower}). {suggested_solution}" ) if self.geometry.zgrid.levels[-1] > thermoprops_upper: raise ValueError( "Attribtues 'geometry' and 'atmosphere' are incompatible: " f"'geometry.zgrid' upper bound ({self.geometry.zgrid.levels[-1]}) " f"exceeds upper bound of 'atmosphere.thermoprops' " f"({thermoprops_upper}). {suggested_solution}" ) def _normalize_atmosphere(self) -> None: """ Enforce the experiment geometry on the atmosphere component(s). """ if self.atmosphere is not None: # Since 'MolecularAtmosphere' cannot evaluate outside of its # vertical extent, we verify here that the experiment's geometry # comply with the atmosphere's vertical extent. if isinstance(self.atmosphere, MolecularAtmosphere): self._check_geometry_comply_with_molecular_atmosphere(self.atmosphere) if isinstance(self.atmosphere, HeterogeneousAtmosphere): if self.atmosphere.molecular_atmosphere is not None: self._check_geometry_comply_with_molecular_atmosphere( self.atmosphere.molecular_atmosphere ) # Override atmosphere geometry with experiment geometry self.atmosphere.geometry = self.geometry # The below call to update is required in the case of a # HeterogeneousAtmosphere, as it will propagate the geometry # override to its components. self.atmosphere.update() def _normalize_measures(self) -> None: """ Ensure that distant measure targets are set to appropriate values. Processed measures will have their ray target and origin parameters overridden if relevant. """ for measure in self.measures: # Override ray target location if relevant if isinstance(measure, DistantMeasure): if isinstance(self.surface, DEMSurface): if measure.target is None: msg = ( f"Measure '{measure.id}' has its target unset " "and the DEM is set. This is not recommended." ) elif isinstance(measure.target, TargetPoint): msg = ( f"Measure '{measure.id}' uses a point target " "and the DEM is set. This is not recommended." ) else: msg = None else: if measure.target is None: measure.target = {"type": "point", "xyz": [0, 0, 0]} msg = None if msg is not None: warnings.warn(UserWarning(msg)) def _dataset_metadata(self, measure: Measure) -> dict[str, str]: result = super()._dataset_metadata(measure) if measure.is_distant(): result["title"] = "Top-of-atmosphere simulation results" return result @property def _context_kwargs(self) -> dict[str, t.Any]: kwargs = {} for measure in self.measures: if measure_inside_atmosphere(self.atmosphere, measure): kwargs[ f"{measure.sensor_id}.atmosphere_medium_id" ] = self.atmosphere.medium_id return kwargs @property def scene_objects(self) -> dict[str, SceneElement]: # Inherit docstring objects = {} # Process atmosphere if self.atmosphere is not None: objects["atmosphere"] = attrs.evolve( self.atmosphere, geometry=self.geometry ) # Process surface if self.surface is not None and isinstance(self.surface, BasicSurface): objects["surface"] = attrs.evolve( self.surface, shape=self.geometry.surface_shape, ) else: objects["surface"] = self.surface objects.update( { "illumination": self.illumination, **{measure.id: measure for measure in self.measures}, "integrator": self.integrator, } ) return objects