Species
Before this page
Species is a composite type (introduced by the keyword struct) and is defined by a human-readable name, a chemical symbol/notation, an underlying Formula object holding composition, charge, and string representations, a physical state aggregate_state, a species class, as well as an extensible map of custom properties (molar mass, thermodynamic data, etc.)
struct Species{T<:Number} <: AbstractSpecies
name::String
symbol::String
formula::Formula{T}
aggregate_state::AggregateState
class::Class
properties::OrderedDict{Symbol,PropertyType}
endAdvanced description
aggregate_statedenotes the state of the species (solid, liquid, gas) for which the possible keywords areAS_AQUEOUS,AS_CRYSTAL,AS_GAS,AS_LIQUIDandAS_UNDEFclassdefines the role played by the species in the solution. The possible keywords areSC_AQSOLVENT,SC_AQSOLUTE,SC_COMPONENT,SC_GASFLUIDandSC_UNDEFpropertiesrefers to the set of properties intrinsic to the species. These properties are detailed below.
Species construction
Species can be created from:
- a
Formula
fH2O = Formula("H2O")
H2O = Species(fH2O, aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)Species{Int64}
name: H2O
symbol: H2O
formula: H2O ◆ H₂O
atoms: H => 2, O => 1
charge: 0
aggregate_state: AS_AQUEOUS
class: SC_AQSOLVENT
properties: M = 0.0180149999937744 kg mol⁻¹- a string
HSO4⁻ = Species("HSO₄⁻", aggregate_state=AS_AQUEOUS, class=SC_COMPONENT)Species{Int64}
name: HSO₄⁻
symbol: HSO₄⁻
formula: HSO₄⁻ ◆ HSO4-
atoms: H => 1, S => 1, O => 4
charge: -1
aggregate_state: AS_AQUEOUS
class: SC_COMPONENT
properties: M = 0.09706399996645676 kg mol⁻¹- a dictionary
CO2 = Species(Dict(:C => 1, :O => 2), aggregate_state=AS_GAS, class=SC_GASFLUID)Species{Int64}
name: CO₂
symbol: CO₂
formula: CO2 ◆ CO₂
atoms: O => 2, C => 1
charge: 0
aggregate_state: AS_GAS
class: SC_GASFLUID
properties: M = 0.04400899998479143 kg mol⁻¹Adding charge
To add a charge when creating species with a dictionary, you must add, after the dictionary, the value of the charge (charge is considered an argument of the composite type).
SiO₃²⁻ = Species(Dict(:Si => 1, :O => 3), -2, aggregate_state=AS_AQUEOUS, class=SC_COMPONENT)Species{Int64}
name: SiO₃-2
symbol: SiO₃-2
formula: SiO3-2 ◆ SiO₃-2
atoms: Si => 1, O => 3
charge: -2
aggregate_state: AS_AQUEOUS
class: SC_COMPONENT
properties: M = 0.0760819999737077 kg mol⁻¹Keyword arguments such as name, symbol, aggregate_state, class can be added during construction.
fH₂O = Formula("H2O")
H₂O = Species(fH₂O; name="Water", symbol="H₂O@", aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)Species{Int64}
name: Water
symbol: H₂O@
formula: H2O ◆ H₂O
atoms: H => 2, O => 1
charge: 0
aggregate_state: AS_AQUEOUS
class: SC_AQSOLVENT
properties: M = 0.0180149999937744 kg mol⁻¹And symbol accept unicode characters.
CO₂ = Species(Dict(:C=>1, :O=>2); name="Carbon dioxide", symbol="CO₂⤴", aggregate_state=AS_GAS, class=SC_GASFLUID)Species{Int64}
name: Carbon dioxide
symbol: CO₂⤴
formula: CO2 ◆ CO₂
atoms: O => 2, C => 1
charge: 0
aggregate_state: AS_GAS
class: SC_GASFLUID
properties: M = 0.04400899998479143 kg mol⁻¹Comparison between species
Comparison between species (or cemspecies) are done by comparing atoms, aggregate_state and class. In the example below, vapor is not equal to H₂O since aggregate_state and class are different despite atoms are identical.
vapour = Species("H2O"; name="Vapour", symbol="H₂O⤴", aggregate_state=AS_GAS, class=SC_GASFLUID)
vapour == H₂ORemark
You will also have noticed that a calculation of the molar mass of the species is systematically carried out.
When are two species the same species?
Two species are equal when their formula, aggregate state and class agree, and when their symbol agrees on top of that — but only insofar as the symbol says something the formula does not.
The symbol is in the contract because the first three do not separate polymorphs, and a polymorph is a different substance. Calcite and aragonite are both CaCO3, both AS_CRYSTAL, both SC_COMPONENT, and in CEMDATA18 their standard Gibbs energies differ by 821 J/mol — at 298 K, 0.33 in ln K, which is the whole difference in solubility between them.
using ChemistryLab
calcite = Species("CaCO3"; symbol = "Cal", aggregate_state = AS_CRYSTAL)
aragonite = Species("CaCO3"; symbol = "Arg", aggregate_state = AS_CRYSTAL)
(calcite == aragonite, length(unique([calcite, aragonite])))(false, 2)At the same time, a symbol that merely spells the species' own formula adds nothing, and is ignored. So the same substance typed two ways is one species, with one hash — which is what a Dict or a Set needs:
a, b = Species("Ca+2"), Species("Ca⁺²")
(a == b, hash(a) == hash(b), Dict(a => 1)[b])(true, true, 1)The symbol is also a lookup key, and stays what you typed
ChemicalSystem indexes its species by symbol — cs["H2O"] — so the stored symbol is never rewritten. The canonicalization above happens inside the comparison only. In particular it does not replace a derived symbol by a canonical spelling, because unicode is not constant on formulas that compare equal: Formula("e") and Formula("e-") are equal yet spell themselves "e" and "e⁻", and Formula("Ca+2") and Formula("Ca⁺²") are equal yet spell themselves "Ca²⁺" and "Ca⁺²". A derived symbol therefore contributes nothing at all rather than a canonical form.
cs = ChemicalSystem([Species("H2O"; aggregate_state = AS_AQUEOUS)])
symbol.(cs.species), cs["H2O"] == Species("H2O"; aggregate_state = AS_AQUEOUS)(["H2O"], true)Species read from a database always carry an explicit symbol — "Cal", "Arg", "H2O@" — so every identity question about them is decided by that symbol, as it should be.
Species properties
The molar mass is automatically calculated and stored in the species properties dict under the key :M. Beyond that, the properties dict is open: any value of type Number, AbstractVector{<:Number}, Function, or AbstractString can be added at any time.
Predefined property:
| Key | Type | Description |
|---|---|---|
:M | Quantity (g/mol) | Molar mass, computed automatically from the formula |
Common user-added thermodynamic properties (loaded from databases or set manually):
| Key | Type | Description |
|---|---|---|
:Cp⁰ | SymbolicFunc(T) | Standard heat capacity (J mol⁻¹ K⁻¹) |
:ΔₐH⁰ | SymbolicFunc(T) | Standard enthalpy of formation (J mol⁻¹) |
:S⁰ | SymbolicFunc(T) | Standard entropy (J mol⁻¹ K⁻¹) |
:ΔₐG⁰ | SymbolicFunc(T) | Standard Gibbs free energy of formation (J mol⁻¹) |
:V⁰ | Quantity or Function | Molar volume (m³ mol⁻¹) |
Properties are accessed and mutated via []:
using ChemistryLab
using DynamicQuantities
H2O = Species("H2O"; aggregate_state = AS_AQUEOUS, class = SC_AQSOLVENT)
# Molar mass is always available
H2O[:M]0.0180149999937744 kg mol⁻¹# Add a scalar property
H2O[:V⁰] = 18.07e-6u"m^3/mol"
H2O[:V⁰]1.807e-5 m³ mol⁻¹# Add a string annotation
H2O[:source] = "CRC Handbook 2024"
H2O[:source]"CRC Handbook 2024"Attaching temperature-dependent thermodynamic functions
The standard workflow uses build_thermo_functions to construct callable SymbolicFunc objects from reference data and Cp polynomial coefficients, then assigns them to the species:
using ChemistryLab
using DynamicQuantities
CO₂ = Species("CO2"; name = "Carbon dioxide", aggregate_state = AS_GAS, class = SC_GASFLUID)
# Reference data at 298.15 K (from e.g. thermoddem.brgm.fr)
params_Cp_CO2 = Dict(
:S⁰ => 213.785u"J/K/mol",
:ΔₐH⁰ => -393510u"J/mol",
:ΔₐG⁰ => -394373u"J/mol",
:a₀ => 33.98u"J/K/mol", # Cp polynomial: only a₀ and a₁ non-zero here
:a₁ => 23.88e-3u"J/(mol*K^2)",
:a₂ => 0.0u"J*K/mol",
:a₃ => 0.0u"J/(mol*K^0.5)",
:T => 298.15u"K", # reference temperature
)
dtf_CO2 = build_thermo_functions(:cp_ft_equation, params_Cp_CO2)OrderedCollections.OrderedDict{Symbol, SymbolicFunc{1, @NamedTuple{T::DynamicQuantities.Quantity{Float64, DynamicQuantities.Dimensions{DynamicQuantities.FRInt32}}}, Float64, DynamicQuantities.Dimensions{DynamicQuantities.FRInt32}}} with 4 entries:
:Cp⁰ => 33.98 + 0.02388T [m² kg s⁻² K⁻¹ mol⁻¹] ◆ vars=(T) ◆ T=298.15 K
:ΔₐH⁰ => -4.04703e5 + 33.98T + 0.01194(T^2) [m² kg s⁻² mol⁻¹] ◆ vars=(T) ◆ T=298.15 K
:S⁰ => 13.0608 + 0.02388T + 33.98log(T) [m² kg s⁻² K⁻¹ mol⁻¹] ◆ vars=(T) ◆ T=298.15 K
:ΔₐG⁰ => -3.41826e5 + 20.9192T - 0.01194(T^2) - 33.98T*log(T) [m² kg s⁻² mol⁻¹] ◆ vars=(T) ◆ T=298.15 K# Assign each function to the species
for (k, v) in dtf_CO2
CO₂[k] = v
end
# Evaluate at different temperatures
CO₂[:Cp⁰](T = 298.15) # J/mol/K at 25 °C41.099821999999996CO₂[:Cp⁰](T = 500.0) # J/mol/K at 227 °C45.919999999999995CO₂[:ΔₐG⁰](T = 500.0u"K", unit = true) # Gibbs energy at 227 °C with units-439937.13910932385 m² kg s⁻² mol⁻¹Cp polynomial
The built-in :cp_ft_equation model uses a 10-term polynomial: 0.0 with appropriate units. Only non-zero terms affect the result. See the Thermodynamic Functions tutorial for the full list of models and parameters.
Reference temperature
Always include :T => reference_temperature in the parameter dict. The functions are adjusted so that S°(T_ref), ΔₐH°(T_ref), ΔₐG°(T_ref) match the provided reference values exactly.
Requalifying a species: with_class
Species is an immutable struct — none of its fields can be modified after construction. with_class returns a copy of the species with only the class field changed. All other fields — name, symbol, formula, aggregate state, properties and thermodynamic functions — are shared by reference and unchanged.
using ChemistryLab
# A crystal species with the class that databases typically assign
cal = Species("CaCO3"; aggregate_state = AS_CRYSTAL, class = SC_COMPONENT)
println("before: ", class(cal)) # SC_COMPONENT
em = with_class(cal, SC_SSENDMEMBER)
println("after: ", class(em)) # SC_SSENDMEMBER
println("formula preserved: ", formula(em) == formula(cal))before: SC_COMPONENT
after: SC_SSENDMEMBER
formula preserved: trueSolid solutions: no requalification needed
SolidSolutionPhase automatically promotes end-members to SC_SSENDMEMBER at construction time. Database species with SC_COMPONENT can therefore be passed directly — no prior call to with_class is required:
substances = build_species(datapath("cemdata18-thermofun.json"))
dict = Dict(symbol(s) => s for s in substances)
# Pass database species directly — SolidSolutionPhase requalifies internally
cshq = SolidSolutionPhase("CSHQ", [dict["CSHQ-TobD"], dict["CSHQ-TobH"],
dict["CSHQ-JenH"], dict["CSHQ-JenD"]])with_class is still useful when you need to track the requalified object explicitly (e.g. to inspect its class, or for types other than solid solution end-members).
Where to go next
Species combine into reactions in Chemical Reactions, and into the conservation matrix of a system in Stoichiometric Matrix. Species are seldom built by hand in practice: Database Interoperability reads them from the shipped databases.