An Introduction to CHNOSZ

```{css, echo=FALSE} html { font-size: 14px; } / zero margin around pre blocks (looks more like R console output) / pre { margin: 0; padding: 0; } .highlight { background-color: #F27121cc; border-radius: 6px; padding: 0px 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); display: inline-block; }

```{js, echo=FALSE}
function ToggleDiv(ID) {
  var D = document.getElementById("D-" + ID);
  var B = document.getElementById("B-" + ID);
  if (D.style.display === "none") {
    // open the div and change button text
    D.style.display = "block";
    B.innerText = "Hide code";
  } else {
    // close the div and change button text
    D.style.display = "none";
    B.innerText = "Show code";
  }
}
# Some frequently used HTML expressions
# Use lowercase because some of these are used as variables in the examples
h2o <- "H<sub>2</sub>O"
h2 <- "H<sub>2</sub>"
o2 <- "O<sub>2</sub>"
co2 <- "CO<sub>2</sub>"
h2s <- "H<sub>2</sub>S"
Psat <- "<i>P</i><sub>sat</sub>"
zc <- "<i>Z</i><sub>C</sub>"
library(knitr)

options(width = 180)

# Function to colorize messages 20171031
# Adapted from https://gist.github.com/yihui/2629886#file-knitr-color-msg-rnw
color_block = function(color) {
  function(x, options) sprintf('<pre style="color:%s">%s</pre>', color, x)
}
# Set knitr hooks
knit_hooks$set(
  warning = color_block('magenta'),
  error = color_block('red'),
  message = color_block('blue'),
  # Use pngquant to optimize PNG images
  pngquant = hook_pngquant
)

# Define a custom hook to use smaller plot margins
knit_hooks$set(small_mar = function(before, options, envir) {
  if(before) {
    par(mar = c(5, 5, 1, 1))
  }
})

# Define variables used below
basedpi <- if(nzchar(Sys.getenv("CHNOSZ_BUILD_LARGE_VIGNETTES"))) 60 else 40
pngquant <- if(nzchar(Sys.getenv("CHNOSZ_BUILD_LARGE_VIGNETTES"))) "--speed=1 --quality=0-25" else "--speed=1 --quality=0-10"
# Avoid errors if pngquant isn't available (perhaps on R-Forge?)
if (!nzchar(Sys.which("pngquant"))) pngquant <- NULL 

# Set chunk options
opts_chunk$set(
  tidy = FALSE,
  # Figure options
  fig.margin = TRUE, fig.width = 6, fig.height = 4,
  out.width = "100%", dpi = basedpi,
  cache = TRUE, pngquant = pngquant,
  # From "Tufte Handout" example dated 2016-12-27
  # Invalidate cache when the tufte version changes
  cache.extra = packageVersion('tufte')
)

This vignette introduces CHNOSZ, an R package for thermodynamic calculations relevant to geochemistry and geochemical biology. CHNOSZ provides functions and a thermodynamic database for calculating properties of reactions involving minerals, aqueous species, and gases across a range of temperatures and pressures.

This vignette was compiled on r Sys.Date() with CHNOSZ r packageDescription("CHNOSZ")$Version.

Getting Started

After installing CHNOSZ from CRAN, load the package:

library(CHNOSZ)

This makes the thermodynamic database and functions available in your R session. To restore default settings at any point, use reset().

Basic Functionality

Organization of CHNOSZ

CHNOSZ is made up of interrelated functions and supporting data. The major components of the package are shown in the flow diagram. Ellipses represent data sources and rectangles represent functions. Bold arrows and functions show the most common workflows (described in italic legends). Dashed arrows represent internal flows of data.

Flow diagram for CHNOSZ{ width=75% }

CHNOSZ offers several primary functions for thermodynamic analysis:

Functions without side effects (return values)

Functions with side effects (modify system state)

Querying the thermodynamic database

The info() function provides access to the OBIGT thermodynamic database.

# Get database index for aqueous methane
info("CH4")

Searching by formula returns the aqueous species if it is available. Use a species name or add the state to get a particular physical state - aq, cr, gas, or liq:

# Two ways to lookup methane gas
info("methane")
info("CH4", "gas")

Use info() recursively to return thermodynamic parameters:

# Get thermodynamic properties for aqueous methane
info(info("CH4"))

You can access fuzzy search functionality by using partial names:

# Search for ribose-related entries
info("ribose+")

Calculating thermodynamic properties

The subcrt() function [loosely named after SUPCRT; @JOH92] calculates standard thermodynamic properties.

The default conditions are 0.01--350 °C along the r Psat curve (defined here as the greater of 1 bar or the vapor-liquid saturation pressure for r h2o):

# Properties of aqueous methane at default T and P
subcrt("CH4")

You can customize the T-P grid by passing the appropriate arguments:

# Custom T, P grid for water in supercritical region
subcrt("H2O", T = c(400, 500), P = c(250, 300))

Unit settings for subcrt() are handled by T.units(), P.units(), and E.units():

# Change energy units from Joules to calories
E.units("cal")
subcrt("CH4", T = 25)
reset()  # Restore defaults

Working with reactions

Define reactions with species names or formulas, states (optional), and coefficients:

NOTE: Reaction coefficients are negative for reactants and positive for products.

# CO2 dissolution reaction
subcrt(c("CO2", "CO2"), c("gas", "aq"), c(-1, 1), T = 25)

Reactions can be automatically balanced using basis species:

basis(c("CO2", "H2O", "H+", "e-"))
subcrt(c("CH4", "acetate"), c("aq", "aq"), c(1, -1), T = 25)
basis(c("CO2", "H2O", "H+", "e-"))
species(c("CH4", "acetate"))

There are some keywords (e.g. CHNOS+, CHNOSe and QEC) for loading predefined sets of basis species. See the help page of basis() for more information.

Chemical affinity and stability diagrams

# Change the default options for the following chunks to hide results and messages
opts_chunk$set(
  results="hide", message=FALSE
)

The affinity() function calculates chemical affinities over ranges of T, P, and activities:

# Setup the C-H-N-O-S basis system with electron
basis("CHNOSe")
# Define aqueous sulfur species
species(c("SO4-2", "HSO4-", "HS-", "H2S"))
# Calculate affinities on an Eh-pH grid
a <- affinity(pH = c(0, 14), Eh = c(-0.8, 1.2))
# Create an Eh-pH (Pourbaix) diagram
diagram(a, col = 4, col.names = 4, italic = TRUE)
# Add legend
TC <- convert(a$T, "C")
legend <- c(describe.property("T", TC), quote(italic(P) == "1 bar"))
legend("topright", legend = legend, bty = "n")

NOTE: diagram() automatically adds shading to regions of water instability with respect to r o2 or r h2.

For more sophisticated diagrams involving speciation of basis species, use the mosaic() function:

# Create a mosaic diagram for Cu-S-Cl-H2O system
basis(c("Cu", "H2S", "Cl-", "H2O", "H+", "e-"))
basis("H2S", -6)
basis("Cl-", -1)
species(c("CuCl", "CuCl2-", "CuCl3-2", "CuCl+", "CuCl2", "CuCl3-", "CuCl4-2"))
species(c("chalcocite", "tenorite", "cuprite", "copper"), add = TRUE)
bases <- c("H2S", "HS-", "HSO4-", "SO4-2")
m <- mosaic(bases, pH = c(0, 12), Eh = c(-1, 1), T = 200)
a <- m$A.species
diagram(a, lwd = 2, bold = species()$state == "cr")
diagram(m$A.bases, add = TRUE, col = 4, col.names = 4, italic = TRUE)
water.lines(m$A.species, lty = 3, col = 8)
# Add legend
legend <- lTP(convert(a$T, "C"), a$P)
legend("topright", legend = legend, bty = "n")

Here we've added dotted lines to help visualize the water stability limits.

Equilibrium calculations

Calculate equilibrium distributions of species:

# Carbonate speciation vs pH
basis(c("CO2", "H2O", "H+", "e-"))
species(c("CO2", "HCO3-", "CO3-2"))
# 25 degrees C
a <- affinity(pH = c(0, 14))
e <- equilibrate(a)
diagram(e, alpha = TRUE)
# 100 degrees C
a <- affinity(pH = c(4, 12), T = 100)
e <- equilibrate(a)
diagram(e, alpha = TRUE, add = TRUE, col = 2, names = NA)
# Add legend
legend <- as.expression(list(lT(25), lT(100)))
legend("left", legend = legend, lty = 1, col = c(1, 2))

Calculate solubility of minerals or gases:

# Corundum solubility vs pH
basis(c("Al+3", "H2O", "H+", "e-"))
species("corundum")
iaq <- info(c("Al(OH)4-", "Al(OH)3", "Al(OH)2+", "AlOH+2", "Al+3"))
s <- solubility(iaq, pH = c(2, 10), in.terms.of = "Al+3")
diagram(s, col = "green4", lwd = 2, ylim = c(-10, -2))
diagram(s, type = "loga.equil", add = TRUE)
legend("topright", c("25 °C", "1 bar"), bty = "n")

Activity coefficients

Incorporate non-ideal behavior using the extended Debye--Hückel equation by setting the ionic strength parameter IS:

# Corundum solubility again
basis(c("Al+3", "H2O", "H+", "e-"))
species("corundum")
iaq <- info(c("Al(OH)4-", "Al(OH)3", "Al(OH)2+", "AlOH+2", "Al+3"))
# Calculate with ionic strength of 0 and 1 molal
s0 <- solubility(iaq, pH = c(2, 10))
s1 <- solubility(iaq, pH = c(2, 10), IS = 1)
diagram(s1, col = 4, lwd = 3, ylim = c(-8, -2))
diagram(s0, col = "green4", lwd = 2, add = TRUE)
legend("topleft", legend = c(1, 0), lwd = c(3, 2), col = c(4, "green4"), title = "I (mol/kg)", bty = "n")
legend("topright", c("25 °C", "1 bar"), bty = "n")

