Author: Jeffrey M. Dick

# An Introduction to CHNOSZ

#### Jeffrey M. Dick

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 2025-06-20 with CHNOSZ 2.2.0.

## 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

CHNOSZ offers several primary functions for thermodynamic analysis:

#### Functions without side effects (return values)

- `info()`: Search the thermodynamic database
- `subcrt()`: Calculate thermodynamic properties of species and reactions
- `affinity()`: Calculate affinities of formation reactions
- `equilibrate()`: Calculate equilibrium chemical activities
- `diagram()`: Plot the results

#### Functions with side effects (modify system state)

- `basis()`: Set basis species and their chemical activities
- `species()`: Set species of interest and their activities
- `reset()`: Reset database and system settings to defaults

### Querying the thermodynamic database

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

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

    ## info.character: found CH4(aq); also available in gas, liq

    ## [1] 732

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.character: found methane(gas); also available in liq

    ## [1] 2698

    info("CH4", "gas")

    ## [1] 2698

Use `info()` recursively to return thermodynamic parameters:

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

    ## info.character: found CH4(aq); also available in gas, liq

    ## check.EOS: calculated Cp° of CH4(aq) differs by -2.61 cal K-1 mol-1 from database value

    ##     name abbrv formula state ref1 ref2       date model E_units     G      H  S    Cp  V    a1    a2     a3     a4    c1    c2  omega Z
    ## 732  CH4  <NA>     CH4    aq PS01 <NA> 2000-10-04   HKF     cal -8140 -20930 21 60.23 36 1.769 -1530 -67.88 114700 40.87 64500 -40000 0

You can access fuzzy search functionality by using partial names:

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

    ## info.approx: 'ribose+' is ambiguous; has approximate matches to 8 species:

    ## [1] "ribose"               "deoxyribose"          "ribose-5-phosphate"   "ribose-5-phosphate-1" "ribose-5-phosphate-2" "ribose"               "deoxyribose"
    ## [8] "ribose-5-phosphate"

    ## [1] NA

### Calculating thermodynamic properties

The `subcrt()` function (loosely named after SUPCRT; Johnson et al., 1992) calculates standard thermodynamic properties.

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

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

    ## info.character: found CH4(aq); also available in gas, liq

    ## subcrt: 1 species at 15 values of T (ºC) and P (bar) (wet) [energy units: J]

    ## $species
    ##     name formula state ispecies model
    ## 732  CH4     CH4    aq      732   HKF
    ##
    ## $out
    ## $out$CH4
    ##         T          P       rho     logK         G         H         S         V         Cp
    ## 1    0.01   1.000000 0.9998289 6.146120 -32141.64 -94362.48  64.02537  28.43371   320.2540
    ## 2   25.00   1.000000 0.9970614 5.966661 -34057.76 -87571.12  87.86400  36.32929   241.0868
    ## 3   50.00   1.000000 0.9880295 5.898249 -36490.28 -81887.10 106.18347  40.21802   217.6954
    ## 4   75.00   1.000000 0.9748643 5.903292 -39346.90 -76565.80 122.04828  42.65169   209.4693
    ## 5  100.00   1.013220 0.9583926 5.960475 -42580.84 -71362.35 136.48265  44.45100   207.5653
    ## 6  125.00   2.320144 0.9390726 6.055409 -46157.27 -66152.32 149.98108  45.99448   209.5788
    ## 7  150.00   4.757169 0.9170577 6.179107 -50057.59 -60843.92 162.88237  47.51660   215.2640
    ## 8  175.00   8.918049 0.8923427 6.325065 -54267.31 -55334.98 175.48219  49.23207   225.6772
    ## 9  200.00  15.536499 0.8647434 6.488877 -58778.47 -49481.61 188.11556  51.41530   243.4995
    ## 10 225.00  25.478603 0.8338733 6.667868 -63591.20 -43048.65 201.24975  54.51539   274.5815
    ## 11 250.00  39.736493 0.7990719 6.861076 -68717.66 -35603.67 215.66282  59.40754   332.5458
    ## 12 275.00  59.431251 0.7592362 7.069725 -74191.11 -26241.56 232.89165  68.08973   454.1118
    ## 13 300.00  85.837843 0.7124075 7.298795 -80088.35 -12702.80 256.64410  86.10141   764.2170
    ## 14 325.00 120.457572 0.6545772 7.562218 -86598.26  12491.49 298.92128 134.18673  1893.9587
    ## 15 350.00 165.211289 0.5746875 7.907228 -94333.67  93338.02 429.08080 355.39612 11384.8458

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))

    ## info.character: found H2O(liq) [water]; also available in cr, gas

    ## subcrt: 1 species at 2 values of T (ºC) and P (bar) (wet) [energy units: J]

    ## $species
    ##    name formula state ispecies          model
    ## 1 water     H2O   liq        1 water.SUPCRT92
    ##
    ## $out
    ## $out$water
    ##     T   P       rho     logK         G         H        S        V        Cp
    ## 1 400 250 0.1666279 21.47188 -276714.5 -241279.6 155.8919 108.1164 239.06019
    ## 2 500 300 0.1152589 19.74903 -292320.6 -232176.0 167.6886 156.3021  77.68281

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

    # Change energy units from Joules to calories
    E.units("cal")

    ## changed energy units to cal

    subcrt("CH4", T = 25)

    ## info.character: found CH4(aq); also available in gas, liq

    ## subcrt: 1 species at 25 ºC and 1 bar (wet) [energy units: cal]

    ## $species
    ##     name formula state ispecies model
    ## 732  CH4     CH4    aq      732   HKF
    ##
    ## $out
    ## $out$CH4
    ##    T P       rho     logK     G      H  S        V       Cp
    ## 1 25 1 0.9970614 5.966661 -8140 -20930 21 36.32929 57.62112

    reset()  # Restore defaults

    ## reset: resetting "thermo" object

    ## OBIGT: loading default database with 1833 aqueous, 3496 total species

