Hi there! Welcome to Dengo. Dengo is a Python system for symbolically describing a system of chemical species, chemical kinetic rate equations, cooling functions, and then producing from these a set of numerical kernels to calculate the “right hand side” and Jacobian of this system. These two numerical kernels can then be linked into one of several ODE solvers.
Dengo is best thought of as a way to create a chemistry and cooling solver that you can stick into a codebase. Rather than trying to tie together the four separate parts – species, reactions, rate coefficients and the solver itself – into a single whole, Dengo allows you to construct each item individually and join them at the final step before inserting them into a simulation code.
A online version of the cookbook and documentation can be found here https://hisunnytang.github.io/dengo-merge/landing.html
For more information, please contact the authors:
Dengo
can also be installed through pypi
using pip install dengo-chemistry
.
While it can work standlone with the solver available in the shipped package, it is advisible to install dengo
with the environement maintained by conda
. This allows user to use the Sundials CVODE with SuiteSparse sparse matrix algorithms. For dengo to function properly, environemtal variables have to be set accordingly for dengo to locate and link the require
packages C
or cython
modules.
# Clone the Current Repo
git clone https://github.com/hisunnytang/dengo-merge.git
# Create the Conda Environment
conda create -n dengo
# Activate the Environment
conda activate dengo
# Install all the neccessary packages
conda install -c conda-forge "sundials>=6.2"
conda install -c conda-forge ChiantiPy Cython h5py Jinja2 numpy setuptools sympy matplotlib pytest unyt pip
# Install dengo
cd dengo-merge
pip install -e .
# Define the environmental variables
export HDF5_PATH=${CONDA}/envs/dengo
export SUITESPARSE_PATH=${CONDA}/envs/dengo
export CVODE_PATH=${CONDA}/envs/dengo
export DENGO_INSTALL_PATH=${your_directory}/dengo_install
export LD_LIBRARY_PATH=${CONDA}/envs/dengo/lib:${DENGO_INSTALL_PATH}/lib:$LD_LIBRARY_PATH
While we recommend our readers to read the cookbook for a more detailed usage, here we provide a small and quick example with 1 reactions. $$\rm H + e^- \rightarrow \rm H^+ + 2 e^- \quad (k01)$$
One would first need to define the chemical species using AtomicSpecies
, and MolecularSpecies
. The weight of the atom would be looked up from the periodic table internally in dengo
. User has to specified the free electrons in each chemical species (hydrogen atom). de
represents the free electrons.
from dengo.reaction_classes import AtomicSpecies, MolecularSpecies
from dengo.chemical_network import species_registry
HI = AtomicSpecies('H', free_electrons=0)
HII = AtomicSpecies("H", free_electrons=1)
de = species_registry['de']
The species would be automatically registered in dengo
. and are accesible through species_registry
.
A reaction to dengo is primarily composed of:
For example, dengo expects the LHS and RHS to have an input of the type for k01:
LHS = [(1, HI), (1, de)]
RHS = [(1, HII), (2, de)]
Reaction are oftentimes dependent on temperature. dengo expects a reaction rate function that take state as input. state contains not only temperature in $K$, but also in Kelvin log scale, and in $eV / K$ (electron volts per kelvin), and $eV / K$ in log scale.
from dengo.reaction_classes import reaction
from dengo.chemical_network import reaction_registry
tiny = 1e-60
# -- k01 --
@reaction('k01', [ (1,HI), (1,de) ], [ (1,HII), (2,de) ])
def rxn(state):
vals = np.exp(-32.71396786375
+ 13.53655609057*state.logtev
- 5.739328757388*state.logtev**2
+ 1.563154982022*state.logtev**3
- 0.2877056004391*state.logtev**4
+ 0.03482559773736999*state.logtev**5
- 0.00263197617559*state.logtev**6
+ 0.0001119543953861*state.logtev**7
- 2.039149852002e-6*state.logtev**8)
# taken from Abel 1999
vals = np.maximum(vals , tiny *np.ones((len(state.T))) )
return vals
class state:
def __init__(self, T_bounds=(1, 1e8), n_bins=1024):
"""Initialize the range of temperature over which the rate tables are generated
Parameters
----------
T_bounds: List[Float, Float], optional (default=(1,1e8))
the range over which the rates table is interpolated
n_bins: int, optional (default=1024)
"""""
self.n_bins = n_bins
self.T = np.logspace(
np.log(T_bounds[0]), np.log(T_bounds[1]), n_bins, base=np.e
)
self.logT = np.log(self.T)
self.tev = self.T / tevk
self.logtev = np.log(self.tev)
self.T_bounds = T_bounds
ChemicalNetwork
We can now assemble them together in this ChemicalNetwork. This object helps us do all the neccessary computations to arrive at the symbolic rhs and jacobian functions, which ultimately eases us of the process of deriving them by hand.
import dengo
from dengo.chemical_network import ChemicalNetwork
simpleNetwork = ChemicalNetwork()
simpleNetwork.add_reaction("k01")
simpleNetwork.init_temperature((1e0, 1e8))
This allows us to write the ODE solver from dengo templates
solver_name = 'simpleNetwork'
output_dir = '.'
simpleNetwork.write_solver(
solver_name,
output_dir=output_dir,
solver_template="be_chem_solve/rates_and_rate_tables",
ode_solver_source="BE_chem_solve.C",
init_values=init_values,
)
simpleNetwork_solver.C
simpleNetwork_solver.h
simpleNetwork_solver_main.C
simpleNetwork_solver_main.py
simpleNetwork_solver_run.pxd
simpleNetwork_solver_run.pyx
simpleNetwork_solver_run.pyxbld
simpleNetwork_solver_run.pyxdep
simpleNetwork_tables.h5
The solver templates come with a .pyx
files that lets you build a python interface to the C-library. A more well-rounded Cython tutorial can be found here Cython Tutorial
import pyximport
import numpy as np
solver_name = "simpleNetwork"
pyximport.install(
setup_args={"include_dirs":np.get_include()},
reload_support=True,
inplace=True,
language_level=3
)
simple_solver_run = pyximport.load_module(
f"{solver_name}_solver_run",
f"{solver_name}_solver_run.pyx",
build_inplace = True,
pyxbuild_dir = "_dengo_temp",
)
{solver_name}_solver_run.run_{solver_name}(init_values, dt, niter)
is the entry point for the built solver. It takes an dictionary that contains the abundances, and thermal energy, and dt, time to advance the fluid parcel, niter the maximum number of iterations as arguments.
It assumes that abundances are in number density with the units of $1/cm^3$, and thermal energy density in $erg/g$ (thermal energy per mass density) $$\epsilon = \frac{1}{\rho} \sum_i \frac{n_i k T}{\gamma_i - 1}$$. niter implicitly sets the initial timestep for the solver, i.e. $dt_s = \rm dt / \rm niter$ .
NCELLS = 1
density = 1e-2
init_array = np.ones(NCELLS) * density
init_values = dict()
init_values['H_1'] = init_array
init_values['H_2'] = init_array
init_values['de'] = init_array
init_values['ge'] = np.ones(NCELLS)*1e13
total_density = simpleNetwork.calculate_total_density(init_values)
init_values = simpleNetwork.convert_to_mass_density(init_values)
init_values['de'] = simpleNetwork.calculate_free_electrons(init_values)
init_values['density'] = simpleNetwork.calculate_total_density(init_values)
number_density = simpleNetwork.calculate_number_density(init_values)
rv, rv_int = simple_solver_run.run_simpleNetwork(init_values, 1e16, niter = 1e5)