Functions that have the IS parameter are subcrt(), affinity(), mosaic(), and solubility(). When a value for IS is specified, inputs and outputs labeled as activities are actually interpreted or reported as molalities.

References

The CHNOSZ package incorporates data and methods from various sources. Use thermo.refs() to view citation information for data sources:

# Return data frame with references for one or more species
thermo.refs(info("CH4", c("aq", "gas")))
# View all references in a browser
thermo.refs()

For citing CHNOSZ itself, see "How should CHNOSZ be cited?" in the FAQ.

Interlude: Affinity, Formation Reactions, and Balancing

The idea for creating stability diagrams in CHNOSZ came from Prof. Harold Helgeson's geochemistry course at UC Berkeley. There, the students constructed diagrams that were "balanced on" a metal. For instance, in a reaction between two minerals balanced on aluminum, Al is only present in the minerals on either side of the reaction and not as an ion.

The line-based method, used for making diagrams by hand, looks at reactions between pairs of species (let's call them transformation reactions), then draws a line between stability fields where the non-standard Gibbs energy of reaction is zero. The grid-based method, used in CHNOSZ, looks at reactions to compose individual species from the basis species (let's call them formation reactions), then selects the most stable species according to their affinity values.

Affinity is just the opposite of non-standard Gibbs energy of reaction. "Standard Gibbs energy of reaction" and "Gibbs energy of reaction" – which are different concepts – have unfortunately similar names except for an optional overall or non-standard in front of the latter [the word choice varies among authors, e.g. @AL19;@STK19]. "Non-standard Gibbs energy of reaction" doesn't lend itself to a short, unambiguous function name, which is why its opposite, affinity, is used in CHNOSZ.

In the line-based method, transformation reactions are said to be balanced on a metal. The grid-based method implements this balancing constraint by dividing the affinities of formation reactions by the stoichiometric coefficients of a basis species. CHNOSZ uses these normalized affinities for making relative stability diagrams, a technique referred to as the maximum affinity method [@Dic19]. The line- and grid-based methods both have the same limitation: every species considered in the relative stability calculation must have non-zero stoichiometry of the metal the transformation reactions are balanced on (or equivalently of the conserved basis species that has that metal).

Caution: Quasisolubility contours on predominance diagrams underestimate total solubility

A useful technique in geochemical modeling is to construct "composite diagrams" [@GC65], where stability fields for minerals and predominance fields for aqueous species are both displayed on the same plot. Because mineral activities are assumed to be unity while aqueous species activities can vary, the choice of aqueous species activity defines a concentration-dependent boundary between mineral and aqueous stability fields on composite diagrams. These solubility boundaries between minerals and aqueous species are referred to as either "equisolubility" [@Pou74] or "isosolubility" [@Hel64;@GC65] lines.

CHNOSZ provides two distinct approaches for calculating isosolubility lines:

First approach (quasisolubility contours): This method loads multiple aqueous species with identical activities, then constructs the predominance diagram using the maximum affinity method. However, isosolubility lines calculated this way represent the reaction between the most stable mineral and a single aqueous species at each point on the diagram. These can be called "quasisolubility contours" because they provide only a partial view of mineral solubility.

Second approach (total solubility): This method sums the activities of all candidate aqueous species in equilibrium with the most stable mineral, providing isosolubility lines that represent the contribution from all relevant aqueous species.

The first approach is implemented in CHNOSZ by setting the activities of formed aqueous species, then creating a predominance diagram with the maximum affinity method using diagram(). See below for an example of this method. In essence, quasisolubility contours on predominance diagrams are equivalent to previously described "solubility limits" on activity diagrams [@OB79]. The term "quasisolubility" emphasizes that these contours provide only first-order estimates of solubility, considering just one aqueous species at a time.

CHNOSZ offers a second approach that improves upon the maximum affinity method by accounting for contributions from multiple aqueous species to total solubility. This approach is implemented in the solubility() function (see below).

The following example demonstrates the difference between these two approaches:

par(mfrow = c(1, 2))

# System definition
metal <- "Fe"
# The concentration to be used for the quasisolubility contour
logm_metal <- -6
T <- 150
P <- "Psat"
Stot <- 1e-3
# Approximate chloride molality and ionic strength for mNaCl = 1.9
m_Clminus <- IS <- 1.5
# Plot variables
res <- 300
pH <- c(2, 12, res)
O2 <- c(-55, -40, res)

# Setup basis species
basis(c(metal, "H2S", "Cl-", "oxygen", "H2O", "H+"))
basis("H2S", log10(Stot))
basis("Cl-", log10(m_Clminus))
# Define basis species to swap through for mosaic calculation
bases <- c("H2S", "HS-", "HSO4-", "SO4-2")

# Get minerals and aqueous species
icr <- info(c("hematite", "magnetite", "pyrite", "pyrrhotite"))
iaq <- retrieve(metal, c("Cl", "S", "O", "H"), state = "aq")

## First plot: predominance diagram (quasisolubility) overlaid with total solubility contours
species(icr)
species(iaq, logm_metal, add = TRUE)
m <- mosaic(bases, pH = pH, O2 = O2, T = T, P = P, IS = IS)
diagram(m$A.species, bold = species()$state == "cr")

# Make total solubility contours
species(icr)
s <- solubility(iaq, bases = bases, pH = pH, O2 = O2, T = T, P = P, IS = IS, in.terms.of = metal)
levels <- seq(-9, -3, 3)
diagram(s, levels = levels, contour.method = "flattest", add = TRUE, col = "green4", lwd = 2)
# Add a legend
leg_list <- c(
  lTP(T, P),
  lNaCl(1.9),
  lS(Stot)
)
leg_expr <- lex(leg_list)
legend("bottomleft", legend = leg_expr, bg = "white")
title("Total solubility is higher than quasisolubility", font.main = 1)

## Second plot: activities of aqueous species in solubility calculation
basis("O2", -46)
s <- solubility(iaq, bases = bases, pH = pH, T = T, P = P, IS = IS, in.terms.of = metal)
diagram(s, col = "green4", lwd = 2, ylim = c(-10, -6))
# Locate the stability boundary between pyrite and magnetite along the the pH axis 
ipH <- which.min(abs(s$loga.equil[[3]] - s$loga.equil[[2]]))
# Show py-mag stability boundary
pH_py_mag <- s$vals$pH[ipH]
abline(v = pH_py_mag, col = 8)
text(pH_py_mag, -6, "pyrite", adj = c(1.2, 1.5), font = 2)
text(pH_py_mag, -6, "magnetite", adj = c(-0.1, 1.5), font = 2)

# Consider each mineral separately to get activities of aqueous speices
species("pyrite")
s_py <- solubility(iaq, bases = bases, pH = pH, T = T, P = P, IS = IS, in.terms.of = metal)
species("magnetite")
s_mag <- solubility(iaq, bases = bases, pH = pH, T = T, P = P, IS = IS, in.terms.of = metal)
# Merge activities for stable regions of each mineral
loga.equil_both <- lapply(1:length(s_py$loga.equil), function(ispecies) {
  c(s_py$loga.equil[[ispecies]][1:(ipH-1)], s_mag$loga.equil[[ispecies]][ipH:res])
})
# Put merged activities into new object for plotting
s_both <- s_py
s_both$loga.equil <- loga.equil_both
diagram(s_both, type = "loga.equil", add = TRUE)

leg_expr <- expr.species("O2", "gas", value = -46, log = TRUE)
legend("bottomleft", legend = leg_expr, bg = "white")
title("Species contributing to total solubility", font.main = 1)

This comparison illustrates how quasisolubility boundaries on classical predominance diagrams systematically underestimate mineral solubility. The discrepancy between quasisolubility and total solubility is minimized when one aqueous species strongly predominates over all others. However, in systems where multiple aqueous species contribute significantly to solubility, this mismatch becomes more pronounced. For such systems, solubility() provides more accurate estimates of isosolubility contours.

It is important to note that neither approach satisfies overall mass balance constraints in complex multicomponent systems. For applications requiring rigorous mass-balance solutions, more sophisticated techniques not currently implemented in CHNOSZ should be considered [@PEHB18; @DK21].

Advanced Uses

Having seen basic examples of the main features of CHNOSZ, here are some ideas for building more complex calculations and diagrams.

1. Use helper functions to create formatted labels for diagrams

Labeling diagrams is an important but often difficult step for creating publication-ready figures.

CHNOSZ provides the axis.label() and expr.species() functions to create formatted axis labels and chemical formulas. Let's revisit the r co2 dissolution example seen earlier and add two other gases (carbon monoxide and methane). This plot is similar to Figure 18 of @MSS13.

T <- seq(0, 350, 10)
CO2_logK <- subcrt(c("CO2", "CO2"), c("gas", "aq"), c(-1, 1), T = T)$out$logK
CO_logK <- subcrt(c("CO", "CO"), c("gas", "aq"), c(-1, 1), T = T)$out$logK
CH4_logK <- subcrt(c("CH4", "CH4"), c("gas", "aq"), c(-1, 1), T = T)$out$logK
logK <- data.frame(T, CO2_logK, CO_logK, CH4_logK)
matplot(logK[, 1], logK[, -1], type = "l", col = 1, lty = 1,
        xlab = axis.label("T"), ylab = axis.label("logK"))
text(80, -1.7, expr.species("CO2"))
text(240, -2.37, expr.species("CO"))
text(300, -2.57, expr.species("CH4"))
# Add legend
legend <- describe.property("P", "Psat")
legend("topright", legend = legend, bty = "n")

CHNOSZ has some other helper functions for labeling diagrams:

There is no single best approach to formatting text for legends, and sometimes it's easiest to use basic R functions:

2. Use retrieve() to search species by elements

Want to find all the Al complexes in the database? List them by calling retrieve() with the main element optionally followed by the ligand elements and state.