### 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)

    ## subcrt: 2 species at 25 ºC and 1 bar (wet) [energy units: J]

    ## $reaction
    ##      coeff           name formula state ispecies model
    ## 2680    -1 carbon dioxide     CO2   gas     2680   CGL
    ## 430      1            CO2     CO2    aq      430   HKF
    ##
    ## $out
    ##    T P       rho      logK        G         H         S        V       Cp
    ## 1 25 1 0.9970614 -1.468942 8384.736 -20288.22 -96.16924 32.63308 159.1761

Reactions can be automatically balanced using basis species:

- Here, the defined basis species are used to balance a reaction with `subcrt()` for calculating standard thermodynamic properties.

    basis(c("CO2", "H2O", "H+", "e-"))

    ##     C H O  Z ispecies logact state
    ## CO2 1 0 2  0      430      0    aq
    ## H2O 0 2 1  0        1      0   liq
    ## H+  0 1 0  1        3      0    aq
    ## e-  0 0 0 -1        2      0    aq

    subcrt(c("CH4", "acetate"), c("aq", "aq"), c(1, -1), T = 25)

    ## subcrt: 2 species at 25 ºC and 1 bar (wet) [energy units: J]

    ## subcrt: reaction is not balanced; it is missing this composition:

    ##  C  H  O  Z
    ##  1 -1  2 -1

    ## subcrt: adding missing composition from basis definition and restarting...

    ## subcrt: 4 species at 25 ºC and 1 bar (wet) [energy units: J]

    ## $reaction
    ##     coeff    name formula state ispecies model
    ## 732     1     CH4     CH4    aq      732   HKF
    ## 922    -1 acetate C2H3O2-    aq      922   HKF
    ## 430     1     CO2     CO2    aq      430   HKF
    ## 3      -1      H+      H+    aq        3   HKF
    ##
    ## $out
    ##    T P       rho     logK         G         H       S        V       Cp
    ## 1 25 1 0.9970614 8.884021 -50710.08 -15355.28 119.244 28.86502 410.4372

- Here, the defined basis species are used to compose the formed species with `species()` for calculating affinities and making diagrams.

    basis(c("CO2", "H2O", "H+", "e-"))

    ##     C H O  Z ispecies logact state
    ## CO2 1 0 2  0      430      0    aq
    ## H2O 0 2 1  0        1      0   liq
    ## H+  0 1 0  1        3      0    aq
    ## e-  0 0 0 -1        2      0    aq

    species(c("CH4", "acetate"))

    ##   CO2 H2O H+ e- ispecies logact state    name
    ## 1   1  -2  8  8      732     -3    aq     CH4
    ## 2   2  -2  7  8      922     -3    aq 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

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

Eh–pH (Pourbaix) diagram for S

    # 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 O2 or H2.

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

Mosaic diagram showing effect of aqueous S speciation on the relative stabilities of Cu minerals and aqueous species

    # 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 as a function of pH and temperature

    # 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:

Solubility of corundum (green line) and equilibrium concentrations of aqueous species (black lines)

    # 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`:

Solubility of corundum dependent on ionic strength

    # 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. Amend and LaRowe, 2019; Solel et al., 2019). “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 (Dick, 2019). 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” (Garrels and Christ, 1965), 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” (Pourbaix, 1974) or “isosolubility” (Helgeson, 1964; Garrels and Christ, 1965) 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 (Osseo-Asare and Brown, 1979). 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:

The total solubility contour (green) at log mFe = -6 lies inside the quasisolubility contour (boundary between tan and blue areas), showing that the latter underestimates solubility; the largest contributions to total solubility are from Fe+2 and FeCl+ at low pH and FeO2- and HFeO2- at high pH

    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 (Pelton et al., 2018; De Lucia and Kühn, 2021).

