Build Cython Module from Templates

In the last chapter, we used our simpleNework to generate 9 files from dengo. We outline how can we build the library from here and use cython to invoke the function written in C.

simpleNetwork_solver.C	      simpleNetwork_solver_run.pyx
simpleNetwork_solver.h	      simpleNetwork_solver_run.pyxbld
simpleNetwork_solver_main.C   simpleNetwork_solver_run.pyxdep
simpleNetwork_solver_main.py  simpleNetwork_tables.h5
simpleNetwork_solver_run.pxd
import dengo
from dengo.chemical_network import \
 ChemicalNetwork, \
 reaction_registry, \
 cooling_registry, species_registry
import dengo.primordial_rates
import dengo.primordial_cooling

dengo.primordial_rates.setup_primordial()

simpleNetwork = ChemicalNetwork()
simpleNetwork.add_reaction("k01")
simpleNetwork.add_reaction("k02")
simpleNetwork.add_cooling("reHII")
simpleNetwork.init_temperature((1e0, 1e8))
Adding reaction: k01 : 1*H_1 + 1*de => 1*H_2 + 2*de
Adding reaction: k02 : 1*H_2 + 1*de => 1*H_1

Compile the Cython Module

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].(https://cython.readthedocs.io/en/latest/src/tutorial/cython_tutorial.html).

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", 
    )
You have suitesparse!
/home/kwoksun2/anaconda3/lib/python3.8/site-packages/Cython/Compiler/Main.py:369: FutureWarning: Cython directive 'language_level' not set, using 2 for now (Py2). This will change in a later release! File: /mnt/gv0/homes/kwoksun2/dengo-merge/cookbook/tutorial/buildingblocks/simpleNetwork_solver_run.pxd
  tree = Parsing.p_module(s, pxd, full_module_name)
cc1plus: warning: command line option '-Wstrict-prototypes' is valid for C/ObjC but not for C++
cc1plus: warning: command line option '-Wstrict-prototypes' is valid for C/ObjC but not for C++
cc1plus: warning: command line option '-Wstrict-prototypes' is valid for C/ObjC but not for C++
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
File ~/anaconda3/lib/python3.8/site-packages/pyximport/pyximport.py:216, in load_module(name, pyxfilename, pyxbuild_dir, is_package, build_inplace, language_level, so_path)
    214     so_path = build_module(module_name, pyxfilename, pyxbuild_dir,
    215                            inplace=build_inplace, language_level=language_level)
--> 216 mod = imp.load_dynamic(name, so_path)
    217 if is_package and not hasattr(mod, '__path__'):

File ~/anaconda3/lib/python3.8/imp.py:342, in load_dynamic(name, path, file)
    340 spec = importlib.machinery.ModuleSpec(
    341     name=name, loader=loader, origin=path)
--> 342 return _load(spec)

File <frozen importlib._bootstrap>:702, in _load(spec)

File <frozen importlib._bootstrap>:657, in _load_unlocked(spec)

File <frozen importlib._bootstrap>:556, in module_from_spec(spec)

File <frozen importlib._bootstrap_external>:1101, in create_module(self, spec)

File <frozen importlib._bootstrap>:219, in _call_with_frames_removed(f, *args, **kwds)

ImportError: /mnt/gv0/homes/kwoksun2/dengo-merge/cookbook/tutorial/buildingblocks/simpleNetwork_solver_run.cpython-38-x86_64-linux-gnu.so.reload1: undefined symbol: _Z18setup_cvode_solverPFidP17_generic_N_VectorS0_PvEPFidS0_S0_P18_generic_SUNMatrixS1_S0_S0_S0_EiP18simpleNetwork_dataP24_generic_SUNLinearSolverS5_S0_dS0_

During handling of the above exception, another exception occurred:

ImportError                               Traceback (most recent call last)
Input In [2], in <cell line: 12>()
      3 solver_name = "simpleNetwork"
      5 pyximport.install(
      6     setup_args={"include_dirs":np.get_include()},
      7     reload_support=True, 
      8     inplace=True, 
      9     language_level=3
     10 )
---> 12 simple_solver_run = pyximport.load_module(
     13     f"{solver_name}_solver_run",
     14     f"{solver_name}_solver_run.pyx",
     15     build_inplace = True, 
     16     pyxbuild_dir = "_dengo_temp", 
     17     )

