Source code for pyradtran.models.source

"""Source configuration model.

Maps to uvspec keywords: source, sza, phi0, day_of_year, solar_flux_file, umu, phi,
latitude, longitude, time, time_interpolate, time_interval, earth_radius, sza_file,
isotropic_source_toa.

Reference: libRadtran src_py/geometry_options.py, src_py/spectral_options.py
"""

from __future__ import annotations

from pydantic import Field, model_validator

from pyradtran.models.base import UvspecOption


[docs] class SourceConfig(UvspecOption): """Solar or thermal radiation source configuration. Attributes: source: Radiation source type -- "solar" or "thermal". sza: Solar zenith angle in degrees [0, 180]. Required for solar source (unless sza_file or isotropic_source_toa is set). phi0: Solar azimuth angle in degrees [-360, 360]. day_of_year: Day of year [1, 366]. solar_flux_file: Path to solar flux file (e.g. kurudz_0.1nm.dat). umu: Viewing zenith angles (cosines). Positive = upward, negative = downward. phi: Viewing azimuth angles in degrees. satellite_geometry: Satellite geometry specification (e.g., SENTINEL2A, MPS). satellite_pixel: Pixel coordinates (x, y) for satellite pixel-based geometry. latitude: Geographic latitude as (hemisphere, degrees, minutes, seconds). longitude: Geographic longitude as (hemisphere, degrees, minutes, seconds). time: Time of day string (e.g. "10:30:00"). time_interpolate: Enable time interpolation. time_interval: Tuple of (start_time, end_time) for time range. earth_radius: Earth radius in km. sza_file: Path to file containing solar zenith angle data. isotropic_source_toa: Use isotropic source at top of atmosphere. """ source: str = Field(pattern=r"^(solar|thermal)$") sza: float | None = Field(default=None, ge=0.0, le=180.0) phi0: float | None = Field(default=None, ge=-360.0, le=360.0) day_of_year: int | None = Field(default=None, ge=1, le=366) solar_flux_file: str | None = None umu: list[float] = Field(default_factory=list) phi: list[float] = Field(default_factory=list) satellite_geometry: str | None = None satellite_pixel: tuple[int, int] | None = None latitude: tuple[str, int, int, int] | None = None longitude: tuple[str, int, int, int] | None = None time: str | None = None time_interpolate: bool = False time_interval: tuple[str, str] | None = None earth_radius: float | None = Field(default=None, ge=0.0) sza_file: str | None = None isotropic_source_toa: bool = False
[docs] @model_validator(mode="after") def check_sza_for_solar(self) -> SourceConfig: if ( self.source == "solar" and self.sza is None and self.sza_file is None and not self.isotropic_source_toa ): msg = "sza, sza_file, or isotropic_source_toa is required when source='solar'" raise ValueError(msg) return self
[docs] @model_validator(mode="after") def check_sza_mutual_exclusion(self) -> SourceConfig: count = sum(1 for x in (self.sza, self.sza_file) if x is not None) count += int(self.isotropic_source_toa) if count > 1: raise ValueError("sza, sza_file, and isotropic_source_toa are mutually exclusive") return self
[docs] @model_validator(mode="after") def check_satellite_consistency(self) -> SourceConfig: if self.satellite_pixel is not None and self.satellite_geometry is None: raise ValueError( "satellite_pixel requires satellite_geometry to be set" ) if self.satellite_geometry is not None and self.satellite_pixel is None: raise ValueError( "satellite_geometry requires satellite_pixel to be set" ) return self
[docs] def to_uvspec_lines(self) -> list[str]: lines: list[str] = [] if self.solar_flux_file: lines.append(f"source {self.source} {self.solar_flux_file}") else: lines.append(f"source {self.source}") if self.sza is not None: lines.append(f"sza {self.sza}") if self.phi0 is not None: lines.append(f"phi0 {self.phi0}") if self.day_of_year is not None: lines.append(f"day_of_year {self.day_of_year}") if self.umu: lines.append(f"umu {' '.join(str(v) for v in self.umu)}") if self.phi: lines.append(f"phi {' '.join(str(v) for v in self.phi)}") if self.satellite_geometry is not None: lines.append(f"satellite_geometry {self.satellite_geometry}") if self.satellite_pixel is not None: x, y = self.satellite_pixel lines.append(f"satellite_pixel {x} {y}") if self.latitude is not None: h, d, m, s = self.latitude lines.append(f"latitude {h} {d} {m} {s}") if self.longitude is not None: h, d, m, s = self.longitude lines.append(f"longitude {h} {d} {m} {s}") if self.time is not None: lines.append(f"time {self.time}") if self.time_interpolate: lines.append("time_interpolate") if self.time_interval is not None: start, end = self.time_interval lines.append(f"time_interval {start} {end}") if self.earth_radius is not None: lines.append(f"earth_radius {self.earth_radius}") if self.sza_file is not None: lines.append(f"sza_file {self.sza_file}") if self.isotropic_source_toa: lines.append("isotropic_source_toa") return lines
[docs] def to_uvspec_items(self) -> list[tuple[int, str]]: phase = 2 return [(phase, line) for line in self.to_uvspec_lines()]