Source code for pyratbay.opacity.cia

# Copyright (c) 2021-2025 Cubillos & Blecic
# Pyrat Bay is open-source software under the GPL-2.0 license (see LICENSE)

__all__ = [
    'Collision_Induced',
]

import numpy as np
import mc3.utils as mu
from .. import io as io
from .. import constants as pc
from .. import tools as pt
from ..lib import _spline as sp


[docs] class Collision_Induced(): """Collision Induced Absorption opacities""" def __init__(self, cia_file, *, wn=None, wl=None, log=None): """ Parameters ---------- cia_file: String A CIA cross section file. wn: 1D float array Wavenumber array (cm-1 units) where to sample the CIA (only one of wl or wn should be provided). wl: 1D float array Wavelength array (micron units) where to sample the CIA (only one of wl or wn should be provided). Examples -------- >>> import pyratbay.spectrum as ps >>> import pyratbay.constants as pc >>> import pyratbay.opacity as op >>> wl = ps.constant_resolution_spectrum(0.61, 10.01, 15000.0) >>> cs_file = f'{pc.ROOT}/pyratbay/data/CIA/CIA_Borysow_H2H2_0060-7000K_0.6-500um.dat' >>> cia = op.Collision_Induced(cs_file, wl=wl) """ if log is None: log = mu.Log() self.cia_file = cia_file absorption, species, temps, tab_wn = io.read_cs(cia_file) self.species = species self.nspec = len(self.species) self.name = 'CIA ' + '-'.join(self.species) # Temperature must be sorted in increasing order t_sort = np.argsort(temps) absorption = absorption[t_sort] self.temps = temps[t_sort] self.ntemp = len(self.temps) self.tmin = np.amin(self.temps) self.tmax = np.amax(self.temps) if wl is not None and wn is not None: raise ValueError('Either provide wl or wn array for CIA, not both') if wl is not None: wn = 1.0 / (wl*pc.um) if wn is None: self.wn = tab_wn self.tab_cross_section = absorption self.nwave = len(self.wn) else: self.wn = wn self.nwave = len(self.wn) # Wavenumber range check: if np.amin(tab_wn) > np.amin(wn) or np.amax(tab_wn) < np.amax(wn): log.warning( f"The tabulated wavenumber range [{np.amin(tab_wn):.2f}, " f"{np.amax(tab_wn):.2f}] cm-1 " "does not cover the whole requested wavenumber range: " f"[{np.amin(wn):.2f}, {np.amax(wn):.2f}] cm-1 " f"for cross-section file: '{cia_file}'" ) # Make sure wavenumbers are in increasing order: if self.wn[1] < self.wn[0]: sorted_self_wn = self.wn[::-1] else: sorted_self_wn = self.wn if tab_wn[1] < tab_wn[0]: sorted_tab_wn = tab_wn[::-1] else: sorted_tab_wn = tab_wn # Interpolate along wavenumber axis: cross_section = np.zeros((self.ntemp, self.nwave), np.double) extrapolate_value = 0.0 for j in range(self.ntemp): ddev = sp.second_deriv(absorption[j], sorted_tab_wn) cross_section[j] = sp.splinterp_1D( absorption[j], sorted_tab_wn, ddev, sorted_self_wn, extrapolate_value, ) if self.wn[1] < self.wn[0]: cross_section = np.fliplr(cross_section) self.tab_cross_section = cross_section # per amagat**N to per (molec cm-3)**N self.tab_cross_section /= pc.amagat**self.nspec # Wavenumber indices where data exists: wn_good = (self.wn >= np.amin(tab_wn)) & (self.wn <= np.amax(tab_wn)) self._wn_lo_idx = np.where(wn_good)[0][0] self._wn_hi_idx = np.where(wn_good)[0][-1] + 1 # Delta-cross section over delta-temperature self._dcs_dt = ( np.diff(self.tab_cross_section, axis=0) / np.expand_dims(np.ediff1d(self.temps), axis=1) )
[docs] def calc_cross_section(self, temperature): """ Calculate cross-section spectra (cm-1) over temperature profiles by interpolating from tabulated values. Parameters ---------- temperature: float or 1D float array Temperature array (Kelvin) at which to interpolate the cross section. Returns ------- cross_section: 1D or 2D float array Cross section spectra (cm-1 /(molec cm-3)**N) where N is the number of species. If temperature is scalar, cross_section is 1D of length self.nwave. Otherwise, cross_section is a 2D array of shape [nlayers,nwave] """ # Enforce np.array() scalar_temp = np.isscalar(temperature) if scalar_temp: temperature = np.array([temperature]) if np.any(temperature < self.tmin) or np.any(temperature > self.tmax): raise ValueError( 'Invalid temperature, values must be in the ' f'{self.tmin:.1f}-{self.tmax:.1f} K range' ) nlayers = len(temperature) cross_section = np.zeros((nlayers, self.nwave)) sp.lin_interp_2D( self.tab_cross_section, self.temps, self._dcs_dt, np.array(temperature), cross_section, self._wn_lo_idx, self._wn_hi_idx, ) if scalar_temp: return cross_section[0] self.cross_section = cross_section return cross_section
[docs] def calc_extinction_coefficient(self, temperature, density): """ Calculate extinction-coefficient spectra (cm-1) over temperature and density profiles by interpolating in temperature from tabulated values. Parameters ---------- temperature: 1D float array Temperature array (Kelvin) at which to interpolate the cross section. density: 2D float array Number density array (molecules cm-3) of self.species over the temperature array. If temperature is 1D array, must be of shape [self.nspec, ntemp] If temperature is scalar, must be of length self.nspec. Returns ------- extinction_coefficient: 1D or 2D float array Extinction coefficient spectra (cm-1) If temperature is scalar, output is a 1D array of length nwave. Otherwise, outout is a 2D array of shape [nlayers,nwave] """ # Dimentional checks: is_scalar = np.isscalar(temperature) dens_shape = np.shape(density) if is_scalar: if np.size(density) != self.nspec: raise ValueError( 'Incompatible dimensions, if temperature is scalar ' 'density must have self.nspec elements' ) elif dens_shape != (len(temperature), self.nspec): ntemp = len(temperature) nspec = self.nspec raise ValueError( 'Incompatible dimensions, density must be a 2D array ' f'of shape [{ntemp}, {nspec}], i.e., [ntemp, nspec]' ) cross_section = self.calc_cross_section(temperature) if is_scalar: return cross_section * np.prod(density) extinction_coefficient = ( cross_section * np.prod(density, axis=1, keepdims=True) ) return extinction_coefficient
def __str__(self): fw = pt.Formatted_Write() fw.write(f"CIA file name (cia_file): '{self.cia_file}'") fw.write(f'CIA species (species): {self.species}') fw.write(f'Number of temperature samples (ntemp): {self.ntemp}') fw.write(f'Number of wavenumber samples (nwave): {self.nwave}') fw.write(f'Temperature array (temps, K):\n{self.temps}') fw.write( 'Wavenumber array (wn, cm-1):\n{}', self.wn, fmt={'float': '{:.3f}'.format}, ) fw.write( 'Wavelength array (um):\n{}', 1/self.wn/pc.um, fmt={'float': '{:.5f}'.format}, ) cm_pow = 3*self.nspec - 1 mol_pow = -self.nspec fw.write( 'Tabulated cross section (tab_cross_section, cm{} ' 'molec{}):\n{}', cm_pow, mol_pow, self.tab_cross_section, fmt={'float': '{:.2e}'.format}) fw.write( 'Minimum and maximum temperatures (tmin, tmax) in K: ' f'[{self.tmin:.1f}, {self.tmax:.1f}]' ) return fw.text