## 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 CO2 dissolution example seen earlier and add two other gases (carbon monoxide and methane). This plot is similar to Figure 18 of Manning et al. (2013).

Equilibrium constants of dissolution reactions

    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:

- `describe.reaction()` to format chemical reactions from the output of `subcrt()`
- `describe.property()` to format property values as equations (e.g. “T = 25 °C”)
- `describe.basis()` to format the activities of basis species
- `lT()`, `lP()`, `lTP()`, and related functions for more compact representations of conditions (e.g. “25 °C, 1 bar”)

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

- See the documentation for `plotmath()` for general information on formatting mathematical expressions in R.
- `bquote()` is useful for putting values of variables into expressions.
- Some of the examples in this vignette and in the demos use `as.expression()` on the combined output from other functions to produce complex legends for the `legend()` function.

### 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]

    ##         name abbrv  formula state ref1
    ## 479     Al+3  <NA>     Al+3    aq TS01
    ## 480 Al(OH)4-  <NA> Al(OH)4-    aq TS01
    ## 481   AlOH+2  <NA>   AlOH+2    aq TS01

    # Use the species index or name in subcrt()
    subcrt(iaq[1:3], T = 25)

    ## $species
    ##         name  formula state ispecies model
    ## 479     Al+3     Al+3    aq      479   HKF
    ## 480 Al(OH)4- Al(OH)4-    aq      480   HKF
    ## 481   AlOH+2   AlOH+2    aq      481   HKF
    ##
    ## $out
    ## $out$`Al+3`
    ##    T P       rho     logK         G         H         S         V        Cp
    ## 1 25 1 0.9970614 85.40242 -487477.8 -538769.6 -339.7534 -45.33933 -119.3444
    ##
    ## $out$`Al(OH)4-`
    ##    T P       rho     logK        G        H        S        V      Cp
    ## 1 25 1 0.9970614 228.7613 -1305772 -1503075 103.5456 46.28513 96.5402
    ##
    ## $out$`AlOH+2`
    ##    T P       rho     logK         G         H         S         V        Cp
    ## 1 25 1 0.9970614 121.9904 -696322.2 -769864.3 -181.1254 -20.62274 -37.44297

    #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 (Shock and others, 1998). 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))

    ##       name     abbrv formula state     ref1 ref2       date model E_units       G       H      S    Cp    V      a1      a2      a3     a4       c1      c2  omega  Z
    ## 3737 AlO2-     AlO2-  AlO2-1    aq SSWS97.4 <NA> 1997-11-07   HKF     cal -198693 -222125  -7.22 -11.9 10.0 0.37221  399.54 -1.5879 -29441  15.2391  -54585 174180 -1
    ## 3738  AlO+      AlO+   AlO+1    aq SSWS97.4 <NA> 1997-11-13   HKF     cal -158188 -170900 -27.00 -30.0  0.6 0.21705 -248.11  6.7241 -26763  -2.5983  -91455  95700  1
    ## 3739 HAlO2 HAlO2(AQ)   HAlO2    aq SSWS97.4 <NA> 1997-11-13   HKF     cal -207700 -227500   5.00 -50.0 13.0 0.35338   84.85  5.4132 -28140 -23.4129 -132195  -3000  0

NOTE: Anhydrous species are commonly used for the revised Helgeson-Kirkham-Flowers (HKF) model. For example, Shock et al. (1997) reported the properties of AlO2−, 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 (Tagirov and Schott, 2001). 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 Shock et al. (1997).

Corundum solubility with species from SLOP98

    # 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-"))

    ## Error in put.basis(ispecies, logact): the number of basis species is less than the number of elements and charge

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− = H2O − H+), we could try this instead:

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

    ## Error in put.basis(ispecies, logact): the number of basis species is less than the number of elements and charge

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.

Pourbaix diagram for Mn 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, Psat (the greater of 1 bar or the vapor-liquid saturation pressure for 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 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 Skirrow and Walshe, 2002).

    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))

    ## [1] "mol NaCl added to 1 kg H2O: 0.8"

    print(paste("Ionic strength (mol/kg):", NaCl$IS))

    ## [1] "Ionic strength (mol/kg): 0.607065915465928"

    print(paste("Chloride concentration (mol/kg):", NaCl$m_Clminus))

    ## [1] "Chloride concentration (mol/kg): 0.607061333179561"