# List aqueous Al species in the default database
iaq <- retrieve("Al", ligands = c("F", "H", "O"), state = "aq")
# Print the first few rows and columns
info(iaq)[1:3, 1:5]
# Use the species index or name in subcrt()
subcrt(iaq[1:3], T = 25)
#subcrt(names(iaq)[1:3], T = 25)  # same as above

The result of retrieve() can also be used to add species to a diagram; see the example in #3 below.

basis(c("Al+3", "H2O", "F-", "H+", "e-"))
species(iaq)
species(names(iaq))  # same as above

3. Load optional data with add.OBIGT()

Perhaps you'd like to use data from older databases that have been superseded by later updates. The OBIGT vignette briefly summarizes the superseded data for SUPCRT92 and SLOP98 [@SLOP98]. Use add.OBIGT() to load these old data entries.

add.OBIGT("SLOP98")
iaq_all <- retrieve("Al", ligands = c("F", "H", "O"), state = "aq")
# Use difference between sets to get the added species
info(setdiff(iaq_all, iaq))

NOTE: Anhydrous species are commonly used for the revised Helgeson-Kirkham-Flowers (HKF) model. For example, @SSWS97 reported the properties of AlO~2~^−^, which is available in the optional SLOP98 database. This species is the anhydrous form of Al(OH)~4~^−^, which is present in the default database (see output above) using parameters from a more recent compilation for Al species [@TS01]. Because they are effectively the same species, only one form of this species is listed in the default database. Unless you have a specific reason to compare them, redundant species should not be considered together in an equilibrium calculation.

4. Use OBIGT() and reset() to restore the default database and settings

OBIGT() restores the default database without affecting other settings. reset() restores the default database and all other settings in CHNOSZ. These functions are useful for both interactive use and scripts that compare different versions of data or plots for different systems or conditions.

Let's put items #1--4 together to remake the corundum solubility plot using only species available in SLOP98. To do this, we use add.OBIGT() followed by retrieve() to gather the species indices for all Al species, then take only those species sourced from @SSWS97.

# Add superseded species from SLOP98
add.OBIGT("SLOP98")
# List all aqueous Al species
iaq <- retrieve("Al", state = "aq")
# Keep only species from Shock et al. (1997)
iaq <- iaq[grepl("SSWS97", info(iaq)$ref1)]
# Plot corundum solubility vs pH
basis(c("Al+3", "H2O", "H+", "e-"))
species("corundum")
s <- solubility(iaq, pH = c(2, 10), in.terms.of = "Al+3")
## Alternatively, we could use the species names
#s <- solubility(names(iaq), pH = c(2, 10), in.terms.of = "Al+3")
diagram(s, col = "green4", lwd = 2, ylim = c(-10, -2))
diagram(s, type = "loga.equil", add = TRUE)
legend("topright", c("25 °C", "1 bar"), bty = "n")
# Reset the database for subsequent calculations
reset()

5. Use basis() species to define the compositional space

A common question is: what are the basis species used for? The basis species define the compositional variables of a system. The composition of any species that can possibly be formed in that system can be represented by a linear combination of the basis species. The basis species may also be referred to as thermodynamic components, although a strict definition of the latter does not include charged species.

CHNOSZ requires that the number of basis species is equal to the number of different elements in the basis species (plus charge, if present). If you were studying the relative stability of F- and OH-complexes with Al, you might be tempted to try this basis definition:

basis(c("Al+3", "F-", "OH-"))

According to the message, we don't have enough basis species for the number of elements. Since the composition of hydroxide is water minus a proton (i.e., OH^−^ = r h2o − H^+^), we could try this instead:

basis(c("Al+3", "F-", "H+", "H2O"))

That's still not enough species. As is often the case, we need to include a basis species representing oxidation-reduction (redox) reactions, even if there are no redox reactions between the formed species. Here are two possible basis definitions that do not give an error.

# Use "oxygen" to get oxygen gas (for log fO2 diagrams)
basis(c("Al+3", "F-", "H+", "H2O", "oxygen"))
# Use "e-" to get aqueous electron (for Eh diagrams)
basis(c("Al+3", "F-", "H+", "H2O", "e-"))

6. Set activities of formed species() to define a quasisolubility contour

In order to make a diagram with stability fields for different species, CHNOSZ needs to know about the activities of all the species in the reaction. The activities of the basis species start with constant values as shown in the output above (logact column). Selected basis species can be assigned to plot axes (with a range of values) in affinity().

NOTE: logact is the logarithm of activity for aqueous species, solids, and liquids, or logarithm of fugacity for gases. All logarithms in CHNOSZ are common logarithms (R function: log10()) unless indicated otherwise.

How about the formed species in the system - that is, the species whose stability fields we want to visualize? We both list the species and set their activities using species(). The function defaults to activities of 10^-3^ (logact of -3) for aqueous species and unit activity (logact = 0) for minerals, gases, and liquids. Let's change this to activities of 10^-6^ for the formed species.

basis(c("Al+3", "F-", "H+", "H2O", "e-"))
iaq <- retrieve("Al", ligands = c("F", "H", "O"), state = "aq")
# Check that the data are from the same source
stopifnot(all(info(iaq)$ref1 == "TS01"))
species(iaq, -6)

NOTE: The value of logact passed to species() defines a quasisolubility contour, which is less than the total solubility to the extent that more than one aqueous species is abundant (see above).

7. When to use add = TRUE

There are two places where you might see add = TRUE. First, in species() to add species not already in the list. Without add = TRUE, any existing species are discarded. Second, in diagram() to add to an existing plot.

Let's put items #5--7 together to make a Pourbaix (Eh--pH) diagram for Al with two quasisolubility contours.

basis(c("Mn+2", "H+", "H2O", "e-"))
icr <- retrieve("Mn", ligands = c("H", "O"), state = "cr")
iaq <- retrieve("Mn", ligands = c("H", "O"), state = "aq")
# First layer, logact(aq) = -3
species(icr)
species(iaq, add = TRUE)
a <- affinity(pH = c(4, 14), Eh = c(-1, 1), T = 100)
# Use names = NA to avoid plotting labels twice
diagram(a, lty = 2, names = NA)
# Second layer, logact(aq) = -4
species(icr)
species(iaq, -4, add = TRUE)
a <- affinity(pH = c(4, 14), Eh = c(-1, 1), T = 100)
d <- diagram(a, bold = species()$state == "cr", add = TRUE)
# Add water stability limits
water.lines(d, lty = 3, col = 8)
# Add legends
legend("topright", legend = c(lT(100), lP("Psat")), bty = "n")
title = as.expression(quote(log~italic(a)*"Mn(aq)"))
legend("bottomleft", legend = c(-3, -4), lty = c(2, 1), title = title, bty = "n")

The shaded areas in the diagram represent water instability regions and are automatically added by diagram(). We use water.lines() here to plot the water stability limits with dotted lines.

8. Set grid resolution and constant T, P, or ionic strength in affinity()

After defining the basis species and formed species (and their constant activities), you have some choices about what variables to put on the plot, the grid resolution, and values for a few other variables. affinity() accepts one or more named arguments that specify ranges of variables as c(min, max) using the default grid resolution of 256, or ranges and a custom grid resolution as c(min, max, res). The number of such arguments is the dimensionality of the final plot. The grid resolution (res) defaults to 256 and can be different for each variable. The names of the variables can be the formulas of any of the basis species, or T, P, or IS for temperature, pressure, or ionic strength. These last three default to 25 °C, r Psat (the greater of 1 bar or the vapor-liquid saturation pressure for r h2o), and 0 mol/kg.

I often start with a low grid resolution to quickly iterate a calculation, then switch to a higher resolution when I'm satisfied with the result.

9. Use NaCl() to estimate ionic strength from NaCl concentration

Sodium chloride (NaCl) solutions are commonly used reference points for geochemical models. The NaCl() function provides a quick-and-dirty way to estimate ionic strength and activity of chloride (Cl^−^) for a given total amount of NaCl added to 1 kg of r h2o. These values can then be used in setting up a calculation that involves these variables.

This function does not use either the basis() or species() definitions. The following example runs a calculation for 0.8 mol/kg NaCl and given T, P, and pH. See demo('sum_S') for a more fully worked-out example that uses this code [based on a diagram in @SW02].

T <- 300
P <- 1000
pH <- 5
m_NaCl = 0.8
NaCl <- NaCl(m_NaCl = m_NaCl, T = T, P = P, pH = pH)
print(paste("mol NaCl added to 1 kg H2O:", m_NaCl))
print(paste("Ionic strength (mol/kg):", NaCl$IS))
print(paste("Chloride concentration (mol/kg):", NaCl$m_Clminus))

10. Use solubility() to draw total solubility contours

It's possible to to make a series of quasisolubility contours by setting activities of aqueous species (see the example for Mn above). A more refined solution that accounts for multiple aqueous species is to use solubility() to visualize total solubility contours. The basic idea is to first load the mineral(s) containing a single metal as the formed species(). Then, list the aqueous species with that metal as the first argument to solubility(). The remaining arguments to the function define the plot variables, just as in affinity() and mosaic().

Let's put items #8--10 together to make a set of diagrams for a single metal. The example here uses Fe; try changing it to Cu, Zn, Pb, Au, or something else!

CHNOSZ generates warning messages about being above the Cp limits for various iron oxyhydroxides. For the present calculation, the warnings are probably harmless because the predicted set of stable minerals (pyrite, pyrrhotite, magnetite, and hematite) is consistent with published diagrams.

NOTE: If you see warning messages like this, it's a good idea not to ignore them, but to consider whether you might be pushing the extrapolations of the Cp equation too far.

par(mfrow = c(1, 4))

# System definition
metal <- "Fe"
# The concentration to be used for the quasisolubility contour
logm_metal <- -6
T <- 150
P <- "Psat"
Stot <- 1e-3
wt_percent_NaCl <- 10
# Plot variables
res <- 300
pH <- c(0, 14, res)
O2 <- c(-55, -35, res)

