Source code for pyradtran.models.surface

"""Surface configuration model.

Maps to uvspec keywords: albedo, albedo_file, albedo_map, albedo_library,
sur_temperature, brdf_ambrals, brdf_hapke, brdf_rpv, brdf_cam,
bpdf_litvinov, bpdf_maignan, bpdf_tsang_u10.

Reference: libRadtran src/uvspec_lex.l (surface/BRDF options)
"""

from __future__ import annotations

from pydantic import Field, model_validator

from pyradtran.models.base import UvspecOption

_VALID_AMBRALS_PARAMS = frozenset({"geo", "iso", "vol"})
_VALID_HAPKE_PARAMS = frozenset({"b0", "h", "w"})
_VALID_RPV_PARAMS = frozenset({"rho0", "k", "theta", "scale"})
_VALID_CAM_PARAMS = frozenset({"pcl", "sal", "u10"})
_VALID_LITVINOV_PARAMS = frozenset({
    "albedo", "rms_slope", "fresnel", "shadowing",
    "refr_index", "refr_index_i", "rms_slope2",
})
_VALID_MAIGNAN_PARAMS = frozenset({"c_maign", "arvi", "refr_index", "refr_index_i"})


[docs] class SurfaceConfig(UvspecOption): """Surface reflection and temperature configuration. Attributes: albedo: Constant Lambertian albedo [0, 1]. Mutually exclusive with BRDF/BPDF. albedo_file: Path to wavelength-dependent albedo file. albedo_map: Path to spatial albedo map NetCDF file, or tuple (path, variable). albedo_library: Albedo library name or path ("IGBP"). sur_temperature: Surface temperature in Kelvin (for thermal IR). brdf_ambrals: Dict of AMBRALS BRDF parameters (iso, vol, geo). brdf_hapke: Dict of Hapke BRDF parameters (w, b0, h). brdf_rpv: Dict of RPV BRDF parameters (rho0, k, theta, scale). brdf_cam: Dict of CAM BRDF parameters (pcl, sal, u10). bpdf_litvinov: Dict of Litvinov BPDF parameters. bpdf_maignan: Dict of Maignan BPDF parameters. bpdf_tsang_u10: Wind speed for Tsang BPDF (m/s). """ albedo: float | None = Field(default=None, ge=0.0, le=1.0) albedo_file: str | None = None albedo_map: str | tuple[str, str] | None = None albedo_library: str | None = None sur_temperature: float | None = None brdf_ambrals: dict[str, float] | None = None brdf_hapke: dict[str, float] | None = None brdf_rpv: dict[str, float] | None = None brdf_cam: dict[str, float] | None = None bpdf_litvinov: dict[str, float] | None = None bpdf_maignan: dict[str, float] | None = None bpdf_tsang_u10: float | None = Field(default=None, ge=0.0, le=100.0) brdf_rossli_file: str | None = None brdf_rossli_hotspot: bool = False brdf_rpv_file: str | None = None surface_type_map: str | None = None surface_temperature_map: str | tuple[str, str, float] | None = None
[docs] @model_validator(mode="after") def check_mutual_exclusion(self) -> SurfaceConfig: brdf_or_bpdf = ( self.brdf_ambrals or self.brdf_hapke or self.brdf_rpv or self.brdf_cam or self.bpdf_litvinov or self.bpdf_maignan or self.bpdf_tsang_u10 is not None ) if self.albedo is not None and brdf_or_bpdf: raise ValueError( "Cannot set albedo together with BRDF/BPDF options" ) if self.albedo_file is not None and brdf_or_bpdf: raise ValueError( "Cannot set albedo_file together with BRDF/BPDF options" ) if self.albedo is not None and self.albedo_file is not None: raise ValueError("Cannot set both albedo and albedo_file") if self.albedo is not None and self.albedo_map is not None: raise ValueError("Cannot set both albedo and albedo_map") if self.albedo_file is not None and self.albedo_map is not None: raise ValueError("Cannot set both albedo_file and albedo_map") if self.brdf_ambrals is not None: for key in self.brdf_ambrals: if key not in _VALID_AMBRALS_PARAMS: raise ValueError( f"Invalid brdf_ambrals parameter '{key}'. " f"Valid: {sorted(_VALID_AMBRALS_PARAMS)}" ) if self.brdf_hapke is not None: for key in self.brdf_hapke: if key not in _VALID_HAPKE_PARAMS: raise ValueError( f"Invalid brdf_hapke parameter '{key}'. " f"Valid: {sorted(_VALID_HAPKE_PARAMS)}" ) if self.brdf_rpv is not None: for key in self.brdf_rpv: if key not in _VALID_RPV_PARAMS: raise ValueError( f"Invalid brdf_rpv parameter '{key}'. " f"Valid: {sorted(_VALID_RPV_PARAMS)}" ) if self.brdf_cam is not None: for key in self.brdf_cam: if key not in _VALID_CAM_PARAMS: raise ValueError( f"Invalid brdf_cam parameter '{key}'. " f"Valid: {sorted(_VALID_CAM_PARAMS)}" ) return self
[docs] def to_uvspec_lines(self) -> list[str]: lines: list[str] = [] if self.albedo is not None: lines.append(f"albedo {self.albedo}") elif self.albedo_file is not None: lines.append(f"albedo_file {self.albedo_file}") elif self.albedo_map is not None: if isinstance(self.albedo_map, tuple): lines.append(f"albedo_map {self.albedo_map[0]} {self.albedo_map[1]}") else: lines.append(f"albedo_map {self.albedo_map}") if self.albedo_library is not None: lines.append(f"albedo_library {self.albedo_library}") if self.brdf_ambrals is not None: for param, value in self.brdf_ambrals.items(): lines.append(f"brdf_ambrals {param} {value}") if self.brdf_hapke is not None: for param, value in self.brdf_hapke.items(): lines.append(f"brdf_hapke {param} {value}") if self.brdf_rpv is not None: for param, value in self.brdf_rpv.items(): lines.append(f"brdf_rpv {param} {value}") if self.brdf_cam is not None: for param, value in self.brdf_cam.items(): lines.append(f"brdf_cam {param} {value}") if self.bpdf_litvinov is not None: vals = " ".join(str(v) for v in self.bpdf_litvinov.values()) lines.append(f"bpdf_litvinov {vals}") if self.bpdf_maignan is not None: vals = " ".join(str(v) for v in self.bpdf_maignan.values()) lines.append(f"bpdf_maignan {vals}") if self.bpdf_tsang_u10 is not None: lines.append(f"bpdf_tsang_u10 {self.bpdf_tsang_u10}") if self.sur_temperature is not None: lines.append(f"sur_temperature {self.sur_temperature}") if self.brdf_rossli_file is not None: lines.append(f"brdf_rossli_file {self.brdf_rossli_file}") if self.brdf_rossli_hotspot: lines.append("brdf_rossli_hotspot") if self.brdf_rpv_file is not None: lines.append(f"brdf_rpv_file {self.brdf_rpv_file}") if self.surface_type_map is not None: lines.append(f"surface_type_map {self.surface_type_map}") if self.surface_temperature_map is not None: if isinstance(self.surface_temperature_map, tuple): f, var, scale = self.surface_temperature_map lines.append(f"surface_temperature_map {f} {var} {scale}") else: lines.append(f"surface_temperature_map {self.surface_temperature_map}") return lines
[docs] def to_uvspec_items(self) -> list[tuple[int, str]]: phase = 7 return [(phase, line) for line in self.to_uvspec_lines()]