### 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.

    ## Warning in subcrt(species = c(1965L, 65L, 29L, 2688L, 1L, 3L, 1865L, 1873L, : above T limit of 400 K for the Cp equation for hydronium jarosite(cr)

    ## Warning in subcrt(species = c(1965L, 65L, 29L, 2688L, 1L, 3L, 1865L, 1873L, : above T limit of 375 K for the Cp equation for goethite(cr)

    ## Warning in subcrt(species = c(1965L, 65L, 29L, 2688L, 1L, 3L, 1865L, 1873L, : above T limit of 390 K for the Cp equation for lepidocrocite(cr)

    ## Warning in subcrt(species = c(1965L, 65L, 29L, 2688L, 1L, 3L, 1865L, 1873L, : above T limit of 400 K for the Cp equation for hydronium jarosite(cr)

    ## Warning in subcrt(species = c(1965L, 65L, 29L, 2688L, 1L, 3L, 1865L, 1873L, : above T limit of 375 K for the Cp equation for goethite(cr)

    ## Warning in subcrt(species = c(1965L, 65L, 29L, 2688L, 1L, 3L, 1865L, 1873L, : above T limit of 390 K for the Cp equation for lepidocrocite(cr)

    ## Warning in subcrt(species = c(1965L, 65L, 29L, 2688L, 1L, 3L, 1865L, 1873L, : above T limit of 400 K for the Cp equation for hydronium jarosite(cr)

    ## Warning in subcrt(species = c(1965L, 65L, 29L, 2688L, 1L, 3L, 1865L, 1873L, : above T limit of 375 K for the Cp equation for goethite(cr)

    ## Warning in subcrt(species = c(1965L, 65L, 29L, 2688L, 1L, 3L, 1865L, 1873L, : above T limit of 390 K for the Cp equation for lepidocrocite(cr)

Stability diagram for minerals; predominance diagram for aqueous species; composite diagram with quasisolubility contour; diagram with total solubility contours in units of log m

    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:

Solubility in units of ppm

    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

    ## $pH
    ## [1] 6 8 6 8
    ##
    ## $Eh
    ## [1]  0.5  0.5 -0.5 -0.5

    # Print affinity values calculated for each species
    a$values

    ## $`16`
    ## [1]   10.71165   28.71165 -124.52393 -106.52393
    ##
    ## $`17`
    ## [1]   9.573951  23.573951 -91.852732 -77.852732
    ##
    ## $`18`
    ## [1] 2.2409947 0.2409947 2.2409947 0.2409947
    ##
    ## $`64`
    ## [1] -1 -1 -1 -1

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.

- The conserved basis species requires an activity to be assign to calculate affinities. For the purposes of stability or predominance diagrams, this is a placeholder value, since the conserved basis species has the same stoichiometry in each normalized formation reaction.
- However, choosing a different conserved basis species can greatly affect the geometry of diagrams, owing to a different normalization vector.
- Also, choosing a balancing constraint is important for comparing affinity (or its opposite, non-standard Gibbs energy of reaction) of different reactions.

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 Shock and Canovas (2010):

Affinities of organic synthesis reactions per mole of C, H2, or formed species

    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, CO2. This is the same as normalizing on C, because no other basis species has C.
2. Balance on a different species, 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 Shock and Canovas (2010).

### 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 Alberty (2003):

    # 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

    ## $MgH2ATP
    ##    T P        G       loggam   IS
    ## 1 25 1 -3289461  0.000000000 0.00
    ## 2 25 1 -3289472 -0.001951595 0.25
    ##
    ## $`MgHATP-`
    ##    T P        G     loggam   IS
    ## 1 25 1 -3267679  0.0000000 0.00
    ## 2 25 1 -3268489 -0.1418484 0.25
    ##
    ## $`MgATP-2`
    ##    T P        G     loggam   IS
    ## 1 25 1 -3236780  0.0000000 0.00
    ## 2 25 1 -3240019 -0.5673936 0.25

    # 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]

    ##     T         G logQ        A
    ## 1  25 -33748.14    0 33748.14
    ## 2  50 -38662.41    0 38662.41
    ## 3 100 -51468.20    0 51468.20

    # Non-unit activity: affinity is opposite of non-standard Gibbs energy
    subcrt(species, coeffs, T = T, logact = c(-5, -4, -3))$out[, -idrop]

    ##     T         G logQ         A
    ## 1  25 -33748.14    6 -499.9146
    ## 2  50 -38662.41    6 1542.6389
    ## 3 100 -51468.20    6 8604.9959

NOTE:

- The `logK`, `G`, `H`, `S`, `V`, and `Cp` columns in the output of `subcrt()` are always standard or adjusted thermodynamic properties, not non-standard ones.
- Columns for `logQ` and `A` are added if the `logact` argument is provided.
- The `logact` argument specifies the activities of species in the same order as the first argument.
- `A` in the output of `subcrt()` has the same units as `G` (J/mol by default); this differs from `affinity()`, which outputs dimensionless values (A/2.303RT).

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

- Only if all species have unit activities, then the affinity values (`A` in the output) are the opposite of the standard Gibbs energy (`G` in the output).

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

- The result shows that affinity values in general are not the opposite of standard Gibbs energy.
- Instead, they are the opposite of non-standard (or overall) Gibbs energy.

### 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 Mayumi et al. (2013). We combine some of the features described above with new ones for swapping basis species and removing formed species:

- `swap.basis()`: Changes one species for another in an existing basis definition
- `species()` with `delete = TRUE`: Removes one or more species from the set of formed species
- `describe.reaction()`: Formats reactions for plots using the output of `subcrt()`
- `names` and `srt` arguments of `diagram()`: Use supplied reaction labels with rotation

Non-standard Gibbs energies of organic reactions as a function of CO2 fugacity

    # 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 H2 and CH4 in the basis species.

    a1$species

    ##   CO2 H2 H2O H+ ispecies logact state    name
    ## 1   1  4  -2  0     2698  -0.18   gas methane
    ## 2   2  4  -2 -1      922  -3.40    aq acetate

    a2$species

    ##   CO2 CH4 H2O H+ ispecies logact state    name
    ## 1   1   1   0 -1      922   -3.4    aq acetate

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 Mayumi et al. (2013). 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.

Eh–pH diagram for metastable S species

    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:

- `plotvals`: Affinity for each species, after normalizing by the balancing constraint
- `predominant.values`: Normalized affinity for the predominant species at each point on the diagram
- `names`: Names used for labeling lines or fields (includes formatting for chemical formulas)
- `namesx`, `namesy`: Locations for labels
- `linesout`: x, y coordinates of boundary lines between stability fields

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)

    ##   Cu   Co    S
    ## 0.92 2.07 4.00

    # 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)

    ## [1] "C2H4O2"

### 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)

    ## List of 13
    ##  $ opt       :List of 18
    ##  $ element   :'data.frame':  134 obs. of  6 variables:
    ##  $ OBIGT     :'data.frame':  3496 obs. of  22 variables:
    ##  $ refs      :'data.frame':  342 obs. of  6 variables:
    ##  $ Berman    :'data.frame':  109 obs. of  30 variables:
    ##  $ buffer    :'data.frame':  39 obs. of  4 variables:
    ##  $ protein   :'data.frame':  505 obs. of  25 variables:
    ##  $ groups    :'data.frame':  5 obs. of  22 variables:
    ##  $ stoich    : num [1:3496, 1:88] 0 0 0 0 0 0 0 0 0 0 ...
    ##   ..- attr(*, "dimnames")=List of 2
    ##  $ Bdot_acirc: Named num [1:59] 2.5 2.5 2.5 2.5 2.5 3 3 3 3 3 ...
    ##   ..- attr(*, "names")= chr [1:59] "Rb+" "Cs+" "NH4+" "Tl+" ...
    ##  $ opar      :List of 66
    ##  $ basis     :'data.frame':  6 obs. of  9 variables:
    ##  $ species   :'data.frame':  21 obs. of  10 variables:

    str(thermo()$OBIGT)

    ## 'data.frame':    3496 obs. of  22 variables:
    ##  $ name        : chr  "water" "e-" "H+" "Li+" ...
    ##  $ abbrv       : chr  NA NA "H+" "Li+" ...
    ##  $ formula     : chr  "H2O" "(Z-1)" "H+" "Li+" ...
    ##  $ state       : chr  "liq" "aq" "aq" "aq" ...
    ##  $ ref1        : chr  "JOH92" "OBIGT06" "Den81" "SH88" ...
    ##  $ ref2        : chr  "HGK84" NA NA NA ...
    ##  $ date        : chr  "2006-10-25" "2006-10-28" "1997-11-06" "1997-11-06" ...
    ##  $ model       : chr  "H2O" "HKF" "HKF" "HKF" ...
    ##  $ E_units     : chr  "cal" "cal" "cal" "cal" ...
    ##  $ G           : num  NA 0 0 -69933 -62591 ...
    ##  $ H           : num  NA 0 0 -66552 -57433 ...
    ##  $ S           : num  NA 15.6 0 2.7 14 ...
    ##  $ Cp          : num  NA 0 0 14.2 9.06 1.98 -3 -6.29 -5.34 -7.53 ...
    ##  $ V           : num  NA 0 0 -0.87 -1.11 ...
    ##  $ a1.a        : num  NA 0 0 -0.0237 1.839 ...
    ##  $ a2.b        : num  NA 0 0 -0.069 -2.285 ...
    ##  $ a3.c        : num  NA 0 0 11.58 3.26 ...
    ##  $ a4.d        : num  NA 0 0 -2.78 -2.73 ...
    ##  $ c1.e        : num  NA 0 0 19.2 18.2 ...
    ##  $ c2.f        : num  NA 0 0 -0.24 -2.98 ...
    ##  $ omega.lambda: num  NA 0 0 0.486 0.331 ...
    ##  $ z.T         : num  NA 0 0 1 1 1 1 1 2 2 ...

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")

    ## $`opt$T.units`
    ## [1] "C"

    # Return to default
    thermo("opt$T.units" = "C")

    ## $`opt$T.units`
    ## [1] "K"