# Work out NaCl molality from weight percent
# Convert to permil to get parts out of 1000 g (1 kg) of solution
wt_permil_NaCl <- wt_percent_NaCl * 10
# Divide by molecular weight to get moles of NaCl in 1000 g of solution
moles_NaCl <- wt_permil_NaCl / mass("NaCl")
# Subtract from 1000 g to get mass of H2O
grams_H2O <- 1000 - wt_permil_NaCl
# This gives the moles of NaCl added to 1 kg of H2O
m_NaCl <- 1000 * moles_NaCl / grams_H2O
# Now calculate ionic strength and molality of Cl-
NaCl_res <- NaCl(m_NaCl, T = T, P = P)
IS = NaCl_res$IS
m_Clminus = NaCl_res$m_Clminus

# Setup basis species
basis(c(metal, "H2S", "Cl-", "oxygen", "H2O", "H+"))
basis("H2S", log10(Stot))
basis("Cl-", log10(m_Clminus))
# Define basis species to swap through for mosaic calculation
bases <- c("H2S", "HS-", "HSO4-", "SO4-2")

# Get minerals and aqueous species
icr <- retrieve(metal, c("Cl", "S", "O", "H"), state = "cr")
iaq <- retrieve(metal, c("Cl", "S", "O", "H"), state = "aq")

# Make diagram for minerals
species(icr)
mcr <- mosaic(bases, pH = pH, O2 = O2, T = T, P = P, IS = IS)
diagram(mcr$A.species, bold = TRUE, fill = "#FAEBD788")
diagram(mcr$A.bases, lty = 2, col = 4, col.names = 4, italic = TRUE, add = TRUE)
title(paste(metal, "minerals"), font.main = 1)
# Add a legend
leg_list <- c(
  lTP(T, P),
  lNaCl(m_NaCl),
  lS(Stot)
)
leg_expr <- lex(leg_list)
legend("topright", legend = leg_expr, bty = "n")

# Make diagram for aqueous species
species(iaq)
maq <- mosaic(bases, pH = pH, O2 = O2, T = T, P = P, IS = IS)
diagram(maq$A.species, fill = "#F0F8FF88")
title(paste(metal, "aqueous species"), font.main = 1)

# Make diagram for minerals and aqueous species
species(icr)
species(iaq, logm_metal, add = TRUE)
m <- mosaic(bases, pH = pH, O2 = O2, T = T, P = P, IS = IS)
diagram(m$A.species, bold = species()$state == "cr")
diagram(m$A.bases, lty = 2, col = 4, col.names = 4, italic = TRUE, add = TRUE)
main = bquote("Quasisolubility contour for" ~ log ~ italic(m)*.(metal)*"(aq)" == .(logm_metal))
title(main, font.main = 1)

# Make total solubility contours
species(icr)
s <- solubility(iaq, bases = bases, pH = pH, O2 = O2, T = T, P = P, IS = IS, in.terms.of = metal)
levels <- seq(-12, 9, 3)
diagram(s, levels = levels, contour.method = "flattest", col = "green4")
title("Total solubility contours", font.main = 1)

NOTE: As explained above, total solubility is higher than quasisolubility.

There are further examples of Eh--pH composite diagrams (denoting quasisolubility) overlaid with total solubility contours in demo("Pourbaix").

11. Use convert() for common unit conversions

The convert() function offers several unit conversions. It implements reciprocal conversion between pairs of units, so only the destination unit needs to be specified. Some simple uses are to convert temperature, pressure, or energy values:

# Convert 100 degrees C to value in Kelvin
convert(100, "K")
# Convert 273.15 K to value in degrees C
convert(273.15, "C")
# Convert 1 bar to value in MPa
convert(1, "MPa")
# Convert 100 MPa to value in bar
convert(100, "bar")
# Convert 1 cal/mol to value in J/mol
convert(1, "J")
# Convert 10 J/mol to value in cal/mol
convert(10, "cal")

Another use of convert() is to convert the output of solubility() from log activity to ppm, ppb, log ppm, or log ppb. The following code continues from the example above:

sppm <- convert(s, "ppm")
levels <- c(1e-6, 1e-3, 1e0, 1e3)
diagram(sppm, levels = levels, col = "green4")

12. Use the transect mode of affinity() for synchronized variables

Specify 4 or more values for one or more variables (each variable should have the same number of values, or be set to a constant) to activate the transect mode of affinity(). The transect mode allows defining an arbitrary path in multidimensional space.

Here's a simple example:

basis("CHNOSe")
species(c("NO3-", "NO2-", "NH4+", "NH3"))
a <- affinity(pH = c(6, 8, 6, 8), Eh = c(0.5, 0.5, -0.5, -0.5))
# Print pH and Eh values used for calculation
a$vals
# Print affinity values calculated for each species
a$values

NOTE: affinity() returns dimensionless values of affinity (i.e., A/2.303RT).

Below we'll see how to convert these to energetic units.

13. Choose non-default balancing constraints in diagram()

diagram() looks for the first basis species that has non-zero coefficients in each of the formed species. This is called the conserved basis species in the documentation. The affinity values are then divided by the coefficients of the conserved basis species to give normalized affinities. This is how "balancing on a metal" is implemented.

Let's put items #11--13 together to calculate affinities of organic synthesis reactions in mixed seawater and hydrothermal fluid from the Rainbow vent field using speciation results from @SC10:

basis(c("CO2", "H2", "NH4+", "H2O", "H2S", "H+"))
# Constant activity of CH4 is a simplification of the model
species("CH4", -3)
# Add other organic species with lower activity
species(c("propanoic acid", "adenine", "leucine", "deoxyribose"), -6, add = TRUE)
# Read file with variable conditions
file <- system.file("extdata/misc/SC10_Rainbow.csv", package = "CHNOSZ")
# Use `check.names = FALSE` to preserve the `NH4+` column name
df <- read.csv(file, check.names = FALSE)
# Reverse the rows to get increasing T
df <- df[rev(rownames(df)), ]
# Define six variables on a transect
a <- affinity(T = df$T, CO2 = df$CO2, H2 = df$H2,
              `NH4+` = df$`NH4+`, H2S = df$H2S, pH = df$pH)
# Get T in Kelvin to make unit conversions
T <- convert(a$vals[[1]], "K")
# Convert dimensionless affinity to Gibbs energy in units of J/mol
# nb. this is the same as converting logK to ΔG of reaction
a$values <- lapply(a$values, convert, "G", T)
# Convert J/mol to cal/mol
a$values <- lapply(a$values, convert, "cal")
# Convert cal/mol to kcal/mol
a$values <- lapply(a$values, `*`, -0.001)
# Setup figure
par(mfrow = c(1, 3))
par(cex = 0.9)
# First plot: balance on C
ylab = quote(italic(A)~"(kcal/mol) per C")
diagram(a, ylim = c(-20, 40), ylab  = ylab, col = 1:5)
abline(h = 0, lty = 2, lwd = 2)
# Second plot: balance on H2
ylab = quote(italic(A)~"(kcal/mol) per H2")
diagram(a, balance = "H2", ylim = c(-15, 10), ylab = ylab, col = 1:5)
abline(h = 0, lty = 2, lwd = 2)
# Third plot: no balancing
ylab <- quote(italic(A)~"(kcal/mol) per species")
diagram(a, balance = 1, ylim = c(-100, 100), ylab = ylab, col = 1:5)
abline(h = 0, lty = 2, lwd = 2)

Although affinity() uses all of the variables in the transect, diagram() labels the x-axis with only the first variable (temperature). We obtain three plots:

  1. The reactions are balanced on the first basis species with non-zero coefficients, r co2. This is the same as normalizing on C, because no other basis species has C.
  2. Balance on a different species, r h2.
  3. No balancing constraint (balance = 1). This just shows the affinity of each reaction as given (that is, per mole of formed species), which is how the results were presented by @SC10.

14. Calculate adjusted and non-standard Gibbs energy with subcrt()

In normal use, subcrt() returns standard molal properties (G, H, S, Cp, and V) for species in their standard states, defined as unit activity or fugacity. Two deviations from this default setting are possible: non-standard properties for specified activity, and adjusted properties for activity coefficients.

First let's look at how adjusted properties depend on activity coefficients. This example uses a particular nonideality model based on @Alb03:

# Set the nonideality model
old_ni <- nonideal("Alberty")
# Calculate standard and adjusted Gibbs energy at 25 °C
species <- c("MgH2ATP", "MgHATP-", "MgATP-2")
subcrt(species, T = 25, IS = c(0, 0.25), property = "G")$out
# Restore the original nonideality model
nonideal(old_ni)

Notice how logarithms of the activity coefficients (loggam) are more negative for the higher-charged species. The activity coefficients have a stabilizing effect: adjusted Gibbs energies (at I > 0) are less than the standard Gibbs energies (at I = 0).

Now let's change the activities to get non-standard properties.

species <- c("Mg+2", "ATP-4", "MgATP-2")
coeffs <- c(-1, -1, 1)
T <- c(25, 50, 100)
# Drop some columns for more compact output
idrop <- c(2:4, 6:9)
# Unit activity: affinity is opposite of standard Gibbs energy
subcrt(species, coeffs, T = T, logact = c(0, 0, 0))$out[, -idrop]
# Non-unit activity: affinity is opposite of non-standard Gibbs energy
subcrt(species, coeffs, T = T, logact = c(-5, -4, -3))$out[, -idrop]

NOTE:

The first call above specifies unit activities of all the species in the reaction.

The second call specifies non-unit activities of the species.

15. Calculate non-standard Gibbs energy with affinity()

And swap basis species, remove formed species, and label reactions

Above we used subcrt() to calculate non-standard Gibbs energy, but doing it with affinity() can be more convenient for making diagrams. This example plots non-standard Gibbs energies for hydrogenotrophic methanogenesis, acetate oxidation, and acetate oxidation, and is based on Fig. 4b of @MDS_13. We combine some of the features described above with new ones for swapping basis species and removing formed species:

