Equilibrium
ChemistryLab.REJ_CHARGE_DEFAULTChemistryLab.REJ_HKFChemistryLab._DEFAULT_SOLVER_FACTORYChemistryLab.AbstractActivityModelChemistryLab.AbstractSolidSolutionModelChemistryLab.AbstractSolidSolutionPhaseChemistryLab.DaviesActivityModelChemistryLab.DaviesActivityModelChemistryLab.DiluteSolutionModelChemistryLab.EquilibriumProblemChemistryLab.EquilibriumProblemChemistryLab.EquilibriumSolverChemistryLab.EquilibriumSolverChemistryLab.HKFActivityModelChemistryLab.HKFActivityModelChemistryLab.IdealSolidSolutionModelChemistryLab.RedlichKisterModelChemistryLab.RedlichKisterModelChemistryLab.SolidSolutionPhaseChemistryLab.SolidSolutionPhaseChemistryLab._build_n0ChemistryLab._build_paramsChemistryLab._excess_ln_gammaChemistryLab._hkf_sigmaChemistryLab._solid_solution_lna!ChemistryLab.activity_modelChemistryLab.activity_modelChemistryLab.activity_modelChemistryLab.build_potentialsChemistryLab.end_membersChemistryLab.equilibrateChemistryLab.hkf_debye_huckel_paramsChemistryLab.modelChemistryLab.name
Activity models
ChemistryLab.REJ_CHARGE_DEFAULT — Constant
REJ_CHARGE_DEFAULT::Dict{Int,Float64}Fallback effective electrostatic radii åᵢ [Å] indexed by formal charge, from ToughReact V2 (Xu et al. 2011, Table A2; after Helgeson et al. 1981).
Used by HKFActivityModel with priority 3 in the radius lookup chain: sp[:å] > REJ_HKF > REJ_CHARGE_DEFAULT > model.å_default.
See also: REJ_HKF, HKFActivityModel.
ChemistryLab.REJ_HKF — Constant
REJ_HKF::Dict{String,Float64}Effective electrostatic radii åᵢ [Å] for aqueous ions from Helgeson, Kirkham & Flowers (1981), Am. J. Sci. 281, Table 3.
Keys are PHREEQC-format formula strings (e.g. "Na+", "Ca+2", "SO4-2"). Used by HKFActivityModel with priority 2 in the radius lookup chain: sp[:å] > REJ_HKF > REJ_CHARGE_DEFAULT > model.å_default.
See also: REJ_CHARGE_DEFAULT, HKFActivityModel.
ChemistryLab.AbstractActivityModel — Type
abstract type AbstractActivityModel endBase type for all activity models. Each concrete subtype must implement activity_model(cs::ChemicalSystem, model::AbstractActivityModel) which returns a closure a(n, p) -> Vector{Float64} of log-activities.
ChemistryLab.DaviesActivityModel — Type
struct DaviesActivityModel{T<:Real} <: AbstractActivityModelDavies (1962) activity model — a simplified Debye-Hückel without ionic radii.
Activity coefficient formula
For ionic species (charge z ≠ 0):
log₁₀ γᵢ = −A zᵢ² (√I / (1 + √I) − b I)For neutral aqueous species (charge z = 0):
log₁₀ γᵢ = bₙ ILog-activity (molality convention): ln aᵢ = ln(10) × log₁₀(γᵢ) + ln(mᵢ)
Water activity: Raoult approximation ln a_w = ln(x_w) (V1; accurate for I < 0.1).
Fields
A: Debye-Hückel A parameter [(kg/mol)^(1/2)]. Default 0.5114 at 25 °C/1 bar.b: Davies empirical constant (default 0.3).bₙ: salting-out coefficient for neutral species (default 0.1).temperature_dependent: iftrue, recompute A fromp.T,p.Pat each call.
References
- Davies, C.W. (1962). Ion Association. Butterworths, London.
ChemistryLab.DaviesActivityModel — Method
DaviesActivityModel(; A=0.5114, b=0.3, bₙ=0.1, temperature_dependent=false)Construct a DaviesActivityModel.
ChemistryLab.DiluteSolutionModel — Type
struct DiluteSolutionModel <: AbstractActivityModelIdeal dilute solution model:
- Solvent: Raoult's law —
ln a = ln(x_solvent) - Solutes: Henry's law —
ln a = ln(c_i / c°)wherec° = 1 mol/L - Crystals: pure solid —
ln a = 0 - Gas: ideal mixture —
ln a = ln(x_i) - SS end-members: ideal mixing —
ln a = ln(xᵢ)within the solid-solution phase
ChemistryLab.HKFActivityModel — Type
struct HKFActivityModel{T<:Real} <: AbstractActivityModelExtended Debye-Hückel activity model with B-dot term (Helgeson 1969 / Helgeson, Kirkham & Flowers 1981). This is the model used by PHREEQC and EQ3/6.
Activity coefficient formulas
For ionic species (charge z ≠ 0):
log₁₀ γᵢ = −A zᵢ² √I / (1 + B åᵢ √I) + Ḃ IFor neutral aqueous species (charge z = 0):
log₁₀ γᵢ = Kₙ ILog-activity (molality convention, standard state m° = 1 mol/kg):
ln aᵢ = ln(10) × log₁₀(γᵢ) + ln(mᵢ)Water activity is computed from the osmotic coefficient φ (Gibbs-Duhem):
ln a_w = −Mw × Σmⱼ × φIonic strength: I = ½ Σ mⱼ zⱼ²
Fields
A: Debye-Hückel A parameter [(kg/mol)^(1/2)]. Default 0.5114 at 25 °C/1 bar.B: Debye-Hückel B parameter [Å⁻¹(kg/mol)^(1/2)]. Default 0.3288.Ḃ: B-dot extended term [kg/mol]. Default 0.041.Kₙ: salting-out coefficient for neutral species [kg/mol]. Default 0.1.å_default: global fallback effective ionic radius [Å]. Default 3.72.temperature_dependent: iftrue, recompute A and B fromp.T,p.Pat each call to the activity closure (requiresTandPinp). Defaultfalse.
Ionic radius lookup
The effective radius åᵢ is resolved in order:
sp[:å]— explicit value in the species properties dict.REJ_HKF— Helgeson et al. (1981) Table 3, keyed by PHREEQC formula.REJ_CHARGE_DEFAULT— fallback by formal charge.model.å_default— global fallback.
Valid range
I ≲ 1 mol/kg. Beyond ~2 mol/kg, the Pitzer model is recommended.
References
- Helgeson, H.C. (1969). Am. J. Sci. 267, 729–804.
- Helgeson, H.C., Kirkham, D.H. & Flowers, G.C. (1981). Am. J. Sci. 281, 1249–1516.
- Parkhurst, D.L. & Appelo, C.A.J. (2013). USGS Techniques Methods, Book 6, ch. A43.
Examples
# Default model at 25 °C / 1 bar (fixed A, B)
model = HKFActivityModel()
# Temperature-dependent A and B (recomputed at each solve)
model_tdep = HKFActivityModel(temperature_dependent=true)
state_eq = equilibrate(state; model=HKFActivityModel())ChemistryLab.HKFActivityModel — Method
HKFActivityModel(; A=0.5114, B=0.3288, Ḃ=0.041, Kₙ=0.1, å_default=3.72,
temperature_dependent=false) -> HKFActivityModelConstruct an HKFActivityModel with the given parameters.
Default values are from Helgeson et al. (1981), Table 1, at 25 °C / 1 bar.
ChemistryLab._excess_ln_gamma — Method
_excess_ln_gamma(model, k, x, T) -> RealReturn the excess log-activity coefficient ln γₖ for end-member k (1-based index) of a solid solution with mole-fraction vector x at temperature T (K).
AD-compatible: all branches preserve ForwardDiff.Dual through computations on x.
Methods:
IdealSolidSolutionModel: returnszero(eltype(x)).RedlichKisterModel: binary Redlich-Kister formula (requireslength(x) == 2).
ChemistryLab._hkf_sigma — Method
_hkf_sigma(x) -> RealCompute the σ function used in the osmotic coefficient formula (Helgeson et al. 1981, Eq. 132–137):
σ(x) = (3/x³)(x − 2 ln(1+x) − 1/(1+x) + 1)For |x| < 1e-3, a Taylor series 1 − (3/2)x + (9/5)x² is used to avoid catastrophic cancellation. Both branches agree to O(x³) at the threshold, so the gradient is continuous.
AD-compatible: branching is on ForwardDiff.value(x), not on the Dual itself.
ChemistryLab._solid_solution_lna! — Method
_solid_solution_lna!(out, _n, ss_groups, ss_models, T, ϵ)Fill out[i] with ln aᵢ = ln xᵢ + ln γᵢ for all solid-solution end-members.
ss_groups[k] and ss_models[k] describe the k-th solid-solution phase. T is the temperature in K (only relevant for non-ideal models). ϵ is a regularisation floor to avoid log(0).
ForwardDiff-compatible.
ChemistryLab.activity_model — Method
activity_model(cs::ChemicalSystem, model::DaviesActivityModel) -> FunctionReturn a closure lna(n, p) -> Vector computing log-activities for the Davies (1962) model. No species-specific ionic radii are required.
AD-compatible: all closure computations accept ForwardDiff.Dual inputs.
ChemistryLab.activity_model — Method
activity_model(cs::ChemicalSystem, ::DiluteSolutionModel) -> FunctionReturn a closure lna(n, p) -> Vector{Float64} computing the vector of log-activities for the dilute ideal solution model.
The returned function has signature lna(n, p) where:
n: dimensionless mole vector (same indexing ascs.species)p:NamedTuplecontaining at leastϵ(floor value to avoid log(0))
Solid-solution end-members (class SC_SSENDMEMBER) receive ln aᵢ = ln xᵢ where xᵢ = nᵢ / Σnⱼ within the same solid-solution phase.
All quantities are dimensionless — units are stripped at construction time.
ChemistryLab.activity_model — Method
activity_model(cs::ChemicalSystem, model::HKFActivityModel) -> FunctionReturn a closure lna(n, p) -> Vector computing log-activities for the extended Debye-Hückel (B-dot) model of Helgeson (1969).
The closure captures all species indices and ionic radii at construction time. Inside lna:
- Solutes: molality convention, B-dot formula for ions, salting-out for neutrals.
- Solvent: osmotic coefficient from Gibbs-Duhem (σ-function).
- Crystals:
ln a = 0(pure solid). - Gas: ideal mixture
ln a = ln(xᵢ).
If model.temperature_dependent=true, p must contain T (K) and P (Pa) — both are provided automatically by _build_params.
AD-compatible: all closure computations accept ForwardDiff.Dual inputs.
ChemistryLab.build_potentials — Method
build_potentials(cs::ChemicalSystem, model::AbstractActivityModel) -> FunctionReturn a closure μ(n, p) -> Vector{Float64} computing dimensionless chemical potentials μ_i / RT for all species.
$\mu_i / RT = \Delta_a G_i^0 / RT + \ln a_i$
The returned function is compatible with SciML solvers:
n: dimensionless mole vectorp:NamedTuplecontaining:ΔₐG⁰overT: vector of standard Gibbs energies of formation divided by RTϵ: regularization floor (e.g.1e-30)
All quantities are dimensionless — caller is responsible for stripping units from ΔₐG⁰overT before passing them in p.
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("Na+"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE),
]);
julia> μ = build_potentials(cs, DiluteSolutionModel());
julia> n = [55.5, 0.1];
julia> p = (ΔₐG⁰overT = [-95.6, -105.6], ϵ = 1e-30);
julia> length(μ(n, p)) == 2
trueChemistryLab.hkf_debye_huckel_params — Method
hkf_debye_huckel_params(T_K, P_Pa) -> NamedTuple{(:A, :B)}Compute the Debye-Hückel A and B parameters from the water density ρ [g/cm³] and dielectric constant εᵣ at temperature T_K (K) and pressure P_Pa (Pa).
Formulas (Helgeson et al. 1981):
A(T,P) = 1.824829238×10⁶ × √ρ / (εᵣ T)^(3/2) [(kg/mol)^(1/2)]
B(T,P) = 50.29158649 × √ρ / √(εᵣ T) [Å⁻¹ (kg/mol)^(1/2)]where ρ is in g/cm³.
Uses water_thermo_props (HGK equation of state) and water_electro_props_jn (Johnson-Norton dielectric constant).
AD-compatible (ForwardDiff-safe). Returns a NamedTuple (A=..., B=...).
Examples
julia> p = hkf_debye_huckel_params(298.15, 1e5);
julia> isapprox(p.A, 0.5114; rtol=1e-3)
true
julia> isapprox(p.B, 0.3288; rtol=1e-3)
trueSolid solutions
ChemistryLab.AbstractSolidSolutionModel — Type
abstract type AbstractSolidSolutionModel endBase type for activity models within a solid solution phase. Each concrete subtype implements _excess_ln_gamma(model, k, x, T).
ChemistryLab.AbstractSolidSolutionPhase — Type
abstract type AbstractSolidSolutionPhase endBase type for solid solution phases. Concrete subtypes group a set of end-member AbstractSpecies and associate them with an AbstractSolidSolutionModel.
ChemistryLab.IdealSolidSolutionModel — Type
struct IdealSolidSolutionModel <: AbstractSolidSolutionModelIdeal (Temkin) solid solution mixing: ln γᵢ = 0, hence ln aᵢ = ln xᵢ.
Valid for any number of end-members.
Example
julia> m = IdealSolidSolutionModel()
IdealSolidSolutionModel()ChemistryLab.RedlichKisterModel — Type
struct RedlichKisterModel{T<:Real} <: AbstractSolidSolutionModelBinary Redlich-Kister (asymmetric Margules) model for non-ideal solid solutions. Requires exactly 2 end-members per solid solution phase.
Parameters a0, a1, a2 are in J/mol and divided by RT inside the activity closure.
Activity coefficients (Guggenheim / ThermoCalc convention):
ln γ₁ = (x₂²/RT)[a₀ + a₁(3x₁ − x₂) + a₂(x₁ − x₂)(5x₁ − x₂)]
ln γ₂ = (x₁²/RT)[a₀ − a₁(3x₂ − x₁) + a₂(x₂ − x₁)(5x₂ − x₁)]AD-compatible: all computations propagate ForwardDiff.Dual numbers.
Examples
julia> m = RedlichKisterModel(a0 = 4000.0, a1 = 500.0)
RedlichKisterModel{Float64}(4000.0, 500.0, 0.0)
julia> m.a0
4000.0ChemistryLab.RedlichKisterModel — Method
RedlichKisterModel(; a0=0.0, a1=0.0, a2=0.0)Keyword constructor for RedlichKisterModel. Parameters are promoted to a common type.
ChemistryLab.SolidSolutionPhase — Type
struct SolidSolutionPhase{T<:AbstractSpecies, M<:AbstractSolidSolutionModel}
<: AbstractSolidSolutionPhaseA solid-solution phase consisting of end_members (species with AS_CRYSTAL aggregate state) mixing according to model.
End-members are automatically requalified to SC_SSENDMEMBER at construction time, so database species with SC_COMPONENT can be passed directly.
Construction
Use the keyword constructor:
SolidSolutionPhase(name, end_members; model = IdealSolidSolutionModel())Validation at construction time:
- All end-members must have
aggregate_state == AS_CRYSTAL. RedlichKisterModelrequires exactly 2 end-members.
Example
julia> em1 = Species("Ca2SiO4"; aggregate_state=AS_CRYSTAL, class=SC_COMPONENT);
julia> em2 = Species("Ca3Si2O7"; aggregate_state=AS_CRYSTAL, class=SC_COMPONENT);
julia> ss = SolidSolutionPhase("CSH", [em1, em2])
SolidSolutionPhase{Species{Int64}, IdealSolidSolutionModel}
name: CSH
end-members (2): Ca2SiO4, Ca3Si2O7
model: IdealSolidSolutionModel
julia> class(end_members(ss)[1])
SC_SSENDMEMBER::Class = 5ChemistryLab.SolidSolutionPhase — Method
SolidSolutionPhase(name, end_members; model=IdealSolidSolutionModel())Construct and validate a SolidSolutionPhase.
End-members whose class is not already SC_SSENDMEMBER are automatically requalified via with_class. Passing database species with SC_COMPONENT therefore works directly, without a prior call to with_class.
ChemistryLab.end_members — Method
end_members(ss::SolidSolutionPhase) -> Vector{<:AbstractSpecies}Return the end-member species of the solid solution phase.
ChemistryLab.model — Method
model(ss::SolidSolutionPhase) -> AbstractSolidSolutionModelReturn the activity model of the solid solution phase.
ChemistryLab.name — Method
name(ss::SolidSolutionPhase) -> StringReturn the name of the solid solution phase.
Problem and solver
ChemistryLab.EquilibriumProblem — Type
EquilibriumProblemDefinition of a chemical equilibrium problem.
Fields
b: conservation vector (elemental abundances).A: stoichiometric matrix (conservation matrix).μ: chemical potential functionμ(n, p).u0: initial guess for species amounts.p: coefficients for the potential function (default:nothing).lb: lower bounds for species amounts.ub: upper bounds for species amounts.
The problem solves for the species distribution that minimizes the Gibbs energy subject to mass conservation constraints A * n = b.
ChemistryLab.EquilibriumProblem — Method
EquilibriumProblem(A, μ, u0; b=A*u0, p=nothing, lb=fill(Tu(1e-16), length(u0)), ub=maximum(abs.(A))/minimum(abs.(A[.!iszero.(A)]))*sum(u0)*one.(u0))Construct an EquilibriumProblem with the given stoichiometric matrix A, chemical potential function μ, and initial guess u0.
Arguments
A: stoichiometric matrix (conservation matrix).μ: chemical potential functionμ(n, p).u0: initial guess for species amounts.b: conservation vector (elemental abundances). Defaults toA * u0.p: coefficients for the potential function. Defaults tonothing.lb: lower bounds for species amounts. Defaults tofill(Tu(1e-16), length(u0)).ub: upper bounds for species amounts. Defaults tomaximum(abs.(A))/minimum(abs.(A[.!iszero.(A)]))*sum(u0)*one.(u0).
Returns
An EquilibriumProblem instance.
ChemistryLab._DEFAULT_SOLVER_FACTORY — Constant
_DEFAULT_SOLVER_FACTORYInternal Ref{Union{Nothing, Function}} — populated by extension __init__ functions to register a default solver factory.
OptimizationIpoptExt.__init__: registers only if nothing is set (low priority).OptimaSolverExt.__init__: always overrides (high priority).
Result: OptimaSolver wins whenever loaded, regardless of load order.
ChemistryLab.EquilibriumSolver — Type
struct EquilibriumSolver{F<:Function, S, V<:Val}Encapsulates all fixed ingredients of a chemical equilibrium calculation: the potential function, the SciML solver, and the variable space.
Construct once, call repeatedly with different ChemicalState inputs.
Fields
μ: chemical potential closureμ(n, p) -> Vector{Float64}.solver: any Optimization.jl-compatible solver (e.g.IpoptOptimizer()).variable_space: variable space —Val(:linear)orVal(:log).kwargs: solver keyword arguments forwarded tosolve.
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("H+"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE),
Species("OH-"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE),
]);
julia> solver = EquilibriumSolver(cs, DiluteSolutionModel(), IpoptOptimizer());
julia> solver isa EquilibriumSolver
trueChemistryLab.EquilibriumSolver — Method
EquilibriumSolver(cs, model, solver; variable_space=Val(:linear), kwargs...)Construct an EquilibriumSolver from a ChemicalSystem, an activity model, and a SciML solver.
The potential function is built once at construction time from cs and model. Repeated calls to solve with different ChemicalState inputs reuse it.
Arguments
cs: theChemicalSystemdefining species and conservation matrix.model: anAbstractActivityModel(e.g.DiluteSolutionModel()).solver: any Optimization.jl solver.variable_space:Val(:linear)(default) orVal(:log).kwargs...: forwarded to the underlyingsolvecall (tolerances, verbosity...).
ChemistryLab._build_n0 — Method
_build_n0(state::ChemicalState) -> VectorExtract the dimensionless mole vector from a ChemicalState. Type is inferred from the state — compatible with ForwardDiff dual numbers.
ChemistryLab._build_params — Method
_build_params(state::ChemicalState; ϵ=1e-16) -> NamedTupleExtract dimensionless parameters from a ChemicalState. ΔₐG⁰overT is evaluated at the current T and P of the state. Units are stripped — compatible with ForwardDiff dual numbers.
Returned fields
ΔₐG⁰overT: vector of standard Gibbs energies divided by RT (dimensionless).T: temperature in K (plain number, Dual-safe).P: pressure in Pa (plain number, Dual-safe).ϵ: regularisation floor (default1e-16).
T and P are included so that temperature-dependent activity models (e.g. HKFActivityModel with temperature_dependent=true) can recompute their parameters inside the potential closure.
ChemistryLab.equilibrate — Method
equilibrate(state::ChemicalState, solver; model=..., variable_space=..., ϵ=...) -> ChemicalState
equilibrate(state::ChemicalState; kwargs...) -> ChemicalStateCompute the chemical equilibrium state by minimising the Gibbs free energy.
Two-argument form (solver explicit, always available once an extension is loaded):
using Optimization, OptimizationIpopt
state_eq = equilibrate(state, IpoptOptimizer())
using OptimaSolver
state_eq = equilibrate(state, OptimaOptimizer())One-argument form (uses the default solver of the loaded extension):
state_eq = equilibrate(state)When both extensions are loaded, OptimaSolverExt always takes priority.
Arguments
state: initialChemicalState— defines the system, T, P, and composition.solver: any SciML-compatible solver (e.g.IpoptOptimizer(),OptimaOptimizer()).model: activity model (default:DiluteSolutionModel()).variable_space:Val(:linear)(default) orVal(:log).ϵ: regularisation floor for mole amounts (default:1e-16).kwargs...: forwarded to the underlying solver.