Chemical systems and states
ChemistryLab.AbstractConstituentChemistryLab.AbstractExtentChemistryLab.CappedExtentChemistryLab.ChemicalStateChemistryLab.ChemicalStateChemistryLab.ChemicalStateChemistryLab.ChemicalSystemChemistryLab.ChemicalSystemChemistryLab.ChemicalSystemChemistryLab.ConstantExtentChemistryLab.DecompositionWindowChemistryLab.DecompositionWindowChemistryLab.KineticsProblemChemistryLab.LogisticExtentChemistryLab.MaterialChemistryLab.MineralConstituentChemistryLab.OxideConstituentChemistryLab.ParrottKillohExtentChemistryLab.PhaseQuantitiesChemistryLab.ProcessResultChemistryLab.RecipeChemistryLab.RecipeStateChemistryLab.SystemDiagnosticChemistryLab.TabulatedExtentBase.:*Base.:*Base.:+Base.:/Base.copyBase.getindexBase.getindexBase.mergeBase.mergeBase.showBase.showBase.sizeChemistryLab._auto_seed_neutral_pH!ChemistryLab._check_fractionsChemistryLab._co2_per_phaseChemistryLab._composition_ratioChemistryLab._compute_V_phasesChemistryLab._compute_m_phasesChemistryLab._compute_n_phasesChemistryLab._compute_pHChemistryLab._compute_pKwChemistryLab._compute_pOHChemistryLab._compute_porosityChemistryLab._compute_saturationChemistryLab._declaredChemistryLab._entry_to_molesChemistryLab._expand_instancesChemistryLab._gel_modelsChemistryLab._has_molar_volumeChemistryLab._ignition_molar_massChemistryLab._instancesChemistryLab._molar_volumeChemistryLab._refuse_overlapping_solid_solutionsChemistryLab._refuse_sites_on_mixing_hostsChemistryLab._refuse_two_gel_modelsChemistryLab._resolve_site_familiesChemistryLab._solid_indicesChemistryLab._update_derived!ChemistryLab._warn_one_substance_two_phasesChemistryLab._warn_pure_phase_beside_nonidealChemistryLab.add_saltChemistryLab.aqueousChemistryLab.blendChemistryLab.bogueChemistryLab.bound_waterChemistryLab.bound_waterChemistryLab.bound_water_per_phaseChemistryLab.budgetChemistryLab.budgetChemistryLab.carbonateChemistryLab.cation_molar_massChemistryLab.chemical_shrinkageChemistryLab.componentsChemistryLab.crystalChemistryLab.decomposeChemistryLab.diagnose_systemChemistryLab.effective_extentChemistryLab.element_amountsChemistryLab.enthalpyChemistryLab.enthalpyChemistryLab.enthalpyChemistryLab.equilibrate_certifiedChemistryLab.freeze_solid_solutionChemistryLab.gasChemistryLab.gasfluidChemistryLab.get_reactionChemistryLab.glass_enthalpyChemistryLab.glass_heat_capacityChemistryLab.glass_speciesChemistryLab.glass_speciesChemistryLab.heat_capacityChemistryLab.heat_capacityChemistryLab.heat_releaseChemistryLab.hydrateChemistryLab.ignition_lossChemistryLab.kinetic_speciesChemistryLab.leachChemistryLab.literature_oxidesChemistryLab.literature_windowsChemistryLab.massChemistryLab.massChemistryLab.massChemistryLab.material_templateChemistryLab.missing_enthalpyChemistryLab.missing_molar_volumesChemistryLab.molesChemistryLab.molesChemistryLab.molesChemistryLab.oxide_budgetChemistryLab.oxide_contentChemistryLab.oxide_materialChemistryLab.pHChemistryLab.pOHChemistryLab.phase_listChemistryLab.phase_list_systemChemistryLab.phase_massesChemistryLab.phases_without_windowsChemistryLab.pore_solutionChemistryLab.pore_solutionChemistryLab.porosityChemistryLab.porosityChemistryLab.porosityChemistryLab.pressureChemistryLab.primary_decompositionChemistryLab.process_tableChemistryLab.reactive_partChemistryLab.released_fractionChemistryLab.released_rateChemistryLab.rescale!ChemistryLab.residual_massChemistryLab.saturationChemistryLab.saturationChemistryLab.set_neutral_pH!ChemistryLab.set_pressure!ChemistryLab.set_quantity!ChemistryLab.set_quantity!ChemistryLab.set_temperature!ChemistryLab.site_familiesChemistryLab.solid_solution_totalsChemistryLab.solid_solutionsChemistryLab.solutesChemistryLab.solventChemistryLab.surfaceChemistryLab.temperatureChemistryLab.thermogramChemistryLab.titrateChemistryLab.volumeChemistryLab.volumeChemistryLab.volumeChemistryLab.volumeChemistryLab.volume_fractionsChemistryLab.volume_fractionsChemistryLab.volume_fractionsChemistryLab.window_intervalChemistryLab.window_parametersChemistryLab.windows_without_phasesChemistryLab.with_csh_surfaceChemistryLab.with_enthalpyChemistryLab.with_extentsChemistryLab.with_glass_speciesChemistryLab.with_instancesChemistryLab.with_speciesChemistryLab.with_window_parameters
ChemicalSystem
ChemistryLab.ChemicalSystem Type
struct ChemicalSystem{T<:AbstractSpecies, R<:AbstractReaction, C, S, SS, SF} <: AbstractVector{T}An immutable, fully typed collection of chemical species and reactions with derived index structures and stoichiometric matrices.
Immutability guarantees that all derived fields (dict_species, dict_reactions, index vectors, CSM, SM) remain consistent with species and reactions throughout the lifetime of the object. To modify the system, use merge to construct a new ChemicalSystem.
Fields
species: ordered list of all species.dict_species: fast O(1) lookup by species symbol.idx_aqueous,idx_crystal,idx_gas: indices by aggregate state.idx_solutes,idx_solvent,idx_components,idx_gasfluid: indices by class.reactions: ordered list of all reactions.dict_reactions: fast O(1) lookup by reaction symbol.CSM: canonical stoichiometric matrix.SM: stoichiometric matrix with respect to primaries.solid_solutions:Nothingwhen no solid solutions are present, or aVector{AbstractSolidSolutionPhase}describing each solid-solution phase and its end-members. Populated via thesolid_solutionskeyword constructor.ss_groups: for each solid solution, the indices of its end-members inspecies.idx_ssendmembers: union of all end-member indices (flattenedss_groups).idx_surface: indices of species inAS_SURFACE, i.e. bound to a site.site_families:Nothingwhen no surface is declared, or aVector{SiteFamily}. Populated through thesite_familieskeyword.site_groups: for each family, the indices of its members inspecies, the free site first — the order the site mixing and the solver's reference member both rely on.idx_kinetic: indices of kinetic species (empty when none declared).
ChemistryLab.ChemicalSystem Method
ChemicalSystem(species, primaries=species; kinetic_species, solid_solutions, site_families) -> ChemicalSystemConstruct a fully typed ChemicalSystem from a vector of species, an optional vector of primary species, optional kinetic species with rates, and optional solid-solution phases.
All derived fields are computed once at construction time and remain consistent for the lifetime of the object.
Arguments
species: vector ofAbstractSpecies. A species listed twice — equal byisequal, which compares formula, aggregate state, class and symbol — is kept once, with a warning naming it; this is what a list concatenated from twospeciationresults carries. Two different species under one symbol are refused, since every lookup by name would reach one of them.primaries: subset used as independent components (default: all species).kinetic_species:nothing(default) or a dictionary / vector of pairs mapping each kinetic species (by nameStringorSpeciesobject) to its rate function. Rate functions must be callable as(T, P, t, n, lna, n_initial) → Real [mol/s](seeKineticFunc). The rate is given per mole of kinetic species (stoichiometric coefficient = 1); the constructor corrects by1/|νₖ|automatically. When provided, the nullspace N of the stoichiometric matrix is diagonalized so that each kinetic species appears in exactly one reaction. Those reactions are stored in thereactionsfield with their rate attached viarxn[:rate].solid_solutions: vector ofSolidSolutionPhase(default:nothing). When provided, end-members must already appear inspecies(matched by symbol) and must carryaggregate_state = AS_CRYSTALandclass = SC_SSENDMEMBER.site_families: vector ofSiteFamily(default:nothing), the surfaces of the system; their members must already appear inspecies(matched by symbol).
Examples
julia> sp = [
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("Na+"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE),
];
julia> cs = ChemicalSystem(sp);
julia> length(cs)
2
julia> cs["H2O"] == sp[1]
truejulia> em1 = Species("AFm1"; aggregate_state=AS_CRYSTAL, class=SC_SSENDMEMBER);
julia> em2 = Species("AFm2"; aggregate_state=AS_CRYSTAL, class=SC_SSENDMEMBER);
julia> ss = SolidSolutionPhase("AFm", [em1, em2]);
julia> cs = ChemicalSystem([em1, em2]; solid_solutions=[ss]);
julia> cs.ss_groups
1-element Vector{Vector{Int64}}:
[1, 2]
julia> cs.idx_ssendmembers
2-element Vector{Int64}:
1
2ChemistryLab.ChemicalSystem Method
ChemicalSystem(species, primaries::AbstractVector{<:AbstractString}; kinetic_species, solid_solutions, site_families) -> ChemicalSystemConvenience constructor that resolves primary species from their symbol strings.
The components must span the species: every species is written as a combination of them, and that combination is the conservation law the equilibrium enforces for it. A list that cannot express a species is refused by name — see StoichMatrix.
Examples
julia> sp = [
Species("H2O"; aggregate_state=AS_AQUEOUS),
Species("NaCl"; aggregate_state=AS_CRYSTAL),
];
julia> cs = ChemicalSystem(sp, ["H2O", "NaCl"]);
julia> symbol.(cs.SM.primaries)
2-element Vector{String}:
"H2O"
"NaCl"Water alone would not do. Sodium and chlorine would have nowhere to be conserved, and ChemicalSystem(sp, ["H2O"]) raises an ArgumentError naming NaCl rather than projecting it onto H2O — a projection that would balance arithmetically and let the solver make salt out of water.
Base.getindex Method
Base.getindex(cs::ChemicalSystem, i::AbstractString) -> AbstractSpeciesReturn the species whose symbol matches i. Runs in O(1) via dict_species.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS)]);
julia> cs["H2O"] == Species("H2O"; aggregate_state=AS_AQUEOUS)
trueBase.getindex Method
Base.getindex(cs::ChemicalSystem, i::Int) -> AbstractSpeciesReturn the species at position i.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS)]);
julia> cs[1] == Species("H2O"; aggregate_state=AS_AQUEOUS)
trueBase.merge Method
Base.merge(cs1::ChemicalSystem, cs2::ChemicalSystem) -> ChemicalSystemConstruct a new ChemicalSystem from the union of two systems.
Species are unioned by symbol — duplicates from cs2 are discarded — and the solid solutions and site families by name. CSM, SM and the reactions are built from scratch from the full species list. Primaries are taken as the union of both systems' primaries, filtered to those actually present in the merged species list. A solid solution declared with several instances is declared again, and its copies are built anew. Kinetic species are not carried over: a kinetic system is to be declared as such.
In case of a conflict of symbol or name, cs1 takes priority over cs2. The return type is inferred from the merged collections and may differ from typeof(cs1) or typeof(cs2) if they contain different concrete types.
Examples
julia> cs1 = ChemicalSystem(
[Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("H+"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE)],
);
julia> cs2 = ChemicalSystem(
[Species("OH-"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE),
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)],
);
julia> cs = merge(cs1, cs2);
julia> length(cs)
3Base.merge Method
Base.merge(css::ChemicalSystem...) -> ChemicalSystemConstruct a new ChemicalSystem from the union of an arbitrary number of systems, processed left-to-right. Earlier systems take priority over later ones in case of symbol conflicts.
Examples
julia> cs1 = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> cs2 = ChemicalSystem([Species("H+"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE)]);
julia> cs3 = ChemicalSystem([Species("OH-"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE)]);
julia> cs = merge(cs1, cs2, cs3);
julia> length(cs)
3Base.size Method
Base.size(cs::ChemicalSystem) -> TupleReturn the size of the underlying species vector.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS)]);
julia> size(cs)
(1,)ChemistryLab._composition_ratio Method
_composition_ratio(a, b) -> Union{Float64, Nothing}The factor k with atoms(b) = k · atoms(a), or nothing when the two compositions are not proportional.
ChemistryLab._declared Method
_declared(ss) -> StringThe name of the declaration a solid-solution phase is an instance of.
Falls back to the phase's own name for any AbstractSolidSolutionPhase that does not carry the field, so a user-defined phase type keeps working.
ChemistryLab._expand_instances Method
_expand_instances(species, solid_solutions) -> (species, solid_solutions)Give every declaration asking for instances > 1 the extra species it needs.
A SolidSolutionPhase with instances = k becomes k phases: the declaration itself, then k-1 copies named "$name#2", "$name#3", … whose end-members are copies of the originals under "$symbol#2", "$symbol#3", … Each copy shares the whole property dictionary of the species it was made from, so the two are one substance under two labels and carry byte-identical thermodynamic data.
Why the copies are needed at all: the composition vector has one entry per species, so a species belongs to exactly one phase. Two coexisting compositions of one substance — which is what the Gibbs minimum is inside a spinodal — cannot be written down without the substance appearing twice.
Why it is done by duplicating the species rather than by letting two phases share them: it keeps ss_groups disjoint, so the activity assembly, the mole fraction fill and the optimality certificate need no change at all. Sharing an index would make the last group written win.
The duplicated column leaves the row rank of the conservation matrix unchanged — it is a copy of a column already there — so conservation is untouched, and the copies start at zero amount, so a budget computed as A n is unchanged too.
Returns the inputs unchanged, and without copying, when no declaration asks for more than one instance. That is every system the shipped data describes.
ChemistryLab._gel_models Method
_gel_models() -> Dict{String, NamedTuple{(:gel, :model)}}The gel model each listed end-member symbol belongs to, read once from data/gel_models.toml.
ChemistryLab._instances Method
_instances(ss) -> IntHow many coexisting compositions a declaration asks for; 1 for a phase type that does not carry the field.
ChemistryLab._refuse_overlapping_solid_solutions Method
_refuse_overlapping_solid_solutions(solid_solutions)Refuse a system in which two declared solid solutions describe the same substance, naming the pair.
The case this exists for is the calcium silicate hydrate. CEMDATA18 (Lothenbach et al., 2019) carries three descriptions of it — CSHQ, CNASH_ss and the ECSH family — and they are three models of one gel, not three phases. Declaring two of them counts the same hydrate twice: the calcium, the silicon and the alkalis all enter the element balance once and come out distributed over two phases that are supposed to be alternatives.
The overlap is exact rather than approximate, which is what makes it detectable here: KSiOH (an end-member of CSHQ), ECSH1-KSH and ECSH2-KSH all carry the formula ((KOH)2.5SiO2H2O)0.2. Two end-members of two different declared phases with the same composition are therefore the signature, and the check is composition-based rather than name-based so that it does not depend on the database's naming.
Two end-members of the SAME phase may of course share nothing — that is a mixture — and a pure phase repeating a mixing phase's composition is a separate question the rank test upstream already refuses.
Composition cannot see every pair, so a second test reads the database's own models. CSHQ and CNASH_ss share no composition — no end-member of one is a substance of the other, so on composition alone the pair would pass and the gel be counted twice without a word. data/gel_models.toml lists the end-member symbols of each model of one gel, and two declared phases whose end-members belong to two different models of the same gel are refused, naming both models. Matching is by symbol, so it does not depend on the name a phase is declared under.
Instances of one declaration are exempt, and that exemption is the whole reason SolidSolutionPhase carries a declared field. A miscibility gap is represented by the same binary present twice, on purpose, as two coexisting compositions — which is composition overlap of exactly the kind this function refuses. Telling the deliberate case from the mistake cannot be done by composition, since they look identical; it is done by provenance, and two phases share a declared name only when ChemicalSystem itself made the second from the first.
ChemistryLab._refuse_sites_on_mixing_hosts Method
_refuse_sites_on_mixing_hosts(site_families, solid_solutions)Refuse a site family whose host is an end member of a declared solid solution that carries an element the family binds, naming the family, the phase and the elements.
The case is the C-S-H again. The published surface models of the C-S-H bind calcium and alkalis on its silanol sites, and CSHQ holds its calcium and its alkalis in its end members. Hosting the sites on CSHQ would let one mole of calcium be counted twice, in the solid and on its surface; in the paste of (Guo et al., 2018) the surface complexes hold about a fifth of the calcium of the C-S-H. The elements a family binds are those of its complexes that its free site does not carry, less hydrogen and oxygen, which a protonation or a hydroxylation exchanges with the water. A family that binds nothing the phase carries is accepted.
The way out is two stages: freeze_solid_solution sets the solid solution aside after a first equilibrium, and the sites go on the frozen solid.
A family whose support names no host is held to the same rule unless the support is declared external = true: its sites could otherwise sit on the solid solution unnoticed, and the declaration is what says they belong to a solid outside the system.
ChemistryLab._refuse_two_gel_models Method
_refuse_two_gel_models(phases)Refuse two declared solid solutions whose end-members belong to two different models of one gel in data/gel_models.toml — CSHQ with CNASH_ss, which share no composition and so pass the composition test of _refuse_overlapping_solid_solutions. Instances of one declaration are exempt, as they are there.
ChemistryLab._resolve_site_families Method
_resolve_site_families(site_families, species, idx_surface) -> (families, groups)Resolve declared SiteFamily objects against the species list.
Returns (nothing, Vector{Int}[]) when none is declared, so that a system without a surface holds nothing of the surface machinery.
Everything refused here is refused rather than discovered later, because the alternative is a wrong number rather than an error:
a member that is not in the species list — the family would share a budget with a species the system cannot see;
an
AS_SURFACEspecies belonging to no family — it would carry a site pseudo-element into the conservation matrix, and so a row, with nothing mixing on it. This is the surface counterpart of the check that already refuses two solid solutions sharing a composition;two families sharing one pseudo-element — one symbol, one budget, one family, or the site balance silently merges them;
a member whose identity does not survive the lookup: the system's species of that name must have the family's formula, must carry the family's site symbol, and must be
AS_SURFACE;a species claimed by two families.
Why the last two are not redundant with the third
They were once believed to be. The reasoning was that two families can only share a member if they share a site symbol, that a species carries exactly one, and that a shared symbol is refused above — so no input could reach the case.
The reasoning skips a step. Members are matched to the system by symbol, and a symbol is a label: the species the lookup lands on need not be the species the family validated. Two families with different site symbols and the same member labels therefore resolved to the same indices, one family's conservation row disappeared, and nothing said so. The same gap let a family hold AS_SURFACE copies while the system kept the caller's unqualified originals, leaving idx_surface empty with the site mixing still running.
ChemistryLab._warn_one_substance_two_phases Method
_warn_one_substance_two_phases(solid_solutions)Warn when two declared solid solutions, one of them non-ideal, hold one substance under two normalizations: an end-member of one whose composition is k times that of an end-member of the other, with Gibbs energies that agree to the same factor within 0.1 RT per formula unit of the smaller, at 25 °C.
The case it was written for is CEMDATA18's aluminate sulfate. ettringite03_ss, the SO4 end-member of the SO4/CO3 AFt binary, is ettringite divided by three, 5 J/mol apart, and GEM-Selektor's CEMDATA18 list declares the binary beside the ettringite solid solution. With ideal mixing the two phases share the substance and the certificate holds. With the published Redlich–Kister model (Redlich and Kister, 1948) on the binary, measured on four cement pastes, the certified search stopped short of the solution: moving the sulfate from one phase to the other changes the Gibbs energy by next to nothing, a flat direction the Newton cannot settle. Declaring the substance once certified all four.
A warning, not a refusal: with ideal mixing the double declaration is harmless, and a page may keep it on purpose rather than decide in advance which normalization the answer uses. Two end-members of exactly one composition are refused earlier, by _refuse_overlapping_solid_solutions, and instances of one declaration are exempt, as they are there.
ChemistryLab._warn_pure_phase_beside_nonideal Method
_warn_pure_phase_beside_nonideal(species, solid_solutions, idx_members)Warn when a pure solid declared in the system repeats, up to a factor, an end-member of a declared non-ideal solid solution, with Gibbs energies that agree to that factor within 0.1 RT per formula unit at 25 °C: ettringite declared pure beside AFt_SO4_CO3, whose SO4 end-member ettringite03_ss is ettringite divided by three. It is the case of _warn_one_substance_two_phases with one of the two phases pure, and the same flat direction: moving the substance between the pure phase and the mixing phase changes the Gibbs energy by next to nothing.
idx_members are the indices, in species, of the end-members of the declared solid solutions; every other crystalline species is a pure phase.
ChemistryLab.aqueous Method
aqueous(cs::ChemicalSystem) -> SubArrayReturn a view of all aqueous species.
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS),
Species("NaCl"; aggregate_state=AS_CRYSTAL),
]);
julia> length(aqueous(cs))
1
julia> aggregate_state(aqueous(cs)[1]) == AS_AQUEOUS
trueChemistryLab.components Method
components(cs::ChemicalSystem) -> SubArrayReturn a view of all component species.
Examples
julia> cs = ChemicalSystem([Species("SiO2"; aggregate_state=AS_CRYSTAL, class=SC_COMPONENT)]);
julia> class(components(cs)[1]) == SC_COMPONENT
trueChemistryLab.crystal Method
crystal(cs::ChemicalSystem) -> SubArrayReturn a view of all crystalline species.
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS),
Species("NaCl"; aggregate_state=AS_CRYSTAL),
]);
julia> aggregate_state(crystal(cs)[1]) == AS_CRYSTAL
trueChemistryLab.gas Method
gas(cs::ChemicalSystem) -> SubArrayReturn a view of all gas-phase species.
Examples
julia> cs = ChemicalSystem([Species("CO2"; aggregate_state=AS_GAS)]);
julia> aggregate_state(gas(cs)[1]) == AS_GAS
trueChemistryLab.gasfluid Method
gasfluid(cs::ChemicalSystem) -> SubArrayReturn a view of all gas/fluid species.
Examples
julia> cs = ChemicalSystem([Species("CO2"; aggregate_state=AS_GAS, class=SC_GASFLUID)]);
julia> class(gasfluid(cs)[1]) == SC_GASFLUID
trueChemistryLab.get_reaction Method
get_reaction(cs::ChemicalSystem, sym::AbstractString) -> AbstractReactionReturn the reaction identified by symbol sym. Runs in O(1) via dict_reactions.
Examples
cs = ChemicalSystem(
[Species("H2O"; aggregate_state=AS_AQUEOUS)];
);
get_reaction(cs, "some_rxn") # returns the Reaction with that symbolChemistryLab.kinetic_species Method
kinetic_species(cs::ChemicalSystem) -> SubArrayReturn a view of the kinetic species declared at construction time. Empty when no kinetic species were declared.
ChemistryLab.site_families Method
site_families(cs::ChemicalSystem) -> Union{Nothing, Vector{<:SiteFamily}}The surface site families declared on cs, or nothing when it has no surface.
Mirrors solid_solutions, and for the same reason: a family is a named group of species with its own mixing, and the system has to carry the declaration because the species alone do not say which budget they share.
ChemistryLab.solid_solutions Method
solid_solutions(cs::ChemicalSystem) -> Nothing | Vector{<:AbstractSolidSolutionPhase}Return the registered solid-solution phases, or nothing if none were declared.
Examples
julia> em1 = Species("Em1"; aggregate_state=AS_CRYSTAL, class=SC_SSENDMEMBER);
julia> em2 = Species("Em2"; aggregate_state=AS_CRYSTAL, class=SC_SSENDMEMBER);
julia> cs = ChemicalSystem(
[em1, em2];
solid_solutions=[SolidSolutionPhase("SS", [em1, em2])],
);
julia> solid_solutions(cs) isa Vector
true
julia> length(solid_solutions(cs))
1ChemistryLab.solutes Method
solutes(cs::ChemicalSystem) -> SubArrayReturn a view of all aqueous solute species.
Examples
julia> cs = ChemicalSystem([Species("Na+"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE)]);
julia> class(solutes(cs)[1]) == SC_AQSOLUTE
trueChemistryLab.solvent Method
solvent(cs::ChemicalSystem) -> AbstractSpeciesReturn the unique solvent species directly (not a view), since a chemical system contains at most one solvent.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> class(solvent(cs)) == SC_AQSOLVENT
trueChemistryLab.surface Method
surface(cs::ChemicalSystem) -> SubArrayA view of the species bound to a surface site, i.e. those in AS_SURFACE.
They are deliberately not in crystal(cs): idx_crystal is read by the solver and by the start repair as "a pure mineral phase", which a site occupancy is not. They are nonetheless counted in the solid compartment of a ChemicalState, because that is where their matter is.
ChemistryLab.with_instances Method
with_instances(cs, name => k, ...; T = 298.15) -> ChemicalSystemcs with the solid solution name declared with k instances, every other species and phase unchanged.
The system is rebuilt from its declarations: the copies of the end-members that earlier instances needed are dropped, the phase is declared again with k instances, and ChemicalSystem builds the copies that number asks for, under the symbols "$symbol#2", … The primaries, and therefore the order of the components of a budget, are those of cs, so a budget written for cs is a budget for the result. with_instances(state, cs) maps a state across.
T is the temperature the phase is checked at: more than one instance is refused where its mixing energy is convex, as at declaration. A system with kinetic species is refused, since a kinetic step works on a system it was given.
ChemicalState
ChemistryLab.ChemicalState Type
struct ChemicalState{C, S, Q<:AbstractQuantity, R<:Real}Immutable container holding the thermodynamic state of a ChemicalSystem.
Molar amounts are always stored internally in mol regardless of the input unit. Each species can be provided independently as a molar amount (mol) or as a mass (g, kg, etc.) — the constructor converts each entry individually using the molar mass M stored in the corresponding species.
The struct itself is immutable — fields cannot be reassigned. However, n, T, and P are stored as Vector to allow in-place mutation via set_quantity!, set_temperature!, and set_pressure!.
system is a shared reference: cloning via Base.copy does not duplicate the underlying ChemicalSystem.
Fields
system: reference to the underlyingChemicalSystem.n: molar amounts (mol), one per species — mutable in place.T: temperature (K) — 1-element Vector, mutable in place.P: pressure (Pa) — 1-element Vector, mutable in place.n_phases: moles per phase(liquid, solid, gas, total)—PhaseQuantities{Q}.m_phases: mass per phase(liquid, solid, gas, total)—PhaseQuantities{Q}.V_phases: volume per phase(liquid, solid, gas, total)—PhaseQuantities{Q}.pH: pH of the liquid phase, ornothingif H⁺ is absent —R | Nothing.pOH: pOH of the liquid phase, ornothingif OH⁻ is absent —R | Nothing.porosity:(V_liquid + V_gas) / V_total, orNaNif volumes unavailable —R.saturation:V_liquid / (V_liquid + V_gas), orNaNif pore volume is zero —R.
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("Na+"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE),
]);
julia> state = ChemicalState(cs; T=298.15u"K", P=1u"bar");
julia> length(state.n)
2
julia> ustrip(state.T[])
298.15ChemistryLab.ChemicalState Method
ChemicalState(system::ChemicalSystem, values::AbstractVector; T, P) -> ChemicalStateConstruct a ChemicalState with explicit initial amounts or masses. Each entry is converted to moles independently — mixed units allowed.
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("NaCl"; aggregate_state=AS_CRYSTAL),
]);
julia> state = ChemicalState(cs, [55.5u"mol", 5.844u"g"]);
julia> ustrip(moles(state, "H2O"))
55.5
julia> isapprox(ustrip(moles(state, "NaCl")), 0.1; rtol=1e-4)
trueChemistryLab.ChemicalState Method
ChemicalState(system::ChemicalSystem; T, P, n) -> ChemicalStateConstruct a ChemicalState from a ChemicalSystem with optional initial temperature, pressure, and molar amounts (default: all zero).
Arguments
system: theChemicalSystemdescribing the species.T: temperature in K (default:298.15u"K").P: pressure (default:1u"bar").n: molar amounts in mol (default: zeros).
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("Na+"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE),
]);
julia> state = ChemicalState(cs; T=298.15u"K", P=1u"bar");
julia> ustrip(state.T[])
298.15
julia> ustrip(state.P[]) ≈ 1e5
true
julia> all(iszero.(ustrip.(state.n)))
trueChemistryLab.PhaseQuantities Type
PhaseQuantities{Q}Named tuple type alias (liquid::Q, solid::Q, gas::Q, total::Q) for phase-aggregated quantities (moles, mass, or volume).
Each field holds the total of the corresponding thermodynamic phase. Q is typically an AbstractQuantity carrying SI units (mol, kg, or m³).
Base.:* Method
Base.:*(state::ChemicalState, α::Real) -> ChemicalStateReturn a new ChemicalState with all molar amounts scaled by α. Temperature, pressure, and the underlying ChemicalSystem are unchanged. The operation is non-mutating — a copy is returned.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs, [2.0u"mol"]);
julia> s2 = state * 3.0;
julia> ustrip(moles(s2, "H2O"))
6.0
julia> ustrip(moles(state, "H2O")) # original unchanged
2.0Base.:+ Method
Base.:+(s1::ChemicalState, s2::ChemicalState) -> ChemicalStateCombine two chemical states by adding their species amounts.
Same system (
s1.system === s2.system): the result shares the system reference.Different systems: a merged system is created via
merge(s1.system, s2.system)(union of species,s1takes priority for duplicates).T, P: taken from
s1. A warning is emitted ifs2has different T or P.Derived quantities (pH, volumes, …) are recomputed from the summed moles.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> s1 = ChemicalState(cs, [2.0u"mol"]);
julia> s2 = ChemicalState(cs, [3.0u"mol"]);
julia> ustrip(moles(s1 + s2, "H2O"))
5.0Base.:/ Method
Base.:/(state::ChemicalState, α::Real) -> ChemicalStateReturn a new ChemicalState with all molar amounts divided by α. The operation is non-mutating — a copy is returned.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs, [4.0u"mol"]);
julia> s = state / 2.0;
julia> ustrip(moles(s, "H2O"))
2.0Base.copy Method
Base.copy(state::ChemicalState) -> ChemicalStateCreate a clone of a ChemicalState that shares the same ChemicalSystem reference but owns independent copies of all mutable fields.
Modifying the clone does not affect the original, and vice versa. The underlying ChemicalSystem is not duplicated.
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("Na+"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE),
]);
julia> state = ChemicalState(cs, [55.5u"mol", 0.1u"mol"]);
julia> clone = copy(state);
julia> set_quantity!(clone, "Na+", 0.5u"mol");
julia> ustrip(moles(state, "Na+"))
0.1
julia> ustrip(moles(clone, "Na+"))
0.5
julia> clone.system === state.system
trueBase.show Method
Base.show(io::IO, state::ChemicalState)Compact single-line representation of a ChemicalState.
Base.show Method
Base.show(io::IO, ::MIME"text/plain", state::ChemicalState)Detailed multi-line display for ChemicalState. Shows molar amounts, masses, and volumes for each species grouped by phase, with phase totals and scalar diagnostics (pH, pOH, porosity, saturation).
ChemistryLab._auto_seed_neutral_pH! Method
_auto_seed_neutral_pH!(state::ChemicalState)When the aqueous solvent (H₂O@) is present with non-zero amount and both H⁺ and OH⁻ are in the system at zero/negligible concentration, seed them at the neutral pH concentration
Does nothing if any of the three species is absent, if H⁺ or OH⁻ already have non-negligible amounts (i.e. the user set them explicitly), or if pKw cannot be computed.
ChemistryLab._compute_V_phases Method
_compute_V_phases(system, n, T, P) -> NamedTupleCompute volume per phase from n, T, P and standard molar volumes V⁰; a gas without a V⁰ takes the ideal gas's, RT/P (see _molar_volume).
ChemistryLab._compute_m_phases Method
_compute_m_phases(system, n) -> NamedTupleCompute mass per phase from species vector n and molar masses.
ChemistryLab._compute_n_phases Method
_compute_n_phases(system, n) -> NamedTupleCompute moles per phase from species vector n.
ChemistryLab._compute_pH Method
_compute_pH(system, n, V_liquid) -> Union{Real, Nothing}Compute pH using the most reliable species between H⁺ and OH⁻.
If c_H⁺ ≥ c_OH⁻: pH = -log10(c_H⁺)
If c_OH⁻ > c_H⁺: pOH = -log10(c_OH⁻), then pH = pKw(T) - pOH
pKw(T) is retrieved from the reaction H2O = H+ + OH- in the system's reaction dictionary, evaluated at the current T and P. Returns nothing if neither H⁺ nor OH⁻ is present, or if liquid volume is zero.
ChemistryLab._compute_pKw Method
_compute_pKw(system::ChemicalSystem, T, P) -> Union{Real, Nothing}Compute pKw = -logK⁰(T, P) for the water dissociation reaction H2O@ = H+ + OH- of the species present in system, that is ΔᵣG⁰ / (RT ln 10) with ΔᵣG⁰ = ΔₐG⁰(H⁺) + ΔₐG⁰(OH⁻) − ΔₐG⁰(H₂O).
It is read from the standard Gibbs energies of the three species directly. Every state the solvers return has its pH computed, and building the reaction object for it, its thermodynamic functions combined and simplified symbolically, cost 1.3 ms a call, a tenth of a warm equilibrium solve; the direct sum costs 10 µs and agrees to the last digit (1.8e-15 at 25 and 90 °C). A species that carries no ΔₐG⁰ goes through the reaction.
Returns nothing if any of H2O@, H+, or OH- is absent from the system.
ChemistryLab._compute_pOH Method
_compute_pOH(system, n, T, P, V_liquid) -> Union{Real, Nothing}Compute pOH symmetrically to _compute_pH:
If c_OH⁻ ≥ c_H⁺: pOH = -log10(c_OH⁻)
If c_H⁺ > c_OH⁻: pH = -log10(c_H⁺), then pOH = pKw(T) - pH
Returns nothing if neither species is present or volume is zero.
ChemistryLab._compute_porosity Method
_compute_porosity(V_phases) -> RealCompute porosity = (V_liquid + V_gas) / V_total. Returns NaN if total volume is zero.
ChemistryLab._compute_saturation Method
_compute_saturation(V_phases) -> RealCompute saturation = V_liquid / (V_liquid + V_gas). Returns NaN if pore volume is zero.
ChemistryLab._entry_to_moles Method
_entry_to_moles(v::AbstractQuantity, s::AbstractSpecies) -> AbstractQuantityConvert a single value v to moles for species s. If v has amount dimension (mol), it is returned as-is. If v has mass dimension, it is divided by the molar mass M of s. Otherwise an error is raised.
ChemistryLab._has_molar_volume Method
_has_molar_volume(s::AbstractSpecies) -> BoolReturn true if species s has a standard molar volume: its own V⁰, or, for a gas built without one, the ideal gas's.
ChemistryLab._molar_volume Method
_molar_volume(s::AbstractSpecies) -> AbstractFuncThe standard molar volume of species s as a function of T and P, to be called as _molar_volume(s)(T = T, P = P; unit = true). A gas built without a V⁰ has the ideal gas's, RT/P, so that every consumer of a volume sees the same one.
ChemistryLab._solid_indices Method
_solid_indices(system) -> Vector{Int}The species counted in the solid compartment: the crystalline ones, and the surface complexes.
Written once rather than three times because it is a decision, not a detail. A species bound to a site is matter, and it sits on a mineral, so its amount, its mass and its volume belong with the solid. It is kept out of idx_crystal itself because that index means "a pure mineral phase" to the solver and to the start repair, which a site occupancy is not.
A surface complex has no standard molar volume, so it contributes nothing to the solid volume — the volume it occupies is the host's, already counted.
ChemistryLab._update_derived! Method
_update_derived!(state::ChemicalState)Recompute and update in place all derived quantities after any mutation of n, T, or P. Called automatically by set_quantity!, set_temperature!, and set_pressure!.
ChemistryLab.enthalpy Method
enthalpy(state::ChemicalState, s) -> Union{AbstractQuantity, Nothing}The contribution n × ΔₐH⁰(T, P) of one species, or nothing when it carries no enthalpy of formation. s may be a species or its symbol.
ChemistryLab.enthalpy Method
enthalpy(state::ChemicalState) -> AbstractQuantitySum of nᵢ ΔₐH⁰ᵢ(T, P) over the species that carry an enthalpy of formation.
Only differences of this quantity are physical. ΔₐH⁰ is referred to the elements, so the absolute number has no meaning on its own; but two states built on the same element budget share that reference exactly, and it cancels in the difference. enthalpy(state₀) - enthalpy(state) is therefore the heat released in going from one to the other at fixed T and P — which is what a calorimeter measures, and what makes a hydration curve computable from the database alone, with no calibrated heat of reaction anywhere.
Species without ΔₐH⁰ contribute nothing; missing_enthalpy lists them, and a run where that list is non-empty is not a closed heat balance.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs, [55.5u"mol"]);
julia> enthalpy(state) isa AbstractQuantity
trueChemistryLab.heat_capacity Method
heat_capacity(state::ChemicalState, s) -> Union{AbstractQuantity, Nothing}The contribution n × Cp⁰(T, P) of one species, or nothing when it carries no heat capacity. s may be a species or its symbol.
ChemistryLab.heat_capacity Method
heat_capacity(state::ChemicalState) -> AbstractQuantitySum of nᵢ Cp⁰ᵢ(T, P) over the species that carry a heat capacity.
This is the standard-state sum: it ignores excess contributions from mixing, which for a cement pore solution are small against the solids. It is what an adiabatic temperature rise ΔT = Q / C needs.
ChemistryLab.mass Method
mass(state::ChemicalState, s::AbstractSpecies) -> AbstractQuantityReturn the mass of species s, computed as n × M.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs, [55.5u"mol"]);
julia> ustrip(uconvert(us"g", mass(state, cs[1]))) ≈ 55.5 * ustrip(us"g/mol", cs[1][:M])
trueChemistryLab.mass Method
mass(state::ChemicalState, sym::AbstractString) -> AbstractQuantityReturn the mass of the species identified by symbol sym.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs, [55.5u"mol"]);
julia> ustrip(uconvert(us"g", mass(state, "H2O"))) ≈ 55.5 * ustrip(us"g/mol", cs[1][:M])
trueChemistryLab.mass Method
mass(state::ChemicalState) -> NamedTupleReturn mass per phase (liquid, solid, gas, total).
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("NaCl"; aggregate_state=AS_CRYSTAL),
]);
julia> state = ChemicalState(cs, [55.5u"mol", 0.05u"mol"]);
julia> mass(state).total isa AbstractQuantity
trueChemistryLab.missing_enthalpy Method
missing_enthalpy(state::ChemicalState) -> Vector{String}Symbols of the species that carry no ΔₐH⁰, and whose amounts are therefore absent from enthalpy.
A heat balance is only closed when this is empty, and it is worth checking rather than assuming: a single missing hydrate silently removes its entire heat of formation from the curve.
ChemistryLab.moles Method
moles(state::ChemicalState, s::AbstractSpecies) -> AbstractQuantityReturn the molar amount of species s in mol.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs, [55.5u"mol"]);
julia> ustrip(moles(state, cs[1]))
55.5ChemistryLab.moles Method
moles(state::ChemicalState, sym::AbstractString) -> AbstractQuantityReturn the molar amount of the species identified by symbol sym.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs, [55.5u"mol"]);
julia> ustrip(moles(state, "H2O"))
55.5ChemistryLab.moles Method
moles(state::ChemicalState) -> NamedTupleReturn moles per phase (liquid, solid, gas, total).
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("NaCl"; aggregate_state=AS_CRYSTAL),
]);
julia> state = ChemicalState(cs, [55.5u"mol", 0.05u"mol"]);
julia> ustrip(moles(state).liquid)
55.5ChemistryLab.pH Method
pH(state::ChemicalState) -> Union{Real, Nothing}Return the pH of the liquid phase, or nothing if H⁺ is absent.
This is −log₁₀ c(H⁺), a concentration in mol/L over the computed liquid volume, and when the solution is alkaline it is reconstructed from OH⁻ through pKw because OH⁻ is the better-resolved species. No activity coefficient enters.
Not the pH another geochemical code reports
GEM-Selektor (Kulik et al., 2013), PHREEQC (Parkhurst and Appelo, 2013) and Reaktoro (Leal et al., 2017) report −log₁₀ a(H⁺), the activity on the molality scale. That is pH(state, model), a different quantity. On a Portland cement pore solution at I ≈ 0.2 mol/kg, with γ(H⁺) ≈ 0.61, the two differ by about 0.21 units — 13.31 here against 13.10 there. Comparing the wrong one against another code means chasing a convention rather than a result.
See also: pH(state, model), pOH, activity_coefficients.
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("H+"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE),
]);
julia> state = ChemicalState(cs, [55.5u"mol", 1e-7u"mol"]);
julia> pH(state) isa Union{Real, Nothing}
trueChemistryLab.pOH Method
pOH(state::ChemicalState) -> Union{Real, Nothing}Return the pOH of the liquid phase, or nothing if OH⁻ is absent.
Like pH, this is a concentration in mol/L, not an activity. For the activity convention use pOH(state, model).
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("OH-"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLUTE),
]);
julia> state = ChemicalState(cs, [55.5u"mol", 1e-7u"mol"]);
julia> pOH(state) isa Union{Real, Nothing}
trueChemistryLab.porosity Method
porosity(state::ChemicalState, reference::ChemicalState) -> NamedTuplePorosity of a setting binder, in the sealed-curing convention: referred to the volume of reference — the fresh material — held fixed, and accounting for the chemical shrinkage.
Returns (; liquid, void, total), all dimensionless fractions of the reference volume:
liquid— the water-filled porosity,V_liquid(state) / V_ref.void— the empty porosity created by the reactions,(V_ref − V_total(state)) / V_ref. This is the Le Chatelier contraction: hydration products occupy less space than the reactants they consume, and in a sealed specimen the deficit is gas, not a reduction in size.total— their sum, the quantity a transport or micromechanical model wants.
Consistent by construction with volume_fractions called with the same reference: total equals the sum of its "water"-like and "void" entries.
Examples
p = porosity(state_28d, state_0)
p.total # 0.375 for a w/c = 0.5 paste
p.void # 0.072 — the chemical shrinkage
p.liquid / p.total # degree of saturationSee also: saturation(state, reference), chemical_shrinkage, volume_fractions.
ChemistryLab.porosity Method
porosity(state::ChemicalState) -> RealReturn (V_liquid + V_gas) / V_total, or NaN if the total volume is zero.
This is not the porosity of a setting binder
Both ends of that ratio are wrong for a hydrating cement:
the denominator is the current total volume, which shrinks as hydration proceeds, whereas a sealed specimen keeps the volume it was cast with;
the numerator has no gas term unless gas species were declared, so the empty porosity left by the chemical shrinkage — products occupying less space than the reactants they consume — is structurally invisible.
The two errors compound. On a w/c = 0.5 paste at 28 days this returns 0.327 where the porosity referred to the specimen is 0.375, the total volume having shrunk by 7.2 %.
Use porosity(state, reference) for a binder. This method remains the right one for a fixed-volume aqueous system, where nothing shrinks.
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("NaCl"; aggregate_state=AS_CRYSTAL),
]);
julia> state = ChemicalState(cs, [55.5u"mol", 0.05u"mol"]);
julia> porosity(state) isa Real
trueChemistryLab.pressure Method
pressure(state::ChemicalState) -> AbstractQuantityReturn the current pressure.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs; T=298.15u"K", P=1u"bar");
julia> isapprox(ustrip(pressure(state)), 1e5; rtol=1e-4)
trueChemistryLab.rescale! Method
rescale!(state::ChemicalState, target::AbstractQuantity) -> ChemicalStateScale all molar amounts in-place so that the total quantity of the matching physical dimension equals target.
target dimension | Quantity brought to target |
|---|---|
| mol | moles(state).total |
| kg (mass) | mass(state).total |
| m³ (volume) | volume(state).total |
All derived quantities (pH, volume, porosity, …) are recomputed after scaling. Returns state for chaining.
Examples
rescale!(state, 1.0u"mol") # total moles → 1 mol
rescale!(state, 1.0u"kg") # total mass → 1 kg
rescale!(state, 1.0u"m^3") # total volume → 1 m³
rescale!(state, 500u"g") # total mass → 500 gChemistryLab.saturation Method
saturation(state::ChemicalState, reference::ChemicalState) -> RealDegree of saturation of a setting binder: the fraction of its porosity that is water-filled, liquid / total from porosity(state, reference).
Falls to NaN when the porosity is zero. Unlike the one-argument method, the empty porosity left by the chemical shrinkage is counted, so a sealed paste desaturates as it hydrates even though no water ever leaves it.
ChemistryLab.saturation Method
saturation(state::ChemicalState) -> RealReturn the saturation V_liquid / (V_liquid + V_gas), or NaN if pore volume is zero.
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("NaCl"; aggregate_state=AS_CRYSTAL),
]);
julia> state = ChemicalState(cs, [55.5u"mol", 0.05u"mol"]);
julia> saturation(state) isa Real
trueChemistryLab.set_neutral_pH! Method
set_neutral_pH!(state::ChemicalState) -> ChemicalStateSet H⁺ and OH⁻ concentrations to neutral pH at the current temperature and pressure, using the water autoprotolysis constant
Requires the system to contain H2O@ (solvent), H+, and OH-. The liquid volume is estimated from the current water amount.
Unlike _auto_seed_neutral_pH! (which only triggers when H⁺/OH⁻ are zero), this function always overwrites the current values — useful inside loops where the state is reused across iterations.
Examples
set_quantity!(s, "H2O@", 1.0u"kg")
set_neutral_pH!(s) # H⁺ and OH⁻ at neutral, T/P-dependentChemistryLab.set_pressure! Method
set_pressure!(state::ChemicalState, P::AbstractQuantity) -> ChemicalStateSet the pressure in place and update all derived quantities.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs; T=298.15u"K", P=1u"bar");
julia> set_pressure!(state, 2u"bar");
julia> isapprox(ustrip(pressure(state)), 2e5; rtol=1e-4)
trueChemistryLab.set_quantity! Method
set_quantity!(state::ChemicalState, s::AbstractSpecies, n::AbstractQuantity) -> ChemicalStateSet the molar amount of species s in place and update all derived quantities. If n has mass dimension, it is automatically converted to moles using M.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs, [55.5u"mol"]);
julia> set_quantity!(state, cs[1], 10.0u"mol");
julia> ustrip(moles(state, "H2O"))
10.0ChemistryLab.set_quantity! Method
set_quantity!(state::ChemicalState, sym::AbstractString, n::AbstractQuantity) -> ChemicalStateSet the molar amount of the species identified by symbol sym in place and update all derived quantities.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs, [55.5u"mol"]);
julia> set_quantity!(state, "H2O", 10.0u"mol");
julia> ustrip(moles(state, "H2O"))
10.0ChemistryLab.set_temperature! Method
set_temperature!(state::ChemicalState, T::AbstractQuantity) -> ChemicalStateSet the temperature in place and update all derived quantities.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs; T=298.15u"K", P=1u"bar");
julia> set_temperature!(state, 350.0u"K");
julia> ustrip(temperature(state))
350.0ChemistryLab.temperature Method
temperature(state::ChemicalState) -> AbstractQuantityReturn the current temperature.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs; T=298.15u"K", P=1u"bar");
julia> ustrip(temperature(state))
298.15ChemistryLab.volume Method
volume(state::ChemicalState, s::AbstractSpecies) -> Union{AbstractQuantity, Nothing}Return the volume contribution of species s as n × V⁰(T,P). Returns nothing if V⁰ is not available for s.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs, [55.5u"mol"]);
julia> volume(state, cs[1]) isa Union{AbstractQuantity, Nothing}
trueChemistryLab.volume Method
volume(state::ChemicalState, sym::AbstractString) -> Union{AbstractQuantity, Nothing}Return the volume contribution of the species identified by symbol sym. Returns nothing if V⁰ is not available.
Examples
julia> cs = ChemicalSystem([Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT)]);
julia> state = ChemicalState(cs, [55.5u"mol"]);
julia> volume(state, "H2O") isa Union{AbstractQuantity, Nothing}
trueChemistryLab.volume Method
volume(state::ChemicalState) -> NamedTupleReturn volume per phase (liquid, solid, gas, total).
Examples
julia> cs = ChemicalSystem([
Species("H2O"; aggregate_state=AS_AQUEOUS, class=SC_AQSOLVENT),
Species("NaCl"; aggregate_state=AS_CRYSTAL),
]);
julia> state = ChemicalState(cs, [55.5u"mol", 0.05u"mol"]);
julia> volume(state).total isa AbstractQuantity
trueA solid solution, read and then frozen
The totals of a solid solution, and the first state of a second stage in which it no longer reacts. See Chloride binding in CEM III/A and CEM III/B.
ChemistryLab.freeze_solid_solution Method
freeze_solid_solution(state, name, target::ChemicalSystem;
release = (:Na, :K), buffer = nothing) -> NamedTupleThe first state of a second stage in which the solid solution name of state no longer reacts: its elements are set aside, and target sees the rest.
Every species of target found in the system of state, under the same symbol and with the same composition, starts at the amount state holds. The end members of name are frozen, and target must not contain them. The elements listed in release leave the frozen solid and return to the solution as their monovalent cation, balanced by as much hydroxide: an end member written as ((NaOH)2.5SiO2H2O)0.2 gives back 0.5 NaOH, and its silica and water stay frozen. release = () freezes everything.
Refused, with the names, rather than computed wrong:
a species that holds matter in
statebut is absent fromtarget(its elements would vanish between the stages), or present under the same symbol with another composition;an end member of
nameamong the species oftarget(it could form again from the frozen elements, which are no longer in the budget);a released element without its cation, or a solution without hydroxide, in
target;buffer, when given, absent fromstate. A frozen composition is a sound approximation while a coexisting phase fixes the activities of the elements it shares with the solid solution. For a C-S-H that phase is portlandite; without it the composition of the gel moves with the solution, and freezing it is not a model of anything. The check is at the first stage; whether the buffer survives the second is a result of that stage and is checked on it.
Returns (state, frozen). state is the ChemicalState of target, at the temperature and pressure of the first. frozen holds the elements and the mass (kg) set aside, the members they came from, and the moles of each released element. With A₁ and A₂ the element matrices of the two systems, A₂ n₂ + frozen = A₁ n₁ holds element by element; what is added to state afterwards, a salt for instance, is the second stage's business.
ChemistryLab.solid_solution_totals Method
solid_solution_totals(state::ChemicalState, name::AbstractString) -> NamedTupleWhat the solid solution name of state holds:
members: the amount of each end member, mol, by symbol;amount: their sum, mol;elements: the moles of each element over the end members;mass: the mass of the phase, kg, from the molar masses of the end members.
Ratios such as Ca/Si are quotients of elements. Each end member counts in its own formula unit, so amount depends on how the database writes them, and elements does not.
The element type of the result is that of the state, so a state carrying dual numbers gives derivatives of these totals.
Volume fractions
ChemistryLab.chemical_shrinkage Method
chemical_shrinkage(state::ChemicalState, state₀::ChemicalState) -> QuantityVolume lost to the chemical reactions between the reference state state₀ and state,
Both states must share the same ChemicalSystem. Species without a standard molar volume V⁰ contribute nothing, exactly as in volume — see missing_molar_volumes to find out which those are before trusting the number.
Examples
δV = chemical_shrinkage(state_28d, state_0)
ustrip(δV / volume(state_0).total) # relative contraction, dimensionlessSee also: volume_fractions, missing_molar_volumes.
ChemistryLab.missing_molar_volumes Method
missing_molar_volumes(state::ChemicalState; atol = 0.0) -> Vector{String}Symbols of the species that are present in state (amount in mol strictly above atol) but carry no standard molar volume V⁰, and are therefore invisible to volume, porosity and volume_fractions.
A non-empty result means the volume balance is incomplete. Databases differ in coverage — CEMDATA18 supplies V⁰ for the cement phases but a species added by hand, or one taken from an aqueous-only dataset, may not have it.
Examples
isempty(missing_molar_volumes(state)) || @warn "incomplete volume balance" missing_molar_volumes(state)ChemistryLab.volume_fractions Method
volume_fractions(state::ChemicalState, groups; kwargs...)
-> OrderedDict{String, <:Real}Volume fractions aggregated into named families.
groups is any iterable of name => symbols pairs, where symbols is a species symbol or a collection of them — the granularity a mean-field homogenization scheme consumes, where "C-S-H" is one phase rather than four solid-solution end members.
Keyword arguments are those of volume_fractions(state); when reference is given, the chemical-shrinkage void is appended under void_key as a group of its own.
Species that appear in no group are collected under other_key when their total fraction exceeds atol in magnitude, and dropped otherwise — silently discarding matter would defeat the purpose of a volume balance, and the leftover may be negative when only aqueous solutes are unassigned. A species listed in two groups raises an error rather than being counted twice.
Examples
groups = [
"anhydrous" => ["C3S", "C2S", "C3A", "C4AF"],
"C-S-H" => ["CSHQ-TobH", "CSHQ-TobD", "CSHQ-JenH", "CSHQ-JenD",
"KSiOH", "NaSiOH"],
"CH" => "Portlandite",
"AFt" => "ettringite",
"water" => "H2O@",
]
f = volume_fractions(state, groups; reference = state_0)See also: volume_fractions(state).
ChemistryLab.volume_fractions Method
volume_fractions(state::ChemicalState; reference = nothing, void_key = "void")
-> OrderedDict{String, <:Real}Volume fraction of every species carrying a standard molar volume, keyed by species symbol. Species with a zero amount are omitted.
An individual fraction may be negative: aqueous solutes have negative standard partial molar volumes, electrostriction contracting the solvent around the ion (V⁰(OH⁻) ≈ -4.7 cm³/mol). Those contributions are kept, so that this function and volume always agree; they are negligible at the concentrations of a pore solution, and grouping folds them back into the liquid.
Two normalizations, selected by reference:
reference = nothing(default) — fractions are relative to the current total volume of the state, and sum to 1 by construction. This is the right choice for a closed, fully-saturated system.reference::ChemicalState— fractions are relative tovolume(reference).total, held fixed. They then sum to less than 1, and the deficit is returned undervoid_keyas the chemical-shrinkage void. This is the sealed-curing convention of Lavergne et al. (2018): the specimen keeps its volume while the reactions consume some, and the resulting empty porosity is a phase of the microstructure, not a rounding error.
Passing reference = state itself is equivalent to the default.
Examples
f = volume_fractions(state_28d; reference = state_0)
f["Portlandite"], f["void"]
sum(values(f)) # 1.0See also: volume_fractions(state, groups), chemical_shrinkage, missing_molar_volumes, porosity.
An oxide analysis as an element budget
ChemistryLab.cation_molar_mass Method
cation_molar_mass(oxides) -> QuantityThe mass of material per mole of the cations of its oxide analysis, in g/mol: oxide_budget.
Given as M to glass_species, it makes one mole of the glass one mole of its cations, the unit in which the dissolution rates of glasses are measured per unit area (snellings2013_glass).
Examples
slag = Dict("CaO" => 0.43, "Al2O3" => 0.19, "SiO2" => 0.38)
sp = glass_species(slag; symbol = "G1", M = cation_molar_mass(slag))
sum(v for (el, v) in atoms(sp) if el != :O) # 1.0ChemistryLab.glass_species Method
glass_species(oxides; symbol, M = 100.0u"g/mol", name, ΔₐG⁰, enthalpy = false) -> SpeciesA pseudo-species for a material that has no formula unit — a blastfurnace slag, a fly ash, a calcined clay — built from its oxide analysis, so that a rate law can consume it and release every element the analysis reports.
oxide_budget solves this for an equilibrium: a glass enters as a contribution to b and never needs to be a species at all. A kinetic run cannot do that, because a rate law consumes a species. So the glass has to be given a formula, and where that formula comes from decides what the dissolution can produce.
Why not pick a representative mineral
Because the elements it leaves out cannot come back. A slag written as anorthite, CaAl₂Si₂O₈, carries no magnesium — so no hydrotalcite can form from it, which is the one phase a slag is certain to make. Writing the formula from the analysis puts every reported element into the budget in its reported proportion, and the question stops being which mineral the glass resembles.
The two masses, and why they differ
The formula unit carries the elements of the reported oxides. M is the mass of material it stands for. These are not the same number, and the difference is the part of the analysis that is not modeled — loss on ignition, and the minor oxides a datasheet omits. The analysis is not renormalized, for the reason oxide_budget gives: scaling the reported fractions up to one would invent material. modeled_mass_fraction in the returned species' properties records what fraction of the material the formula actually accounts for.
ΔₐG⁰ is optional; a glass has no measured Gibbs energy, and without one a rate law of the waller or Parrott–Killoh kind, which never reads the saturation ratio, is the sound choice. A mechanistic rate driven by Ω needs a real Gibbs energy, which a glass does not have.
With enthalpy = true the species carries an enthalpy of formation and a heat capacity, functions of the temperature: glass_enthalpy at 298.15 K, carried to T by glass_heat_capacity, per formula unit of M. Its heat then counts in a calorimeter (the enthalpy of the whole composition, under partial equilibrium), as an estimate whose construction and uncertainty The enthalpy of a glass gives.
Examples
slag = Dict("CaO" => 0.41, "SiO2" => 0.36, "Al2O3" => 0.11,
"MgO" => 0.08, "SO3" => 0.02)
sp = glass_species(slag; symbol = "GGBS", M = 95.0u"g/mol")
atoms(sp) # Ca, Si, Al, Mg, S and O, in the reported ratio
τ_slag = 100.0u"d" # ASSUMED for the example: give the value of a source
rate = waller(merge(WALLER_PARAMS_FLY_ASH, (τ = τ_slag,)), "GGBS"; α_max = 0.9)See also: oxide_budget, waller.
ChemistryLab.oxide_budget Method
oxide_budget(oxides, primaries; mass = 100.0u"g") -> Vector{<:Real}The component totals b contributed by a material reported as an oxide analysis, for use as the right-hand side of A n = b.
oxides maps an oxide formula to its mass fraction — the shape of a cement or slag datasheet, "CaO" => 0.41 and so on. mass is how much of the material the budget is for. The analysis is not renormalized: if the fractions do not sum to one, what is missing is loss on ignition and minor oxides the sheet does not report, and silently scaling them up would invent material.
Each oxide's molar mass is computed from its formula by Species, never written down, and each is decomposed over primaries by primary_decomposition, which refuses an oxide the primaries cannot express.
# A ground granulated blast-furnace slag, as a datasheet reports it
slag = Dict("CaO" => 0.41, "SiO2" => 0.36, "Al2O3" => 0.11, "MgO" => 0.08)
b = oxide_budget(slag, cs.SM.primaries; mass = 60.0u"g")This says what a glass contains, not what it does
An oxide budget is a statement of composition and carries no information about reactivity. A slag and a quartz sand of the same analysis give the same b, and an equilibrium calculation will dissolve both completely. How much of the glass has actually reacted is a kinetic quantity, supplied from outside — by a degree of reaction, or by a rate law such as waller — and multiplying the budget by it is the caller's responsibility, not this function's.
ChemistryLab.primary_decomposition Method
primary_decomposition(species, primaries) -> Vector{Float64}The coefficients writing species as a linear combination of primaries.
Solved by column-pivoted QR rather than \, because the matrix is singular whenever the primaries are not all exercised, and refused above a residual of 1e-8: a species outside the span of the primaries has no decomposition, and returning a least-squares approximation of one would put elements into the budget that the species does not contain.
The elements fix the decomposition but not the charge it carries. An element at the valence of its primary (Ca in CaO, Al in Al₂O₃) gives a charge that matches the species', and nothing more is needed. An element below it does not: CO over CO3-2, H+ and H2O@ balances carbon, oxygen and hydrogen with a charge of +2 where CO has none, the two electrons of carbon (II) against carbon (IV). The difference is the unit charge Zz, the component that tracks the oxidation state when the system has one, so it is put there: CO is CO3-2 + 4 H+ − 2 H2O@ − 2 Zz, as its column of the conservation matrix is. Without Zz among the primaries such a species is refused: the system cannot hold carbon (II), and a budget that silently gave it the charge of carbon (IV) would describe neither.
The enthalpy and heat capacity of a glass
Built from measured silicate glasses; the construction and what it assumes are in The enthalpy of a glass.
ChemistryLab.glass_enthalpy Method
glass_enthalpy(oxides; T = 298.15u"K", reference = nothing, ignore = ()) -> NamedTupleThe standard enthalpy of formation of a silicate glass known by its oxide analysis, per gram of material, built from measurements on silicate glasses and stating what it assumes. oxides maps oxide formulas to mass fractions, as for oxide_budget (not renormalized).
The CaO-MgO-Al2O3-SiO2 part of the glass is written as a combination of the six glasses whose enthalpy of vitrification is measured at 298 K, gehlenite, akermanite, pseudowollastonite, anorthite, diopside and silica (Richet and Bottinga (1986), Richet and Bottinga (1984), Navrotsky et al. (1980)), mixed ideally. The combination is chosen to leave as little of the four oxides outside the measured glasses as possible; what is left (a slag holds more lime than the measured glasses can take) is counted as the crystalline oxide, its enthalpy of vitrification assumed zero. When several combinations leave the same amount, their enthalpies differ by the measure of the ideal-mixing assumption, and the midpoint is returned with the half-range as span. The enthalpies of the crystals and of the four oxides are taken from the aq17 database together, so that the enthalpy of forming the glass from its oxides is consistent within one database; the enthalpies of formation of the oxides themselves come from reference, an ordered list of species collections searched in turn (by default aq17 then slop98 (Johnson et al., 1992)), so that the glass can be put on the scale of the database of a calculation. The other oxides of the analysis (Fe2O3, Na2O, K2O, TiO2, …) are counted as their crystals from reference, with no enthalpy of vitrification (assumed). SO3, which no database holds as a crystal, is counted as anhydrite less lime, both from the first collection of reference that holds them: the sulfate is taken as bound to the lime of the glass, as in a calcium sulfate (the sulfur of a slag, mostly a sulfide, is reported as SO3 by the analysis and counted the same way).
At a temperature T other than 298.15 K the heat capacity of the glass, glass_heat_capacity, is integrated from 298.15 K.
An oxide with no crystal in reference is refused, unless it is listed in ignore: it is then left out, its mass counted at zero enthalpy, and reported in ignored with its mass fraction.
Returns (; enthalpy, formation_298, from_oxides, vitrification, span, uncertainty, norm, unassigned, others, ignored, oxide_sources): the enthalpy of formation at T and at 298.15 K, the part from the oxides to the glass, the vitrification part and its span, the uncertainty the printed uncertainties of the measured enthalpies of vitrification give (those printed), all in J/g; norm the moles of each measured glass per gram, unassigned the moles of the four oxides left crystalline per gram, others the moles per gram of the oxides outside them, and oxide_sources the database each oxide's enthalpy came from (its position in reference). The construction is written out in The enthalpy of a glass.
Examples
slag = Dict("CaO" => 0.416, "SiO2" => 0.352, "Al2O3" => 0.113, "MgO" => 0.060)
g = glass_enthalpy(slag)
OxideConstituent("glass", slag; enthalpy = g.enthalpy, source = "glass_enthalpy")ChemistryLab.glass_heat_capacity Method
glass_heat_capacity(oxides; T = 298.15u"K", reference = nothing) -> QuantityThe heat capacity of a silicate glass per gram, from its oxide analysis, as Richet (1987) gives it: the sum over the oxides of their moles times their partial molar heat capacity in the glass, aᵢ + bᵢT + cᵢ/T² + dᵢ/√T (his Eqs. 1 and 3, Table I), to about 1 % below the glass transition and between 270 K and the upper temperature of each oxide's data. An oxide the paper does not cover takes the heat capacity of its crystal (the paper's footnote), from reference as in glass_enthalpy, SO3 that of anhydrite less lime.
Loss on ignition
What a solid assemblage would lose on heating, from the formulas its phases carry — the total a thermogram integrates to. See The water budget of a hydrating paste for what this gives and what a full thermogravimetric operator would still need.
ChemistryLab._ignition_molar_mass Method
_ignition_molar_mass(system, symbols..., atoms) -> QuantityThe molar mass of a released gas, taken from the system's own species when it carries one under any of symbols, and computed from atoms otherwise.
Preferring the system's own is the rule this package keeps everywhere: a molar mass written here would drift from the database the rest of the calculation uses. The fallback exists because an assemblage may release a species it never declared — a paste with no gas phase still loses water on ignition — and it is weighed with the same atomic masses as every species.
ChemistryLab.bound_water Method
bound_water(state::ChemicalState) -> QuantityThe water a solid assemblage would release on ignition, in kilograms, from the hydrogen written into its phases.
Every hydrogen in a solid leaves as water — Ca(OH)₂ → CaO + H₂O, CaSO₄·2H₂O → CaSO₄ + 2H₂O — so the amount is H/2 per formula unit, whatever form the hydrogen is written in. That is why this counts hydrogen and not formula water: Ca(OH)₂ has no H₂O in its formula and loses one on ignition, and a rule that looked for H₂O would report zero.
The aqueous phase is not included. Pore solution is water, and evaporable water is not what a hydrate bound — the distinction the water budget is about. Only species in the solid buckets are counted.
See also: ignition_loss, bound_water_per_phase.
ChemistryLab.bound_water_per_phase Method
bound_water_per_phase(state::ChemicalState) -> Vector{Pair{String,Quantity}}The ignition water each solid species carries, largest first.
What a thermogram would resolve if the decomposition windows were known — so this is the quantity a TGA observation operator needs per phase, and the place those windows would attach.
ChemistryLab.ignition_loss Method
ignition_loss(state::ChemicalState) -> NamedTupleWhat a solid assemblage would lose on ignition, as (; water, carbon_dioxide, total) in kilograms.
Hydrogen leaves as water and carbon as carbon dioxide, which is the convention a loss-on-ignition measurement reports and which is exact for the carbonates and hydrates a cement or a clay assemblage is made of.
Carbon is assumed to be carbonate
C in a solid is counted as CO₂. For a cement, a clay or a soil mineral assemblage that is what it is. For organic carbon it is not — the mass is right and the species is not, and a sample with organic matter needs the split made before this is read.
What this is and is not
It is the total a thermogram integrates to, computed from the formulas the database gives. It is not a thermogram: which phase releases what between which temperatures is a set of decomposition windows, and those are literature values rather than consequences of a formula. This package does not carry them, and inventing them would be fabricating the part of the measurement that does the identifying.
Example
loss = ignition_loss(state)
ustrip(us"g", loss.water), ustrip(us"g", loss.total)Thermogravimetry
Turning that total into a curve: a decomposition window per phase, carrying its own provenance, and the operator that makes them identifiable from a measured thermogram rather than only supplied. See Bound water of CEM I 52.5 N and slag pastes, and the thermogram it integrates to.
ChemistryLab.DecompositionWindow Type
struct DecompositionWindow{T<:Real}When a phase releases what it releases, as a step in temperature, in one of two forms.
A logistic step, the form a peak fitted to a thermogram takes:
A logistic rather than a step because a decomposition is not instantaneous, and rather than something with more shape parameters because two — a midpoint and a width — are already at the edge of what one peak in a thermogram determines.
A temperature interval, the form a thermogravimetric reading takes when it attributes the mass lost between two temperatures to one phase ("the weight loss between 350 and 500 °C" for portlandite): the whole release happens between T₁ and T₂, and none outside,
a smooth step whose rate vanishes at both ends, so that the curve and its derivative are continuous and the loss between T₁ and T₂ is exactly the phase's content. The logistic has tails: a phase centered in an interval loses part of its release outside it.
In either form f is the fraction already released at temperature T, so the phase's contribution to a thermogram is m_i f(T) and to its derivative m_i f'(T).
Fields
phase: the species symbol the window belongs to, as the system spells it.midpoint:T₁/₂in kelvin, where half of the release has happened (for an interval, its center).width:win kelvin. The logistic release runs from roughlyT₁/₂ − 3wtoT₁/₂ + 3w, so a peak 100 K wide hasw ≈ 17 K; for an interval, its half-span,(T₂ − T₁)/2.releases::wateror:carbon_dioxide.fraction: how much of that phase's release this window accounts for,1by default. A phase can go in stages — gypsum loses its two waters in two steps,CaSO₄·2H₂O → CaSO₄·½H₂O → CaSO₄— and one window per stage with fractions summing to one is how that is written. The sum is checked, inthermogram, against each phase and product it is given for.shape::logisticor:interval.
Both temperature parameters are Traced, which is the point rather than a decoration: a window read from a paper and a window guessed to get a picture on the screen are the same two numbers and are not the same claim.
See also: thermogram, window_parameters, window_interval.
ChemistryLab.DecompositionWindow Method
DecompositionWindow(phase, midpoint, width; releases = :water, fraction = 1,
kind = PROV_UNSTATED, source = "") -> DecompositionWindow
DecompositionWindow(phase; between = (T₁, T₂), releases = :water, fraction = 1,
kind = PROV_UNSTATED, source = "") -> DecompositionWindowBuild a window: a logistic step from its midpoint and width, or a temperature interval from its two ends between. Temperatures are in kelvin, as plain numbers, as quantities, or as Traced values that keep their own provenance (an interval read with literature_table carries its source).
Passing bare numbers with no kind leaves them PROV_UNSTATED, which is the weakest claim there is — deliberately, so a window nobody sourced never strengthens a result.
ChemistryLab._check_fractions Method
_check_fractions(windows)Refuse a set of windows whose fractions do not sum to one for some phase and product.
Two windows on one phase with the default fraction would release its mass twice, and the only symptom is a curve that integrates to more than ignition_loss — a silent doubling rather than an error. The check is here rather than at construction because a window does not know what it will be used with.
ChemistryLab._co2_per_phase Method
_co2_per_phase(state) -> Dict{String, Quantity}The carbon dioxide each solid species would release, by symbol.
ChemistryLab.literature_windows Function
literature_windows(state) -> Vector{DecompositionWindow}
literature_windows(state, windows) -> Vector{DecompositionWindow}A window for every phase => product of state (phases_without_windows), each the temperature interval in which the cement literature reads that loss, with its source; given windows, those first, then one for every phase => product they leave without a window: one's own intervals for some releases, the literature's for the others.
| what | interval | source |
|---|---|---|
| the water of portlandite | 350 – 500 °C | (De Weerdt et al., 2011) |
| the water of the ettringites | 30 – 150 °C | (Möschner et al., 2009) |
| the water of every other solid | 30 – 550 °C, the total bound water | (L'Hôpital et al., 2016) |
| the carbon dioxide of every solid | 300 – 850 °C | (Shi et al., 2016) |
The intervals are read from the data files of those papers. A phase releases everything inside its interval and nothing outside it, along a smooth step (DecompositionWindow with between): the curve says where the papers attribute a loss, not how a phase decomposes. The water of the hydrates other than portlandite and ettringite has no interval of its own in them, and is spread over the interval of the total bound water; a peak of the DTG there is a reading aid, not a measurement. The thermogram integrates to ignition_loss all the same, every hydrogen and carbon of the solids having a window.
Examples
tg = thermogram(state, literature_windows(state); temperatures = range(303.15, 1253.15; length = 191))
tg.loss_percent[end] # the loss on ignition, in percent of the sample at 30 °C
# portlandite read between 400 and 520 °C, every other release as the papers read it
own = [DecompositionWindow("Portlandite"; between = (673.15, 793.15))]
tg = thermogram(state, literature_windows(state, own))ChemistryLab.phases_without_windows Method
phases_without_windows(state, windows) -> Vector{Pair{String,Symbol}}What state would release on heating and has no window saying when, as phase => product pairs.
A thermogram computed with one of these present integrates to less than ignition_loss says, and the difference is silent. This is how to see it before reading a curve.
Why the product is in the answer
A phase can need two windows. A carbonated hydrate carries hydrogen and carbon, releases water and carbon dioxide, and does so at different temperatures — a hemicarboaluminate is not an exotic case in a cement. Coverage counted per phase rather than per phase => product reports such a phase as covered when only half of it is, and the missing half is precisely the silent shortfall this exists to catch.
ChemistryLab.released_fraction Method
released_fraction(w::DecompositionWindow, T) -> RealThe fraction of w's phase already released at temperature T in kelvin.
ChemistryLab.released_rate Method
released_rate(w::DecompositionWindow, T) -> Realdf/dT — the shape one peak of a DTG curve has, per kelvin.
ChemistryLab.thermogram Method
thermogram(state, windows; temperatures, relative_to = :initial) -> NamedTupleA thermogravimetric curve for state under windows, as (; temperature, mass, loss, dtg, by_phase, reference_mass, mass_percent, loss_percent):
temperature: the grid, in kelvin, as given.mass: the sample mass remaining, in kilograms — the solid mass ofstate, residue included, because that is what sits on the pan.loss: what has been released, in kilograms, counted from nothing rather than from the first point of the grid — somass = mass₀ - losswithmass₀the solid mass, andloss[1]is the tail already gone before the grid begins rather than zero by definition.dtg:-dm/dTin kilograms per kelvin, which is the curve a thermogravimetric analysis actually resolves peaks in.by_phase: the same loss, per phase, so a peak can be attributed.reference_mass,mass_percent,loss_percent: the curve as published thermograms give it, the mass and the mass lost since the first temperature of the grid, in percent of a reference mass, whichrelative_tonames::initial, the sample at the first temperature of the grid;a temperature (kelvin, or a quantity), the sample at that temperature: a dry mass (Schöler et al. (2015) take the weight at 500 °C, Shi et al. (2016) at 800 °C) or an ignited one (Shi and Lothenbach (2020) give bound water in percent of the sample ignited at 980 °C);
:ignited, the sample once every window has released.
See the thermogravimetry page for the conventions.
A window whose phase state has nothing to release from contributes nothing and raises nothing — windows_without_phases is how a typo in a phase name is caught rather than read off a missing peak.
Every phase of state that carries hydrogen or carbon and has no window contributes its mass to the starting point and never leaves — which is the honest behavior, because a phase nobody said when to decompose has not been modeled. phases_without_windows is how to find out that this happened rather than to discover it from a curve that integrates to the wrong total.
A logistic has infinite tails
The curve does not start at exactly zero: a window centered at 400 K with a width of 15 K is already 1.3 ‰ through at 300 K, and that mass is counted as released before the grid begins. It is the model rather than an error — no renormalization happens here, because silently rescaling a curve to make it start at zero would put the discrepancy somewhere a reader cannot see.
Start the grid at least 6w below the lowest midpoint and the leak is under 0.3 %; 10w puts it under 5 × 10⁻⁵. thermogram(...).loss[1] is what it actually is.
What this is
A deconvolution model, not a kinetic one: it says what fraction of a phase has gone at a temperature, not how fast it goes at a heating rate. That is the form published TGA interpretations take, and it is what makes the windows identifiable from one curve — a rate model would need several heating rates before its parameters meant anything.
ChemistryLab.window_interval Method
window_interval(w::DecompositionWindow) -> (T₁, T₂)The two ends, in kelvin, of an interval window; for a logistic one, the temperatures at which 1 % and 99 % of its release have happened.
ChemistryLab.window_parameters Method
window_parameters(windows) -> (θ, names)The windows' parameters as one vector, [T₁/₂, w] per logistic window and [T₁, T₂] per interval, with names — what an optimizer and identifiability take.
The two are returned together because a parameter vector whose entries are not named is a parameter vector nobody can report.
ChemistryLab.windows_without_phases Method
windows_without_phases(state, windows) -> Vector{String}Which windows name a phase state has nothing to release from.
The mirror of phases_without_windows, and the one that catches a typo: a window on "Portlandit" contributes nothing, raises nothing, and leaves a curve that is simply missing a peak. Both directions are silent by themselves, which is why both are reported rather than one.
A window on a phase that is genuinely absent — a paste with no calcite — is not an error and shows up here too; the two are indistinguishable from inside, and naming them is all this can honestly do.
ChemistryLab.with_window_parameters Method
with_window_parameters(windows, θ; kind = PROV_FITTED, source = "") -> Vectorwindows with their parameters replaced by θ, in the order window_parameters produced.
The replacements are marked PROV_FITTED by default, because that is what a number coming out of an optimizer is. Pass kind explicitly when it is not — stepping a parameter to differentiate against it, for instance, does not make the result a fit.
Recipes and processes
Materials, their extents, recipes and the processes built on them; see Recipes: materials, extents and what has not reacted.
ChemistryLab.AbstractExtent Type
abstract type AbstractExtent endThe degree of reaction of a material or of one of its constituents, as a function of time: extent(e, t) is a number between 0 and 1, t a time (a Quantity, or a number of days). A Recipe multiplies the extent of a material by the extent of each of its constituents; what has not reacted is kept aside as the residue, with its mass and its volume.
Concrete extents: ConstantExtent, TabulatedExtent, LogisticExtent, ParrottKillohExtent and CappedExtent.
ChemistryLab.CappedExtent Type
CappedExtent(inner, cap)inner, never above cap. The cap of a clinker is usually the water limit of Powers (1948), CappedExtent(inner, powers_alpha_max(w_c)).
ChemistryLab.ConstantExtent Type
ConstantExtent(value)The same degree of reaction at every time; ConstantExtent(1) is complete reaction, the extent of water and of salts.
ChemistryLab.LogisticExtent Type
LogisticExtent(; final, half_time, slope, initial = 0)A logistic curve in the logarithm of time,
a generic shape to fit to measured degrees of reaction when a table is too sparse to interpolate. half_time is the time at which half of the final gain is reached (days, or a Quantity); nothing here is fitted by the package.
ChemistryLab.ParrottKillohExtent Type
ParrottKillohExtent(phase; T = 293.15u"K", α_max = 1.0, blaine = nothing, w_c = nothing,
horizon_days = 3650.0, parameters = nothing, H = nothing)The degree of hydration of the clinker phase phase ("C3S", "C2S", "C3A" or "C4AF") under the rate law of Parrott and Killoh (1984) in the form and with the parameters parrott_killoh_avrami uses, at the constant temperature T: the ordinary differential equation of that law integrated from zero, on a logarithmic grid of time. α_max is the ceiling of the law (Powers' water limit, for instance), blaine the fineness correction. The grid ends at horizon_days, and the degree is held at its last value beyond it.
w_c applies instead the water/cement factor of Parrott and Killoh (1984), as Lothenbach and Winnefeld (2006), Section 4.1 state it: the rate is multiplied by
which stops the hydration at
H replaces the critical degree 1.333 of that factor by a value of the phase's own, as Lothenbach et al. (2008), Section 3.2 fit one per clinker phase: parameters replaces some of the constants of the law, a NamedTuple with any of k₁, n₁, k₂, k₃ and n₃ (the same paper adapts k₂ and k₃ of belite); the rate constants are per day, as the papers print them, unless given with a unit. Left out, both give the law above unchanged.
ChemistryLab.TabulatedExtent Type
TabulatedExtent(times, values)A measured degree of reaction, interpolated linearly in time between the tabulated points and held at the first and last values outside them. times are in days (or Quantitys), values between 0 and 1. The usual source is a table of data/literature, for instance literature_table("Durdzinski2017", "degree_of_reaction"; material = "S1", technique = "SEM-IA").
ChemistryLab.AbstractConstituent Type
abstract type AbstractConstituent endOne part of a Material: a database phase (MineralConstituent) or a part known only by its oxides (OxideConstituent), such as a glass. Each carries its mass fraction of the material and its own degree of reaction.
ChemistryLab.Material Type
Material(name, kind = :other; constituents, extent = 1.0, source = nothing)A material of a recipe: a clinker or a cement (:cement), a supplementary cementitious material (:scm), water (:water), a salt (:salt), anything else (:other), made of constituents. extent is the degree of reaction of the material as a whole, multiplied by that of each constituent. Water and salts always react completely: an extent below one on them is refused.
The mass fractions of the constituents may sum to less than one (the rest is not described and not counted), never to more.
ChemistryLab.MineralConstituent Type
MineralConstituent(species; mass_fraction, extent = 1.0, name = symbol(species))A crystalline phase of a material, given by its database species: alite in a clinker, calcite in a limestone, quartz in a fly ash. What reacts enters the equilibrium as that species; what does not keeps its mass, its molar volume and its enthalpy of formation in the residue. extent is a number, a function of time or an AbstractExtent.
ChemistryLab.OxideConstituent Type
OxideConstituent(name, oxides; mass_fraction = 1.0, extent = 1.0,
density = nothing, enthalpy = nothing, source = nothing)A part of a material known by its oxide analysis only: the glass of a slag or of a fly ash, the minor oxides of a clinker. oxides maps oxide formulas to their mass fractions in this constituent (fractions, not percent), as published: an analysis that closes a little above or below one is not renormalized, and what it leaves unaccounted for (a loss on ignition, an undetermined part) is not counted. What reacts enters the equilibrium through its elements (oxide_budget); what does not is residue.
A glass has no formula, so the density (g/cm³) and the standard enthalpy of formation (J/g) its residue needs are given by the caller, with their source, or left nothing: a volume or a heat that needs them is then reported missing, never estimated.
ChemistryLab.bogue Method
bogue(oxides, species; sulfate = "Gp") -> (; phases, remainder)Bogue's calculation, from the formulas of the library rather than from its printed coefficients: the mass fractions of the four clinker phases (C3S, C2S, C3A, C4AF), of the calcium sulfate sulfate (gypsum Gp by default, anhydrite Anh, one sulfur per formula) carrying the SO3 and of calcite (Cal) carrying the CO2, that reproduce the oxide analysis oxides (mass fractions). Gypsum brings its water, which an oxide analysis counts in the loss on ignition: take the carrier the cement actually holds. species maps those symbols to database species (a Dict or a ChemicalSystem). The lime of the gypsum and of the calcite is taken off first; the four clinker phases then reproduce CaO, SiO2, Al2O3 and Fe2O3 exactly. remainder holds the oxides no phase takes (MgO, the alkalis, TiO2 …), for an OxideConstituent.
A negative fraction means the analysis is not a Portland clinker's, and is refused.
ChemistryLab.decompose Method
decompose(oxides, phases, species) -> (; fractions, misfit, remainder)The non-negative mass fractions of the phases phases (symbols, resolved in species) whose oxides best reproduce the analysis oxides, in the least squares sense over the oxides the analysis gives: the generalization of Bogue's calculation to any set of phases, where a square system would give negative fractions or have none. misfit is the largest absolute difference between the analysis and the oxides of the decomposition (a mass fraction), remainder the oxide fractions left over, clipped at zero.
ChemistryLab.effective_extent Method
effective_extent(material, constituent, t) -> RealThe degree of reaction of constituent in material at time t: the extent of the material times the extent of the constituent.
ChemistryLab.literature_oxides Method
literature_oxides(key, table, material) -> OrderedDict{String, Float64}The oxide analysis of material in the table table of data/literature/<key>.json, as mass fractions. Two layouts are read: a long table with the columns material, oxide and percent (literature_oxides("Durdzinski2017", "chemical_composition", "S1")), and a wide one with one column per oxide (literature_oxides("Gruyaert2010", "oxides", "BFS-CAL")). What is not an oxide (a loss on ignition, a fineness) is left out.
ChemistryLab.oxide_content Method
oxide_content(species, oxides) -> OrderedDict{String, Float64}The mass fraction of each oxide of oxides in the phase species, from its formula and the molar masses of the library: the oxide analysis of a pure phase.
ChemistryLab.oxide_material Method
oxide_material(name, oxides; kind = :scm, extent = 1.0, density, enthalpy, source)
oxide_material(name, key, table, material; kwargs...)A material described by its oxide analysis alone, one OxideConstituent that reacts as a whole: a slag, a fly ash, a calcined clay. The second form reads the analysis with literature_oxides and records the source.
ChemistryLab.reactive_part Method
reactive_part(oxides, crystalline, species) -> (; oxides, mass_fraction, clipped)The glass of a material by difference: its oxide analysis minus the oxides of its crystalline phases (crystalline maps phase symbols to mass fractions of the material, as a Rietveld analysis gives them). oxides of the result are fractions of the glass, mass_fraction the glass's share of the material; an oxide the crystals take more of than the analysis holds is clipped at zero and listed in clipped.
ChemistryLab.Recipe Type
Recipe(binder...; w_b, binder_mass = 100u"g", additions = [],
T = 293.15u"K", P = 1u"bar", balance = nothing)A cementitious mix: the materials of the binder with their mass fractions (material => fraction pairs, summing to one), the water/binder ratio w_b, and optional additions by mass (material => mass, a salt or an admixture outside the binder). binder_mass sets the scale: every amount the recipe produces is for that mass of binder, 100 g by default, so that a phase mass read off the answer is in grams per 100 g of binder.
Mass fractions that do not sum to one are refused, unless balance names the material that takes the remainder; its given fraction is then ignored.
See budget for what enters the equilibrium, and equilibrate_certified, given the recipe and a system, to solve it.
ChemistryLab.RecipeState Type
RecipeStateThe certified equilibrium of a Recipe at a time, with what the recipe kept aside. Fields: state (the equilibrated ChemicalState), certificate, recipe, t, initial (the reactants, from budget), b, residual and model (the activity model of the solve).
Read it with volume, porosity, phase_masses, bound_water, pore_solution and enthalpy, which all count the residue where it belongs.
ChemistryLab.bound_water Method
bound_water(rs::RecipeState; window = nothing, windows = nothing, min_mass = 1e-6)
-> RealThe bound water per gram of binder (g/g): the water the solids of the equilibrium would lose on ignition (ignition_loss), plus that of an unreacted mineral which holds some, over the binder mass.
With window = (T₁, T₂) (kelvin, as plain numbers or as quantities) and the decomposition windows of the solids (DecompositionWindow), it is instead the water released between the two temperatures, which is what a thermogravimetric reading over that range weighs: a source that reports bound water between 105 °C and 550 °C is compared with the same range. Every solid holding more than min_mass grams of water, an unreacted mineral included, needs a window releasing water; one without is refused by name, since its water cannot be placed inside or outside the range.
ChemistryLab.budget Method
budget(state::ChemicalState) -> VectorThe budget of state: the totals of the system's primaries (state.system.SM.primaries), conservation_matrix(system) * n, the right-hand side every equilibrium solve of state conserves when it is given no b. In the number type of the amounts and of the matrix, so that a composition or a site density being differentiated carries its derivative.
Where a site family follows its host, the matrix is the solver's own conservation_matrix, not SM.A, which would leave the coupling out. It counts the family's sites as laid out in state: one whose host alone holds them is made consistent first by host_consistent_state.
Examples
b = budget(state) # what `equilibrate_certified(state)` conserves
eq, cert = equilibrate_certified(state2; b = budget(state)) # state2 as a start onlyChemistryLab.budget Method
budget(recipe, system; t = nothing) -> (; state, b, residual)What the recipe puts into the equilibrium at time t, and what it keeps aside.
state: aChemicalStateofsystemholding the water and the reacted part of every mineral constituent, as species, at the recipe's temperature and pressure: its enthalpy is that of the reactants.b: the element budget, that ofstateplus the reacted part of every oxide constituent throughoxide_budget.residual: what is kept aside, one entry per constituent with itsmass(g),volume(cm³) andenthalpy(J, of formation),nothingwhere a density or an enthalpy the constituent needs has no source, andreason::unreacted, or:not_in_systemfor the reacted part of an oxide whose element the system has no primary for (the TiO2 or the P2O5 of a cement in a system without titanium or phosphorus).
The reacted part of a constituent is its mass times its effective extent (the material's extent times its own, effective_extent). A mineral constituent that reacts must be a species of system; one that does not need not be.
ChemistryLab.element_amounts Method
element_amounts(state::ChemicalState) -> OrderedDict{Symbol, <:Real}The amount, in moles, of each atom of the system in state: the rows of its canonical stoichiometric matrix (state.system.CSM), the elements, the charge :Zz and any site symbol, summed over every species. In the number type of the amounts.
The dissolved part alone, per kilogram of water or per liter of solution, is pore_solution.
Examples
element_amounts(eq)[:Ca] # mol of calcium in the whole stateChemistryLab.enthalpy Method
enthalpy(rs::RecipeState) -> RealThe enthalpy (J) of the paste, the equilibrium's plus the residue's; NaN when an unreacted constituent has no sourced enthalpy of formation, so that a heat computed from it cannot pass for complete.
ChemistryLab.equilibrate_certified Method
equilibrate_certified(recipe::Recipe, system::ChemicalSystem; t = nothing,
model = DiluteSolutionModel(), start = nothing, kwargs...)
-> (RecipeState, certificate)The certified equilibrium of recipe at time t in system: its budget solved by equilibrate_certified, from start (a state of system, typically the answer at an earlier time) when given, from the reactants otherwise. The other keywords go to equilibrate_certified.
ChemistryLab.heat_release Method
heat_release(rs1::RecipeState, rs2::RecipeState; set_aside = nothing) -> RealThe heat (J) a paste releases from the state rs1 to the state rs2 of the same recipe, at the same temperature and pressure: the fall of its enthalpy, -(H₂ - H₁). The residue counts by what changed between the two. A constituent set aside with the same mass in both (an inert crystal, an oxide the system has no primary for) adds nothing, whether or not its enthalpy is sourced. One whose unreacted mass changed needs a sourced enthalpy of formation, and the heat is NaN without it.
An oxide the system has no primary for, released by a constituent that reacts (the titanium of a slag glass), is set aside with a mass that changes. Its enthalpy of formation per gram is given by set_aside, a map from the oxide's formula to it (J/g, or an energy per mass); the one glass_enthalpy counted it at, its crystal's, keeps the balance closed. Without an entry the heat is NaN.
ChemistryLab.phase_masses Method
phase_masses(rs::RecipeState; min_mass = 1e-6) -> OrderedDict{String, <:Real}The mass (g, for the recipe's binder mass, so g per 100 g of binder by default) of every solid of the equilibrium above min_mass, largest first, then of each unreacted constituent, under "unreacted <name>".
ChemistryLab.pore_solution Method
pore_solution(state::ChemicalState, model; per = :kg) -> NamedTupleThe pore solution of state: pH in the activity convention of model, and elements, the total amount of each element dissolved, all solutes counted, hydrogen and oxygen left out: per kilogram of water (per = :kg, a molality) or per liter of solution (per = :L, the liquid volume of the state). In the number type of the amounts.
Examples
ps = pore_solution(eq, model)
ps.pH, ps.elements[:Ca] # mol of dissolved calcium per kg of water
pore_solution(eq, model; per = :L).elements[:Na]ChemistryLab.pore_solution Method
pore_solution(rs::RecipeState) -> NamedTupleThe pore solution of the recipe's equilibrium, pore_solution(rs.state, rs.model).
ChemistryLab.porosity Method
porosity(rs::RecipeState) -> NamedTupleThe porosity of a sealed paste, as fractions of its initial volume (the reactants, the water and the residue): liquid (filled by the pore solution and any gas), void (the chemical shrinkage, what the reactions did not refill) and total. The residue is counted in the solid of both volumes, which is what makes the porosity of a paste with an unreacted part the right one.
ChemistryLab.residual_mass Method
residual_mass(rs::RecipeState) -> RealThe mass (g) of what has not reacted.
ChemistryLab.volume Method
volume(rs::RecipeState) -> NamedTupleThe volumes (cm³) of the paste: liquid, gas and solid of the equilibrium, residual of what has not reacted (NaN when a constituent has no density, see missing), and total.
ChemistryLab.volume_fractions Method
volume_fractions(rs::RecipeState; void_key = "void") -> OrderedDict{String, <:Real}The volume fraction of every species of the equilibrium and of every unreacted constituent (under "unreacted <name>"), relative to the initial volume of the paste: the reactants, the water and the residue, the reference of porosity. The chemical-shrinkage void closes the sum under void_key, so the fractions add up to one and void is porosity(rs).void.
A constituent whose residue has no sourced density leaves every fraction undefined, and is refused by name rather than counted as nothing. volume_fractions(state, groups) groups the species of the state alone.
ChemistryLab.ProcessResult Type
ProcessResultA process as a sequence of RecipeStates, one per value of its parameter: parameter (the times of hydrate, the replacement fractions of blend, the amounts of titrate, the steps of leach), label (what the parameter is) and states. Each state was solved from the previous one. process_table tabulates it.
ChemistryLab.add_salt Method
add_salt(rs, salt, amounts; kwargs...) -> ProcessResulttitrate with the salt salt: its formula ("NaCl", "CaCl2"), or a solid salt the system declares. A salt enters as a whole, since one ion alone would make the budget charged.
ChemistryLab.blend Method
blend(base, scm, fractions, system; t = nothing, model = DiluteSolutionModel(), kwargs...)
-> ProcessResultbase with the fraction f of its binder replaced by the material scm, for each f of fractions: every other material of the binder keeps its share of what remains, the water/binder ratio and the binder mass stay those of base.
ChemistryLab.carbonate Method
carbonate(rs, amounts; kwargs...) -> ProcessResulttitrate with dissolved carbon dioxide, CO2@.
ChemistryLab.hydrate Method
hydrate(recipe, system, times; model = DiluteSolutionModel(), kwargs...) -> ProcessResultThe recipe at each of times (days, or Quantitys), its extents read at that time, each equilibrium started from the previous answer. The keywords go to equilibrate_certified.
ChemistryLab.leach Method
leach(rs, steps; renewal = nothing, solution = nothing, kwargs...) -> ProcessResultsteps renewals of the pore solution: at each, the whole aqueous phase of the last equilibrium is removed and replaced by renewal of pure water (g; by default the water of the recipe), and the paste is equilibrated again with what remains. The residue is unchanged. Each budget depends on the previous answer, which is what makes this a sequence rather than a sweep.
solution replaces the pure water by a solution: its budget in the primaries of the system, water included, added at each renewal (a seawater, a sulfate solution). It must be electrically neutral, and renewal is then not used.
ChemistryLab.process_table Method
process_table(p::ProcessResult; phases = nothing) -> DataFrameOne row per state of p: the parameter, whether it certified, the pH, the porosity, the bound water (g/g of binder) and the mass (g per the recipe's binder mass) of each of phases, by default every solid that appears in some state.
ChemistryLab.titrate Method
titrate(rs, species, amounts; kwargs...) -> ProcessResultThe paste rs with each of amounts (mol, or Quantitys) of species added to its element budget, the residue unchanged, each equilibrium started from the previous one: the gas of a carbonation, the salt of a chloride ingress. species is a symbol of the system, or a formula ("NaCl") whose elements its primaries hold, which need not be a species of it. carbonate and add_salt are this with a chosen species.
ChemistryLab.material_template Method
material_template(name, species) -> Material
material_templates() -> Vector{String}A material of a published mix, built from data/recipe_templates.toml and the literature files it names: every number is read from data/literature, and the file holds only references and choices. species maps database symbols to species (a Dict, or a ChemicalSystem holding them all). material_templates() lists the names.
An entry is built one of five ways:
phases = "<key>:<table>:<material>": from an XRD-Rietveld table, each phase aMineralConstituentor, when the database has no record of it, anOxideConstituent(seeChemistryLab.RIETVELD_PHASES); theAmorphousentry, when given, is the glass by difference fromanalysis(reactive_part); withremainder = trueand noAmorphousentry, what the analysis holds beyond the phases is one oxide constituent,"minor oxides"(the free lime, alkalis and magnesia of a clinker), in the mass the phases leave; withremainder = "analysis"it keeps the amounts of the analysis, and the phases are scaled down to make room when the two disagree (a clinker whose phases, counted with their pure formulas, leave less than its magnesia, alkalis and sulfate);analysis = …androute = "bogue": Bogue's calculation on the oxide analysis (bogue), the sulfate carried bysulfate(gypsum by default), with the oxides no phase takes as one oxide constituent unlessminor_oxides = false;analysis = …androute = "oxides": the analysis alone, one oxide constituent;analysis = …andcrystalline = "<key>:<table>:<material>": the glass by difference, reacting, beside its crystals, inert;clinker = "<key>:<table>", the Bogue phases of a clinker in percent of the clinker (phase,percent), withgypsumandcalcitenaming the quantities that give them in percent of the cement: the clinker is what the two leave, its phases scaled to it, and what they do not account for (its minor phases) is left out of the material.
Every constituent of a template reacts completely unless the entry says otherwise; the extents of a real mix are the caller's, set with with_extents. The polymorphs a Rietveld analysis tells apart and the database does not (the α′ and β C₂S, the cubic and orthorhombic C₃A) are one constituent, named by the database symbol ("C2S", "C3A"): two constituents of one species would be dissolved twice by a rate law each.
ChemistryLab.with_enthalpy Method
with_enthalpy(material, enthalpies::AbstractDict; source) -> Materialmaterial with the standard enthalpy of formation (J/g, or an energy per mass) of the constituents known by their oxides named by the keys of enthalpies, and its source: a glass given its enthalpy by glass_enthalpy, so that a heat which changes its unreacted mass can be computed (heat_release). A constituent with a formula takes its enthalpy from its database record and is refused here.
glass = only(c for c in slag.constituents if c.name == "glass")
g = glass_enthalpy(glass.oxides)
slag = with_enthalpy(slag, Dict("glass" => g.enthalpy); source = "glass_enthalpy")ChemistryLab.with_extents Method
with_extents(material, extents::AbstractDict; material_extent = nothing) -> Materialmaterial with the extents of its constituents set by name ("C3S" => 0.7, "glass" => TabulatedExtent(…)), the others unchanged, and the extent of the material as a whole replaced when material_extent is given.
ChemistryLab.with_species Method
with_species(material, species::AbstractDict) -> Materialmaterial with the constituents named by the keys of species described by those species: a constituent known by its oxides (a glass) becomes a MineralConstituent of its species, with its mass fraction and its extent, which a rate law can dissolve (KineticsProblem of a recipe). The constituent takes the symbol of the species as its name, the name a rate law reads its amount by. The species of a glass is built from the constituent itself by glass_species.
glass = only(c for c in slag.constituents if c.name == "glass")
sp = glass_species(glass, system; symbol = "BFS")
slag = with_species(slag, Dict("glass" => sp)) # its constituent "BFS"ChemistryLab.phase_list Method
phase_list(name) -> NamedTuple
phase_lists() -> Vector{String}A phase list of data/phase_lists.toml: the phases a paste may form, written after the assemblage of the paper it names. Its fields are name, database (the file its symbols are records of), reactants and products (pure phases), solid_solutions (names in data/solid_solutions.toml), instances (what the list declares a solid solution with, where it departs from that file), exclude_aqueous and source. phase_lists() gives the names; phase_list_system builds the system.
ChemistryLab.phase_list_system Method
phase_list_system(name, substances; add = String[], remove = String[],
replace = Dict(), exclude_aqueous = String[],
primaries = CEMDATA_PRIMARIES) -> ChemicalSystemThe chemical system of the phase list name (phase_list): its reactants and products as pure phases, its solid solutions as data/solid_solutions.toml declares them (with the instances the list gives), and the aqueous species of substances its elements allow, those of exclude_aqueous left out. substances are the species of the list's database, build_species(datapath(phase_list(name).database)).
A calculation that departs from the list says so: add names pure phases to declare beside the list's, remove pure phases of the list to leave out, replace maps a solid solution of the list to another one of data/solid_solutions.toml (another model of the same gel, say), and exclude_aqueous names aqueous species to leave out besides the list's.
substances = build_species(datapath("cemdata18-thermofun.json"))
cs = phase_list_system("Portland paste (Lothenbach and Winnefeld 2006)", substances;
add = ["C4AClH10"]) # and Friedel's saltChemistryLab.KineticsProblem Method
KineticsProblem(recipe::Recipe, system::ChemicalSystem, rates, tspan; kwargs...)
-> KineticsProblemThe hydration of recipe in system as a kinetic problem. rates maps the name of a mineral constituent to its rate law (a KineticFunc): that constituent enters whole and unreacted, and dissolves into the primaries of system at that rate, by the reaction the conservation matrix of system gives it. The other keywords go to the KineticsProblem of a list of reactions (activity_model, equilibrium_solver, calorimeter).
Every other constituent is taken as budget takes it at tspan[1]: its reacted part is in the equilibrium from the start, and its unreacted part stays aside for the whole run, out of the problem. The reacted part of a constituent known by its oxides enters as the primaries that carry its elements, with the protons an oxide consumes written as hydroxide taken from the mixing water (CaO + H₂O → Ca²⁺ + 2 OH⁻); its heat of dissolution is therefore not part of the heat of the run.
A constituent given a rate must be a mineral constituent of the recipe whose species is in system, and no two of them may share a species, which would be dissolved twice. A glass, known by its oxides only, has no formula to dissolve and is refused: its pseudo-species is built from the constituent itself by glass_species, put in its place by with_species, and added to system (What a recipe puts into the equilibrium, Section 6).
ChemistryLab.glass_species Method
glass_species(c::OxideConstituent, system; symbol, M = 100.0u"g/mol", name = symbol)
-> SpeciesThe pseudo-species of a constituent known by its oxides, for a rate law to dissolve: glass_species of the oxides of c whose element system has a primary for. An oxide the system cannot hold (the TiO₂ or the P₂O₅ of a fly ash) stays out of the formula and in the mass M of material a formula unit stands for, as the part of it the formula does not model (modeled_mass_fraction), which is how budget keeps it aside on the route of imposed extents. The two routes then put the same elements into the equilibrium at the same degree of reaction, to the five decimals at which the system writes every species over its primaries (5e-6 mol of a primary per mole of glass at most).
ChemistryLab.with_glass_species Method
with_glass_species(recipe, system, names; enthalpy = true, M = 100.0u"g/mol")
-> (; recipe, system, species)The recipe and the system in which each constituent of recipe named in names and known by its oxides only (a glass: a slag, the glass of a fly ash) is given a formula: its pseudo-species (glass_species, named after the constituent) takes its place in its material (with_species) and joins the system. That is what a rate law needs to dissolve a glass in a KineticsProblem; a constituent of names that is a mineral already, or that no material has, is left alone, and with no glass among names the recipe and the system come back as they were. species maps each name to its pseudo-species. The elements the glass brings are those of its oxides (glass_species), to the rounding of its column over the primaries.
With enthalpy = true (the default) each pseudo-species carries an enthalpy of formation and a heat capacity estimated from its oxides (glass_species), so that a calorimeter counts the heat of its dissolution; without, a run with a calorimeter is refused, a glass having no enthalpy of its own in a database.
Examples
rates = Dict("glass" => waller(WALLER_PARAMS_FLY_ASH, "glass"))
(; recipe, system) = with_glass_species(recipe, system, keys(rates))
kp = KineticsProblem(recipe, system, rates, (0.0, 28 * 86400.0))ChemistryLab.SystemDiagnostic Type
SystemDiagnostic(level, code, message, fix, subjects)One finding of diagnose_system: its level (:blocking, the calculation cannot be trusted; :warning, a result will be incomplete or doubtful; :info), a code naming the check, a message saying what was found and what it costs, a fix saying what to do, and the subjects, the species or constituents concerned.
ChemistryLab.diagnose_system Method
diagnose_system(cs; recipe = nothing, T = nothing, t = nothing, model = nothing,
reactants = String[], large = 250) -> Vector{SystemDiagnostic}The findings worth reading before computing with system cs, and with recipe (at age t, for laws of the degree of reaction that depend on it):
a constituent of the recipe that the system cannot hold (no species carries it, or one of its elements), set aside unreacted;
ettringite declared both as a pure phase and as a member of a solid solution, one phase counted twice;
a crystal used outside the temperature range its data declare (the temperature is
T, or the recipe's), reported as information for the reactants, which only dissolve: the minerals of the recipe, and the species named inreactants(those of a phase list,phase_list(name).reactants);the Al-Si hydrogarnets of Cemdata18, which it notes precipitate very slowly at 20 °C and are generally left out;
crystals without a standard molar volume (volumes and porosity incomplete) or without an enthalpy of formation (heat incomplete);
with a SIT
model, the pairs of ions without an interaction coefficient;a system of more than
largespecies, whose first solve compiles longer.
An empty result means that none of these was found, not that the calculation is right.
cs = phase_list_system("Portland paste (Lothenbach and Winnefeld 2006)", substances)
foreach(println, diagnose_system(cs; recipe))ChemistryLab.with_csh_surface Method
with_csh_surface(rs::RecipeState; donnan = nothing, water = :taken)
-> NamedTupleThe paste of rs with the surface of its C-S-H, as the model of Guo et al. (2018) writes it: its site density and its specific surface, read from its data file and referred to the silicon of the gel (the molar mass per silicon of their C-S-H), its surface complexes with the ions the system holds (those whose ion it lacks are left out), and a diffuse layer (DiffuseLayer). The route is that of Chloride binding in CEM III/A and CEM III/B, route A:
the solid solution
CSHQofrsis frozen at its composition (freeze_solid_solution), its alkalis returned to the solution, portlandite fixing the activities it shares with the gel;the sites are put on a support outside the system, the frozen gel;
the paste is solved again with them (
equilibrate_certified).
donnan, a DonnanLayer or a thickness (m, or :debye for one Debye length of the solution), adds the layer of water on the diffuse layer (equilibrate_donnan, its water water = :taken from the pore solution of a closed paste, or :added). It is refused when the pore water is a film thinner than the layer or than the Debye length, the case of a hardened paste at the area of Guo et al. (2018): a Donnan layer assumes a free solution beyond it, and in such a film the diffuse layers of the walls overlap (a layer made thin enough to fit has to hold the charge of the surface in so little water that it has no fixed point). It is meant for a suspension.
Two numbers belong to the model rather than to the paste, and are worth reading before the rest: the published model counts calcium on the surface on top of a gel whose measured Ca/Si includes it, so that portlandite gives some up; and the area makes the pore water a film of a nanometer or less on the gel, which film reports, against the Debye length.
Refused, with the reason: a paste without CSHQ in its system, without C-S-H, or without portlandite.
Returns (; state, certificate, family, area, sites, film, debye, potential, complexes, bound, frozen, donnan): the state of the second stage and its certificate, the site family, the area (m²) and the sites (mol), the thickness of the pore water on the area and the Debye length (m), the potential of the surface (V), the moles of each surface species, the moles of each element held by the complexes, what the freezing set aside, and, with donnan, the result of equilibrate_donnan and the contents of the layer (diffuse_layer_contents).