# Format reactions with describe.reaction()
basis(c("CO2", "H2", "H2O", "H+"))
reactions <- c(
  # hydrogenotrophic methanogenesis
  describe.reaction(subcrt("CH4", 1)$reaction),
  # acetate oxidation
  describe.reaction(subcrt("acetate", -1)$reaction),
  # acetoclastic methanogenesis
  describe.reaction(subcrt(c("acetate", "CH4"), c(-1, 1))$reaction)
)
# Add space before "CO2" to avoid getting cut off to "O2" by the plot limits
reactions[[1]][2][[1]][[2]][[2]][[2]][[3]] <- "       CO"

# System 1: one C species in basis; two C species in formation reactions
basis(c("CO2", "H2", "H2O", "H+"))
# Change CO2 and H2 to gas
basis(c("CO2", "H2"), "gas")
# Set H2 (log fugacity) and pH
basis(c("H2", "pH"), c(-3.92, 7.3))
# Write reactions to form methane (gas) and acetate (aq) at given activity and fugacity
species(c("methane", "acetate"), c(-0.18, -3.4))
# Calculate affinities of formation reactions with varying log fugacity of CO2
a1 <- affinity(CO2 = c(-3, 3), T = 55, P = 50)

# System 2: two C species in basis; three C species in formation reactions
# Swap H2 for CH4 and set fugacity
swap.basis("H2", "CH4")
basis("CH4", "gas")
basis("CH4", -0.18)
# Remove methane as formed species - we only want acetate now
species(1, delete = TRUE)
a2 <- affinity(CO2 = c(-3, 3), T = 55, P = 50)

# Convert from dimensionless affinity to Gibbs energy in kJ/mol
TK <- convert(55, "K")
a1$values[[1]] <- convert(a1$values[[1]], "G", T = TK) / 1000
# Negate to reverse reaction (acetate as a reactant)
a1$values[[2]] <- -convert(a1$values[[2]], "G", T = TK) / 1000
a2$values[[1]] <- -convert(a2$values[[1]], "G", T = TK) / 1000

# Make diagram without balancing constraint (no normalization by C)
diagram(a1, balance = 1, lty = 1, lwd = 2, col = c(4, 2), names = reactions[1:2], ylab = axis.label("DG", prefix = "k"), srt = c(-13.5, 26))
diagram(a2, balance = 1, lwd = 2, col = 3, names = reactions[3], srt = 14, add = TRUE)
abline(h = 0, lty = 2, col = 8)

After running the code to make the diagram, we can print the formation reactions for each of the species. This shows how the two acetate reactions (acetate oxidation and acetoclastic methanogenesis) are implemented by swapping r h2 and CH~4~ in the basis species.

a1$species
a2$species

NOTE: This example uses balance = 1 in the call to diagram() to prevent normalizating the reactions by a balancing constraint (i.e., normalization by number of C) in order to reproduce the calculations of @MDS_13. In most other cases (especially for making relative stability diagrams), this argument should not be used.

16. Extract results from the output of diagram()

Sometimes it's useful to make further computations on the results of a diagram() call. For example, a system might dominated by a few stable species, but you'd rather visualize the relative stabilities of less stable (i.e., metastable) species. Here we do this for all the aqueous S species in the OBIGT database, accessed using retrieve(). We use plot.it = FALSE to suppress the first plot (which would look like the Eh--pH diagram above for S), but save the output with d <- diagram() to access the identified stable species in d$predominant. After removing these stable species from the system, we recalculate affinities for the remaining metastable species and make a diagram for them.

basis("CHNOSe")
iaq <- retrieve("S", c("H", "O"), state = "aq")
species(iaq)
a <- affinity(pH = c(0, 14), Eh = c(-0.8, 1.2))
# Save results but don't plot the first diagram
d <- diagram(a, plot.it = FALSE)
# Remove the stable species
istable <- unique(as.numeric(d$predominant))
species(istable, delete = TRUE)
# Make a diagram for the metastable species
a <- affinity(pH = c(0, 14), Eh = c(-0.8, 1.2))
d <- diagram(a, col = 4, col.names = 4, italic = TRUE)

Other possibly useful parts of the diagram() output are:

NOTE: If diagram() was passed the output of equilibrate() or solubility(), then its output contains logarithms of activities instead of dimensionless affinities.

17. Writing chemical formulas and counting and summing elements with makeup()

makeup() is used internally by some functions in CHNOSZ but is also available for the user. It counts the elements (and charge, if present) in a chemical formula(s) formatted as a character object. If supplied with a species index in the OBIGT database, it uses their formulas. Setting sum = TRUE in the function call instructs makeup() to sum the elemental compositions.

The data frame returned by makeup() can be used in as.chemical.formula() to generate the character string for a formula.

# Element count of a species in the database
iCli <- info("carrollite")
makeup(iCli)
# Sum the elements of formulas supplied as character strings
summed_elements <- makeup(c("CO2", "CH4"), sum = TRUE)
# Use the result to write a new chemical formula
as.chemical.formula(summed_elements)

18: Accessing and changing settings with thermo()

All the data used by CHNOSZ - from the thermodynamic data in OBIGT to the basis species defined by the user - are stored in an object named thermo in the package environment. Sometimes it's useful to peek inside CHNOSZ's memory banks, or more rarely to directly modify them. The thermo() function returns the current value of this object and can also update it. Here we display the first level structure of thermo, then show the structure of the database (thermo()$OBIGT) in more detail.

str(thermo(), max.level = 1)
str(thermo()$OBIGT)

Call thermo() with a named argument to assign a value. In this case we change the temperature units for subcrt():

# This has the same effect as T.units("K")
thermo("opt$T.units" = "K")
# Return to default
thermo("opt$T.units" = "C")

See the help page for the Berman() function for a practical example of adding thermodynamic data with the @Ber88 model, which are stored outside of the OBIGT database.

Interlude: From Activity to Molality

The default model for activity coefficients uses the extended Debye--Hückel equation with parameters for NaCl-dominated solutions from @HKF81. The species-species parameters are charge and (for the default Bdot model) ion-size parameters used in the HCh program [@SB99]. By contrast, the bgamma model uses an extended term parameter that is derived from data of @Hel69, @HKF81, and high-P extrapolations of @MSS13. The Alberty model uses parameters listed in Chapter 3 of @Alb03, which are applicable to relatively low temperatures. Choose from these models with nonideal().

NOTE: By default, H^+^ is assumed to have unit activity coefficient for any ionic strength. Enable calculations of activity coefficients for H^+^ by running thermo("opt$ideal.H" = FALSE).

Invoke calculations of activity coefficients by setting the IS argument in subcrt(), affinity(), mosaic(), or solubility(). This has the effect of transforming activity to molality in the CHNOSZ workflow. A set of calculations demonstrating this tranformation is in test-logmolality.R in the package test directory. Key variables affected by this transformation are listed here:

Because function arguments have static names, we're stuck with logact even when it means log molality. However, diagram() automatically changes labels from "log a" to "log m" when run on the output of affinity() with a non-NULL value for IS.

Buffers

Buffers are assemblages of one or more species whose presence constrains the chemical activities (or fugacities) of basis species in a thermodynamic system. Buffers play a critical role in geochemical modeling by providing realistic constraints on system variables like pH and oxidation state. This section explores the implementation and application of buffers in CHNOSZ.

1. Defining buffers with mod.buffer()

The mod.buffer() function defines or modifies buffers by specifying the species that constitute the buffer and their activities:

# View available buffers
thermo()$buffer
# Define a new buffer or modify an existing one
mod.buffer("PPM", c("pyrite", "pyrrhotite", "magnetite"), "cr", 0)
# Buffer made of one species (acetic acid with log activity = -10)
mod.buffer("AC", "acetic acid", "aq", -10)

2. Retrieving buffered activities with affinity(return.buffer = TRUE)

Use basis() to associate one or more basis species with a buffer (all the elements in the buffer must be present in the basis species). Then use affinity(return.buffer = TRUE) to calculate and retrieve the buffered activities or fugacities of basis species:

# Specify basis species
basis(c("FeCHNOS"))
# Associate O2 with the PPM buffer
basis("O2", "PPM")
# Calculate and retrieve the buffered fugacity of O2
a <- affinity(T = 200, P = 2000, return.buffer = TRUE)
# Access the buffered O2 fugacity (log fO2)
log_fO2 <- a$O2  # -44.28
# Calculate buffered fugacities across temperature range
a <- affinity(T = c(200, 400, 11), P = 2000, return.buffer = TRUE)

3. Working with multiple buffered species (e.g., r h2s and r o2 in PPM)

Some buffers constrain multiple basis species simultaneously:

# Setup basis species
basis(c("FeS2", "H2S", "O2", "H2O"), c(0, -10, -50, 0))
# Associate both H2S and O2 with the PPM buffer
basis(c("H2S", "O2"), c("PPM", "PPM"))
# Retrieve values for both buffered species
buffer_values <- affinity(T = 300, P = 2000, return.buffer = TRUE)
log_aH2S <- buffer_values$H2S  #  -2.57
log_fO2 <- buffer_values$O2    # -37.16

4. Using diagram(type = ) to display buffered activities

The diagram() function with the type argument can solve for and display activities of basis species. This example reproduces part of Fig. 6 in @SS95:

# Setup a system in terms of gases and liquid water
basis(c("hydrogen", "carbon dioxide", "nitrogen", "water"))
# Use 10 bar of CO2 and 1 bar of other gases (default)
basis("CO2", 1)
# Load aqueous species with given log activity
species(c("HCN", "formaldehyde"), c(-6, -6))
# Calculate affinities to form aqueous species from basis species
a <- affinity(T = c(0, 350), P = 300)
# Create diagram showing H2 activity where affinity = 0
d <- diagram(a, type = "H2", lty = c(2, 3))
legend("bottomright", c("HCN", "formaldehyde"), lty = c(2, 3))

