Elements
Elements are not something you declare — JAFF derives them. When a
Network loads, it walks every species' atom list
(Specie.exploded), collects the distinct atomic symbols, and
exposes them through Network.elements. This page is about what
Network.elements contains and the composition matrices it builds.
As with species and reactions, there are two layers:
- a single
Element— one entry from the periodic table, with its mass, atomic number, and isotope counts; - the
Elementscatalogue — the sorted, de-duplicated set of elements the network actually uses, plus the stoichiometry matrices.
from jaff import Network
net = Network("networks/h_photoionization/h_photo.jet")
net.elements.count # 1
net.elements.symbols() # ['H']
Why only H?
The hydrogen network contains H, H+, and e-, yet a single element
comes out. Element extraction keeps only alphabetic atom tokens, so the
charge markers (+, -) and the electron (e-) are dropped — they are not
chemical elements. What remains is the set of real periodic-table symbols.
The two layers
An Element knows about itself — "I am oxygen, 8 protons, atomic weight
15.999". It carries no notion of which species it appears in.
The Elements catalogue knows about the set — which elements the network
uses, in a fixed sorted order, and how each species is composed from them (the
composition matrices).
Both classes are flyweights: identical instances are reused rather than rebuilt. Constructing the same element, or an element set over the same species, hands back the very same object:
This keeps element objects unique across the whole program, so identity
(is) and equality agree.
The individual Element
Attributes
| Attribute | Type | Description |
|---|---|---|
symbol |
str |
Periodic-table symbol ("H", "C", "O") |
name |
str |
Full element name, capitalised ("Hydrogen") |
mass |
float |
Most-common-isotope mass in grams (CGS) |
atomic_mass |
float |
Standard atomic weight in atomic mass units |
protons |
int |
Atomic number |
neutrons |
int |
Neutron count of the most common isotope |
electrons |
int |
Electron count of the neutral atom |
Note the two mass fields: mass is the physical mass in grams (like
Specie.mass), while atomic_mass is the dimensionless atomic
weight.
h = net.elements["H"]
h.symbol # 'H'
h.name # 'Hydrogen' ← capitalised
h.mass # 1.673773e-24 ← grams (CGS)
h.atomic_mass # 1.008 ← amu
h.protons # 1
h.neutrons # 0
h.electrons # 1
An Element has no index
Unlike a Specie or Reaction, an Element carries no position field. Its
place in the matrices is its rank in the sorted symbol list — recover it with
net.elements.symbols().index("H"), not element.index.
Comparing and printing an Element
An Element's identity for comparison is its symbol. Equality, ordering,
and hashing all reduce to that string:
net1 = Network("networks/uclchem_small_gas/uclchem_small_gas_network.jet")
C, H, O = net1.elements["C"], net1.elements["H"], net1.elements["O"]
C == H # False — different symbols
C == net1.elements["C"] # True
C < H # True — '<' / '>' compare symbols lexicographically
H > C # True
sorted([O, H, C]) # [ElementObject(symbol='C'), ElementObject(symbol='H'), ElementObject(symbol='O')]
Because the symbol is the hash key (and instances are flyweights), elements work
cleanly in sets and as dict keys.
Comparisons only work element-to-element
Comparing an Element against a bare string raises TypeError:
Printing an element gives its symbol; repr wraps it for debugging:
str(net.elements["H"]) # 'H' ← __str__ is the symbol
repr(net.elements["H"]) # "ElementObject(symbol='H')"
The Elements catalogue
net.elements is sorted alphabetically by symbol and de-duplicated. That
fixed order is what pins the row order of the composition matrices, so it never
shifts between runs.
A single species also has its own element set, reachable via
Specie.elements:
How many elements
net.elements.count is the number of unique elements, and the catalogue is
sized, so len(net.elements) returns the same value — count is the cached
attribute, len() the Pythonic spelling:
Finding an element
net1.elements["O"] # by symbol → Element
net1.elements[0] # by index into the set → Element('C')
net1.elements.from_symbol("O") # same as ["O"]
net1.elements.from_name("Oxygen") # by full name (capitalised)
"C" in net1.elements # True — membership is by symbol
Lookup is by symbol (or full name via from_name); there is no ne
flag here, because the electron is never an element in the first place.
Bulk accessors
The hydrogen network has a single element, so switch to a richer one to see
these. Each returns a Vector aligned to the sorted symbol order.
net1 = Network("networks/uclchem_small_gas/uclchem_small_gas_network.jet")
net1.elements.symbols() # ['C', 'H', 'He', 'Mg', 'O']
net1.elements.names() # ['Carbon', 'Hydrogen', 'Helium', 'Magnesium', 'Oxygen']
net1.elements.atomic_masses() # [12.011, 1.008, 4.003, 24.305, 15.999]
net1.elements.protons() # [6, 1, 2, 12, 8]
net1.elements.masses() # most-common-isotope masses, grams (CGS)
net1.elements.neutrons() # neutron counts
net1.elements.electrons() # electron counts
Composition matrices
The point of the Elements catalogue is to describe how every species is
built from elements. Two matrices do this, both with shape
(n_elements, n_species) — element rows, species columns — and both returned as
plain list[list[int]] (not NumPy arrays), cached after the first call.
Row = element, column = species
Index as matrix[element_row][species_col]. The row order matches
net.elements.symbols(); the column order matches net.species (so the
column is just specie.index). Since an Element has no index, get the
row from the symbol list.
Density matrix
density_matrix()[i][j] is the number of atoms of element i in species
j — the stoichiometric composition.
net1 = Network("networks/uclchem_small_gas/uclchem_small_gas_network.jet")
density = net1.elements.density_matrix()
syms = list(net1.elements.symbols()) # ['C', 'H', 'He', 'Mg', 'O']
h_row = syms.index("H") # 1
o_row = syms.index("O") # 4
h2o = net1.species["H2O"].index # column for H2O
density[h_row][h2o] # 2 — two H atoms in H2O
density[o_row][h2o] # 1 — one O atom
Truth matrix
truth_matrix()[i][j] is the presence mask — 1 if element i appears in
species j, else 0 — i.e. density_matrix() clipped to 0/1. Same shape and
indexing as the density matrix.
truth = net1.elements.truth_matrix()
syms = list(net1.elements.symbols())
truth[syms.index("H")][net1.species["H2O"].index] # 1 — H2O contains H
Common patterns
Print the elemental composition of each species
net1 = Network("networks/uclchem_small_gas/uclchem_small_gas_network.jet")
density = net1.elements.density_matrix()
syms = list(net1.elements.symbols())
print(f"{'Species':<10}", " ".join(f"{s:>3}" for s in syms))
print("-" * (10 + 5 * len(syms)))
for sp in net1.species:
col = [density[row][sp.index] for row in range(len(syms))]
counts = " ".join(f"{n:>3}" for n in col)
print(f"{sp.name:<10} {counts}")
Find every species containing a given element
def species_with_element(net, symbol):
density = net.elements.density_matrix()
row = list(net.elements.symbols()).index(symbol)
return [s for s in net.species if density[row][s.index] > 0]
carbon_species = species_with_element(net1, "C")
print([s.name for s in carbon_species])