File ~/anaconda3/lib/python3.8/site-packages/pyximport/pyximport.py:231, in load_module(name, pyxfilename, pyxbuild_dir, is_package, build_inplace, language_level, so_path)
    228 exc = ImportError("Building module %s failed: %s" % (
    229     name, traceback.format_exception_only(*sys.exc_info()[:2])))
    230 if sys.version_info[0] >= 3:
--> 231     raise exc.with_traceback(tb)
    232 else:
    233     exec("raise exc, None, tb", {'exc': exc, 'tb': tb})

File ~/anaconda3/lib/python3.8/site-packages/pyximport/pyximport.py:216, in load_module(name, pyxfilename, pyxbuild_dir, is_package, build_inplace, language_level, so_path)
    213         module_name = name
    214     so_path = build_module(module_name, pyxfilename, pyxbuild_dir,
    215                            inplace=build_inplace, language_level=language_level)
--> 216 mod = imp.load_dynamic(name, so_path)
    217 if is_package and not hasattr(mod, '__path__'):
    218     mod.__path__ = [os.path.dirname(so_path)]

File ~/anaconda3/lib/python3.8/imp.py:342, in load_dynamic(name, path, file)
    338 # Issue #24748: Skip the sys.modules check in _load_module_shim;
    339 # always load new extension
    340 spec = importlib.machinery.ModuleSpec(
    341     name=name, loader=loader, origin=path)
--> 342 return _load(spec)

File <frozen importlib._bootstrap>:702, in _load(spec)

File <frozen importlib._bootstrap>:657, in _load_unlocked(spec)

File <frozen importlib._bootstrap>:556, in module_from_spec(spec)

File <frozen importlib._bootstrap_external>:1101, in create_module(self, spec)

File <frozen importlib._bootstrap>:219, in _call_with_frames_removed(f, *args, **kwds)

ImportError: Building module simpleNetwork_solver_run failed: ['ImportError: /mnt/gv0/homes/kwoksun2/dengo-merge/cookbook/tutorial/buildingblocks/simpleNetwork_solver_run.cpython-38-x86_64-linux-gnu.so.reload1: undefined symbol: _Z18setup_cvode_solverPFidP17_generic_N_VectorS0_PvEPFidS0_S0_P18_generic_SUNMatrixS1_S0_S0_S0_EiP18simpleNetwork_dataP24_generic_SUNLinearSolverS5_S0_dS0_\n']

Invoking the Cython Solver

In the below we show how the cython module can be built and used. {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, an \(\rm dt\), time to advance the fluid parcel, and niter the maximum number of iterations as arguments. It assumes that abundances are in number density with the units of \(\rm cm^{-3}\), and thermal energy in \(\rm erg/g\). niter implicitly sets the initial timestep for the solver, i.e. \(dt_{solver} = \rm dt / \rm niter\). Our cython module gives the same trajectories as what we had done from scratch with the previous chapters!

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)
Successful iteration[    0]: (1.000e+11) 1.000e+11 / 1.000e+16
End in 97 iterations: 1.00000e+16 / 1.00000e+16 (0.00000e+00)

Visualize the Trajectories

They match with what we have built from scratch before!

import matplotlib.pyplot as plt
flags = rv_int['t'] > 0
sp_names = [s.name for s in sorted(simpleNetwork.required_species)]
output = np.zeros((len(sp_names),sum(flags)))
                  
for i, s in enumerate(sp_names):
    output[i] = rv_int[s][:,flags]

H_1_traj, H_2_traj, de_traj, ge_traj = output
T_traj    = rv_int['T'][0,flags]
timesteps = rv_int['t'][flags]


f, ax = plt.subplots()
l1= ax.loglog(timesteps, H_1_traj)
l2= ax.loglog(timesteps, H_2_traj)
l3= ax.loglog(timesteps, de_traj)
ax2 = ax.twinx()
l4= ax2.loglog(timesteps, T_traj, color='C3')


ax.set_ylabel(r"Abundance ($\rm cm^{-3}$)")
ax2.set_ylabel("Temperature (K)")

ax.set_xlabel("Time (s)")

f.legend(
    [l1,l2,l3,l4], 
    labels=[r'$\rm H$',r'$\rm H^+$',r'$\rm e^-$',r'$\rm T$'],
    loc=[0.15,0.4]
)
plt.subplots_adjust(right=0.85)
/tmp/ipykernel_22118/4291065558.py:27: UserWarning: You have mixed positional and keyword arguments, some input may be discarded.
  f.legend(
../../_images/dengo_cython_7_1.png