See demo("buffer") for a fully worked-out example based on the figure in @SS95.

NOTE: This feature works independently from buffers defined in thermo()$buffer, but produces equivalent results for certain systems; see test-diagram.R in the package test directory.

5. Using fr o2 Buffers in downstream calculations

Redox buffers like QFM, HM, and PPM can be used as inputs for subsequent calculations:

# Setup system for gold solubility calculation
basis(c("Au", "Fe", "H2S", "H2O", "oxygen", "H+"))
# Apply PPM buffer for fO2 and H2S
basis("O2", "PPM")
basis("H2S", "PPM")
# Calculate gold solubility using the buffered values
species("Au")
iaq <- info(c("Au(HS)2-", "AuHS", "AuOH"))
s <- solubility(iaq, pH = c(2, 8), T = 300, P = 1000)
# Create solubility diagram
diagram(s, ylim = c(-10, -5), col = "green4", lwd = 2)
col <- c("#ED4037", "#F58645", "#0F9DE2")  # Au(HS)2-, AuHS, AuOH
diagram(s, type = "loga.equil", add = TRUE, col = col)

6. Using buffer calculations in transects with affinity()

Buffers can be used in transect calculations to model changes across gradients. An interesting application is to add a delta to values obtained with return.buffer = TRUE:

# Set basis species
basis(c("Fe", "SiO2", "CO3-2", "H2O", "oxygen", "H+"))
# Calculate log fO2 in QFM buffer across temperature range
basis("O2", "QFM")
T <- seq(600, 1000, 100)
buf <- affinity(T = T, P = 5000, return.buffer = TRUE)
# Use buffered fO2 values in downstream calculations
species(c("CH4", "CO2", "HCO3-", "CO3-2"))
# Set values of pH for transect
pH <- seq(3.8, 4.3, length.out = length(T))
# Adding 2 log units below QFM buffer
a <- affinity(T = T, O2 = buf$O2 - 2, pH = pH, P = 5000)

7. Calculating the neutral pH of water

Neutral pH at various temperatures and pressures can be determined from the dissociation constant of water:

# Calculate neutral pH at 300°C and 1000 bar
T <- 300
P <- 1000
neutral_pH <- -subcrt(c("H2O", "H+", "OH-"), c(-1, 1, 1), T = T, P = P)$out$logK/2

8. Working with mineral pH buffers

Mineral assemblages like K-feldspar--muscovite--quartz (KMQ) can buffer pH:

# Define the KMQ buffer
mod.buffer("KMQ", c("quartz", "muscovite", "K-feldspar"), "cr", 0)
# Setup the system
basis(c("Al2O3", "quartz", "K+", "H2O", "oxygen", "H+"))
# Set K+ molality for KCl solution
basis("K+", log10(0.5))
# Associate H+ with the KMQ buffer
basis("H+", "KMQ")
# Calculate buffered pH
pH_KMQ <- -affinity(T = 300, P = 1000, return.buffer = TRUE)$`H+`

Comprehensive example: Ore formation environments

Mineral buffers constrain both pH and redox state in geological systems, which in turn control metal solubility and transport in ore-forming fluids. This example illustrates how different buffers affect gold solubility in hydrothermal systems.

# Setup the system
basis(c("Au", "Al2O3", "quartz", "Fe", "K+", "Cl-", "H2S", "H2O", "oxygen", "H+"))
# Set log activities for 0.5 m NaCl + 0.1 m KCl solution (assuming ideality for now)
basis("Cl-", log10(0.6))
basis("K+", log10(0.1))
# Set 0.01 m H2S
basis("H2S", -2)
# Setup the KMQ pH buffer
mod.buffer("KMQ", c("quartz", "muscovite", "K-feldspar"), "cr", 0)
basis("H+", "KMQ")
# Define colors for gold species
col <- c("#ED4037", "#F58645", "#0F9DE2", "#22CC88")  # Au(HS)2-, AuHS, AuOH, AuCl2-
# Create temperature transect with HM and PPM redox buffers
temps <- seq(200, 500, 10)
P <- 1000
# Initialize plot layout
par(mfrow = c(1, 4), mar = c(4, 4, 3, 1), font.main = 1, cex.main = 1.1)

# 1. Compare gold species distribution under HM buffer
basis("O2", "HM")
species("Au")
iaq <- info(c("Au(HS)2-", "AuHS", "AuOH", "AuCl2-"))
s_HM <- solubility(iaq, T = temps, P = P)
diagram(s_HM, xlab = "Temperature (°C)", ylab = "log activity", ylim = c(-10, -4),
        col = "green4", lwd = 2, main = "Solubility vs T (KMQ: pH, HM: fO2)")
diagram(s_HM, type = "loga.equil", col = col, add = TRUE)

# 2. Compare gold species distribution under PPM buffer
basis("O2", "PPM")
basis("H2S", "PPM")
s_PPM <- solubility(iaq, T = temps, P = P)
diagram(s_PPM, xlab = "Temperature (°C)", ylab = "log activity", ylim = c(-10, -4),
        col = "green4", lwd = 2, main = "Solubility vs T (KMQ: pH, PPM: fO2 and aH2S)")
diagram(s_PPM, type = "loga.equil", col = col, add = TRUE)

# 3. Gold solubility as function of pH at 350°C under PPM buffer
# Remove KMQ buffer
basis("H+", 0)
T_fixed <- 350
# Calculate solubility across pH range
s_pH <- solubility(iaq, pH = c(2, 8), T = T_fixed, P = P)
diagram(s_pH, xlab = "pH", ylab = "log activity", ylim = c(-10, -4),
        col = "green4", lwd = 2, main = "Solubility vs pH (350 °C, PPM: fO2 and aH2S)")
diagram(s_pH, type = "loga.equil", col = col, add = TRUE)
# Get neutral pH at this temperature
neutral_pH <- -subcrt(c("H2O", "H+", "OH-"), c(-1, 1, 1), T = T_fixed, P = P)$out$logK/2
# Add neutral pH line
abline(v = neutral_pH, lty = 2)
text(neutral_pH + 0.2, -5.5, "neutral pH", srt = 90, cex = 0.8)
# Add KMQ pH buffer line
basis("H+", "KMQ")
pH_KMQ <- -affinity(T = T_fixed, P = P, return.buffer = TRUE)$`H+`
abline(v = pH_KMQ, lty = 3, col = 4)
text(pH_KMQ - 0.2, -5.5, "KMQ", srt = 90, col = 4, cex = 0.8)

# 4. log fO2-pH diagram with dominant gold species at 350°C
# Remove KMQ pH buffer
basis("H+", 0)
# Remove PPM fO2 buffer
basis("O2", 0)
# Use constant aH2S from PPM buffer
a <- affinity(pH = c(2, 8), T = T, P = P, return.buffer = TRUE)
log_aH2S <- unique(a$H2S)
basis("H2S", log_aH2S)
# Load aqueous species for relative stability calculation
species(iaq)
# Define basis species to swap through for mosaic calculation
bases <- c("H2S", "HS-", "SO4-2", "HSO4-")
m <- mosaic(bases = bases, pH = c(2, 8), O2 = c(-32, -24), T = T_fixed, P = P)
a <- m$A.species
fill <- adjustcolor(col, alpha.f = 0.5)
d <- diagram(a, fill = fill, main = "Predominance fO2 vs pH (350°C, PPM: aH2S)")
water.lines(d, lty = 3)
# Add mineral buffer lines
# PPM buffer line
basis("O2", "PPM")
O2_PPM <- affinity(T = T_fixed, P = P, return.buffer = TRUE)$O2
abline(h = O2_PPM, lty = 2, col = "darkred")
text(2.5, O2_PPM, "PPM", adj = c(0, -0.5), col = "darkred", cex = 0.8)
# HM buffer line
basis("O2", "HM")
O2_HM <- affinity(T = T_fixed, P = P, return.buffer = TRUE)$O2
abline(h = O2_HM, lty = 2, col = "darkred")
text(2.5, O2_HM, "HM", adj = c(0, -0.5), col = "darkred", cex = 0.8)
# Add KMQ pH buffer line and neutral pH
abline(v = neutral_pH, lty = 2)
text(neutral_pH + 0.2, -28, "neutral pH", srt = 90, cex = 0.8)
abline(v = pH_KMQ, lty = 2, col = 4)
text(pH_KMQ - 0.2, -28, "KMQ", srt = 90, col = 4, cex = 0.8)

The diagrams show:

  1. Temperature dependence of gold species distribution under the hematite-magnetite (HM) buffer [cf. Fig. 2A in @WBM09]
  2. Temperature dependence under the pyrite-pyrrhotite-magnetite (PPM) buffer [cf. Fig. 2B in @WBM09]
  3. pH dependence under PPM at fixed temperature, with neutral pH and KMQ buffer lines [cf. Fig. 7 in @AZ01]
  4. Species predominance in log fr o2--pH space with redox and pH buffer lines

Note the following limitation:

See also:

Proteins

Proteins in CHNOSZ are treated differently from other chemical species. Instead of direct thermodynamic data, CHNOSZ uses amino acid group additivity to calculate the thermodynamic properties of proteins. This approach requires knowledge of the amino acid composition of each protein.

1. Identifying proteins

In CHNOSZ, protein identifiers have a specific format that combines the protein name and organism with an underscore separator, modeled after UniProt names (e.g., LYSC_CHICK for chicken lysozyme C). This naming convention uniquely identifies each protein in the database.

# Search by name in thermo()$protein
ip1 <- pinfo("LYSC_CHICK")       # Using protein_organism format
ip2 <- pinfo("LYSC", "CHICK")    # Using separate arguments
# Search for the same protein in different organisms
ip3 <- pinfo("MYG", c("HORSE", "PHYCA"))

2. Adding proteins from FASTA or CSV files

CHNOSZ has a small built-in database of amino acid compositions for selected proteins, but you can expand this by adding proteins from FASTA or CSV files.

