Source code for sasktran2.optical.xsec_absorber

from __future__ import annotations

from pathlib import Path

import numpy as np
import xarray as xr

from sasktran2._core_rust import PyXsecAbsorber
from sasktran2.database.web import StandardDatabase
from sasktran2.optical import database
from sasktran2.optical.base import OpticalProperty


[docs] class XsecAbsorber(OpticalProperty): """ Cross section absorber loaded from HITRAN fixed-width format files. This absorber loads cross section data from .xsc files and performs interpolation in temperature (linear) and uses the nearest pressure level. Wavenumber interpolation uses zero padding outside the available range. Parameters ---------- source : str or list of str or Path Can be: - Path to a single .xsc file - Path to a folder containing .xsc files - List of paths to .xsc files Examples -------- Load from a folder: >>> absorber = XsecAbsorber("/path/to/hitran/xsec/CCl4") Load from a single file: >>> absorber = XsecAbsorber("/path/to/hitran/xsec/CCl4_v1.xsc") Load from multiple files: >>> absorber = XsecAbsorber(["/path/to/file1.xsc", "/path/to/file2.xsc"]) """ _internal: PyXsecAbsorber
[docs] def __init__(self, source: str | Path | list[str] | list[Path]): if isinstance(source, str | Path): source_path = Path(source) if source_path.is_dir(): # Load from folder self._internal = PyXsecAbsorber.from_folder(source_path.as_posix()) else: # Load from single file self._internal = PyXsecAbsorber.from_file(source_path.as_posix()) elif isinstance(source, list): # Load from list of files file_paths = [Path(f).as_posix() for f in source] self._internal = PyXsecAbsorber.from_files(file_paths) else: msg = "source must be a path to a file, folder, or list of file paths" raise TypeError(msg)
[docs] @classmethod def from_lbl_database(cls, species_name: str) -> XsecAbsorber: """ Load cross sections from the LBLRTM database for a given species. This method reads the FSCDXS index file to determine which cross section files to load for the requested species, then loads them. Parameters ---------- species_name : str Name of the species. Case insensitive. Supported species: ACET, ACETICACI, BRO, C2CL2F4 (F114), C2CL3F3 (F113), C2CLF5 (F115), C2F6, C2HCl2F3CF2, CCL2FCH3, CCL4, CCLF3 (F13), CF3CH2CF3, CF3CH3, CF4 (F14), CFH2CF3, CH2F2, CH3CCLF2, CH3CHF2, CH3CN, CHCl2C2F5, CHCl2CF3, CHCL2F, CHClF2, CHClFCF3, CHF2CF3, CHF2CH2CF3, CHF3, CLONO2, F11, F12, FURAN, GLYCOLALD, HCHO, HNO3, HNO4, ISOP, N2O5, NF3, NO2, PAN, PROPENE, SF6, SO2 Returns ------- XsecAbsorber XsecAbsorber instance with the loaded cross section data Raises ------ ValueError If the species name is not found in the LBLRTM database Examples -------- >>> absorber = XsecAbsorber.from_lbl_database('CCL4') >>> absorber = XsecAbsorber.from_lbl_database('HNO3') >>> absorber = XsecAbsorber.from_lbl_database('f11') # Case insensitive """ db = StandardDatabase() fscdxs_path = db.path("cross_sections/lblrtm/FSCDXS") # Parse the FSCDXS file to find entries for this species file_list = [] species_upper = species_name.upper() with fscdxs_path.open() as f: # Skip header line next(f) for line in f: # Skip empty lines if not line.strip(): continue # Parse the molecule name (first field, whitespace separated) parts = line.split() if not parts: continue molecule = parts[0] # Check if this line matches our species if molecule.upper() == species_upper: # Extract file names from the line # Files can be concatenated like: xs/HNO3AT6xs/HNO3AT5xs/HNO3AT4... # Split the entire line on 'xs/' to extract all filenames line_parts = line.split("xs/") for part in line_parts[1:]: # Skip first part (before first 'xs/') # Extract just the filename (everything before next space or end) filename = part.split()[0] if part.strip() else "" if filename: file_path = db.path(f"cross_sections/lblrtm/xs/{filename}") file_list.append(file_path) if not file_list: msg = f"No cross section files found for species '{species_name}' in LBLRTM database" raise ValueError(msg) # Create and return the XsecAbsorber from the file list return cls(file_list)
[docs] def atmosphere_quantities(self, atmo, **kwargs): """ Calculate optical quantities for a given atmosphere. Parameters ---------- atmo : Atmosphere Atmosphere object containing temperature and pressure profiles **kwargs Must include wavenumbers_cminv as an array Returns ------- OpticalQuantities Object containing cross sections and other optical properties """ return self._internal.atmosphere_quantities(atmo, **kwargs)
[docs] def optical_derivatives(self, atmo, **kwargs): """ Calculate optical derivatives for a given atmosphere. Parameters ---------- atmo : Atmosphere Atmosphere object containing temperature and pressure profiles **kwargs Must include wavenumbers_cminv as an array Returns ------- dict Dictionary with "temperature_k" key containing temperature derivatives """ return self._internal.optical_derivatives(atmo, **kwargs)
def _into_rust_object(self): """Return the internal Rust object.""" return self._internal
class O2SchumannRunge(XsecAbsorber): def __init__(self) -> None: """ Schumann runge absorption taken from https://lweb.cfa.harvard.edu/amp/ampdata/cfamols.html. Includes both the Continuum from 130 to 175 nm and the Schumann-Runge bands from 175 to 203 nm. The data is reduced down to 0.1 nm resolution and is provided in vacuum wavelengths. Raises ------ OSError If the specified file cannot be found """ data_file = StandardDatabase().path("cross_sections/o2/O2SCHRUNG") if data_file.exists(): super().__init__(data_file) else: msg = f"Could not find O2 Schumann-Runge database at {data_file}" raise OSError(msg) class O2LymanAlpha(database.OpticalDatabaseGenericAbsorber): """ O2 Lyman-alpha effective absorption cross section for attenuation at 121.567 nm. This is represented as a narrow pseudo-line optical database so it can be used by the existing cross-section interpolation machinery. The default cross section is derived from the Yankovsky O2 Lyman-alpha TOA photolysis rate, ``3.40e-9 s^-1``, and a nominal line-integrated solar Lyman-alpha flux, ``3.2e15 photons m^-2 s^-1``. """ WAVELENGTH_NM = 121.567 TOA_RATE_S = 3.40e-9 TOA_FLUX_PHOTONS_M2_S = 3.2e15 EFFECTIVE_CROSS_SECTION_M2 = TOA_RATE_S / TOA_FLUX_PHOTONS_M2_S def __init__( self, cross_section_m2: float = EFFECTIVE_CROSS_SECTION_M2, center_wavelength_nm: float = WAVELENGTH_NM, half_width_nm: float = 5.0e-4, ) -> None: if cross_section_m2 < 0: msg = "cross_section_m2 must be non-negative" raise ValueError(msg) if half_width_nm <= 0: msg = "half_width_nm must be positive" raise ValueError(msg) wavelength_nm = np.array( [ center_wavelength_nm - half_width_nm, center_wavelength_nm, center_wavelength_nm + half_width_nm, ], dtype=np.float64, ) xs = np.array([0.0, cross_section_m2, 0.0], dtype=np.float64) ds = xr.Dataset( data_vars={"xs": (["wavelength_nm"], xs)}, coords={"wavelength_nm": wavelength_nm}, ) database.OpticalDatabase.__init__(self, db=ds) class OClOGeisa(XsecAbsorber): def __init__(self) -> None: """ OClO absorption cross sections from the GEISA database, reduced to 0.1 nm resolution from the 20 cm-1 source files. Raises ------ OSError If the specified file cannot be found """ data_file = StandardDatabase().path("cross_sections/oclo/OCLO20") if data_file.exists(): super().__init__(data_file) else: msg = f"Could not find OClO GEISA database at {data_file}" raise OSError(msg) class IOGeisa(XsecAbsorber): """ IO (Iodine Oxide) cross sections from the GEISA database. These are fixed-width format cross sections (0.1 nm averaged) at 298 K temperature. Raises ------ OSError If the IO file is not found in the database """ def __init__(self): """Load IO cross sections from the standard database.""" db = StandardDatabase() file_path = db.path("cross_sections/io/IO") super().__init__(file_path)