See the help page for the `Berman()` function for a practical example of adding thermodynamic data with the Berman (1988) 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 Helgeson et al. (1981). The species-species parameters are charge and (for the default `Bdot` model) ion-size parameters used in the HCh program (Shvarov and Bastrakov, 1999). By contrast, the `bgamma` model uses an extended term parameter that is derived from data of Helgeson (1969), Helgeson et al. (1981), and high-P extrapolations of Manning et al. (2013). The `Alberty` model uses parameters listed in Chapter 3 of Alberty (2003), 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:

- In `subcrt()`, the `logact` argument stands for log molality of aqueous species and calculated values of `G` are the adjusted Gibbs energy at specified ionic strength (this is written as ΔG°(I) by Alberty, 1996).

- In `affinity()`, the following stand for log molality of aqueous species:

  - `logact` set by `basis()`
  - `logact` set by `species()`

- In `solubility()` and `equilibrate()`, the following stand for log molality of aqueous species:

  - `loga.balance` (logarithm of total molality of the conserved basis species)
  - `loga.equil` (logarithm of molality of each species).

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., H2S and 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 Schulte and Shock (1995):

Equilibrium log H2 fugacity for 10-6 activity of HCN or formaldehyde with water, 1 bar of N2 and 10 bar of CO2

    # 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 Schulte and Shock (1995).

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 fO2 Buffers in downstream calculations

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

Gold solubility at 300 °C with PPM buffer for fO2 and aH2S

    # 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.

Effects of different buffers on gold solubility

    # 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 Williams-Jones et al., 2009)
2. Temperature dependence under the pyrite-pyrrhotite-magnetite (PPM) buffer (cf. Fig. 2B in Williams-Jones et al., 2009)
3. pH dependence under PPM at fixed temperature, with neutral pH and KMQ buffer lines (cf. Fig. 7 in Akinfiev and Zotov, 2001)
4. Species predominance in log fO2–pH space with redox and pH buffer lines

Note the following limitation:

- `mosaic()` calculations currently aren’t supported for basis species that are associated with a buffer.
- Because of this, for the last diagram we precomputed the value of log aH2S from the PPM buffer then assigned that value in `basis()` to use in the mosaic calculation.

See also:

- `demo("gold")` for chloride and ionic strength effects (plots show molality instead of activity)
- The FAQ for handling the K/Cl activity ratio for the KMQ buffer

## 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)

    ## $length
    ## [1] 129
    ##
    ## $formula
    ##              C   H   N   O  S
    ## LYSC_CHICK 613 959 193 185 10

#### 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, ]

    ##        T        P       rho     logK         G         H        S        V       Cp
    ## 1   0.01 1.000000 0.9998289 3465.966 -18125553 -44827095 16093.36 10049.21 18448.59
    ## 2  25.00 1.000000 0.9970614 3250.225 -18552315 -44238615 18149.61 10420.95 26842.53
    ## 3  50.00 1.000000 0.9880295 3076.728 -19034574 -43528036 20436.77 10600.23 29597.54
    ## 4  75.00 1.000000 0.9748643 2936.705 -19573864 -42770559 22693.98 10708.15 30863.62
    ## 5 100.00 1.013220 0.9583926 2823.190 -20168495 -41989264 24860.88 10782.93 31582.69
    ## 6 125.00 2.320144 0.9390726 2730.687 -20814620 -41191982 26925.13 10840.94 32071.19

#### Ionization effects

For more accurate calculations, especially in biological systems, protein ionization must be considered (Dick et al., 2006). 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 (Dick and Shock, 2011):

Metastable equilibrium of proteins from hot-spring metagenomes

    # 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:

Unoptimized predicted abundances of proteins compared to experimental abundances, both scaled to unit total activity of residues; dashed line is 1:1 line

    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 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 (Ghaemmaghami et al., 2003):

