Species

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}
end
Advanced description
  • aggregate_state denotes the state of the species (solid, liquid, gas) for which the possible keywords are AS_AQUEOUS, AS_CRYSTAL, AS_GAS and AS_UNDEF
  • class defines the role played by the species in the solution. The possible keywords are SC_AQSOLVENT, SC_AQSOLUTE, SC_COMPONENT, SC_GASFLUID and SC_UNDEF
  • properties refers 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, aggregatestate and class. In the example below, vapour is not equal to H₂O since *aggregatestate* 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₂O
Remark

You will also have noticed that a calculation of the molar mass of the species is systematically carried out.


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:

KeyTypeDescription
:MQuantity (g/mol)Molar mass, computed automatically from the formula

Common user-added thermodynamic properties (loaded from databases or set manually):

KeyTypeDescription
: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 FunctionMolar 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 °C
41.099821999999996
CO₂[:Cp⁰](T = 500.0)     # J/mol/K at 227 °C
45.919999999999995
CO₂[:Δₐ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: $\text{Cp}°(T) = a_0 + a_1 T + a_2 T^{-2} + a_3 T^{-0.5} + \ldots + a_{10} \log T$ Unused coefficients should be set to 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°(Tref), ΔₐH°(Tref), Δₐ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: true
Solid 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("data/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).