Basic Concepts
JAFF does two things: it gives every common chemical-network format a single in-memory representation, and it turns that representation into source code in the language of your choice — with first-class support for explicit photochemistry.
This page walks the objects you will meet, in the order you meet them. The running example throughout is the hydrogen photo-ionization network, the same one used across the rest of the user guide. It is small enough to print in full yet still contains a neutral atom, a cation, the electron, and a genuine photo-reaction:
Two reactions, three species, and rate coefficients that depend on temperature or other parameters. A rate coefficient tells you how fast a reaction runs. For a reaction
with stoichiometric coefficients \(\alpha\), \(\beta\), \(\gamma\), the rate is
where \(k\) is the rate coefficient and \([A]\), \([B]\) are the number densities of \(A\) and \(B\). In astrophysics a reaction can fire for many reasons — thermal collisions, cosmic-ray impacts, photons from a nearby source, or spontaneous decay — and each cause carries its own rate law. Keeping those causes distinguishable is one of the things JAFF is built to do.
The shape of the library
There are two halves, and keeping them apart is the key to the whole API:
- the model — a loaded
Networkand the typed objects it holds:Species,Reactions, andElements; - the code generator — the
jaffgenCLI, which expands your$JAFFtemplates against the model to emit rate, ODE, and Jacobian source.
Network ← the loaded model
├── net.species → Species ← the chemical entities
├── net.reactions → Reactions ← the transformations
└── net.elements → Elements ← the atoms they are made of
│
▼
jaffgen --network … --files … ← expands $JAFF templates against the model
→ generated/ ← network-specific source out
Everything below follows that seam: first the model, then the generator.
The Network
Network is the entry point for every JAFF workflow. One constructor call
reads a file, auto-detects its format, parses it into the typed catalogues, and
validates the chemistry.
from jaff import Network
net = Network("networks/h_photoionization/h_photo.jet")
len(net.species) # 3 — number of species
len(net.reactions) # 2 — number of reactions
net.label # 'h_photo' — network identifier
A loaded Network carries:
species— theSpeciescatalogue; look an entity up by name withnet.species["H"];reactions— theReactionscatalogue;elements— theElementscatalogue, plus mass and composition information;file_name— the path the network was read from;label— a human-readable identifier (defaults to the file stem).
Validate while you load
Pass errors=True to turn on chemistry checks — missing sinks and sources,
duplicate reactions, isomer clashes, and element-conservation violations are
reported as warnings:
Species
A Specie is one chemical entity — atom, molecule, or ion. From the single
name string in the file it derives the composition, mass, charge, and the
identifiers code generation needs.
H = net.species["H"]
H.name # 'H'
H.mass # 1.673773e-24 ← grams, not amu
H.charge # 0
H.index # 0
H.latex() # '{\\rm H}' ← latex() is a method, not an attribute
The attributes worth knowing up front:
name— the formula exactly as written in the file;mass— total mass in grams (CGS), summed over the constituent atoms;charge— net charge in elementary-charge units (0neutral,>0cation,<0anion);index— zero-based position insidenet.species, used everywhere arrays are indexed.
Mass is in grams (CGS), not atomic mass units
specie.mass is the physical mass in grams — H is 1.673773e-24, not
1.008. JAFF works in CGS so the value drops straight into rate and energy
expressions. For the atomic weight in amu, read it from the element data
instead.
The Species page covers the
derived identifiers (exploded, serialized, fidx), the catalogue's bulk
accessors, and the electron's special handling.
Reactions
A Reaction is one chemical transformation. It carries the species going
in, the species coming out, a symbolic rate, optional temperature bounds, and
an energy budget.
rxn = net.reactions[0]
rxn.verbatim # 'H -> H+ + e-' ← verbatim is an attribute
rxn.rtype() # 'photo' ← rtype() is a method
rxn.rate # a SymPy expression for the rate coefficient
rxn.reactants # species consumed
rxn.products # species created
reactants/products— the species on each side, asSpeciescatalogues;rate— the rate coefficient as a SymPy expression, not a number; it still contains the temperature symbol so it can be differentiated and emitted as code;rtype()— the classification (photo,cosmic_ray,photo_av,3_body,unknown), read from the rate expression;tmin/tmax— the temperature window the rate is valid over (Nonemeans unbounded);verbatim— the human-readable reaction string.
Rate expressions. Most thermal reactions use an Arrhenius-type law:
with \(\alpha\) the pre-exponential factor, \(\beta\) the temperature exponent,
\(\gamma\) the activation parameter, and \(T\) the temperature in Kelvin.
Photo-reactions instead carry a photorates(...) call, which is what rtype()
keys on. The Reactions page
goes through every reaction type and the catalogue API.
Network files
JAFF reads several community formats and detects which one it is looking at automatically:
- KIDA — the KInetic Database for Astrochemistry;
- UDFA — the UMIST Database for Astrochemistry;
- PRIZMO — the PRIZMO astrochemical code;
- KROME — the KROME package for astrochemistry;
- UCLCHEM — the UCL Chemistry and Dust code.
Whatever the source, the loaded model is identical — that uniform representation is the point of the parser.
Code generation
JAFF's code generation is template-driven, and the
jaffgen CLI is how you run it. You
write an ordinary source file — C, C++, Fortran, Python, Rust, Julia, or R — and
mark up the parts that depend on the network with $JAFF directives. jaffgen
loads a network, expands every directive against it, and writes the result out.
A template is real source code with small generated regions. Every line that is not a directive is copied verbatim; directive blocks are filled in:
// rates.cpp — a template
// $JAFF SUB nreact
const int NREACT = $nreact$;
// $JAFF END
void compute_rates(double* k, double T) {
// $JAFF REPEAT idx, rate IN rates
k[$idx$] = $rate$;
// $JAFF END
}
Run it against the network:
The expanded file lands in generated/, keeping its name. SUB swaps a single
value; REPEAT loops a line once per item in a collection (here rates):
const int NREACT = 2;
void compute_rates(double* k, double T) {
k[0] = photorates(0, …);
k[1] = …;
}
The directive language is small — SUB, REPEAT, REDUCE, GET, HAS,
END, plus a REPLACE modifier. The
Template Syntax page is the
full reference. Three ideas explain what the collections emit: indexing, CSE,
and the ODE/Jacobian pair.
Array indexing
Languages disagree on where arrays start and how they are bracketed. JAFF reads
the target language from each file's extension (or the --lang fallback) and
emits the right convention automatically:
| Language | Starting index | Example |
|---|---|---|
| C/C++ | 0 | arr[0] |
| Python | 0 | arr[0] |
| Fortran | 1 | arr(1) |
So the same template gives k[0] in C++ and k(1) in Fortran with no change.
Common Subexpression Elimination (CSE)
The indexed collections (rates, odes, jacobian, …) can factor repeated
work into temporaries so each sub-expression is computed once. You opt in by
adding the cse variable to a REPEAT:
ODEs and the Jacobian
A chemical network is a system of ODEs describing how concentrations change:
with \(y_i\) the concentration of species \(i\), \(R_j\) the rate of reaction \(j\), and
\(\nu_{ij}\) the stoichiometric coefficient of species \(i\) in reaction \(j\). The
odes collection emits exactly this right-hand side; implicit solvers also need
the Jacobian \(J_{ij} = \partial f_i / \partial y_j\) (where \(f_i = dy_i/dt\)),
which the jacobian collection emits. Because every rate is a symbolic
expression, JAFF differentiates it exactly rather than numerically:
// $JAFF REPEAT idx, ode IN odes
dydt[$idx$] = $ode$;
// $JAFF END
// $JAFF REPEAT idx, expr IN jacobian
J[$idx$] = $expr$;
// $JAFF END
Driving runs with jaff.toml
Spelling out the network, inputs, output, and radiation on every jaffgen line
gets old. A jaff.toml declares
the run once, so the command collapses to:
CLI flags still win over the file when both set the same thing, so a jaff.toml
is a baseline you can override per run.
Putting it together
A first analysis pass touches only the model — no code generation at all:
from jaff import Network
net = Network("networks/h_photoionization/h_photo.jet", errors=True)
for sp in net.species:
print(f"{sp.name}: {sp.mass:.3e} g charge {sp.charge:+d}")
for rxn in net.reactions:
print(f"{rxn.verbatim} [{rxn.rtype()}]")
The code-generation pass is a jaffgen invocation over your templates, exactly
as shown above. Everything else in the user guide is a deeper cut through one of
these objects.
Common terms
| Term | Definition |
|---|---|
| Species | A chemical entity (atom, molecule, ion) |
| Reaction | A chemical transformation between species |
| Rate coefficient | Function determining reaction speed |
| Stoichiometry | Ratio of reactants to products |
| ODE | Ordinary Differential Equation describing concentration changes |
| Jacobian | Matrix of partial derivatives of the ODEs |
| CSE | Common Subexpression Elimination (an optimization) |
| Template | A source file with $JAFF directives for code generation |
| Network | A collection of species and reactions |
| Index offset | Starting index for generated arrays (0 or 1) |
Next steps
Now that the pieces have names:
- Working with Networks — inspect species, reactions, and elements in depth;
- Code Generation — run
jaffgenover your templates; - Template Syntax — write your own
$JAFFtemplates; - API Reference — the complete surface.