Gas-grain simulations#
Write NAUTILUS input files#
Writing Nautilus input files requires three preparatory steps before calling
write_nautilus(): adding a dust model to the grid, defining the radial positions
of the chemistry columns, and setting the vertical grid for each column. Each step is
described below.
Step 1 — Add a dust model#
Important
A dust model must be attached to the pipeline before any chemistry input
files can be written. Nautilus works with grain number densities and therefore
needs to know the grain sizes and material density in order to compute
the dust-to-gas abundance ratios written to 1D_static.dat and
1D_grain_sizes.in.
Use CustomDistrib or :class:`~astromugs.dust.MRNDistrib`to load grain sizes from a file (e.g.
produced by a DustPy simulation) and register the model on the grid:
from astromugs import dust
d = dust.CustomDistrib(
rho_m = 1.67, # grain material density [g cm⁻³]
filename = "outputs/dust_sizes.inp", # one grain size per line, in microns
units = 'microns'
)
pipe.grid.add_dust(d)
rho_m is the bulk grain material density in g cm-3. Common values are
1.5–3.3 g cm-3 depending on composition (silicates, carbonaceous grains,
ice mixtures). filename points to a plain-text file with one grain size per line
in the units specified by units ('microns', 'mm', 'cm', or 'm').
Step 2 — Define the radial grid#
The radial positions of the chemistry columns are a plain NumPy array of radii in AU. Choose them to sample the physical gradients of your disk model — logarithmic spacing works well over a wide radial range:
import numpy as np
r_chem = np.array([5, 10, 15, 20, 30, 50, 75, 100, 150, 200, 300]) # AU
Step 3 — Set the vertical chemistry grid#
Two methods are available to build the vertical grid.
set_chemdisk_grid — scale-height based (recommended for disk models)#
The grid runs from max_H gas scale heights down to the midplane.
The number of vertical points nz_chem is the same at all radii.
pipe.grid.set_chemdisk_grid(
r = r_chem, # radial positions [AU]
max_H = 4, # upper boundary in gas scale heights (default: 4)
nz_chem = 64, # number of vertical points (default: 64)
stretch = 1.5 # > 1 concentrates points near the midplane (default: 1.0)
)
The stretch parameter redistributes the vertical points without changing their
total number: stretch > 1 packs resolution near the midplane where density
gradients are steepest; stretch < 1 packs resolution near the disk surface.
set_chem_grid — altitude based (envelope or custom geometries)#
When the maximum altitude cannot be expressed as a multiple of the scale height
(e.g. envelope models, or when coupling directly to a hydrodynamical grid), use
set_chem_grid instead. Two modes are available:
Uniform ceiling (
zmaxin AU): the same maximum altitude at every radius.Spherical ceiling (
msizein AU): the maximum altitude at each radius follows \(z_\mathrm{max}(r) = \sqrt{r_\mathrm{model}^2 - r^2}\), giving a spherical outer boundary.
# Uniform ceiling — same zmax at all radii
pipe.grid.set_chem_grid(r=r_chem, zmax=200, nbcells=70, stretch=1.0)
# Spherical ceiling — outer boundary is a sphere of radius msize AU
pipe.grid.set_chem_grid(r=r_chem, msize=300, nbcells=70, stretch=1.0)
In both cases, nbcells sets the number of vertical points and stretch controls
the spacing in the same way as for set_chemdisk_grid.
Note
Vertical coordinates are stored in decreasing order (surface → midplane) as required by Nautilus. Both grid methods handle this automatically.
Step 4 — Write Nautilus input files#
With the dust model and grids in place, call write_nautilus() to generate one
complete set of Nautilus input files per radial column:
pipe.write_nautilus(
stop_time = 1e6, # chemistry integration time [yr]
nb_outputs = 64, # number of output snapshots (log-spaced by default)
multi_grain = True, # use NMGC multi-grain mode
network = '/path/to/network', # copy network files from this directory
abundances = 'atomic', # initial abundance preset
)
A sub-directory named <radius>AU/ is created inside the chemistry path for each
radial column, and the following files are written into it:
1D_static.dat— vertical physical structure (gas density, temperature, \(A_V\), UV, grain properties)parameters.in(orparameters_nmgc.in) — Nautilus run control parameters1D_grain_sizes.in— grain radii, inverse abundances, and temperatures per bin (multi-grain mode)temperatures.dat— grain temperature table (multi-grain mode)abundances.in— initial chemical abundanceselement.in— elemental compositionnetwork files —
gas_species.in,grain_species.in,gas_reactions.in, etc.
Key arguments#
Argument |
Default |
Description |
|---|---|---|
|
|
Total chemistry integration time in years. |
|
|
Number of output timesteps written to |
|
|
If |
|
|
Network file source. |
|
|
Initial abundance preset. |
|
|
If |
|
|
Minimum gas number density [cm-3]. With the default |
|
|
1-indexed list of grain size bins to drop from |
Note
The complete list of arguments and their defaults is documented in the API
reference under write_nautilus(). The table
above covers only the arguments most commonly adjusted in practice.
Tip
write_nautilus() accepts **keywords that are forwarded directly to
parameters_nmgc(), which writes the
parameters.in file. This means you can override any Nautilus
parameter from your call to write_nautilus() — for example:
pipe.write_nautilus(
stop_time = 1e6,
multi_grain = True,
cr_ionisation_rate = 1.3e-16, # 10× standard CR rate
is_crid = 1, # enable CR-induced diffusion
nb_active_lay = 4.0, # 4 active surface monolayers
phase = 0, # 2-phase instead of 3-phase
)
The full list of available keywords is in the Other Parameters section of
write_nautilus().
Best practices#
Stay in the Nautilus safe zone#
Nautilus can encounter convergence issues when placed in extreme physical conditions. For example, a very strong UV field with little attenuation can cause the chemical computation to fail or make the run extremely long.
Nautilus was initially designed for gas-grain simulations in the interstellar medium. Protoplanetary disk models are particularly prone to pushing Nautilus into stiff chemistry regimes, especially when post-processing hydrodynamical simulations. Many such simulations — whether computed from a single core collapse or from a 1-D surface density profile — can produce extreme conditions in the disk upper atmosphere: very low gas densities, strong UV fields, and negligible visual extinction. Nautilus is not designed for these environments.
Here are examples where Nautilus will struggle to converge:
Very low density (\(n_\mathrm{H} \lesssim 10^2\,\mathrm{cm}^{-3}\)) combined with a strong UV field and low \(A_V\): the spread between fast photodissociation rates and slow two-body formation rates (which scale as \(n_\mathrm{H}^2\)) creates an extremely stiff system.
Near-balanced UV shielding: when the UV flux and \(A_V\) partially cancel, small abundance changes shift the shielding, which shifts the photodissociation rates, which shifts the abundances — a feedback loop that prevents convergence.
To mitigate these issues, astroMUGS provides floor and ceiling parameters (min_gas_density, min_av, max_uv)
that clamp physical quantities to user-defined bounds. These can be passed as keyword arguments to write_nautilus().
Applying these bounds should not impact the results of your simulations, because they typically apply to regions of your model that
don’t correspond to observed physical environments in stellar formation regions and/or that you are not interested in (very high altitudes, very large/small radii, etc.).
Warning
Floor and ceiling values must be chosen with care. Poorly chosen bounds can create convergence problems rather than prevent them. Here are two examples:
Av floor too high for a low-UV model. A high \(A_V\) floor pushes the chemistry into a shielded regime, which is stable when the UV field is strong. But in a low-UV model, it can place the chemistry in a transition zone where photodissociation and molecule formation rates nearly balance, creating a feedback loop that makes the solver struggle. Always consider the strength of your UV field when choosing an \(A_V\) floor.
Density floor in the wrong regime. Raising the gas density floor does not always help. At intermediate densities (e.g., \(n_\mathrm{H} \sim 10^5\,\mathrm{cm}^{-3}\)), grain-surface adsorption and desorption rates become competitive with gas-phase reactions, introducing a new source of chemical stiffness. A density floor can thus shift the chemistry from one problematic regime into another.
Note
A future version of astroMUGS will include a scaling law that ties the floor and ceiling
values of \(n_\mathrm{H}\), \(A_V\), and the UV factor together, ensuring the
chemistry always stays in a well-posed regime. This can be enabled with
safezone=True in write_nautilus(). However, this option may alter the physical
conditions of your model, so use it with care — you may prefer to set specific floor or
ceiling values manually.
Read NAUTILUS output binaries#
Once your Nautilus simulation has completed (or while it is still running — see Monitoring a running job), astroMUGS provides a high-level interface to load and explore the output.
The recommended entry point is Interface, which auto-detects
whether your chemistry directory contains a single run or a full disk model, and stores
everything in a consistent structure ready for analysis and radiative-transfer post-processing.
Loading chemistry results#
import astromugs.pipeline as pipeline
pipe = pipeline.Interface()
pipe.add_chemical_path('path_to_your_chemistry/') # path to the chemistry directory
pipe.add_chemistry() # reads all completed radii
add_chemistry() scans the chemistry directory for sub-folders whose names end with AU
(e.g. 5AU/, 30AU/, 100AU/). If such folders exist it operates in disk mode;
otherwise it falls back to single-run mode for 0-D or single-column models.
Any radius whose abundances.out is not yet present (e.g. still running on a cluster) is
silently skipped and reported in the terminal — you can call add_chemistry() again later to
pick up newly finished radii.
The optional itime argument selects the timestep used to populate
pipe.grid.chemmodel, which is consumed by convert_nautilus2radmc() for line
radiative-transfer post-processing (default: itime=-1, the last output).
pipe.add_chemistry(itime=-1) # use the last timestep for the RADMC-3D grid
Disk mode: accessing data per radius#
After add_chemistry(), results are stored in pipe.chemistry, a dictionary keyed by
radius in AU (integer):
# List all radii that were successfully loaded
print(list(pipe.chemistry.keys()))
# e.g. [5, 10, 30, 100, 200, 300]
# Access a single radius
chem_30 = pipe.chemistry[30] # dict for the 30 AU column
Each per-radius entry is itself a dictionary with the following keys:
Key |
Shape |
Description |
|---|---|---|
|
|
Output times in seconds. Divide by |
|
|
Fractional abundances \(n_X / n_\mathrm{H}\) stored as an xarray DataArray with named dimensions and coordinates (see below). |
|
|
Hydrogen number density \(n_\mathrm{H}\) [cm-3] as read back from Nautilus. |
|
|
Gas temperature [K]. |
|
|
Dust temperature [K] (first grain bin). |
|
|
Visual extinction \(A_V\) [mag]. |
|
list of str |
Ordered list of species names, matching axis 1 of |
# Time axis in years
yr = 365.25 * 24 * 3600
times_yr = pipe.chemistry[30]['time'] / yr
print(f"Last output time: {times_yr[-1]:.2e} yr")
# Gas density at the last timestep, all z-points
nH = pipe.chemistry[30]['H_number_density'][-1] # array, shape (nz,)
Working with the abundances DataArray (xarray)#
The 'abundances' entry is an xarray.DataArray with three named dimensions:
time— the output times in seconds (coordinate)species— the species names (coordinate)spatial— the vertical grid index (no coordinate; use the'z'column of1D_static.dat)
This makes it straightforward to select by species name or timestep index without having to track integer indices manually.
ab = pipe.chemistry[30]['abundances'] # DataArray (nb_timesteps, nb_species, nz)
# Select a species by name — all timesteps and z-points
co = ab.sel(species='CO') # DataArray (nb_timesteps, nz)
# Select the last timestep
co_last = ab.isel(time=-1).sel(species='CO') # DataArray (nz,)
# Select a specific timestep by index
co_t48 = ab.sel(species='CO').isel(time=48)
# Convert to a plain NumPy array
co_arr = co_last.values # shape (nz,)
# Compute number density [cm⁻³]
nH = pipe.chemistry[30]['H_number_density'][-1] # (nz,)
n_co = nH * co_arr
# List all available species
print(pipe.chemistry[30]['species'])
Plotting a vertical abundance profile#
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
r = 30 # AU
species = 'CO'
# Load z-grid from the static file
static = pd.read_table(
f'chemistry/{r}AU/1D_static.dat',
sep=r'\s+', comment='!', header=None, engine='python'
)
z = static[0].values # z [AU], surface → midplane
ab = pipe.chemistry[r]['abundances'].isel(time=-1).sel(species=species).values
nH = pipe.chemistry[r]['H_number_density'][-1]
n_X = nH * ab
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4), sharey=True)
ax1.semilogy(ab, z); ax1.set_xlabel(f'n({species})/n$_H$')
ax2.semilogy(n_X, z); ax2.set_xlabel(f'n({species}) [cm$^{{-3}}$]')
for ax in (ax1, ax2):
ax.set_ylabel('z [AU]')
plt.tight_layout()
plt.show()
Single-run mode (0-D / single column)#
If no AU sub-folders are found, add_chemistry() reads the abundances.out file
directly from the chemistry path and stores the result in pipe.chemistry as a single
dict (same keys as above, but 'abundances' has nz = 1 for a 0-D run).
pipe.add_chemical_path('single_run/')
pipe.add_chemistry()
ab = pipe.chemistry['abundances'] # DataArray (nb_timesteps, nb_species, 1)
co = ab.sel(species='CO').isel(spatial=0) # time series for CO
Monitoring a running job#
The function progress() reads only the 8-byte time record
from each timestep in abundances.out and seeks past the large abundance and physical-state
blocks. It is therefore essentially instantaneous regardless of file size, and safe to call
while Nautilus is still writing to the file.
from astromugs import nautilus
# Simple call — just the folder path
n, t = nautilus.read.progress('chemistry/30AU/')
# → Timesteps done: 45 last time: 3.162e+05 yr
# With percentage — reads nb_outputs from parameters.in
n, t = nautilus.read.progress('chemistry/30AU/', parameters_path='chemistry/30AU/')
# → Timesteps done: 45 / 64 (70.3 %) last time: 3.162e+05 yr
# Loop over all radii to get an overview
import os, re
chempath = 'chemistry/'
radii = sorted([int(d.replace('AU','')) for d in os.listdir(chempath)
if re.match(r'^\d+AU$', d)])
for r in radii:
nautilus.read.progress(f'{chempath}{r}AU/', parameters_path=f'{chempath}{r}AU/')