Optimizing redox potential to fit experimental protein abundances

    # 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 (Makarova et al., 2020).

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 (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 ZC.

Carbon oxidation state and size of CRISPR-Cas effector modules

    # 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 42 (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.

Groupwise relative stabilities of effector modules in different types of CRISPR-Cas systems as a function of Eh and pH at 25 °C; dashed lines are water stability limits

    # 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 (Makarova et al., 2022). 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 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()

- `mosaic`: Speciating more than one set of basis species
- `sum_S`, `uranyl`: Using summed activities of speciated basis species
- `comproportionation`: Non-standard Gibbs energy of reaction with speciated basis species
- `arsenic`, `copper`: More examples of Eh–pH diagrams
- `sphalerite`, `contour`, `minsol`: Solubility calculations with speciated basis species

### Solubility contours with solubility()

- `Pourbaix`: Isosolubility lines for various metals (try Fe, Cu, Mn)
- `contour`: Solubility contours for gold
- `minsol` Solubility contours for multiple minerals
- `solubility`: CO2 and calcite

### Other contour plots

- `saturation`: Saturation lines (where affinity = 0) and labels for activity ratios
- `ionize`: Protein ionization properties
- `TCA`: Citric acid cycle energetics
- `comproportionation`: Using a color scale (image map)

### Calculations using the output of diagram()

- `buffer`: Place labels next to lines
- `MgATP`: Calculate number of protons bound per ATP molecule

### Activity buffers

- `buffer`: Plotting buffers as a function of temperature
- `DEW`: Applying calculated values of log fO2 in `affinity()`
- `gold`: Settting pH and fO2 buffers in `basis()` for solubility of gold
- `protbuff`: Using proteins as buffer species

### Other thermodynamic models

- `DEW`: Deep Earth Water model (extension of HKF to high pressures)
- `AD`: Akinfiev-Diamond model for aqueous nonelectrolytes

### Calculations with proteins

- `Shh`: Relative stabilities of transcription factors along redox and water activity gradient
- `carboxylase`: Predicted rank abundance with varying temperature and redox
- `rank.affinity`: Average rank of affinity for groupwise stability comparisons

## 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:

- If you haven’t installed CHNOSZ yet, run `install.packages("CHNOSZ")`
- Run `help.start()` to open a browser window for the R help system
- Choose “Packages” followed by “CHNOSZ”
- Select the help topic for any function to see detailed documentation and examples
- Follow the links at the top of the main page for demos (longer examples) and vignettes (more in-depth documentation; this document is a vignette)

You can also:

- Browse the package documentation with `help(package = "CHNOSZ")`
  - See function-specific help: `?info`, `?subcrt`, etc.
- Use the Discussions forum on GitHub for questions about CHNOSZ
- Contact the maintainer for bug reports or feature requests
  - Use `maintainer("CHNOSZ")` to get contact info

## Document History

- 2010-09-30 First version, titled “Getting started with CHNOSZ”.
- 2017-02-15 Rewritten and switched from Sweave to knitr.
- 2025-05-10 Major revision; used AI assistance for Basic Functionality, Buffers, and Proteins.

       ######    ##   ##    ##   ##    ######     #####  #####
     ##         ##===##    ## \\##   ##    ##     \\       //
     ######    ##   ##    ##   ##    ######    #####      #####

## References

Akinfiev NN, Zotov AV. 2001. Thermodynamic description of chloride, hydrosulfide, and hydroxo complexes of Ag(I), Cu(I), and Au(I) at temperatures of 25-500°C and pressures of 1-2000 bar. Geochemistry International 39(10): 990–1006.

Alberty RA. 1996. Recommendations for nomenclature and tables in biochemical thermodynamics. European Journal of Biochemistry 240(1): 1–14. doi: 10.1111/j.1432-1033.1996.0001h.x

Alberty RA. 2003. Thermodynamics of Biochemical Reactions. Hoboken, New Jersey: Wiley-Interscience. Available at https://search.worldcat.org/title/51242181.

Amend JP, LaRowe DE. 2019. Mini-review: Demystifying microbial reaction energetics. Environmental Microbiology 21(10): 3539–3547. doi: 10.1111/1462-2920.14778

Berman RG. 1988. Internally-consistent thermodynamic data for minerals in the system Na2O–K2O–CaO–MgO–FeO–Fe2O3–Al2O3–SiO2–TiO2–H2O–CO2. Journal of Petrology 29(2): 445–522. doi: 10.1093/petrology/29.2.445

De Lucia M, Kühn M. 2021. Geochemical and reactive transport modelling in R with the RedModRphree package. Advances in Geosciences 56: 33–43. doi: 10.5194/adgeo-56-33-2021

Dick JM. 2019. CHNOSZ: Thermodynamic calculations and diagrams for geochemistry. Frontiers in Earth Science 7: 180. doi: 10.3389/feart.2019.00180

Dick JM, LaRowe DE, Helgeson HC. 2006. Temperature, pressure, and electrochemical constraints on protein speciation: Group additivity calculation of the standard molal thermodynamic properties of ionized unfolded proteins. Biogeosciences 3(3): 311–336. doi: 10.5194/bg-3-311-2006

Dick JM, Shock EL. 2011. Calculation of the relative chemical stabilities of proteins as a function of temperature and redox chemistry in a hot spring. PLOS One 6(8): e22782. doi: 10.1371/journal.pone.0022782

Garrels RM, Christ CL. 1965. Solutions, Minerals, and Equilibria. New York: Harper & Row. Available at https://search.worldcat.org/title/517586.

Ghaemmaghami S, Huh W-K, Bower K, Howson RW, Belle A, Dephoure N, O’Shea EK, Weissman JS. 2003. Global analysis of protein expression in yeast. Nature 425(6959): 737–741. doi: 10.1038/nature02046

Helgeson HC. 1964. Complexing and Hydrothermal Ore Deposition. New York: Pergamon Press. Available at https://search.worldcat.org/title/8080200.

Helgeson HC. 1969. Thermodynamics of hydrothermal systems at elevated temperatures and pressures. American Journal of Science 267(7): 729–804. doi: 10.2475/ajs.267.7.729

Helgeson HC, Kirkham DH, Flowers GC. 1981. Theoretical prediction of the thermodynamic behavior of aqueous electrolytes at high pressures and temperatures: IV. Calculation of activity coefficients, osmotic coefficients, and apparent molal and standard and relative partial molal properties to 600°C and 5 Kb. American Journal of Science 281(10): 1249–1516. doi: 10.2475/ajs.281.10.1249

Johnson JW, Oelkers EH, Helgeson HC. 1992. SUPCRT92: A software package for calculating the standard molal thermodynamic properties of minerals, gases, aqueous species, and reactions from 1 to 5000 bar and 0 to 1000°C. Computers & Geosciences 18(7): 899–947. doi: 10.1016/0098-3004(92)90029-Q

Makarova KS, Wolf YI, Iranzo J, Shmakov SA, Alkhnbashi OS, Brouns SJJ, Charpentier E, Cheng D, Haft DH, Horvath P, et al. 2020. Evolutionary classification of CRISPR-Cas systems: A burst of class 2 and derived variants. Nature Reviews Microbiology 18(2): 67–83. doi: 10.1038/s41579-019-0299-x

Makarova KS, Wolf YI, Koonin EV. 2022. Evolutionary classification of CRISPR-Cas systems. In: CRISPR: Biology and Applications. John Wiley & Sons, Ltd. pp. 13–38. doi: 10.1002/9781683673798.ch2

Manning CE, Shock EL, Sverjensky DA. 2013. The chemistry of carbon in aqueous fluids at crustal and upper-mantle conditions: Experimental and theoretical constraints. Reviews in Mineralogy and Geochemistry 75(1): 109–148. doi: 10.2138/rmg.2013.75.5

Mayumi D, Dolfing J, Sakata S, Maeda H, Miyagawa Y, Ikarashi M, Tamaki H, Takeuchi M, Nakatsu CH, Kamagata Y. 2013. Carbon dioxide concentration dictates alternative methanogenic pathways in oil reservoirs. Nature Communications 4: 1998. doi: 10.1038/ncomms2998

Osseo-Asare K, Brown TH. 1979. A numerical method for computing hydrometallurgical activity–activity diagrams. Hydrometallurgy 4(3): 217–232. doi: 10.1016/0304-386X(79)90013-6

Pelton AD, Eriksson G, Hack K, Bale CW. 2018. Thermodynamic calculation of aqueous phase diagrams. Monatshefte für Chemie - Chemical Monthly 149(2): 395–409. doi: 10.1007/s00706-017-2094-6

Pourbaix M. 1974. Atlas of Electrochemical Equilibria in Aqueous Solutions. 2nd ed. Houston, Texas; Brussels: National Association of Corrosion Engineers; CEBELCOR. Available at https://search.worldcat.org/title/563921897.

Schulte MD, Shock EL. 1995. Thermodynamics of Strecker synthesis in hydrothermal systems. Origins of Life and Evolution of the Biosphere 25(1-3): 161–173. doi: 10.1007/BF01581580

Shock EL, Canovas P. 2010. The potential for abiotic organic synthesis and biosynthesis at seafloor hydrothermal systems. Geofluids 10(1-2): 161–192. doi: 10.1111/j.1468-8123.2010.00277.x

Shock EL, others. 1998. slop98.dat: Sequential-access thermodynamic datafile used by PROGRAM supcrt92. Last updated on 1998-08-20. Available at https://doi.org/10.5281/zenodo.2630820.

Shock EL, Sassani DC, Willis M, Sverjensky DA. 1997. Inorganic species in geologic fluids: Correlations among standard molal thermodynamic properties of aqueous ions and hydroxide complexes. Geochimica et Cosmochimica Acta 61(5): 907–950. doi: 10.1016/S0016-7037(96)00339-0

Shvarov Y, Bastrakov E. 1999. HCh: A Software Package for Geochemical Equilibrium Modelling. User’s Guide. Australian Geological Survey Organisation, Record 1999/25. Available at https://pid.geoscience.gov.au/dataset/ga/25473.

Skirrow RG, Walshe JL. 2002. Reduced and oxidized Au-Cu-Bi iron oxide deposits of the Tennant Creek inlier, Australia: An integrated geologic and chemical model. Economic Geology 97(6): 1167–1202. doi: 10.2113/gsecongeo.97.6.1167

Solel E, Tarannam N, Kozuch S. 2019. Catalysis: Energy is the measure of all things. Chemical Communications 55(37): 5306–5322. doi: 10.1039/C9CC00754G

Tagirov B, Schott J. 2001. Aluminum speciation in crustal fluids revisited. Geochimica et Cosmochimica Acta 65(21): 3965–3992. doi: 10.1016/S0016-7037(01)00705-0

Williams-Jones AE, Bowell RJ, Migdisov AA. 2009. Gold in solution. Elements 5(5): 281–287. doi: 10.2113/gselements.5.5.281