"""Aerosol configuration models.
Provides a strict class hierarchy for aerosol configuration:
- OpacPreset: OPAC preset mixture profiles (continental_average, maritime_clean, etc.)
- OpacCustom: OPAC custom species profile files
- ExternalAerosol: External optical property files (explicit, gg, ssa, tau, moments)
Reference: libRadtran src/uvspec_lex.l (aerosol options)
Reference: Hess et al. (1998), Bull. Amer. Meteor. Soc., 79, 831-844
"""
from __future__ import annotations
from abc import abstractmethod
from enum import Enum
from pydantic import Field, model_validator
from pyradtran.models.base import UvspecOption
_VALID_FILE_TYPES = frozenset({"gg", "ssa", "tau", "explicit", "moments", "ref", "siz"})
_VALID_MODIFY_VARIABLES = frozenset({"gg", "ssa", "tau", "tau550"})
_VALID_MODIFY_ACTIONS = frozenset({"scale", "set"})
_VALID_OPAC_SPECIES = frozenset({
"inso", "waso", "soot", "ssam", "sscm",
"minm", "miam", "micm", "mitr", "suso",
})
def _validate_opac_species_names(names: list[str]) -> None:
"""Validate that species names are valid OPAC species."""
invalid = set(names) - _VALID_OPAC_SPECIES
if invalid:
raise ValueError(
f"Invalid OPAC species: {sorted(invalid)}. "
f"Valid: {sorted(_VALID_OPAC_SPECIES)}"
)
[docs]
class OpacPresetName(str, Enum):
"""OPAC preset mixture profile names.
These correspond to files in data/aerosol/OPAC/standard_aerosol_files/.
"""
CONTINENTAL_AVERAGE = "continental_average"
CONTINENTAL_CLEAN = "continental_clean"
CONTINENTAL_POLLUTED = "continental_polluted"
URBAN = "urban"
MARITIME_CLEAN = "maritime_clean"
MARITIME_POLLUTED = "maritime_polluted"
MARITIME_TROPICAL = "maritime_tropical"
DESERT = "desert"
DESERT_SPHEROIDS = "desert_spheroids"
ANTARCTIC = "antarctic"
[docs]
class AerosolModifyEntry(UvspecOption):
"""A single aerosol_modify directive.
Attributes:
variable: Property to modify (gg, ssa, tau, tau550).
action: How to modify (scale or set).
value: Numeric value.
"""
model_config = {"extra": "forbid", "frozen": True, "populate_by_name": True}
variable: str
action: str
value: float
[docs]
@model_validator(mode="after")
def validate_entry(self) -> AerosolModifyEntry:
if self.variable not in _VALID_MODIFY_VARIABLES:
raise ValueError(
f"Invalid aerosol_modify variable '{self.variable}'. "
f"Valid: {sorted(_VALID_MODIFY_VARIABLES)}"
)
if self.action not in _VALID_MODIFY_ACTIONS:
raise ValueError(
f"Invalid aerosol_modify action '{self.action}'. "
f"Valid: {sorted(_VALID_MODIFY_ACTIONS)}"
)
return self
def _format_line(self) -> str:
return f"aerosol_modify {self.variable} {self.action} {self.value}"
[docs]
class AerosolModel(UvspecOption):
"""Abstract base class for all aerosol configurations.
Subclasses implement mode-specific ``to_uvspec_lines()``.
Common capabilities (set_tau_at_wvl, king_byrne, modify) are handled here.
"""
set_tau_at_wvl: tuple[float, float] | None = None
king_byrne: tuple[float, float, float] | None = None
modify: list[AerosolModifyEntry] = Field(default_factory=list)
[docs]
@abstractmethod
def to_uvspec_lines(self) -> list[str]:
...
[docs]
def to_uvspec_items(self) -> list[tuple[int, str]]:
phase = 5
items = [(phase, line) for line in self.to_uvspec_lines()]
if self.set_tau_at_wvl is not None:
wl, tau = self.set_tau_at_wvl
items.append((phase, f"aerosol_set_tau_at_wvl {wl} {tau}"))
if self.king_byrne is not None:
a0, a1, a2 = self.king_byrne
items.append((phase, f"aerosol_king_byrne {a0} {a1} {a2}"))
for entry in self.modify:
items.append((phase, entry._format_line()))
return items
[docs]
class OpacPreset(AerosolModel):
"""OPAC preset mixture profile aerosol.
Uses pre-defined aerosol species mixture profiles from the OPAC library.
The ``name`` selects from 10 predefined mixture profiles.
Optionally filter to specific species via ``species_names``.
Attributes:
name: Preset mixture profile name.
library: OPAC library path or "OPAC" for uvspec default resolution.
species_names: Optional species filter (e.g. ["inso", "soot"]).
"""
name: OpacPresetName
library: str = "OPAC"
species_names: list[str] | None = None
[docs]
@model_validator(mode="after")
def validate_species(self) -> OpacPreset:
if self.species_names:
_validate_opac_species_names(self.species_names)
return self
[docs]
def to_uvspec_lines(self) -> list[str]:
lines = [f"aerosol_species_library {self.library}"]
if self.species_names:
names = " ".join(self.species_names)
lines.append(f"aerosol_species_file {self.name.value} {names}")
else:
lines.append(f"aerosol_species_file {self.name.value}")
return lines
[docs]
class OpacCustom(AerosolModel):
"""OPAC custom species profile aerosol.
Uses a user-provided mass concentration profile file with the OPAC library.
Attributes:
species_file: Path to an ASCII profile file.
library: OPAC library path or "OPAC" for default resolution.
species_names: Optional species filter.
"""
species_file: str = Field(min_length=1)
species_names: list[str] | None = None
library: str = "OPAC"
[docs]
@model_validator(mode="after")
def validate_species(self) -> OpacCustom:
if self.species_names:
_validate_opac_species_names(self.species_names)
return self
[docs]
def to_uvspec_lines(self) -> list[str]:
lines = [f"aerosol_species_library {self.library}"]
if self.species_names:
names = " ".join(self.species_names)
lines.append(f"aerosol_species_file {self.species_file} {names}")
else:
lines.append(f"aerosol_species_file {self.species_file}")
return lines
[docs]
class ExternalFile(AerosolModel):
"""External aerosol optical property files.
Attributes:
files: List of (file_type, file_path) tuples.
Types: "gg", "ssa", "tau", "explicit", "moments", "ref", "siz".
"""
files: list[tuple[str, str]] = Field(min_length=1)
[docs]
@model_validator(mode="after")
def validate_files(self) -> ExternalFile:
for file_type, _ in self.files:
if file_type not in _VALID_FILE_TYPES:
raise ValueError(
f"Unknown aerosol file type '{file_type}'. "
f"Valid: {sorted(_VALID_FILE_TYPES)}"
)
return self
[docs]
def to_uvspec_lines(self) -> list[str]:
return [f"aerosol_file {ft} {fp}" for ft, fp in self.files]
# Backwards-compatibility alias — remove after one release.
ExternalAerosol = ExternalFile