Source code for astromugs.pipeline.Pipeline

import glob
import os
import sys
import shutil
import numpy as np


from astromugs import radmc3d
from astromugs import nautilus
from astromugs.pipeline.Grid import Grid
from astromugs.modeling.Disk import Disk
from astromugs.utils.struct import StructureParams
from astromugs.utils.thermal import ThermalParams
from astromugs.utils import custom as custom_io

from astromugs.constants.constants import autocm, M_sun, R_sun, c, amu, mu, black_body
from astromugs.modeling.InterstellarRadFields import InterstellarRadFields

import matplotlib.pyplot as plt


[docs] class Pipeline: """Main user-facing class for setting up and running disk models. Orchestrates the full modeling pipeline: dust continuum radiative transfer with RADMC-3D, line radiative transfer, local radiation field computation, and chemical modeling with Nautilus. Holds the grid, structural parameters, thermal parameters, and provides methods to read, write, and run each modeling stage. Attributes ---------- params : StructureParams Structural parameters for the disk model. thermalparams : ThermalParams Thermal and wavelength parameters for radiative transfer. thermalpath : str Relative path to the directory containing thermal / RADMC-3D files. chempath : str Relative path to the directory containing chemistry (Nautilus) files. radmc3d_cmd : str Command used to invoke RADMC-3D (can be overridden to point to a specific binary). grid : Grid The spatial grid object holding coordinates, dust, and field data. nautilus : module Reference to the Nautilus chemistry module. Ta : ndarray Surface-area-weighted dust temperature array, populated after ``run_continuum`` or ``write_nautilus``. """
[docs] def __init__(self): """Initialize the Pipeline with default parameters, grid, and paths.""" self.params = StructureParams() self.thermalparams = ThermalParams() self.thermalpath = 'thermal/' self.chempath = 'chemistry/' self.radmc3d_cmd = 'radmc3d' #in case the user wants to point to a specific radmc3d self.nmgc_cmd = 'nmgc' #in case the user wants to point to a specific nmgc binary self.grid = Grid(params=self.params.disk, wave=self.thermalparams.wave) self.nautilus = nautilus
[docs] def run_continuum(self, write_control=False, **keywords): """Run dust continuum radiative transfer and compute area-weighted temperature. Executes the RADMC-3D Monte Carlo dust radiative transfer, reads the resulting dust temperature, and computes the surface-area-weighted temperature ``self.Ta``. If the output files already exist they are read directly; missing files produce a warning but do not halt execution (useful when running chemistry without a dust structure). Parameters ---------- write_control : bool, optional If True, write the RADMC-3D control file before running. Default is False. **keywords Additional keyword arguments forwarded to ``run_thermal_radmc3d``. """ # FOLDER WITH THERMAL FILES thermpath = self.thermalpath # WRITE THERMAL FILES if write_control == True: self.write_continuum(control=True) self.run_thermal_radmc3d(**keywords) # THIS SECTION READS THE THERMAL FILES IF THE FILES EXIST. # IF THE FILES DO NOT EXIST, A WARNING IS PRINTED BUT THE CODE CONTINUES. ERRORS CAN BE RAISED. # WHY DOES THE CODE CONTINUE? BECAUSE SOMETIMES THE USER MAY WANT TO RUN THE CHEMISTRY WITHOUT HAVING A DUST STRUCTURE (E.G. IF THEY WANT TO DERIVE DUST_DENSITY.INP FROM AN EXISTING CHEMISTRY MODEL). # ---- READ SECTION ---- densityfile = thermpath + "dust_density.inp" starfile = thermpath + "stars.inp" temperaturefile = thermpath + "dust_temperature.dat" if os.path.exists(densityfile): # READ THERMAL FILES nx, ny, nz, x, y, z = radmc3d.read.grid(thermpath) self.grid.set_spherical_grid(r_edge=np.array(x)/autocm, theta_edge=y, phi_edge=z) dust_density = radmc3d.read.dust_density(thermpath) # Gives a list of one numpy array. Will be updated when multiple structures nbspecies = int(len(dust_density[0])/(nx*ny*nz)) dust_density = np.reshape(dust_density[0], (nbspecies, nz, ny, nx)) dust_density[dust_density<=1e-100] = 1e-100 # get a minimum dust density in order to avoid absurd values for chemistry. if os.path.exists(temperaturefile): self.grid.temperature = radmc3d.read.dust_temperature(thermpath) if len(self.grid.temperature) > 0: self.grid.temperature[0] = np.reshape(self.grid.temperature[0], (nbspecies, nz, ny, nx)) if nbspecies > 1: a = [] dustmass = [] for istruc in range(0, len(self.grid.dust)): # create a loop in order to gather all grain sizes into one single array that will be used to compute the area-weighted temperature Ta. dustmodel = self.grid.dust[istruc] #dustmass = dustmodel.grainmass() # gram sizes = dustmodel.sizes() #a = sizes[-1]*1e-4 # cm a.append(sizes[-1]*1e-4) # cm dustmass.append(dustmodel.grainmass()) # gram a = np.hstack(a) # in order to get a numpy list of all sizes no matter how many structures there are. dustmass = np.hstack(dustmass) Ta_num = np.zeros((nz, ny, nx)) Ta_denum = np.zeros((nz, ny, nx)) for idx in range(len(a)): Ta_num += self.grid.temperature[0][idx, :, :, :]*(dust_density[idx, :, :, :]/dustmass[idx])*a[idx]**2 Ta_denum += (dust_density[idx, :, :, :]/dustmass[idx])*a[idx]**2 self.Ta = Ta_num/Ta_denum #Ta is the area-weighted dust temperature. else: self.Ta = self.grid.temperature[0][0,:,:,:] else: print(f"\n{temperaturefile} exist but is empty or corrupted.\n") else: print(f"WARNING: {temperaturefile} does not exist.") else: print(f"WARNING: {densityfile} does not exist.") if os.path.exists(starfile): nb_lam, lam, r_star, m_star, Tstar, spectrum = radmc3d.read.stars(thermpath) #read stars file. else: print(f"WARNING: {starfile} does not exist.")
#---- END OF READ SECTION
[docs] def run_line(self, make_image=True, \ write_control=False, \ lines_format='leiden', \ path='chemistry/', #relative or absolute path\ incl=90, \ npix=800, \ iline=None, \ lambda_micron=None, \ widthkms=10, \ linenlam=100, \ itime=59, \ species='CO', \ **keywords): """Run line radiative transfer and optionally produce a channel map image. Parameters ---------- make_image : bool, optional If True, generate a synthetic image via RADMC-3D. Default is True. write_control : bool, optional If True, write the RADMC-3D control file before running. Default is False. lines_format : str, optional Format of the molecular data file (e.g., 'leiden'). Default is 'leiden'. path : str, optional Relative or absolute path to the chemistry model directory. Default is 'chemistry/'. incl : float, optional Disk inclination angle in degrees. Default is 90. npix : int, optional Number of pixels per side of the output image. Default is 800. iline : int or None, optional Line transition index for RADMC-3D. Default is None. lambda_micron : float or None, optional Wavelength in microns at which to produce the image. Default is None. widthkms : float, optional Velocity width of the line channel map in km/s. Default is 10. linenlam : int, optional Number of wavelength channels across the line. Default is 100. itime : int, optional Index of the chemistry time output to use. Default is 59. species : str, optional Chemical species for the line transfer (e.g., 'CO'). Default is 'CO'. **keywords Additional keyword arguments forwarded to ``run_image_radmc3d``. """ thermpath = self.thermalpath if make_image == True: self.run_image_radmc3d(incl=incl, npix=npix, iline=iline, lambda_micron=lambda_micron, **keywords)
[docs] def convert_nautilus2radmc(self, species='CO', dust_density=False, dust_temperature=False, numberdens=False): """Convert Nautilus chemistry output to RADMC-3D input files. Interpolates chemistry model results onto the radiative transfer grid and writes the corresponding RADMC-3D input files. The chemistry model must first be attached via ``add_chemistry()``. An ``amr_grid.inp`` file must already exist (or the grid must be set) because the chemistry grid is typically at lower resolution than the RT grid and requires interpolation. Parameters ---------- species : str or list of str, optional Chemical species to convert (e.g., 'CO', 'H2O'). Used for creating ``numberdens_XX.inp``. Default is 'CO'. dust_density : bool, optional If True, write a ``dust_density.inp`` file. Default is False. dust_temperature : bool, optional If True, write a ``dust_temperature.inp`` file. Default is False. numberdens : bool, optional If True, write a ``numberdens_XX.inp`` file for the given species. Default is False. """ thermpath = self.thermalpath if os.path.isfile(thermpath + 'amr_grid.inp'): nx, ny, nz, x, y, z = radmc3d.read.grid(thermpath) # read grid. the amr_grid.inp shows the border of the cells so we convert them. self.grid.set_spherical_grid(r_edge=np.array(x)/autocm, theta_edge=y, phi_edge=z) x, y, z = self.grid.r, self.grid.theta, self.grid.phi elif hasattr(self.grid, 'r_edge') and self.grid.r_edge is not None: nx, ny, nz, x, y, z = self.grid.nr, self.grid.ntheta, self.grid.nphi, self.grid.r, self.grid.theta, self.grid.phi nx = nx-1 ny = ny-1 nz = nz-1 else: print(f"Error: amr_grid not found. Make sure there is one (example: create one using grid.set_spherical_grid())") sys.exit(1) # Exit the program with a non-zero status to indicate an error if numberdens==True: species_list = [species] if isinstance(species, str) else list(species) for sp in species_list: numberdens_sph = nautilus.coupling.to_spherical(self.grid.chemmodel[sp], nx, ny, x, y, struct='numberdens_species') radmc3d.write.numberdens_mol(numberdens_sph, species=sp, gridstyle="regular", thermpath=thermpath) if dust_density==True: print('create dust_density.inp') if dust_temperature == True: print('create dust_temperature.inp')
[docs] def run_localfield(self, nphot_mono=None, write_mcmono=False, run=True, **keywords): """Compute the local mean radiation field with RADMC-3D monochromatic Monte Carlo. Optionally writes the monochromatic wavelength file, runs the RADMC-3D monochromatic Monte Carlo, and reads the resulting ``mean_intensity.out`` into ``self.grid.localfield``. Parameters ---------- nphot_mono : int or None, optional Number of monochromatic photon packages. If None, RADMC-3D uses its default. Default is None. write_mcmono : bool, optional If True, write the ``mcmono_wavelength_micron.inp`` file before running. Default is False. run : bool, optional If True, execute the RADMC-3D monochromatic run. Set to False to skip execution and only read existing output. Default is True. **keywords Additional keyword arguments forwarded to ``write_continuum``. """ self.write_continuum(mcmono=write_mcmono, **keywords) if run == True: self.run_localfield_radmc3d(nphot_mono=nphot_mono) thermpath = self.thermalpath nx, ny, nz, x, y, z = radmc3d.read.grid(thermpath) nlam_mono, lam_mono, self.grid.localfield = radmc3d.read.localfield(thermpath) lam_mono = 1e6*(c/lam_mono) self.grid.localfield = np.reshape(self.grid.localfield, (nlam_mono, nz, ny, nx))
[docs] def run_thermal_radmc3d(self, verbose=True, timelimit=7200, \ nice=None, **keywords): """Run the RADMC-3D thermal Monte Carlo dust radiative transfer. Parameters ---------- verbose : bool, optional If True, print RADMC-3D output to stdout. Default is True. timelimit : int, optional Maximum wall-clock time in seconds before the run is killed. Default is 7200 (2 hours). nice : int or None, optional Unix nice priority for the RADMC-3D process. Default is None. **keywords Additional keyword arguments (currently unused). """ radmc3d.run.thermal(verbose=verbose, timelimit=timelimit, nice=nice, thermpath=self.thermalpath, radmc3d_cmd=self.radmc3d_cmd)
[docs] def run_chemistry(self, verbose=True, timelimit=None): """Run the NMGC gas-grain chemistry simulation. Calls ``nmgc run`` in ``self.chempath``, which must already contain all required NMGC input files. Set ``self.nmgc_cmd`` beforehand if the ``nmgc`` binary is not on PATH. Parameters ---------- verbose : bool, optional If True, NMGC output streams to the terminal. If False, it is redirected to ``<chempath>/nmgc.out``. Default is True. timelimit : float or None, optional Timeout in seconds. None means no timeout. Default is None. """ nautilus.run.run(chempath=self.chempath, nmgc_cmd=self.nmgc_cmd, verbose=verbose, timelimit=timelimit)
[docs] def run_chemistry_custompath(self, custom_path, verbose=True, timelimit=None): """Run the NMGC gas-grain chemistry simulation. Calls ``nmgc run`` in ``custom_path``, which must already contain all required NMGC input files. Set ``self.nmgc_cmd`` beforehand if the ``nmgc`` binary is not on PATH. Parameters ---------- verbose : bool, optional If True, NMGC output streams to the terminal. If False, it is redirected to ``<chempath>/nmgc.out``. Default is True. timelimit : float or None, optional Timeout in seconds. None means no timeout. Default is None. """ nautilus.run.run(chempath=custom_path, nmgc_cmd=self.nmgc_cmd, verbose=verbose, timelimit=timelimit)
[docs] def run_localfield_radmc3d(self, nphot_mono=None, verbose=True, timelimit=7200): """Run the RADMC-3D monochromatic Monte Carlo for the local radiation field. Parameters ---------- nphot_mono : int or None, optional Number of monochromatic photon packages. If None, RADMC-3D uses its default. Default is None. verbose : bool, optional If True, print RADMC-3D output to stdout. Default is True. timelimit : int, optional Maximum wall-clock time in seconds. Default is 7200 (2 hours). """ radmc3d.run.localfield(nphot_mono=nphot_mono, verbose=verbose, timelimit=timelimit, thermpath=self.thermalpath, radmc3d_cmd=self.radmc3d_cmd)
[docs] def run_image_radmc3d(self, npix=300, lambda_micron=None, iline=None, incl=None, verbose=True): """Run RADMC-3D to produce a synthetic image. Parameters ---------- npix : int, optional Number of pixels per side of the image. Default is 300. lambda_micron : float or None, optional Wavelength of the image in microns. Default is None. iline : int or None, optional Line transition index. Default is None. incl : float or None, optional Inclination angle in degrees. Default is None. verbose : bool, optional If True, print RADMC-3D output to stdout. Default is True. """ radmc3d.run.image(npix=npix, lambda_micron=lambda_micron, iline=iline, incl=incl, verbose=verbose, timelimit=7200, thermpath=self.thermalpath, radmc3d_cmd=self.radmc3d_cmd)
[docs] def write_continuum(self, dens=False, grid=False, opac=False, control=False, stars=False, wave=False, mcmono=False, ext=False): """Write RADMC-3D input files for dust continuum radiative transfer. Each boolean flag controls whether the corresponding input file is written. If no flags are set to True, a warning is printed. The thermal directory is created if it does not exist. Parameters ---------- dens : bool, optional Write ``dust_density.inp``. Default is False. grid : bool, optional Write ``amr_grid.inp``. Default is False. opac : bool, optional Write ``dustopac.inp`` (reads existing ``dustkap*`` files in the thermal directory). Default is False. control : bool, optional Write ``radmc3d.inp`` control file. Default is False. stars : bool, optional Write ``stars.inp`` with stellar properties. Default is False. wave : bool, optional Write ``wavelength_micron.inp``. Default is False. mcmono : bool, optional Write ``mcmono_wavelength_micron.inp`` for monochromatic Monte Carlo. Default is False. ext : bool, optional Write ``external_source.inp`` for the interstellar radiation field. Default is False. """ #os.system("rm thermal/*.inp") thermpath = self.thermalpath if not os.path.exists(thermpath): os.makedirs(thermpath) if control==True: print('\nWriting radmc3d.inp:') print('----------------------------') radmc3d.write.control(self.thermalparams.control, thermpath=thermpath) if stars==True: print('\nWriting stars.inp:') print('----------------------------') mstar = [] rstar = [] xstar = [] ystar = [] zstar = [] tstar = [] for i in range(len(self.grid.stars)): mstar.append(self.grid.stars[i].mass*M_sun) rstar.append(self.grid.stars[i].radius*R_sun) xstar.append(self.grid.stars[i].x*autocm) ystar.append(self.grid.stars[i].y*autocm) zstar.append(self.grid.stars[i].z*autocm) tstar.append(self.grid.stars[i].temperature) radmc3d.write.stars(rstar, mstar, self.grid.lam, xstar, ystar, zstar, \ tstar=tstar, thermpath=thermpath) if wave==True: print('\nWriting wavelength_micron.inp:') print('----------------------------') radmc3d.write.wavelength_micron(self.grid.lam, thermpath=thermpath) if mcmono==True: print('\nWriting mcmono_wavelength_micron.inp:') print('----------------------------') radmc3d.write.mcmono_wavelength_micron(self.grid.monolam, thermpath=thermpath) if ext==True: if len(self.grid.isrf) != 0: radmc3d.write.external_rad(self.grid.isrf[0], thermpath=thermpath) if grid==True: print('\nWriting amr_grid.inp:') print('----------------------------') if self.grid.coordsystem == 'spherical': radmc3d.write.amr_grid(self.grid.r_edge*autocm, self.grid.theta_edge, self.grid.phi_edge, gridstyle="regular", coordsystem=self.grid.coordsystem, thermpath=thermpath) if dens==True: print('\nWriting dust_density.inp:') print('----------------------------') radmc3d.write.dust_density(self.grid.dustdensity, gridstyle="regular", thermpath=thermpath) if opac==True: print('\nWriting dustopac.inp:') print('----------------------------') dustopac = [] filelist = glob.glob(thermpath + 'dustkap*') for files in sorted(filelist): dustopac.append(files) radmc3d.write.dustopac(dustopac, thermpath=thermpath) if len(self.grid.accretionheating) > 0: radmc3d.write.accretion_heating(self.grid.w1*autocm, self.grid.w2, self.grid.w3, self.grid.accretionheating[0], gridstyle="regular", thermpath=thermpath) if dens == False and grid == False and control == False and opac == False and stars == False and wave == False: print('\nWARNING: no RADMC3D file are created.\n') print('----------------------------\n')
[docs] def write_line(self, control=False, line=False, gasvelocity=False, gastemp=False, microturb=False, line_format='leiden', species='CO', star_mass=1): """Write RADMC-3D input files for line radiative transfer. Parameters ---------- control : bool, optional Write ``radmc3d.inp`` control file. Default is False. line : bool, optional Write ``lines.inp`` molecular line data file. Default is False. gasvelocity : bool, optional Write ``gas_velocity.inp`` assuming Keplerian rotation. Default is False. gastemp : bool or str, optional Controls gas temperature handling. - ``False`` (default): no ``gas_temperature.inp`` is written. Use ``pipe.thermalparams.control.tgas_eq_tdust = 1`` before ``write_line(control=True)`` to let RADMC-3D equate gas and dust temperatures internally. - ``'1D_static'``: read the gas temperature column ``Tg`` from each ``<radius>AU/1D_static.dat`` file in the chemistry directory, remap it to the spherical RADMC-3D grid via bilinear interpolation (the same method as :meth:`convert_nautilus2radmc`), and write ``gas_temperature.inp``. This reproduces the gas temperature that was actually fed into Nautilus and is independent of the chemical timestep. microturb : bool, optional Write microturbulence file (not yet implemented). Default is False. line_format : str, optional Format of the molecular data file (e.g., 'leiden'). Default is 'leiden'. species : str, optional Chemical species for the line file. Default is 'CO'. star_mass : float, optional Stellar mass in solar masses, used for Keplerian velocity computation. Default is 1. """ thermpath = self.thermalpath if not os.path.exists(thermpath): os.makedirs(thermpath) if control==True: print('\nWriting radmc3d.inp:') print('----------------------------') radmc3d.write.control(self.thermalparams.control, thermpath=thermpath) if line==True: print('\nWriting lines.inp:') print('----------------------------') radmc3d.write.lines(species=species, format=line_format, thermpath=thermpath) if gasvelocity == True: print('\nWriting gas_velocity.inp:') print('----------------------------') radmc3d.write.gas_velocity(star_mass=star_mass, r=self.grid.r, theta=self.grid.theta, phi=self.grid.phi, object="disk", thermpath=thermpath) if gastemp == '1D_static': print('\nWriting gas_temperature.inp from 1D_static.dat:') print('----------------------------') chempath = self.chempath # Discover chemistry radii from folder names au_folders = sorted( [d for d in os.listdir(chempath) if d.endswith('AU') and os.path.isdir(os.path.join(chempath, d))], key=lambda x: int(x.replace('AU', '')) ) if not au_folders: print(' [write_line] no AU/ folders found in chempath — skipping gas_temperature.inp') else: # Build a chemmodel-style dict for Tg, keyed by radius in AU # Columns in 1D_static.dat: z(0) nH(1) Tg(2) Av(3) ... tg_chemmodel = {} for folder in au_folders: r_au = int(folder.replace('AU', '')) static_file = os.path.join(chempath, folder, '1D_static.dat') if not os.path.isfile(static_file): print(f' [write_line] {folder}/1D_static.dat not found — skipping') continue data = np.loadtxt(static_file, comments='!') tg_chemmodel[r_au] = { 'z': data[:, 0], # AU, surface → midplane (descending) 'Tg': data[:, 2], # gas temperature [K] } if not tg_chemmodel: print(' [write_line] no valid 1D_static.dat files found — skipping') else: # Read spherical grid (same pattern as convert_nautilus2radmc) nx, ny, nz, x, y, z = radmc3d.read.grid(thermpath) self.grid.set_spherical_grid(r_edge=np.array(x)/autocm, theta_edge=y, phi_edge=z) x, y = self.grid.r, self.grid.theta tg_sph = nautilus.coupling.to_spherical( tg_chemmodel, nx, ny, x, y, struct='Tg' ) # Replace zero/negative cells (inner cavity, outer edge) with a floor of 10 K tg_sph = np.where(tg_sph <= 0, 10.0, tg_sph) radmc3d.write.gas_temperature(tg_sph, thermpath=thermpath)
[docs] def write_nautilus(self, sizes=np.array([[0.1]]), uv_ref=3400, nH_to_AV_conversion=1.600e+21, rsingle=0.1, dtogas=1e-2, ref_radius=100, stop_time=3e6, nb_outputs = 64, tunneling=1, is_h2_formation_rate=0, min_gas_density=1e0, min_av=1e-2, max_uv=None, cap_uv_floor=True, cut_cap=True, max_inv_ab=None, exclude_bins=None, temp_gas='dust', static=True, param=True, element=True, abundances='atomic', network=True, multi_grain=True, tempdecoup=True, coupling_dens=False, coupling_temp=True, coupling_av=True, **keywords): """Write Nautilus chemistry input files for each radial point. Reads the RADMC-3D thermal output (grid, dust density, dust temperature, local radiation field), couples the physical quantities onto the chemistry grid, and writes one set of Nautilus input files per radial grid point inside the chemistry directory. The existing chemistry directory is removed and recreated. When ``coupling_dens`` is True the code derives dust and gas number densities from the RADMC-3D dust density rather than from an analytically added disk/envelope structure. Parameters ---------- sizes : ndarray, optional Grain size array in microns, shape ``(n_structures, n_bins)``. Default is ``np.array([[0.1]])``. uv_ref : float, optional Reference UV field strength in Habing units. Default is 3400. nH_to_AV_conversion : float, optional Column density to visual extinction conversion factor in cm^-2. Default is 1.6e21. rsingle : float, optional Single representative grain radius in microns. Default is 0.1. dtogas : float, optional Dust-to-gas mass ratio. Default is 1e-2. ref_radius : float, optional Reference radius in AU for the UV scaling. Default is 100. stop_time : float, optional Chemical evolution stop time in years. Default is 3e6. nb_outputs : int, optional Number of time outputs written by Nautilus. Default is 64. tunneling : int, optional Tunneling flag for Nautilus (0 or 1). Default is 1. is_h2_formation_rate : int, optional H2 formation rate flag for Nautilus. Default is 0. min_gas_density : float, optional Minimum gas number density in cm^-3 enforced in the chemistry input. Default is 1. min_av : float, optional Minimum visual extinction in mag enforced in the chemistry input. Default is 1e-2. max_uv : float or None, optional Maximum UV field value. If None, no cap is applied. Default is None. cap_uv_floor : bool, optional If True (default), cap the UV factor at 10 Habing for cells at the gas density floor. Set to False to keep the full geometric UV value in those cells (removes the discontinuity at the floor-density boundary visible when plotting the uv column). temp_gas : str, optional Gas temperature source: 'dust' uses the area-weighted dust temperature, 'param' uses a parametrized gas temperature added to the grid. Default is 'dust'. static : bool, optional Write the ``1D_static.dat`` (or multi-grain equivalent) input file. Default is True. param : bool, optional Write the Nautilus ``parameters.in`` file. Default is True. element : bool, optional Write the ``element.in`` file. Default is True. abundances : str, optional Initial abundances preset name (e.g., 'atomic'). Default is 'atomic' for solar-composition. It can also be a filepath. network : bool or str, optional Controls network file copying. ``False`` skips it entirely. ``True`` (default) copies from the built-in ``astromugs/nautilus/network/`` directory. A string path copies from that directory instead (e.g. ``network='/path/to/my_network'``). multi_grain : bool, optional If True, use multi-grain chemistry mode (NMGC). Default is True. tempdecoup : bool, optional If True, compute a separate area-weighted dust temperature from the full size distribution. If False, use the single-bin temperature. Default is True. coupling_dens : bool, optional If True, derive dust and gas densities from the RADMC-3D dust density file. Default is False. coupling_temp : bool, optional If True, interpolate dust temperature from the RADMC-3D output onto the chemistry grid. Default is True. coupling_av : bool, optional If True, compute visual extinction from the local radiation field. Default is True. **keywords Any keyword argument accepted by :func:`~astromugs.nautilus.write.parameters_nmgc` can be passed here and will be forwarded verbatim to that function. This lets you override any entry in the ``parameters.in`` file without subclassing. See *Other Parameters* below for the full list. Other Parameters ---------------- phase : int, optional Chemical phase model. ``0`` = 2-phase, ``1`` = 3-phase (default ``1``). cr_ionisation_rate : float, optional Cosmic-ray ionisation rate [s⁻¹]. Default ``1.3e-17``. x_ionisation_rate : float, optional X-ray ionisation rate [s⁻¹]. Default ``0.0``. is_photodesorb : int, optional Photodesorption of ices: ``1`` = on (default), ``0`` = off. is_crid : int, optional Cosmic-ray induced diffusion (CRID): ``1`` = on, ``0`` = off (default). is_er_cir : int, optional Eley-Rideal and complex-induced reactions: ``1`` = on, ``0`` = off (default). is_absorption_h2 : int, optional H₂ self-shielding (Lee & Herbst 1996): ``1`` = on (default). is_absorption_co : int, optional CO self-shielding method. ``1`` = Lee & Herbst (1996), ``2`` = Visser et al. (2009) (default ``2``). is_absorption_n2 : int, optional N₂ self-shielding (Li et al. 2013): ``1`` = on (default). nb_active_lay : float, optional Number of chemically active grain surface layers. Default ``2.0``. diff_binding_ratio_surf : float, optional Ratio of diffusion barrier to binding energy for surface species. Default ``0.4``. diff_binding_ratio_mant : float, optional Ratio of diffusion barrier to binding energy for mantle species. Default ``0.8``. cr_peak_grain_temp : float, optional Peak grain temperature reached during a cosmic-ray heating event [K]. Default ``70.0``. cr_peak_duration : float, optional Duration of a cosmic-ray heating peak [s]. Default ``1e-5``. Fe_ionisation_rate : float, optional Cosmic Fe-ion–grain encounter rate [s⁻¹ grain⁻¹]. Default ``3e-14``. sticking_coeff_neutral : float, optional Sticking coefficient for neutral species. Default ``1.0``. sticking_coeff_positive : float, optional Sticking coefficient for positively charged species. Default ``0.0``. sticking_coeff_negative : float, optional Sticking coefficient for negatively charged species. Default ``0.0``. surface_site_density : float, optional Surface site density on a grain [cm⁻²]. Default ``8e14``. chemical_barrier_thickness : float, optional Grain reaction activation energy barrier width [cm]. Default ``1e-8``. relative_tolerance : float, optional Relative tolerance of the ODE solver. Default ``1e-4``. output_type : str, optional Output time spacing: ``'log'`` (default) or ``'linear'``. start_time : float, optional First output time [yr]. Default ``1.0``. modify_rate_flag : int, optional Rate modification flag. ``0`` = none (default), ``1`` = H only, ``2`` = H + H₂, ``3`` = all species. ED_H2 : float, optional H₂ binding energy on itself [K]. Default ``23.0``. vib_to_dissip_freq_ratio : float, optional Ratio of surface-bond vibrational frequency to energy dissipation frequency (RRK desorption mechanism). Default ``1e-2``. """ #----------------------------------------- # REMOVE IF EXISTS AND CREATE CHEMISTRY FOLDER #----------------------------------------- thermpath = self.thermalpath chempath = self.chempath if os.path.exists(chempath): shutil.rmtree(chempath) os.makedirs(chempath) #----------------------------------------- # READ THERMAL FILES (just like in function thermal, but sometimes the user does not need to run thermal so this reads the thermal files even though they might already be read by thermal) #----------------------------------------- nx, ny, nz, x, y, z = radmc3d.read.grid(thermpath) # read grid # x is r_edges in cm, y is theta_edges, z is phi_edges self.grid.set_spherical_grid(r_edge=np.array(x)/autocm, theta_edge=y, phi_edge=z) dust_density = radmc3d.read.dust_density(thermpath) # read dust_density file nbspecies = int(len(dust_density[0])/(nx*ny*nz)) #get number of species dust_density = np.reshape(dust_density[0], (nbspecies, nz, ny, nx)) #reshape it dust_density[dust_density<=1e-100] = 1e-100 # get a minimum dust density in order to avoid absurd NaN issues. nb_lam, lam, r_star, m_star, T_star, spectrum = radmc3d.read.stars(thermpath) #read stars file. external = radmc3d.read.external_source(thermpath) #read external_source.inp # define temperature self.grid.temperature = radmc3d.read.dust_temperature(thermpath) if len(self.grid.temperature) > 0: self.grid.temperature[0] = np.reshape(self.grid.temperature[0], (nbspecies, nz, ny, nx)) a = [] dustmass = [] for istruc in range(0, len(self.grid.dust)): # create a loop in order to gather all grain sizes into one single array that will be used to compute the area-weighted temperature Ta. dustmodel = self.grid.dust[istruc] sizes = dustmodel.sizes() rho_m = dustmodel.rho_m #a = sizes[-1]*1e-4 # cm a.append(sizes[-1]*1e-4) # convert to cm dustmass.append(dustmodel.grainmass()) # gram a = np.hstack(a) # in order to get a numpy list of all sizes no matter how many structures there are. dustmass = np.hstack(dustmass) if tempdecoup == True: Ta_num = np.zeros((nz, ny, nx)) Ta_denum = np.zeros((nz, ny, nx)) for idx in range(len(a)): Ta_num += self.grid.temperature[0][idx, :, :, :]*(dust_density[idx, :, :, :]/dustmass[idx])*a[idx]**2 Ta_denum += (dust_density[idx, :, :, :]/dustmass[idx])*a[idx]**2 self.Ta = Ta_num/Ta_denum #Ta is the area-weighted dust temperature. else: self.Ta = self.grid.temperature[0][0,:,:,:] else: print('\nNo dust temperature file was found. If coupling_temp is True the chemistry model cannot be created.\n\n') nlam_mono, lam_mono, self.grid.localfield = radmc3d.read.localfield(thermpath) lam_mono = 1e6*(c/lam_mono) #from Hz to microns. Should be same number as in mcmono_wavelength.inp. isrf = InterstellarRadFields(cut=2.e-1, d78=True, vdb82=False) #calculate Draine isrf. if len(self.grid.localfield) > 0: try: self.grid.localfield = np.reshape(self.grid.localfield, (nlam_mono, nz, ny, nx)) except IOError: print('\nPlease, check consistency between the grid size and mean_intensity.out.\n') sys.exit(1) else: print('Warning: No mean_intensity.out file was found. If coupling_av is True then the chemistry model cannot be created. Please check mean_intensity.out file or set coupling_av=False.') #----------------------------------------- # COUPLING ROUTINES #----------------------------------------- if coupling_dens == True: n_dust, n_gas = nautilus.coupling.dust_density(dtogas, rho_m, a, dust_density, self.grid.rchem*autocm, self.grid.zchem*autocm, self.grid.r, self.grid.theta) # If custom sigma, override n_gas with gas density from the custom file, because the n_gas is derived from dtogas in the coupling, which is wrong if sigma_gas is custom. if self.params.disk.sigma_compute == 'custom' and len(self.grid.hg_chem) == 0: r_custom, _, siggas_table, _ = custom_io.surfacedensities(self.params.disk.sigma_path) temp_disk = Disk(params=self.params.disk, dust=self.grid.dust[0]) hg = temp_disk.scaleheight(self.grid.rchem) # cm for idx, r in enumerate(self.grid.rchem): sig_g = np.interp(r, r_custom, siggas_table) z_cm = self.grid.zchem[idx, :] * autocm n_gas[idx, :] = (sig_g / (np.sqrt(2*np.pi) * hg[idx] * mu * amu)) * np.exp(-z_cm**2 / (2*hg[idx]**2)) if coupling_temp == True: if not self.grid.temperature: print('coupling_temp==True: The file thermal/dust_temperature.dat is not present or is corrupted. Chemistry model cannot created.') sys.exit(1) if len(self.grid.hg_chem) > 0: T_dust = nautilus.coupling.dust_temperature_disk(self.grid.temperature[0], self.grid.rchem*autocm, self.grid.zchem, self.grid.r, self.grid.theta, self.grid.hg_chem[0]) # dim(a, rchem, zchem) T_dust_single = nautilus.coupling.dust_temperature_single_disk(self.Ta, self.grid.rchem*autocm, self.grid.zchem, self.grid.r, self.grid.theta, self.grid.hg_chem[0]) else: T_dust = nautilus.coupling.dust_temperature(self.grid.temperature[0], self.grid.rchem*autocm, self.grid.zchem*autocm, self.grid.r, self.grid.theta) # dim(a, rchem, zchem) T_dust_single = nautilus.coupling.dust_temperature_single(self.Ta, self.grid.rchem*autocm, self.grid.zchem*autocm, self.grid.r, self.grid.theta) else: T_dust = np.expand_dims(self.grid.tgas_chem[0], axis=0) # expend to one extra dimension in order to match the shape of coupled T_dust. if coupling_av == True: if len(self.grid.hg_chem) > 0: av_z = nautilus.coupling.avz_disk(self.grid.localfield, lam_mono, r_star, T_star, self.grid.rchem*autocm, self.grid.zchem, self.grid.r, self.grid.theta, self.grid.hg_chem[0]) # dim(rchem, zchem) else: av_z = nautilus.coupling.av_z(self.grid.localfield, lam_mono, r_star, T_star, self.grid.rchem*autocm, self.grid.zchem*autocm, self.grid.r, self.grid.theta) # dim(rchem, zchem) else: av_z = self.grid.avz[0] #works only if a disk or envelope model is created before. #----------------------------------------- # WRITE NAUTILUS INPUT FILES FOR EACH RADIUS #----------------------------------------- print('WRITING NAUTILUS INPUT FILES:') print('-----------------------------\n') # write input NAUTILUS files if multi_grain == True: #if multi_grain is True, then the code assumes the user will use the NMGC version of Nautilus in multi-grain mode. print('Multi-grain mode: Yes\n') print('Dust temperature structure: multiple dust temperatures (dust parameters stored in 1D_grain_sizes.in).\n') else: print('Multi-grain mode: No\n') #if ngmc is False, Nautilus with a single grain bin is assumed to be used. This can be true even if the thermal model has multiple dust bins. print('Dust temperature structure: single or area-weigthed (dust parameters stored in 1D_static.dat).\n') for idx, r in enumerate(self.grid.rchem): path = chempath + '/' + str(int(r)) + 'AU/' os.makedirs(path, exist_ok=False) #---temporary defining cavity to increase the Av in that area so we don't have convergence issue. This should be removed in the main branch. z0 = 200 phi = (16*np.pi)/180 zcav = z0*(r/(z0*np.tan(phi/2)))**1.55 ###!!!! maybe add a factor to account for the excess of UV in the protostar spectrum? if len(self.grid.hg_chem) > 0: uvfactor = nautilus.write.uv_factordisk(uv_ref, ref_radius, r, self.grid.hg_chem[0][idx]/autocm) else: uvfactor = nautilus.write.uv_factor(isrf, lam_mono, r_star, T_star, r*autocm, self.grid.zchem[idx,:]*autocm, external) avnh_fact = nautilus.write.avnh_factor(nH_to_AV_conversion, dtogas, rsingle, self.grid.nz_chem) if temp_gas == 'dust': T_gas = T_dust_single[idx,:] elif temp_gas == 'param': if len(self.grid.tgas_chem) == 0: print("because you set temp_gas='param', you have to add a parametrized Tgas in the model first.") sys.exit(1) else: T_gas = self.grid.tgas_chem[0][idx] if multi_grain == False: nz_actual = self.grid.nz_chem # default if static not written if static == True: if len(self.grid.hg_chem) > 0: dist = self.grid.zchem*self.grid.hg_chem[0][idx]/autocm nH = self.grid.gasdensity_chem[0][idx,:] nd = self.grid.dustdensity_single_chem[0][idx,:] elif coupling_dens == True: dist = self.grid.zchem[idx, :] nH = n_gas[idx, :] md = (4/3)*np.pi*rho_m*(rsingle*1e-4)**3 nd = dtogas*n_gas[idx, :]*amu*mu/md nz_actual = nautilus.write.static(path, \ dist, \ nH, \ T_gas, \ av_z[idx, :], \ T_dust_single[idx,:], \ nd, \ rsingle, \ avnh_fact, uvfactor, min_gas_density=min_gas_density, min_av=min_av, max_uv=max_uv, cap_uv_floor=cap_uv_floor, cut_cap=cut_cap, rho_m=rho_m) if param == True: nautilus.write.parameters_nmgc(path, grain_temp='table_1D', nb_outputs=nb_outputs, multi_grain=0, tunneling=tunneling, is_h2_formation_rate=is_h2_formation_rate, resolution=nz_actual, stop_time=stop_time, uv_flux=np.mean(uvfactor), **keywords) if multi_grain == True: nz_actual = self.grid.nz_chem # default if static not written if static == True: if len(self.grid.hg_chem) > 0: dist = self.grid.zchem*self.grid.hg_chem[0][idx]/autocm nH = self.grid.gasdensity_chem[0][idx,:] nd = self.grid.dustdensity_single_chem[0][idx,:] elif coupling_dens == True: dist = self.grid.zchem[idx, :] nH = n_gas[idx, :] md = (4/3)*np.pi*rho_m*(rsingle*1e-4)**3 nd = dtogas*n_gas[idx, :]*amu*mu/md ##!!!!!!! TO BE REMOVED !!!!!!! #av_z[idx, :] = np.where(dist>zcav, av_z[idx, :]*10, av_z[idx, :]) ##!!!!!!!!!!!!!!!!!!!!!!!!!!!!! nz_actual = nautilus.write.static(path, \ dist, \ nH, \ T_gas, \ av_z[idx, :], \ T_dust_single[idx,:], #if nbspecies > 1, the column is not read.\ nd, #if nbspecies > 1, the column is not read. \ rsingle, #if nbspecies > 1, the column is not read. \ avnh_fact, uvfactor, min_gas_density=min_gas_density, min_av=min_av, max_uv=max_uv, cap_uv_floor=cap_uv_floor, cut_cap=cut_cap, rho_m=rho_m) if param == True: nautilus.write.parameters_nmgc(path, grain_temp='fixed_to_dust_size', nb_outputs=nb_outputs, multi_grain=1, resolution=nz_actual, tunneling=tunneling, is_h2_formation_rate=is_h2_formation_rate, stop_time=stop_time, uv_flux=np.mean(uvfactor), **keywords) if nbspecies > 1: if len(self.grid.hg_chem) > 0: nH = self.grid.gasdensity_chem[0][idx,:] nd = self.grid.dustdensity_chem[0][:,idx,:] elif coupling_dens == True: nH = n_gas[idx, :] nd = n_dust[:, idx, :] nautilus.write.grain_sizes(path, sizes, nH, nd, T_dust[:,idx,:], min_gas_density=min_gas_density, cut_cap=cut_cap, max_inv_ab=max_inv_ab, exclude_bins=exclude_bins, rho_m=rho_m) else: print('WARNING: multi_grain = True, but the model has only one grain bin. Please, check the number of grain size or switch multi_grain to False.') nautilus.write.abundances(path, abundances) if network: network_path = network if isinstance(network, str) else None nautilus.write.network(path, network_path=network_path) if element == True: nautilus.write.elements(path)