# Reading amino acid compositions from a CSV file
file <- system.file("extdata/protein/POLG.csv", package = "CHNOSZ")
aa <- read.csv(file)
# Add the proteins to CHNOSZ and get their indices
iprotein <- add.protein(aa)

# For FASTA files, use the canprot package
fasta_file <- system.file("extdata/protein/rubisco.fasta", package = "CHNOSZ")
aa <- canprot::read_fasta(fasta_file)
iprotein <- add.protein(aa)

The add.protein() function adds the amino acid compositions to the database and returns the row indices of the added proteins.

3. Calculating protein properties

Once proteins are added to the database, you can calculate various properties such as length, formula, and thermodynamic properties.

Protein length and formula

# Get protein length (number of amino acids)
pl <- protein.length("LYSC_CHICK")
# Get chemical formula
pf <- protein.formula("LYSC_CHICK")
# Display results
list(length = pl, formula = pf)

Carbon oxidation state

The average oxidation state of carbon, calculated with ZC(), provides insight into the redox state of protein sequences:

# Calculate ZC for a protein
ZC_value <- ZC(protein.formula("LYSC_CHICK"))
# For multiple proteins
proteins <- c("LYSC_CHICK", "MYG_HORSE", "RNAS1_BOVIN")
ZC_values <- ZC(protein.formula(proteins))

4. Thermodynamic calculations for proteins

CHNOSZ provides several functions for calculating thermodynamic properties of proteins.

Standard thermodynamic properties

Calculate standard thermodynamic properties of non-ionized proteins using subcrt():

# Properties of non-ionized protein
subcrt("LYSC_CHICK")$out[[1]][1:6, ]

Ionization effects

For more accurate calculations, especially in biological systems, protein ionization must be considered [@DLH06]. CHNOSZ handles this through the ionize.aa() function, which allows specifying the temperature, pressure and pH conditions:

# Calculate ionization properties
aa <- pinfo(pinfo("LYSC_CHICK"))
charge <- ionize.aa(aa, pH = c(4, 7, 9))
# Calculate heat capacity of ionization
Cp_ionization <- ionize.aa(aa, property = "Cp", pH = 7, T = c(25, 100))

5. Setting up a chemical system with proteins

To include proteins in a chemical system, first define the basis species, then add proteins to the system:

# Define the basis species with H+
basis("CHNOS+")
# Add proteins to the system
species(c("LYSC_CHICK", "MYG_HORSE", "RNAS1_BOVIN"))

6. Calculating affinities and equilibrium distributions

Affinities of formation reactions

The affinity() function accounts for ionization effects when calculating affinities of formation reactions:

# Calculate affinity as a function of pH
basis("CHNOS+")
species(c("LYSC_CHICK", "MYG_HORSE", "RNAS1_BOVIN"))
a1 <- affinity(pH = c(0, 14))

For performance optimization, use protein indices directly with the iprotein argument to affinity(). This doesn't require proteins to be added with species():

species(delete = TRUE)
ip <- pinfo(c("LYSC_CHICK", "MYG_HORSE", "RNAS1_BOVIN"))
# Set logarithm of activity with loga.protein
a2 <- affinity(pH = c(0, 14), iprotein = ip, loga.protein = -3)

# Check that both methods produce equivalent results
for(i in 1:3) stopifnot(all.equal(a1$values[[i]], a2$values[[i]]))

Equilibrium distributions

Calculate the relative abundance of proteins in metastable equilibrium using equilibrate(). This example uses averaged amino acid compositions of protein sequences in metagenomes from a temperature and chemical gradient in a hot spring [@DS11]:

# Calculate equilibrium distribution as a function of Eh
basis("CHNOSe")
proteins <- paste("overall", paste0("bison", c("N", "S", "R", "Q", "P")), sep = "_")
ip <- pinfo(proteins)
a <- affinity(Eh = c(-0.35, -0.15), iprotein = ip)
# Normalize by protein length to get residue-equivalent distribution
# Set loga.balance to distribute proteins with total activity of residues equal to 1
e <- equilibrate(a, normalize = TRUE, loga.balance = 0)
col <- c("darkred", "red", "darkgray", "blue", "darkblue")
diagram(e, ylim = c(-4, -2.5), col = col, lwd = 2)
legend("bottomleft", c("High-T reducing", NA, NA, NA, "Low-T oxidizing"),
  lty = 1:5, col = col, title = "Environmental Conditions", inset = c(0.1, 0))

The normalize = TRUE option is important for proteins because their large size leads to extreme separation of activities in metastable equilibrium. Normalizing by protein length (calculating per-residue equivalents) compresses the range of relative abundances to be more experimentally realistic.

Scaling protein abundances

Use unitize() to scale abundances of proteins so that number of residues sums to unity:

# Sample protein data from YeastGFP study
protein <- c("YDL195W", "YHR098C", "YLR208W", "YNL049C", "YPL085W")
abundance <- c(1840, 12200, 21400, 1720, 358)
# Find protein indices in CHNOSZ database
ip <- match(protein, thermo()$protein$protein)
# Get protein lengths
pl <- protein.length(ip)
# Scale protein abundance so total abundance of residues is unity
scaled_abundance <- 10^unitize(log10(abundance), pl)
# Check that sum for residues is unity
stopifnot(all.equal(sum(scaled_abundance * pl), 1))

Unit total activity of residues is set by equilibrate(loga.balance = 0), allowing comparison between experimental and predicted abundances:

basis("CHNOSe")
# Make a guess for Eh
basis("Eh", -0.5)
a <- affinity(iprotein = ip)
e <- equilibrate(a, normalize = TRUE, loga.balance = 0)
# Check for unit total abundance of residues
predicted_abundance <- 10^unlist(e$loga.equil)
stopifnot(all.equal(sum(predicted_abundance * pl), 1))
plot(log10(scaled_abundance), log10(predicted_abundance), pch = 19)
# Show 1:1 line
lims <- range(par("usr"))
lines(c(lims[1], lims[2]), c(lims[1], lims[2]), lty = 2)
MAE <- mean(abs(log10(scaled_abundance) - log10(predicted_abundance)))
legend("topleft", paste("MAE =", round(MAE, 2)), bty = "n")

7. Additional protein analysis

The canprot package provides a different interface for calculating r zc and other chemical analyses of proteins from their amino acid composition:

# Load canprot package
library(canprot)
# Get amino acid compositions
ip <- pinfo(c("LYSC_CHICK", "MYG_HORSE", "RNAS1_BOVIN"))
aa <- pinfo(ip)
# canprot has Zc(); CHNOSZ has ZC()
Zc(aa)
# Stoichiometric oxygen and water content
nO2(aa)
nH2O(aa)
# Isoelectric point and GRAVY
pI(aa)
GRAVY(aa)

Comprehensive example: Parameter optimization to fit experimental protein abundances

Let's analyze the relative abundances of proteins from the ER-to-Golgi location in S. cerevisiae (yeast) and compare theoretical predictions with experimental measurements from the YeastGFP study [@GHB_03]:

# Protein data from YeastGFP study
protein <- c("YDL195W", "YHR098C", "YLR208W", "YNL049C", "YPL085W")
abundance <- c(1840, 12200, 21400, 1720, 358)
# Find protein indices in CHNOSZ database
ip <- match(protein, thermo()$protein$protein)
# Calculate protein lengths
pl <- protein.length(ip)
# Scale experimental abundances to unit total abundance of residues
scaled_logabundance <- unitize(log10(abundance), pl)

# Assign colors from low to high oxidation state (red to blue)
col_in <- c("darkred", "red", "darkgray", "blue", "darkblue")
aa <- pinfo(ip)
Zc_values <- canprot::Zc(aa)
col <- col_in[rank(Zc_values)]

# Setup chemical system with H+ and e-
basis("CHNOSe")
# Calculate affinities as a function of redox potential
a <- affinity(Eh = c(-0.6, 0), iprotein = ip)

# Calculate equilibrium distributions without normalization
e <- equilibrate(a, loga.balance = 0)
# Plot results
par(mfrow = c(1, 4))
diagram(e, ylim = c(-5, -2), col = col, lwd = 2, main = "Without normalization")

# With normalization
e_norm <- equilibrate(a, normalize = TRUE, loga.balance = 0)
diagram(e_norm, ylim = c(-5, -2.5), col = col, lwd = 2, main = "With normalization")

# Calculate and plot mean absolute error as a function of Eh
predicted_logabundances <- sapply(e_norm$loga.equil, c)
MAE <- apply(abs(t(predicted_logabundances) - scaled_logabundance), 2, mean)
par(xaxs = "r", yaxs = "r")
plot(a$vals$Eh, MAE)
imin <- which.min(MAE)
abline(v = a$vals$Eh[imin], lty = 2)
title("Optimized model Eh")

# Show calculated and experimental abundances
optimized_logabundance <- predicted_logabundances[imin, ]
plot(scaled_logabundance, optimized_logabundance, pch = 19, col = col)
# Show 1:1 line
lims <- range(par("usr"))
lines(c(lims[1], lims[2]), c(lims[1], lims[2]), lty = 2)
title("Predicted vs experimental abundance")
# Add legends
legend("topleft", paste("MAE =", round(MAE[imin], 2)), bty = "n")
legend("bottomright", c("High Zc", NA, NA, NA, "Low Zc"), pch = 19, col = rev(col_in))

The diagrams show:

  1. Theoretical abundances of proteins calculated with normalize = FALSE are highly divergent.
  2. The normalization step compresses the range of abundances, making the comparison with experimental data more meaningful.
  3. Mean absolute error between logarithms of experimental and predicted abundances, both scaled to unit total abundance of residues. The MAE minimizes at the Eh indicated by the vertical line.
  4. Comparison of experimental and optimized theoretical relative abundances of proteins (with normalization).

