Species
When a Network is loaded, every chemical entity it mentions is
turned into a structured object. They are
produced by the parser and can be accessed through net.species. This page is
about what net.species actually contains and how to use it.
There are two layers, and keeping them apart is the key to the whole API:
- A single
Specie— one atom, molecule, or ion, with its mass, charge, composition, and the identifiers JAFF needs for code generation; - the
SpeciesCatalogue — the ordered, multi-keyed collection that holds everySpeciein the network and lets you query them in bulk.
net.species is the Catalogue. Indexing into it (net.species["H"]) hands
you one Specie object. Throughout this page the running example is the hydrogen
photo-ionization network, which is small enough to print in full yet still
contains a neutral atom, a cation, and the electron:
from jaff import Network
net = Network("networks/h_photoionization/h_photo.jet")
net.species.count # 3
net.species.names() # ['H', 'H+', 'e-']
The two layers
A Specie knows about itself — "I am H⁺, my mass is 1.67e-24 g, my charge is
+1". It has no idea what reactions it takes part in or where it sits relative
to other species; that context belongs to the catalogue.
The Species catalogue knows about the set — ordering (each specie's
index), how to find a specie three different ways, and how to project any
per-species attribute into a flat array for the solver and code generator. It
holds Specie objects but adds no chemistry of its own.
net.species ← the Species catalogue (the set)
├── net.species[0] → Specie ← one entity (H)
├── net.species[1] → Specie ← one entity (H+)
└── net.species[2] → Specie ← one entity (e-)
Everything below is split along this seam: first the individual specie, then the catalogue, then the one specie that bends both — the electron.
The individual Specie
Parsing a name like "H2O+" is the single most important thing a Specie
does. From that one string it derives the composition, the mass, the charge,
and the identifiers used downstream. Once built, a Specie is just a bundle of
those derived facts.
Attributes
| Attribute | Type | Description |
|---|---|---|
name |
str |
Chemical name exactly as written in the network file ("H", "H+", "e-") |
mass |
float |
Total mass in grams (CGS), summed over the constituent atoms |
charge |
int |
Net charge in elementary-charge units (0 neutral, >0 cation, <0 anion) |
index |
int |
Zero-based position of this specie inside net.species |
exploded |
list[str] |
Sorted list of atomic symbols with repetition; the charge token is included |
serialized |
str |
Canonical identity string: exploded sorted and joined with / |
fidx |
str |
Code-safe flat identifier used in generated C/Fortran/Python source |
elements |
Elements |
Lazily built Elements collection for this single specie |
Specie latex representation
latex is a method, not an attribute — call it (specie.latex()).
h = net.species["H"]
h.name # 'H'
h.mass # 1.673773e-24 ← grams, not amu
h.charge # 0
h.index # 0
h.exploded # ['H']
h.serialized # 'H'
h.fidx # 'idx_h'
h.latex() # '{\\rm H}'
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. If you want the atomic weight in amu, read it from the element
data instead.
How a name becomes composition
The interesting attributes are the ones the parser derives. The same H⁺ specie shows all of them at once:
hp = net.species["H+"]
hp.charge # 1 ← counted from trailing '+' signs
hp.exploded # ['+', 'H'] ← atoms (sorted), charge carried as a token
hp.serialized # '+/H' ← canonical identity
hp.fidx # 'idx_hj' ← '+' → 'j' so it's a legal identifier
hp.latex() # '{\\rm H^{+}}'
explodedis the formula flattened into individual atoms and sorted, soH2Obecomes['H', 'H', 'O']. The net charge rides along as a'+'or'-'token, which is what lets the serialized form encode charge too.serializedis the canonical identity. Because it is sorted, structural isomers collapse onto the same string —HCO+andHOC+both serialize to'+/C/H/O'. The catalogue uses this as an alternate lookup key (see below), and the network uses it to detect isomers and duplicate reactions.chargeis read only from+/-characters at the end of the name.fidxis the name made safe for generated source: lower-cased,'+' → 'j','-' → 'k', soH2O+becomesidx_h2oj. This is the symbol the code generator emits to index this specie.latex()renders the name for plots and tables — subscripted counts, superscripted charge, roman element font. Passdollars=Trueto wrap it in$…$math delimiters.
Comparing and printing a Specie
A Specie's identity for comparison is its name. Equality, ordering, and
hashing all reduce to the name string:
H, Hp, e = net.species["H"], net.species["H+"], net.species["e-"]
H == Hp # False — different names
H == net.species[0] # True — same specie, compared by name
H < Hp # True — '<' / '>' compare names lexicographically
Hp > H # True
sorted([Hp, e, H]) # [SpecieObject('H'), SpecieObject('H+'), SpecieObject('e-')]
Because the name is also the hash key, species can be used directly in sets
and as dict keys. Sorting is by raw string order, so capitals sort before
lowercase ('H' < 'H+' < 'e-').
Comparisons only work specie-to-specie
Comparing a Specie against a bare string raises TypeError — use the name
explicitly instead:
Printing a specie gives back its chemical name; repr wraps it for debugging:
str(net.species["H+"]) # 'H+' ← __str__ is just the name
repr(net.species["H+"]) # "SpecieObject('H+')"
print(net.species["H+"]) # H+
str(specie) returning the plain name is what lets you drop a Specie straight
into f-strings and joins without touching .name.
The electron, a special citizen
The electron is a species like any other in the catalogue, but it breaks enough rules to deserve its own section — and the catalogue API is built around that.
It must be named e-. The parser rejects E, E-, electron, and
similar spellings with a fatal error, so there is exactly one spelling for the
electron everywhere in JAFF.
It does not decompose into atoms. Where H+ explodes to ['+', 'H'], the
electron stays whole:
e = net.species["e-"]
e.charge # -1
e.mass # 9.109383e-28 ← the electron mass, in grams
e.exploded # ['e-'] ← not split, not an atom token
e.serialized # 'e-'
e.fidx # 'idx_e' ← special-cased, not 'idx_ek'
It is often handled separately by solvers, so almost every catalogue
accessor takes an ne ("no electron") flag. Set ne=True and the electron
drops out of the returned vector while every other specie keeps its place:
net.species.names() # ['H', 'H+', 'e-']
net.species.names(ne=True) # ['H', 'H+']
net.species.charges() # [0, 1, -1]
net.species.charges(ne=True) # [0, 1]
When you need the electron's slot directly, ask the catalogue:
The Species Catalogue
net.species is an ordered collection that you can query three ways and slice
into bulk arrays. The ordering is fixed at load time and matches every
Specie.index, the stoichiometry matrices, and the generated code.
Three ways to find a specie
net.species["H"] # by name → Specie('H')
net.species["+/H"] # by serialized → Specie('H+')
net.species[1] # by index → Specie('H+')
net.species[-1] # negative index → Specie('e-')
Lookup by serialized form is what makes isomer-insensitive queries possible: any arrangement of the same atoms and charge resolves to the same specie.
How many species
net.species.count is the number of species in the catalogue, and the catalogue
is sized — so len(net.species) returns the same value. count is a cached
attribute kept in step as species are added; len() is the Pythonic spelling.
Use whichever reads better:
Iteration and membership
Iterating yields each Specie in index order. Membership accepts a name or a
serialized string:
for sp in net.species:
print(f"{sp.index:>3} {sp.name:<4} {sp.mass:.3e} g charge {sp.charge:+d}")
"H" in net.species # True (by name)
"+/H" in net.species # True (by serialized form)
Bulk accessors
Each method returns a Vector — a flat, index-aligned sequence ready to feed
the solver or code generator. Every method below takes the ne flag described
in the electron section.
| Method | Returns | h_photo result |
|---|---|---|
count |
int (attribute) |
3 |
names() |
Vector[str] |
['H', 'H+', 'e-'] |
normalized_names() |
Vector[str] |
['h', 'hp', 'en'] |
masses() |
Vector[float] |
[1.674e-24, 1.674e-24, 9.109e-28] |
charges() |
Vector[int] |
[0, 1, -1] |
charge_truths() |
Vector[int] |
[0, 1, 1] |
serialized() |
Vector[str] |
['H', '+/H', 'e-'] |
exploded() |
Vector[list[str]] |
[['H'], ['+', 'H'], ['e-']] |
latex() |
Vector[str] |
['${\\rm H}$', '${\\rm H^{+}}$', …] |
elements() |
Vector[Elements] |
one Elements collection per specie |
e_idx() |
int or None |
2 |
A few are easy to misread:
normalized_names()makes each name a legal lowercase identifier:'+' → 'p','-' → 'n'. SoH+becomes'hp'ande-becomes'en'. (This is not the same asfidx, which usesj/k.)charge_truths()is a 0/1 mask,1where the specie is charged — useful for charge-conservation terms.latex()on the catalogue defaults todollars=True(wrapped in$…$), whereasSpecie.latex()defaults todollars=False.
Filtering by charge
neutral() and charged() return the matching Specie objects, or — if you
pass an attribute name — that attribute projected out of each match. charged()
also honours the ne flag.
net.species.neutral() # [Specie('H')]
net.species.neutral("name") # ['H']
net.species.charged() # [Specie('H+'), Specie('e-')]
net.species.charged("name") # ['H+', 'e-']
net.species.charged("index") # [1, 2]
net.species.charged("name", ne=True) # ['H+'] (electron excluded)
The attribute name must be a real Specie attribute (name, mass, charge,
index, exploded, serialized, fidx, elements); anything else raises
ValueError.
Common patterns
Print a formatted summary
print(f"{'Idx':>3} {'Name':<5} {'Mass (g)':>12} {'Charge':>7}")
print("-" * 34)
for sp in net.species:
print(f"{sp.index:>3} {sp.name:<5} {sp.mass:>12.3e} {sp.charge:>+7d}")
Find every ion
ions = net.species.charged("name", ne=True) # exclude the electron
print(f"{len(ions)} ions: {list(ions)}")
Find hydrogen-bearing species
exploded makes "contains an H atom" an exact test rather than a substring
guess ("H" also appears in "He"):
Mass-sorted list
by_mass = sorted(net.species, key=lambda s: s.mass)
for s in by_mass:
print(f"{s.name}: {s.mass:.3e} g")