Codegen
Most of the time you generate code through the normal workflow,
and when you want a whole project in one call you reach for
Builder. Codegen sits one level below both: it is the engine
they are built on, and it hands you the pieces — the actual lines of code
for rates, fluxes, ODEs, the Jacobian — as strings you drop into your own files.
Use it when writing Builder's plugin or assembling a file your own way.
You give Codegen two things up front — a parsed
Network and a target language — and
from then on every get_*_str() call returns a ready-to-paste block in that
language.
from jaff import Network
from jaff.codegen import Codegen
net = Network("networks/GOW/GOW.jet")
cg = Codegen(net, lang="python")
print(cg.get_rates_str()) # k[0] = ... , k[1] = ... , ...
print(cg.get_ode_str()) # f[0] = ... , f[1] = ... , ...
Codegen is exported from the top level as well, so
from jaff import Codegen works too.
Every quantity comes in two forms, and the rest of this page leans on that split:
- a formatted string —
get_rates_str(),get_ode_str(), … — the paste-ready text, with brackets, indices, and line terminators already in the target language; - a structured form —
get_indexed_rates(),get_indexed_odes(), … — the same expressions as(index, expression)pairs, for when you'd rather format them yourself.
The string methods are what you want genrally; the indexed ones are covered at the end.
Choosing a target language
The language you pass to the constructor decides four things at once: the array
brackets, whether indices start at 0 or 1, the comment marker, and the
assignment operator. Pick it with a name or any common alias.
| Parameter | Type | Default | Description |
|---|---|---|---|
network |
Network |
— | Parsed chemical reaction network |
lang |
str |
"c++" |
Target language — name or alias (see table) |
brac_format |
str |
"" |
Override the 1-D array brackets: "[]", "()", "{}", "<>" |
matrix_format |
str |
"" |
Override the 2-D Jacobian brackets/separator (see Jacobian) |
An unsupported lang, brac_format, or matrix_format raises ValueError
listing what is supported, so a typo fails loudly at construction rather than
producing wrong code.
| Alias(es) | Canonical | Brackets | Index base | Comment | Assignment |
|---|---|---|---|---|---|
c++, cpp, cxx |
cxx |
[] |
0 | // |
= |
c |
c |
[] |
0 | // |
= |
fortran, f90 |
fortran |
() |
1 | ! |
= |
python, py |
python |
[] |
0 | # |
= |
rust, rs |
rust |
[] |
0 | // |
= |
julia, jl |
julia |
[] |
1 | # |
= |
r |
r |
[] |
1 | # |
<- |
The index base is the one to watch: Fortran, Julia, and R are 1-based, so the
same network emits k(1) there and k[0] in C++. You don't need to set it by hand —
the language already knows.
The shared knobs
Almost every get_*_str() method takes the same handful of formatting
arguments, so they're explained once here. Each defaults to "do what the
language says", so you only pass them to deviate.
| Argument | What it does | Empty / -1 means |
|---|---|---|
idx_offset |
Number added to every array subscript | -1 → the language's base |
brac_format |
Override the 1-D brackets for this one call | "" → the language's brackets |
assignment_op |
Override the assignment operator | "" → the language's =/<- |
line_end |
Override the statement terminator | "" → the language's ; / nothing |
use_cse |
Factor out repeated sub-expressions (see CSE) | default True |
cse_var |
Name prefix for the CSE temporaries | per method (x, cse, rcse) |
idx_offset defaults are not all the same
The lower-level emitters (get_commons, get_rates_str,
get_flux_expressions_str, get_ode_expressions_str) default
idx_offset=-1, i.e. use the language base — so Fortran gets 1-based
subscripts automatically. The assembled-RHS emitters (get_ode_str,
get_rhs_str, get_radode_str, get_jacobian_str) default idx_offset=0,
a literal zero. If you emit those for a 1-based language, pass
idx_offset=-1 explicitly to pick the language base back up.
The methods that emit CSE temporaries also take a type-declaration prefix so
the temporaries are valid declarations in typed languages — it's called
var_prefix on get_rates_str/get_jacobian_str and def_prefix on
get_ode_str/get_rhs_str/get_radode_str. Left empty it becomes the
language's const double/f64/… ; for an untyped language (Python, Fortran,
R) it's empty.
The generation pipeline
The methods mirror the math, and they build on each other in the same order a solver evaluates them: indices → rates → fluxes → ODEs, with the Jacobian off to the side. You can call any one in isolation; they don't share state.
The running examples below are excerpts, shown in the language that reads clearest for each step.
Species indices — get_commons()
The starting point: one named constant per species mapping its index name
(fidx) to its slot, then the network sizes nspecs and nreactions. Every
other block indexes against these.
definition_prefix is prepended to every line (here "const int "); the index
names themselves come from each specie's fidx, so they're already lowercase
and identifier-safe (H2 → idx_h2, H+ → idx_hj).
Reaction rates — get_rates_str()
One assignment per reaction, k[i] = <rate>, with CSE temporaries emitted first
so later lines can reference them.
const double x0 = exp(-1.0/tgas);
k[0] = 1.8e-11 * x0;
k[1] = photorates(1, G0, av, chi);
k[2] = 3.2e-17;
Photochemical rates carry a photorates($IDX$, ...) placeholder; get_rates_str
substitutes the concrete reaction index ($IDX$ → 1) so the line compiles as
written. The CSE prefix here defaults to x (giving x0, x1, …) rather than
the cse used elsewhere.
Fluxes — get_flux_expressions_str()
Each reaction's flux: its rate times the densities of its reactants.
Pass idx_prefix="idx_" only if you want an extra prefix on top of the
already-prefixed fidx; usually you leave it empty.
Per-species ODEs — get_ode_expressions_str()
The density derivatives written as sums over a pre-computed flux array —
reactants subtract, products add. This is the cheap form: it assumes flux[...]
already exists in the generated code.
The derivative array is named by derivative_prefix + species_var (default
"d" + "y" → dy), or set derivative_var outright. The subscript is the
species fidx, so it lines up with the constants from get_commons().
The full right-hand side — get_ode_str() and get_rhs_str()
get_ode_str() is the self-contained version of the species ODEs: it inlines
the rate expressions instead of leaning on a flux array, and runs CSE across
every species at once.
const double cse0 = exp(-1.0/tgas);
const double cse1 = 1.8e-11 * cse0;
f[0] = -cse1 * y[0] * y[1] + 3.2e-17 * y[2];
f[1] = cse1 * y[0] * y[1] - 2.7e-10 * y[1];
get_rhs_str() is the same idea taken to the whole state vector — species ODEs,
then the energy derivative, then (optionally) radiation — with CSE shared across
all of them so sub-expressions are factored out jointly. The output comes in a
fixed order:
- CSE temporaries
f[0]…f[N-1]— species density ODEsf[N]— energy time-derivativedE/dtf[N+1]… — radiation ODEs (only whenradiation=True)
cg.get_rhs_str(
specific_eint=False, # True → divide dE/dt by total density
norm=0, # 0 = mass density, 1 = number density
radiation=True, # append the radiation moment ODEs
rad_order=0, # radiation moment closure order (0–3)
)
Energy derivative — get_dedt()
Just the energy time-derivative dE/dt, as a single expression with no
assignment or line terminator — handy when you splice it into your own line.
Radiation ODEs — get_radode_str()
The radiation moment ODEs on their own, formatted exactly like get_ode_str().
order selects the moment closure (0–3); the CSE prefix defaults to rcse.
The Jacobian — get_jacobian_str()
The analytical Jacobian J[i][j] = ∂f_i/∂y_j, computed symbolically and emitted
sparse — only the non-zero elements appear, ready for a sparse solver.
const double cse0 = exp(-1.0/tgas);
J[0][0] = -1.8e-11 * cse0 * y[1];
J[0][1] = -1.8e-11 * cse0 * y[0];
J[1][1] = -2.7e-10;
A few specifics:
use_dedt=Trueadds the energy equation's row and column, coupling the chemistry to temperature through the ideal-gas EOS.- Rate functions SymPy can't differentiate symbolically (e.g.
photorates(...)) become named partial calls —photorates_partial_0(...), where the suffix is the argument differentiated against — for you to supply. -
matrix_formatpicks the 2-D indexing style independently of the 1-D brackets. Use","-variants for a single subscript and the plain/doubled forms for nested ones:matrix_formatEmits "[]"J[i][j]"[,]"J[i, j]"(,)"J(i, j)"{,}"J{i, j}(
"()","[][]","<>", and their,-variants follow the same pattern.) An unsupported value raisesValueError.
Common subexpression elimination
exp(-1.0/tgas) showing up in three rates is wasted work. With use_cse=True
(the default on every method that supports it) JAFF runs SymPy's
cse
over the expressions, lifts each repeated piece into a temporary (cse0, cse1,
…), and emits those before the assignments that use them. Unused temporaries are
pruned, so nothing dead reaches the output.
cg.get_rates_str(use_cse=False) # every rate fully expanded inline
cg.get_rates_str(use_cse=True, cse_var="tmp") # shared pieces lifted to tmp0, tmp1, …
Two kinds of rate are left out of CSE because they can't be simplified
symbolically: rates stored as raw code strings, and photorates(...) calls
(the $IDX$ placeholder can't be folded into a shared sub-expression).
The structured form: get_indexed_*()
When you don't want a finished string — you're feeding the expressions into a
template engine, renumbering them, or post-processing — every emitter has an
get_indexed_* sibling that returns the expressions as data instead of text.
| String method | Structured sibling |
|---|---|
get_rates_str() |
get_indexed_rates() |
get_flux_expressions_str() |
get_indexed_flux_expressions() |
get_ode_expressions_str() |
get_indexed_ode_expressions() |
get_ode_str() |
get_indexed_odes() |
get_rhs_str() |
get_indexed_rhs() |
get_radode_str() |
get_indexed_radodes() |
get_jacobian_str() |
get_indexed_jacobian() |
The rate/ODE/RHS/Jacobian variants return a dict with two keys —
"extras"["cse"] (the CSE temporaries) and "expressions" (the main
expressions) — each an IndexedList of (index, expression_string) pairs. The
flux and ode-expression variants return that IndexedList directly. The string
methods are just thin formatters over these.
ir = cg.get_indexed_rates()
for idx, expr in ir["expressions"]:
print(idx, expr) # ([0], '1.8e-11*x0'), ([1], 'photorates($IDX$, ...)'), ...
Inspecting the language tables
get_language_tokens() is a static method exposing the raw syntax table behind
every language — useful if you're matching JAFF's output to a hand-written file.
tokens = Codegen.get_language_tokens()
tokens["python"]["comment"] # '#'
tokens["fortran"]["idx_offset"] # 1
Each entry is a LangModifier with brac, assignment_op, line_end,
matrix_sep, code_gen, idx_offset, comment, types, and extras.
Full example
Stitching the pieces into a minimal C++ source file — Codegen gives the
bodies, you own the scaffolding:
from jaff import Network
from jaff.codegen import Codegen
net = Network("networks/GOW/GOW.jet")
cg = Codegen(net, lang="c++")
with open("output/network.cpp", "w") as f:
f.write("#include <cmath>\n\n")
f.write("// Species indices\n")
f.write(cg.get_commons(definition_prefix="const int "))
f.write("\nvoid rates(double* k, double tgas) {\n")
f.write(cg.get_rates_str())
f.write("}\n\n")
f.write("void rhs(double* f, double* y, double tgas) {\n")
f.write(cg.get_rhs_str())
f.write("}\n")
If you find yourself writing the whole scaffold too, that's the cue to step up
to Builder — it does exactly this from a template.