The correspondence between theoretical predictions and experimental measurements depends on normalization of protein formulas and optimizing physicochemical parameters. The metastable equilibrium model provides a theoretical framework for predicting how chemical conditions influence relative protein abundances.

Environmental controls on protein evolution: A case study with CRISPR-Cas systems

Evolution doesn't happen in a vacuum – organisms and their molecular machinery must cope with changing environmental conditions over geological time. Just as geochemists use relative stability diagrams to predict which minerals are stable under different physicochemical conditions, we can apply similar thermodynamic principles to understand protein evolution. The central hypothesis is that environmental variables, such as pH and redox conditions (Eh), have shaped the amino acid compositions of proteins throughout Earth's history. This approach becomes especially powerful when comparing not individual proteins, but entire evolutionary lineages.

CRISPR-Cas systems are molecular scissors that bacteria and archaea use as immune systems against viruses, and which biotechnologists have adapted for precise gene editing. These systems evolved into six major types (I--VI), each representing an evolutionary branch with multiple representatives across different genomes [@MWI_20].

Rather than comparing individual proteins, we can ask: which types of CRISPR-Cas systems would be most thermodynamically stable under different environmental conditions? The following diagram introduces the players by showing the carbon oxidation state (r zc) and size of the effector modules; the effector module combines with CRISPR RNA (crRNA) to form the effector complex that does the actual cutting work on a target DNA sequence. Class 1 systems tend to have larger effector modules made up of multiple proteins, which were combined with the sum_aa() function from canprot before calculating r zc.

# Read data table
file <- system.file("extdata/protein/Cas/Cas_uniprot.csv", package = "CHNOSZ")
dat <- read.csv(file)
# Use UniProt ID as the file name
ID <- dat$UniProt
# In case UniProt ID is missing, use alternate ID
ID[ID == ""] <- dat$Protein[ID == ""]
# Store ID in data frame
dat$ID <- ID
# Remove missing IDs
dat <- subset(dat, ID != "")
# Keep proteins in effector complexes - the functional units that cut DNA
dat <- subset(dat, Effector)

# Read amino acid compositions
aafile <- system.file("extdata/protein/Cas/Cas_aa.csv", package = "CHNOSZ")
aa <- read.csv(aafile)

# Loop over evolutionary subtypes (I-A, I-B etc.)
subtypes <- unique(dat$Subtype)
effector_aa_list <- lapply(subtypes, function(subtype) {
  # Get the IDs for this evolutionary subtype
  idat <- dat$Subtype == subtype
  ID <- dat$ID[idat]
  # Combine amino acid compositions of all effector proteins in this subtype
  iaa <- aa$ref %in% ID
  all_aa <- aa[iaa, ]
  summed_aa <- canprot::sum_aa(all_aa)
  # Store gene names and IDs for reference
  summed_aa$protein <- paste(all_aa$protein, collapse = ",")
  summed_aa$ref <- paste(all_aa$ref, collapse = ",")
  # Label with the evolutionary subtype
  summed_aa$abbrv <- subtype
  # Return the combined amino acid composition for this evolutionary branch
  summed_aa
})
# Create data frame of amino acid compositions for each evolutionary branch
effector_aa <- do.call(rbind, effector_aa_list)

# Identify the six major evolutionary types (I to VI)
type_names <- c("I", "II", "III", "IV", "V", "VI")
# Map subtypes to their parent types
subtype_type <- sapply(strsplit(effector_aa$abbrv, "-"), "[", 1)
# Assign evolutionary classes (1 or 2) based on system architecture
class <- ifelse(subtype_type %in% c("I", "III", "IV"), 1, 2)

# Color scheme to distinguish evolutionary classes
col1 <- hcl.colors(5, "Peach")[1:3]
col2 <- hcl.colors(5, "Purp")[1:3]
type_col <- c(
  # Class 1 - multi-protein complexes
  I = col1[1], III = col1[2], IV = col1[3],
  # Class 2 - single-protein effectors
  II = col2[1], V = col2[2], VI = col2[3]
)
# Point sizes to show types in each class
type_cex <- c(
  I = 1, III = 1.5, IV = 2,
  II = 1, V = 1.5, VI = 2
)
# Apply colors and sizes to subtypes
subtype_col <- type_col[subtype_type]
subtype_cex <- type_cex[subtype_type]

# Calculate chemical features of effector complexes
Zc <- canprot::Zc(effector_aa)
naa <- protein.length(effector_aa)
# Plot chemical features
plot(naa, Zc, pch = class + 20, cex = subtype_cex, col = subtype_col, bg = subtype_col, xlab = "Number of amino acids", ylab = quote(italic(Z)[C]))
legend("topright", c("I               ", "III", "IV"), pch = 21, pt.cex = type_cex[1:3], col = col1, pt.bg = col1, title = "Class 1               ", cex = 0.95)
legend("topright", c("II", "V", "VI"), pch = 22, pt.cex = type_cex[4:6], col = col2, pt.bg = col2, title = "Class 2", bty = "n", cex = 0.95)

Each type (I--VI) represents an evolutionary branch containing multiple genome representatives – some branches have just a few members, others have more. This creates a methodological challenge: how do we fairly assess the relative stability of groups with unequal membership?

The solution lies in CHNOSZ's rank.affinity() function, which calculates formation energies for all individual proteins, ranks them, then finds the average rank for each evolutionary group. A rescaling step ensures that groups with different numbers of members can be compared fairly. To see why rescaling is necessary, consider that the average rank of a group with one member is bounded by 1 and r length(Zc) (the total number of genomes in this calculation), but the average rank of a group with three members is bounded by 2 (the average of 1, 2, and 3) and 41 (the average of 40, 41, and 42).

Rather than representing maximum affinity as in previous diagrams, the resulting stability fields represent maximum average rank of formation affinity after rescaling. This provides a thermodynamic framework for predicting which CRISPR-Cas types would predominate under different environmental conditions.

NOTE: This code normalizes proteins to single residue equivalents before calling affinity() by using the as.residue = TRUE argument in add.protein(). If we didn't do that, then larger effector complexes would have higher affinity rankings just because of their size.

# Setup figure
par(mfrow = c(1, 2))

# Setup basis species to represent chemical variables
basis("QEC+")
swap.basis("O2", "e-")

# Load amino acid compositions normalized to one residue equivalent
iprotein <- add.protein(effector_aa, as.residue = TRUE)

# Calculate formation affinities for Class 1 effectors across environmental gradients
aout <- affinity(pH = c(0, 14), Eh = c(-0.8, 0.8), iprotein = iprotein[class == 1])
# Group proteins by evolutionary type for comparative analysis
class_1_groups <- sapply(c("I", "III", "IV"), `==`, subtype_type[class == 1], simplify = FALSE)
# Calculate average rank of formation affinity for each evolutionary group
arank <- rank.affinity(aout, groups = class_1_groups)
# Create stability diagram showing thermodynamically favored types
d <- diagram(arank, fill = col1)
water.lines(d, lty = 2)
title("Class 1", font.main = 1)

# Generate second diagram for Class 2
aout <- affinity(pH = c(0, 14), Eh = c(-0.8, 0.8), iprotein = iprotein[class == 2])
class_2_groups <- sapply(c("II", "V", "VI"), `==`, subtype_type[class == 2], simplify = FALSE)
arank <- rank.affinity(aout, groups = class_2_groups)
d <- diagram(arank, fill = col2)
water.lines(d, lty = 2)
title("Class 2", font.main = 1)

The stability diagrams reveal a compelling pattern: Type III systems dominate at reducing conditions (low Eh). This finding gains evolutionary significance when we consider that Type III was likely the first CRISPR-Cas system to evolve in Class 1 [@MWK22]. The thermodynamic preference for reducing conditions aligns with the hypothesis that these ancient immune systems arose when Earth's atmosphere and oceans were more reducing than today.

Another interesting result is that Type I systems appear to be less stable compared to others in Class 1 at low pH. This could be due to lower frequencies of acidic amino acids in Type I effector modules. Furthermore, Type II systems (in Class 2) are not visualized as stable relative to other Class 2 systems in this chemical space. Changes to other physicochemical variables – represented by some combination of r h2o and the elements C, N, and O, as well as temperature and pressure – might be needed to stabilize this group.

The key innovation here – using rank.affinity() to compare groups rather than individuals – opens up possibilities for analyzing any system where evolutionary lineages contain multiple representatives, from enzyme families to entire metabolic pathways. This groupwise approach to relative stability analysis enriches the geobiologist's toolkit with methods for mapping the environmental niches where different protein families achieve maximum thermodynamic stability.

Further Resources - Demos

Explore demos with demo(package = "CHNOSZ"). You can also use demos() to run all the demos without pausing or just one (e.g. demos("mosaic")).

More use cases for mosaic()

Solubility contours with solubility()

Other contour plots

Calculations using the output of diagram()

Activity buffers

Other thermodynamic models

Calculations with proteins

Further Resources - Vignettes

Additional vignettes cover:

Frequently asked questions

The FAQ is a non-comprehensive collection of questions and answers about CHNOSZ.

OBIGT thermodynamic database

The OBIGT vignette is generated from reference information in the database and lists all literature citations for species arranged by default and optional data files.

Customizing the thermodynamic database

The custom_data vignette describes add.OBIGT() for adding data from files, mod.OBIGT() for updating or adding parameters of particular species, and logK.to.OBIGT() for generating parameters from logK values.

Fitting thermodynamic data

The eos-regress vignette shows how to fit experimental data (volume and heat capacity) using constructed equation-of-state models.

Creating multi-metal diagrams

The multi-metal vignette has some techniques for overcoming the limitation of balancing reactions on a single metal.

Getting Help

R provides convenient access to documentation in a local browser:

You can also:

Document History

wzxhzdk:80

## For finding what versions of packages are on R-Forge and winbuilder
#sessionInfo()

References



Try the CHNOSZ package in your browser

Any scripts or data that you put into this service are public.

CHNOSZ documentation built on June 20, 2025, 9:08 a.m.