def __init__(self, args=None, log=None): if args is not None: super(Namespace, self).__init__(**args) if log is None: self._log = mu.Log(logname=None, verb=2, width=80) else: self._log = log
def log_error(log=None, error=None): """Capture exceptions into a log.error() call.""" try: yield except Exception as e: if log is None: log = mu.Log(logname=None, verb=1, width=80) if error is None: error = str(e) log.error(error, tracklev=-4)
def abundance(pressure, temperature, species, elements=None, quniform=None, atmfile=None, punits='bar', xsolar=1.0, escale={}, solar_file=None, log=None, verb=1, ncpu=1): """ Compute atmospheric abundaces for given pressure and temperature profiles with either uniform abundances or TEA. Parameters ---------- pressure: 1D float ndarray Atmospheric pressure profile (barye). temperature: 1D float ndarray Atmospheric temperature profile (Kelvin). species: 1D string list Output atmospheric composition. elements: 1D strings list Input elemental composition (default to minimum list of elements required to form species). quniform: 1D float ndarray If not None, the output species abundances (isobaric). atmfile: String If not None, output file where to save the atmospheric model. punits: String Output pressure units. xsolar: Float Metallicity enhancement factor. escale: Dict Multiplication factor for specified atoms (dict's keys) by the respective values (on top of the xsolar scaling). solar_file: String Input solar elemental abundances file (Default Asplund et al. 2009). log: Log object Screen-output log handler. verb: Integer Verbosity level. ncpu: Integer Number of parallel CPUs to use in TEA calculation. Returns ------- q: 2D float ndarray Atmospheric volume mixing fraction abundances of shape [nlayers, nspecies]. Example ------- >>> import pyratbay.atmosphere as pa >>> import pyratbay.constants as pc >>> atmfile = "pbtea.atm" >>> nlayers = 100 >>> press = np.logspace(-8, 3, nlayers) * pc.bar >>> temp = 900+500/(1+np.exp(-(np.log10(press)+1.5)*1.5)) >>> species = ['H2O', 'CH4', 'CO', 'CO2', 'NH3', 'C2H2', 'C2H4', 'HCN', >>> 'N2', 'H2', 'H', 'He'] >>> # Automatically get 'elements' necessary from the list of species: >>> Q = pa.abundance(press, temp, species) """ if solar_file is None: solar_file = pc.ROOT + "pyratbay/data/AsplundEtal2009.txt" if log is None: log = mu.Log(verb=verb) # Uniform-abundances profile: if quniform is not None: log.head("\nCompute uniform-abundances profile.") q = uniform( pressure, temperature, species, quniform, punits, log, atmfile) return q # TEA abundances: log.head("\nCompute TEA thermochemical-equilibrium abundances profile.") # Prep up files: atomic_file, patm = "PBatomicfile.tea", "PBpreatm.tea" make_atomic(xsolar, escale, atomic_file, solar_file) if elements is None: elements, dummy = stoich(species) specs = elements + list(np.setdiff1d(species, elements)) make_preatm( pressure/pt.u(punits), temperature, atomic_file, elements, specs, patm) # Run TEA: pt.make_tea(abun_file=atomic_file, verb=verb, ncpu=ncpu) proc = subprocess.Popen([pc.ROOT+"pyratbay/TEA/tea/runatm.py", patm, "TEA"]) proc.communicate() # Reformat the TEA output into the pyrat format: if atmfile is None: atmfile = 'TEA.tea' io.import_tea("TEA.tea", atmfile, species) q = io.read_atm(atmfile)[4] os.remove(atomic_file) os.remove(patm) os.remove("TEA.cfg") os.remove("TEA.tea") return q
def uniform(pressure, temperature, species, abundances, punits="bar", log=None, atmfile=None): """ Generate an atmospheric file with uniform abundances. Save it into atmfile. Parameters ---------- pressure: 1D float ndarray Monotonously decreasing pressure profile (in punits). temperature: 1D float ndarray Temperature profile for pressure layers (in Kelvin). species: 1D string ndarray List of atmospheric species. abundances: 1D float ndarray The species mole mixing ratio. punits: String Pressure units. log: Log object Screen-output log handler. atmfile: String If not None, filename to save atmospheric model. Returns ------- qprofiles: 2D Float ndarray Abundance profiles of shape [nlayers,nspecies] Examples -------- >>> import pyratbay.atmosphere as pa >>> nlayers = 11 >>> punits = 'bar' >>> pressure = pa.pressure(1e-8, 1e2, nlayers, punits) >>> tmodel = pa.tmodels.Isothermal(nlayers) >>> species = ["H2", "He", "H2O", "CO", "CO2", "CH4"] >>> abundances = [0.8496, 0.15, 1e-4, 1e-4, 1e-8, 1e-4] >>> qprofiles = pa.uniform(pressure, tmodel(1500.0), species, >>> abundances=abundances, punits=punits) >>> print(qprofiles) [[8.496e-01 1.500e-01 1.000e-04 1.000e-04 1.000e-08 1.000e-04] [8.496e-01 1.500e-01 1.000e-04 1.000e-04 1.000e-08 1.000e-04] [8.496e-01 1.500e-01 1.000e-04 1.000e-04 1.000e-08 1.000e-04] [8.496e-01 1.500e-01 1.000e-04 1.000e-04 1.000e-08 1.000e-04] [8.496e-01 1.500e-01 1.000e-04 1.000e-04 1.000e-08 1.000e-04] [8.496e-01 1.500e-01 1.000e-04 1.000e-04 1.000e-08 1.000e-04] [8.496e-01 1.500e-01 1.000e-04 1.000e-04 1.000e-08 1.000e-04] [8.496e-01 1.500e-01 1.000e-04 1.000e-04 1.000e-08 1.000e-04] [8.496e-01 1.500e-01 1.000e-04 1.000e-04 1.000e-08 1.000e-04] [8.496e-01 1.500e-01 1.000e-04 1.000e-04 1.000e-08 1.000e-04] [8.496e-01 1.500e-01 1.000e-04 1.000e-04 1.000e-08 1.000e-04]] """ if log is None: log = mu.Log() nlayers = len(pressure) # Safety checks: if len(temperature) != nlayers: log.error(f"Pressure array length ({nlayers}) and temperature array " f"length ({len(temperature)}) don't match.") if len(species) != len(abundances): log.error(f"Species array length ({len(species)}) and abundances " f"array length ({len(abundances)}) don't match.") # Expand abundances to 2D array: qprofiles = np.tile(abundances, (nlayers,1)) if atmfile is not None: header = ("# This is an atmospheric file with pressure, temperature,\n" "# and uniform mole mixing ratio profiles.\n\n") io.write_atm(atmfile, pressure, temperature, species, qprofiles, punits=punits, header=header) return qprofiles
def parse(pyrat, cfile, no_logfile=False, mute=False): """ Read the command line arguments. Parameters ---------- cfile: String A Pyrat Bay configuration file. no_logfile: Bool If True, enforce not to write outputs to a log file (e.g., to prevent overwritting log of a previous run). mute: Bool If True, enforce verb to take a value of -1. """ with pt.log_error(): if not os.path.isfile(cfile): raise ValueError(f"Configuration file '{cfile}' does not exist.") config = configparser.ConfigParser() config.optionxform = str # Enable case-sensitive variable names config.read([cfile]) if "pyrat" not in config.sections(): raise ValueError( f"\nInvalid configuration file: '{cfile}', no [pyrat] section." ) args = dict(config.items("pyrat")) # Parse data type: with pt.log_error(): parse_str(args, 'runmode') parse_int(args, 'ncpu') parse_int(args, 'verb') parse_array(args, 'dblist') parse_array(args, 'pflist') parse_array(args, 'dbtype') parse_array(args, 'tlifile') parse_array(args, 'csfile') parse_str(args, 'molfile') parse_array(args, 'extfile') # Spectrum sampling options: parse_str(args, 'wlunits') parse_str(args, 'wllow') parse_str(args, 'wlhigh') parse_float(args, 'wnlow') parse_float(args, 'wnhigh') parse_float(args, 'wnstep') parse_int(args, 'wnosamp') parse_float(args, 'resolution') # Atmospheric sampling options: parse_str(args, 'tmodel') parse_array(args, 'tpars') parse_str(args, 'radlow') parse_str(args, 'radhigh') parse_str(args, 'radstep') parse_str(args, 'runits') parse_str(args, 'punits') parse_int(args, 'nlayers') parse_str(args, 'ptop') parse_str(args, 'pbottom') parse_str(args, 'atmfile') parse_str(args, 'radmodel') # Variables for chemistry calculations parse_str(args, 'chemistry') parse_array(args, 'species') parse_array(args, 'uniform') parse_str(args, 'ptfile') parse_str(args, 'solar') parse_float(args, 'xsolar') parse_array(args, 'escale') parse_array(args, 'elements') # Extinction options: parse_float(args, 'tmin') parse_float(args, 'tmax') parse_float(args, 'tstep') parse_float(args, 'ethresh') # Voigt-profile options: parse_float(args, 'vextent') parse_float(args, 'vcutoff') parse_float(args, 'dmin') parse_float(args, 'dmax') parse_int(args, 'ndop') parse_float(args, 'lmin') parse_float(args, 'lmax') parse_int(args, 'nlor') parse_float(args, 'dlratio') # Hazes and clouds options: parse_array(args, 'clouds') parse_array(args, 'cpars') parse_array(args, 'rayleigh') parse_array(args, 'rpars') parse_float(args, 'fpatchy') parse_array(args, 'alkali') parse_float(args, 'alkali_cutoff') # Optical depth options: parse_str(args, 'rt_path') parse_float(args, 'maxdepth') parse_array(args, 'raygrid') parse_int(args, 'quadrature') # Data options: parse_str(args, 'dunits') parse_array(args, 'data') parse_array(args, 'uncert') parse_array(args, 'filters') # Abundances: parse_array(args, 'molmodel') parse_array(args, 'molfree') parse_array(args, 'molpars') parse_array(args, 'bulk') # Retrieval options: parse_str(args, 'mcmcfile') parse_array(args, 'retflag') parse_float(args, 'qcap') parse_array(args, 'params') parse_array(args, 'pstep') parse_float(args, 'tlow') parse_float(args, 'thigh') parse_array(args, 'pmin') parse_array(args, 'pmax') parse_array(args, 'prior') parse_array(args, 'priorlow') parse_array(args, 'priorup') parse_str(args, 'sampler') parse_int(args, 'nsamples') parse_int(args, 'nchains') parse_int(args, 'burnin') parse_int(args, 'thinning') parse_float(args, 'grbreak') parse_float(args, 'grnmin') parse_int(args, 'resume') # False, action='store_true') # Stellar models: parse_str(args, 'starspec') parse_str(args, 'kurucz') parse_str(args, 'marcs') parse_str(args, 'phoenix') # System parameters: parse_str(args, 'rstar') parse_float(args, 'gstar') parse_float(args, 'tstar') parse_str(args, 'mstar') parse_str(args, 'rplanet') parse_str(args, 'refpressure') parse_str(args, 'mplanet') parse_str(args, 'mpunits') parse_float(args, 'gplanet') parse_str(args, 'smaxis') parse_float(args, 'tint') # Outputs: parse_str(args, 'specfile') parse_str(args, 'logfile') parse_array(args, 'logxticks') parse_array(args, 'yran') # Cast into a Namespace to make my life easier: args = Namespace(args) args.configfile = cfile if mute: args.verb = -1 pyrat.verb = args.get_default('verb', 'Verbosity', 2, lt=5) runmode = pyrat.runmode = args.get_choice('runmode', 'running mode', pc.rmodes, take_none=False) # Define logfile name and initialize log object: pyrat.lt.tlifile = args.get_path('tlifile', 'TLI') pyrat.atm.atmfile = args.get_path('atmfile', 'Atmospheric') pyrat.spec.specfile = args.get_path('specfile', 'Spectrum') pyrat.ex.extfile = args.get_path('extfile', 'Extinction-coefficient') pyrat.ret.mcmcfile = args.get_path('mcmcfile', 'MCMC') outfile_dict = { 'tli': args.tlifile, 'atmosphere': args.atmfile, 'spectrum': args.specfile, 'opacity': args.extfile, 'mcmc': args.mcmcfile, } outfile = outfile_dict[args.runmode] if args.logfile is None and outfile is not None: if args.runmode in ['tli', 'opacity']: outfile = outfile[0] args.logfile = os.path.splitext(outfile)[0] + '.log' args.logfile = args.get_path('logfile', 'Log') # Override logfile if requested: if no_logfile: args.logfile = None args.logfile = pt.path(args.logfile) log = pyrat.log = mu.Log(logname=args.logfile, verb=pyrat.verb, width=80, append=args.resume) args._log = log # Welcome message: log.head( f"{log.sep}\n" " Python Radiative Transfer in a Bayesian framework (Pyrat Bay).\n" f" Version {__version__}.\n" f" Copyright (c) 2021-{date.today().year} Patricio Cubillos.\n" " Pyrat Bay is open-source software under the GNU GPLv2 lincense " "(see LICENSE).\n" f"{log.sep}\n\n") log.head(f"Read command-line arguments from configuration file: '{cfile}'") # :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: # Parse valid inputs and defaults: pyrat.inputs = args phy = pyrat.phy spec = pyrat.spec atm = pyrat.atm pyrat.lt.dblist = args.get_path('dblist', 'Opacity database', exists=True) pyrat.mol.molfile = args.get_path('molfile', 'Molecular data', exists=True) pyrat.cs.files = args.get_path('csfile', 'Cross-section', exists=True) pyrat.atm.ptfile = args.get_path('ptfile', 'Pressure-temperature') spec.wlunits = args.get_default('wlunits', 'Wavelength units') if spec.wlunits is not None and not hasattr(pc, spec.wlunits): log.error(f'Invalid wavelength units (wlunits): {spec.wlunits}') spec.wllow = args.get_param('wllow', spec.wlunits, 'Wavelength lower boundary', gt=0.0) spec.wlhigh = args.get_param('wlhigh', spec.wlunits, 'Wavelength higher boundary', gt=0.0) if spec.wlunits is None: spec.wlunits = args.get_units('wllow') spec.wnlow = args.get_default('wnlow', 'Wavenumber lower boundary', gt=0.0) spec.wnhigh = args.get_default('wnhigh', 'Wavenumber higher boundary', gt=0.0) spec.wnstep = args.get_default('wnstep', 'Wavenumber sampling step', gt=0.0) spec.wnosamp = args.get_default('wnosamp', 'Wavenumber oversampling factor', ge=1) spec.resolution = args.get_default('resolution', 'Spectral resolution', gt=0.0) atm.runits = args.get_default('runits', 'Planetary-radius units') if atm.runits is not None and not hasattr(pc, atm.runits): log.error(f'Invalid radius units (runits): {atm.runits}') phy.rplanet = args.get_param('rplanet', atm.runits, 'Planetary radius', gt=0.0) if atm.runits is None: atm.runits = args.get_units('rplanet') atm.nlayers = args.get_default('nlayers', 'Number of atmospheric layers', gt=1) atm.rmodelname = args.get_choice('radmodel', 'Radius-profile model', pc.radmodels) # Pressure inputs: atm.punits = args.get_default('punits', 'Pressure units') if atm.punits is not None and not hasattr(pc, atm.punits): log.error(f'Invalid pressure units (punits): {atm.punits}') atm.pbottom = args.get_param('pbottom', atm.punits, 'Pressure at bottom of atmosphere', gt=0.0) atm.ptop = args.get_param('ptop', atm.punits, 'Pressure at top of atmosphere', gt=0.0) atm.refpressure = args.get_param('refpressure', atm.punits, 'Planetary reference pressure level', gt=0.0) if atm.punits is None and atm.pbottom is not None: atm.punits = args.get_units('pbottom') elif atm.punits is None and atm.ptop is not None: atm.punits = args.get_units('ptop') elif atm.punits is None and atm.refpressure is not None: atm.punits = args.get_units('refpressure') # else, set atm.punits from atmospheric file in read_atm(). # Radius boundaries: atm.radlow = args.get_param('radlow', atm.runits, 'Radius at bottom of atmosphere', ge=0.0) atm.radhigh = args.get_param('radhigh', atm.runits, 'Radius at top of atmosphere', gt=0.0) atm.radstep = args.get_param('radstep', atm.runits, 'Radius sampling step', gt=0.0) # Chemistry: atm.chemistry = args.get_choice('chemistry', 'Chemical model', pc.chemmodels) escale = args.get_default('escale', 'Elemental abundance scaling factors', []) atm.escale = { atom: float(fscale) for atom, fscale in zip(escale[::2], escale[1::2]) } # System physical parameters: phy.mpunits = args.get_default('mpunits', 'Planetary-mass units') if phy.mpunits is not None and not hasattr(pc, phy.mpunits): log.error(f'Invalid planet mass units (mpunits): {phy.mpunits}') phy.mplanet = args.get_param('mplanet', phy.mpunits, 'Planetary mass', gt=0.0) if phy.mpunits is None: phy.mpunits = args.get_units('mplanet') phy.gplanet = args.get_default('gplanet', 'Planetary surface gravity (cm s-2)', gt=0.0) phy.tint = args.get_default('tint', 'Planetary internal temperature', 100.0, ge=0.0) phy.smaxis = args.get_param('smaxis', None, 'Orbital semi-major axis', gt=0.0) phy.rstar = args.get_param('rstar', None, 'Stellar radius', gt=0.0) phy.mstar = args.get_param('mstar', None, 'Stellar mass', gt=0.0) phy.gstar = args.get_default('gstar', 'Stellar surface gravity', gt=0.0) phy.tstar = args.get_default('tstar', 'Stellar effective temperature (K)', gt=0.0) pyrat.voigt.extent = args.get_default('vextent', 'Voigt profile extent in HWHM', 100.0, ge=1.0) pyrat.voigt.cutoff = args.get_default('vcutoff', 'Voigt profile cutoff in cm-1', 25.0, ge=0.0) pyrat.voigt.ndop = args.get_default('ndop', 'Number of Doppler-width samples', 40, ge=1) pyrat.voigt.dmin = args.get_default('dmin', 'Minimum Doppler HWHM (cm-1)', gt=0.0) pyrat.voigt.dmax = args.get_default('dmax', 'Maximum Doppler HWHM (cm-1)', gt=0.0) pyrat.voigt.nlor = args.get_default('nlor', 'Number of Lorentz-width samples', 40, ge=1) pyrat.voigt.lmin = args.get_default('lmin', 'Minimum Lorentz HWHM (cm-1)', gt=0.0) pyrat.voigt.lmax = args.get_default('lmax', 'Maximum Lorentz HWHM (cm-1)', gt=0.0) pyrat.voigt.dlratio = args.get_default( 'dlratio', 'Doppler/Lorentz-width ratio threshold', 0.1, gt=0.0) pyrat.ex.tmin = args.get_param('tmin', 'kelvin', 'Minimum temperature of opacity grid', gt=0.0) pyrat.ex.tmax = args.get_param('tmax', 'kelvin', 'Maximum temperature of opacity grid', gt=0.0) pyrat.ex.tstep = args.get_default( 'tstep', "Opacity grid's temperature sampling step in K", gt=0.0) pyrat.rayleigh.pars = args.rpars pyrat.cloud.pars = args.cpars pyrat.rayleigh.model_names = args.get_choice('rayleigh', 'Rayleigh model', pc.rmodels) pyrat.cloud.model_names = args.get_choice('clouds', 'cloud model', pc.cmodels) pyrat.alkali.model_names = args.get_choice('alkali', 'alkali model', pc.amodels) pyrat.alkali.cutoff = args.get_default( 'alkali_cutoff', 'Alkali profiles hard cutoff from line center (cm-1)', 4500.0, gt=0.0) pyrat.cloud.fpatchy = args.get_default('fpatchy', 'Patchy-cloud fraction', ge=0.0, le=1.0) pyrat.od.rt_path = args.get_choice( 'rt_path', 'radiative-transfer observing geometry', pc.rt_paths) pyrat.spec._rt_path = pyrat.od.rt_path pyrat.ex.ethresh = args.get_default('ethresh', 'Extinction-cofficient threshold', 1e-15, gt=0.0) pyrat.od.maxdepth = args.get_default('maxdepth', 'Maximum optical-depth', 10.0, ge=0.0) phy.starspec = args.get_path('starspec', 'Stellar spectrum', exists=True) phy.kurucz = args.get_path('kurucz', 'Kurucz model', exists=True) phy.marcs = args.get_path('marcs', 'MARCS model', exists=True) phy.phoenix = args.get_path('phoenix', 'PHOENIX model', exists=True) spec.raygrid = args.get_default('raygrid', 'Emission raygrid (deg)', np.array([0, 20, 40, 60, 80.])) spec.quadrature = args.get_default('quadrature', 'Number of Gaussian-quadrature points', ge=1) pyrat.obs.units = args.get_default('dunits', 'Data units', 'none', wflag=args.data is not None) if not hasattr(pc, pyrat.obs.units): log.error(f'Invalid data units (dunits): {pyrat.obs.units}') pyrat.obs.data = args.get_param('data', pyrat.obs.units, 'Data') pyrat.obs.uncert = args.get_param('uncert', pyrat.obs.units, 'Uncertainties') pyrat.obs.filters = args.get_path('filters', 'Filter pass-bands', exists=True) pyrat.ret.retflag = args.get_choice('retflag', 'retrieval flag', pc.retflags) if pyrat.ret.retflag is None: pyrat.ret.retflag = [] pyrat.ret.params = args.params pyrat.ret.pstep = args.pstep pyrat.ret.pmin = args.pmin pyrat.ret.pmax = args.pmax pyrat.ret.prior = args.prior pyrat.ret.priorlow = args.priorlow pyrat.ret.priorup = args.priorup pyrat.ret.qcap = args.get_default('qcap', 'Metals volume-mixing-ratio cap', 1.0, gt=0.0, le=1.0) pyrat.ret.params = args.params pyrat.ret.pstep = args.pstep pyrat.ret.tlow = args.get_default('tlow', 'Retrieval low-temperature (K) bound', 0, wflag=(runmode == 'mcmc')) pyrat.ret.thigh = args.get_default('thigh', 'Retrieval high-temperature (K) bound', np.inf, wflag=(runmode == 'mcmc')) pyrat.ret.sampler = args.sampler pyrat.ret.nsamples = args.get_default('nsamples', 'Number of MCMC samples', gt=0) pyrat.ret.burnin = args.get_default('burnin', 'Number of burn-in samples per chain', gt=0) pyrat.ret.thinning = args.get_default('thinning', 'MCMC posterior thinning', 1, ge=1) pyrat.ret.nchains = args.get_default('nchains', 'Number of MCMC parallel chains', ge=1) pyrat.ret.grbreak = args.get_default('grbreak', 'Gelman-Rubin convergence criteria', 0.0, ge=0) pyrat.ret.grnmin = args.get_default('grnmin', 'Gelman-Rubin convergence fraction', 0.5, gt=0.0) atm.molmodel = args.get_choice('molmodel', 'molecular-abundance model', pc.molmodels) atm.molfree = args.molfree atm.molpars = args.molpars atm.bulk = args.bulk atm.tmodelname = args.get_choice('tmodel', 'temperature model', pc.tmodels) atm.tpars = args.tpars pyrat.ncpu = args.get_default('ncpu', 'Number of processors', 1, ge=1) return
def exomol(pf_files, outfile=None): """ Extract ExoMol partition-function values from input files. If requested, write the partition-function into a file for use with Pyrat Bay. Parameters ---------- pf_files: String or List of strings Input Exomol partition-function filenames. If there are multiple isotopes, all of them must correspond to the same molecule. outfile: String If not None, save output to file. If outfile == 'default', save output to file named as PF_exomol_molecule.dat Returns ------- pf: 2D float ndarray TIPS partition function for input molecule. isotopes: 1D string list List of isotopes. temp: 1D float ndarray Partition-function temperature samples (K). Examples -------- >>> # First, download ExoMol data to current dictory, e.g.: >>> # wget http://www.exomol.com/db/NH3/14N-1H3/BYTe/14N-1H3__BYTe.pf >>> # wget http://www.exomol.com/db/NH3/15N-1H3/BYTe-15/15N-1H3__BYTe-15.pf >>> import pyratbay.opacity.partitions as pf >>> # A single file: >>> pf_data, isotopes, temp = pf.exomol('14N-1H3__BYTe.pf', >>> outfile='default') Written partition-function file: 'PF_exomol_NH3.dat' for molecule NH3, with isotopes ['4111'], and temperature range 1--1600 K. >>> # Multiple files (isotopes) for a molecule: >>> pf_data, isotopes, temp = pf.exomol( >>> ['14N-1H3__BYTe.pf', '15N-1H3__BYTe-15.pf'], outfile='default') :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Warning: Length of PF files do not match. Trimming to shorter size. :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Written partition-function file: 'PF_exomol_NH3.dat' for molecule NH3, with isotopes ['4111', '5111'], and temperature range 1--1600 K. """ # Put into list if necessary: if isinstance(pf_files, str): pf_files = [pf_files] # Read and extract data from files: isotopes = [] data, temps = [], [] molecule = '' for pf_file in pf_files: # Get info from file name: mol, iso = pt.get_exomol_mol(pf_file) # Check all files correspond to the same molecule. if molecule == '': molecule = mol elif molecule != mol: raise ValueError('All files must correspond to the same molecule.') isotopes.append(iso) # Read data: temp, z = np.loadtxt(pf_file).T data.append(z) temps.append(temp) # Number of isotopes: niso = len(isotopes) # Check temp sampling: minlen = min(len(temp) for temp in temps) maxlen = max(len(temp) for temp in temps) ntemp = minlen if minlen != maxlen: for temp in temps: if np.any(temp[0:minlen] - temps[0][0:minlen] != 0): raise ValueError( 'Temperature sampling in PF files are not compatible.') with mu.Log() as log: log.warning('Length of PF files do not match. Trimming to ' 'shorter size.') pf = np.zeros((niso, ntemp), np.double) for i,z in enumerate(data): pf[i] = z[0:minlen] temp = temps[i][0:minlen] # Write output file: if outfile == 'default': outfile = f'PF_exomol_{molecule}.dat' if outfile is not None: header = (f'# This file incorporates the tabulated {molecule} ' 'partition-function data\n# from Exomol\n\n') io.write_pf(outfile, pf, isotopes, temp, header) print("\nWritten partition-function file:\n '{:s}'\nfor molecule " "{:s}, with isotopes {},\nand temperature range {:.0f}--{:.0f} " "K.".format(outfile, molecule, isotopes, temp[0], temp[-1])) return pf, isotopes, temp